Commit bbc1e04c authored by mey's avatar mey
Browse files

MutableStorage.java: added store method

    improved documentation

BaseStorage.java
    stores an amount without error as simple double / unit combination
        not needed and causes problems
        added protected accessors
ConfigurableStorage.java
    removed storeError flag
    added store method
    updated LimitedTestStorage and test
AbstractLimitedStoragePipeline.java
    delayed storages are hold back when removal is rejected by sum storage
    added store method
    added toString method in properties proxy displaying simple class name
added AmountIsCloseTo.java Matcher class for tests
parent 4ff2989d
package de.zmt.storage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.PriorityQueue;
......@@ -85,21 +86,34 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
@Override
public Amount<Q> drainExpired() {
Amount<Q> returnAmount = AmountUtil.zero(sum.getAmount());
/*
* elements that cannot be removed due to sum and need to be put back
* into queue
*/
Collection<DelayedStorage<Q>> holdBack = new ArrayList<>();
while (true) {
DelayedStorage<Q> head = queue.poll();
if (head != null) {
Amount<Q> amount = head.getAmount();
// subtract amount of this storage from sum
ChangeResult<Q> changeResult = sum.add(amount.opposite());
Amount<Q> required = sum.store(amount.opposite());
// sum the amount received from storage
returnAmount = returnAmount.minus(changeResult.getStored());
// if amount could be subtracted:
if (required != null) {
// sum the amount received from storage (it is negative)
returnAmount = returnAmount.minus(required);
} else {
holdBack.add(head);
}
} else {
// no expired elements
break;
}
}
queue.addAll(holdBack);
// clear to prevent ever increasing numeric error
if (queue.isEmpty()) {
clear();
......@@ -115,22 +129,39 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
/**
* @throws IllegalArgumentException
* if {@code amountToAdd} is negative
* if {@code amount} is negative
*/
@Override
public ChangeResult<Q> add(Amount<Q> amount) {
ChangeResult<Q> result = sum.add(amount);
addToPipeline(result.getStored());
return result;
}
/**
* @throws IllegalArgumentException
* if {@code amount} is negative
*/
@Override
public ChangeResult<Q> add(Amount<Q> amountToAdd) {
if (amountToAdd.getEstimatedValue() < 0) {
throw new IllegalArgumentException("amountToAdd must be positive.");
public Amount<Q> store(Amount<Q> amount) {
Amount<Q> required = sum.store(amount);
if (required != null) {
addToPipeline(amount);
}
return required;
}
ChangeResult<Q> result = sum.add(amountToAdd);
private void addToPipeline(Amount<Q> amount) {
if (amount.getEstimatedValue() < 0) {
throw new IllegalArgumentException(amount + " cannot be added, must be positive.");
}
// do not add storage for zero amounts, e.g. storage is already at limit
if (result.getStored().getEstimatedValue() > 0) {
queue.offer(createDelayedStorage(result.getStored()));
if (amount.getEstimatedValue() > 0) {
queue.add(createDelayedStorage(amount));
}
return result;
}
@Override
......@@ -171,18 +202,18 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
*
*/
public static abstract class DelayedStorage<Q extends Quantity> extends BaseStorage<Q> implements Delayed {
private static final long serialVersionUID = 1L;
public DelayedStorage(Amount<Q> amount) {
this.setAmount(amount);
}
@Override
public int compareTo(Delayed o) {
// from TimerQueue#DelayedTimer
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (diff == 0) ? 0 : ((diff < 0) ? -1 : 1);
}
private static final long serialVersionUID = 1L;
public DelayedStorage(Amount<Q> amount) {
super(amount);
}
@Override
public int compareTo(Delayed o) {
// from TimerQueue#DelayedTimer
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (diff == 0) ? 0 : ((diff < 0) ? -1 : 1);
}
}
/**
......@@ -221,7 +252,7 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
public Storage<Q> getSum() {
return sum;
}
public Collection<? extends Storage<Q>> getContent() {
return AbstractLimitedStoragePipeline.this.getContent();
}
......@@ -229,5 +260,11 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
public int getContentSize() {
return getContent().size();
}
@Override
public String toString() {
// will appear in window title when viewing in MASON GUI
return AbstractLimitedStoragePipeline.this.getClass().getSimpleName();
}
}
}
package de.zmt.storage;
import javax.measure.quantity.Quantity;
import javax.measure.unit.Unit;
import org.jscience.physics.amount.Amount;
import sim.util.Valuable;
/**
* Basic implementation of {@link Storage}.
* Basic implementation of {@link Storage}. Stores an amount without error.
*
* @author mey
*
* @param
* <Q>
* type of {@link Quantity}
* the type of {@link Quantity}
*/
public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable {
private static final long serialVersionUID = 1L;
private Amount<Q> amount;
/** Value of the storage. */
private double value;
/** Unit the value is stored in. */
private Unit<Q> unit;
public BaseStorage(Amount<Q> amount) {
setAmount(amount);
}
public BaseStorage(double value, Unit<Q> unit) {
super();
this.value = value;
this.unit = unit;
}
@Override
public Amount<Q> getAmount() {
return amount;
return Amount.valueOf(value, unit);
}
protected void setAmount(Amount<Q> amount) {
this.amount = amount;
this.value = amount.getEstimatedValue();
this.unit = amount.getUnit();
}
protected double getValue() {
return value;
}
protected void setValue(double value) {
this.value = value;
}
protected Unit<Q> getUnit() {
return unit;
}
protected void setUnit(Unit<Q> unit) {
this.unit = unit;
}
@Override
......@@ -36,6 +67,6 @@ public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable {
@Override
public double doubleValue() {
return amount.getEstimatedValue();
return value;
}
}
\ No newline at end of file
......@@ -14,41 +14,32 @@ import sim.util.Valuable;
* A {@link MutableStorage} that rejects any amount exceeding its limits. Apart
* from that, there are factors for incoming and outgoing amounts, simulating
* losses and gains during exchange.
* <p>
* Gains and losses are applied on the amount that changes the storage.
* Therefore, in factors below 1 lead to a loss. They are applied on positive
* values. Out factors with the same value lead to a gain because they are
* applied on negative values. To apply the same gain or the same loss on both
* ends, one factor needs to be the inverse of the other.
*
* @author mey
*
* @param
* <Q>
* the type of {@link Quantity}
*/
public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> implements LimitedStorage<Q>, Proxiable {
private static final long serialVersionUID = 1L;
private static final int DIRECTION_UPPER = 1;
private static final int DIRECTION_LOWER = -1;
/**
* Set to false for preventing error calculation in amount.
*/
private final boolean storeError;
/**
* Create an empty storage (at lower limit) with the given unit.
* Create an empty storage with the given unit.
*
* @param unit
*/
public ConfigurableStorage(Unit<Q> unit) {
this(unit, false);
}
/**
* Create an empty storage with the given unit and if storage should take
* calculation errors into account. Amount is initialized to zero.
*
* @param unit
* @param storeError
*/
public ConfigurableStorage(Unit<Q> unit, boolean storeError) {
this.storeError = storeError;
setAmount(AmountUtil.zero(unit));
super(0, unit);
}
/** @return True if storage is at its lower limit. */
......@@ -125,65 +116,106 @@ public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> impl
}
/**
* Add given amount without exceeding the limits. Use a negative value to
* remove from storage. If the amount is positive, stored amount will be
* decreased by loss factor, otherwise the removed amount increases.
* Adds given amount to the storage without exceeding the limits. If the
* amount is positive, factor in is applied, otherwise factor out.
* <p>
* <b>NOTE:</b> The stored amount includes the factor while the rejected
* will not:<br>
* {@code stored + rejected != amountToAdd * factor} if {@code factor != 1}.
*
* @param amountToAdd
* @param amount
* @return {@link de.zmt.storage.MutableStorage.ChangeResult} including the
* amount actually added / removed from storage, and the rejected
* one.
* one
*
*/
@Override
public ChangeResult<Q> add(Amount<Q> amountToAdd) {
double estimatedValue = amountToAdd.getEstimatedValue();
if (estimatedValue == 0) {
public ChangeResult<Q> add(Amount<Q> amount) {
double value = amount.doubleValue(getUnit());
if (value == 0) {
// nothing added
return new ChangeResult<>(AmountUtil.zero(amountToAdd), amountToAdd);
return new ChangeResult<>(AmountUtil.zero(amount), amount);
}
boolean positive = estimatedValue > 0;
Amount<Q> limit = positive ? getUpperLimit() : getLowerLimit();
int direction = positive ? DIRECTION_UPPER : DIRECTION_LOWER;
double factor = positive ? getFactorIn() : getFactorOut();
ChangeData data = new ChangeData(value);
double productValue = value * data.factor;
double rejectedValue = checkCapacity(data, productValue);
if (atLimit(limit, direction)) {
// if already at limit, return full amount
return new ChangeResult<>(AmountUtil.zero(amountToAdd), amountToAdd);
// if limit exceeded, return rejected amount without the factor
if (rejectedValue != 0) {
double storedValue = productValue - rejectedValue;
setAmount(data.limit);
rejectedValue /= data.factor;
return new ChangeResult<>(createAmount(storedValue), createAmount(rejectedValue));
}
Amount<Q> productAmount = amountToAdd.times(factor);
// limit not exceeded or not set, full amount can be stored
double storedValue = productValue;
setValue(getValue() + storedValue);
if (limit != null) {
Amount<Q> capacityLeft = limit.minus(getAmount());
Amount<Q> rejectedAmount = productAmount.minus(capacityLeft);
return new ChangeResult<>(createAmount(storedValue), createAmount(rejectedValue));
}
// limit exceeded, return rejected amount without the factor
if (rejectedAmount.getEstimatedValue() > 0 == positive) {
Amount<Q> storedAmount = capacityLeft;
setAmount(limit);
// remove the factor
rejectedAmount = rejectedAmount.divide(factor);
return new ChangeResult<>(storedAmount, rejectedAmount);
}
/**
* Stores exactly the given amount. If it exceeds a limit, <code>null</code>
* will be returned. The returned amount includes the factor applied to
* incoming or outgoing amounts. If passed to {@link #add(Amount)}, the
* stored amount would be equal to the given amount.
*/
@Override
public Amount<Q> store(Amount<Q> amount) {
double value = amount.doubleValue(getUnit());
if (value == 0) {
// nothing added
return AmountUtil.zero(amount);
}
// limit not exceeded or not set, rejected amount is zero
Amount<Q> storedAmount = productAmount;
Amount<Q> rejectedAmount = AmountUtil.zero(getAmount());
setAmount(getAmount().plus(storedAmount));
ChangeData data = new ChangeData(value);
if (checkCapacity(data, value) != 0) {
// amount exceeds capacity, cannot be stored
return null;
}
if (!storeError) {
// clean error
setAmount(Amount.valueOf(getAmount().getEstimatedValue(), getAmount().getUnit()));
// limit not exceeded or not set: change is accepted
setValue(getValue() + value);
return createAmount(value / data.factor);
}
/**
* Checks if given value would exceed limits when added and return rejected.
*
* @param data
* @param value
* @return value exceeding limits
*/
private double checkCapacity(ChangeData data, double value) {
if (atLimit(data.limit, data.direction)) {
// if already at limit: nothing can be accepted
return value;
}
if (data.limit != null) {
double capacityLeft = data.limit.doubleValue(getUnit()) - getValue();
double rejectedValue = value - capacityLeft;
// limit exceeded, return capacity left
if (rejectedValue > 0 == data.positive) {
return rejectedValue;
}
}
return new ChangeResult<>(storedAmount, rejectedAmount);
// value does not exceed limits
return 0;
}
/**
* @param value
* @return {@link Amount} with given value with the unit of this storage
*/
private Amount<Q> createAmount(double value) {
return Amount.valueOf(value, getUnit());
}
/** Set the storage to its lower limit or to zero if no limit set. */
......@@ -210,6 +242,20 @@ public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> impl
return new MyPropertiesProxy();
}
private class ChangeData {
private final boolean positive;
private final Amount<Q> limit;
private final int direction;
private final double factor;
public ChangeData(double value) {
positive = value > 0;
limit = positive ? getUpperLimit() : getLowerLimit();
direction = positive ? DIRECTION_UPPER : DIRECTION_LOWER;
factor = positive ? getFactorIn() : getFactorOut();
}
}
public class MyPropertiesProxy {
public Valuable getAmount() {
return ValuableAmountAdapter.wrap(ConfigurableStorage.this.getAmount());
......
......@@ -5,21 +5,41 @@ import javax.measure.quantity.Quantity;
import org.jscience.physics.amount.Amount;
/**
* Storage with an amount that can be changed.
* Storage with an amount that can be changed. There are two different methods
* for changing the stored amount. {@link #add(Amount)} may store the given
* amount differently and report about it while {@link #store(Amount)} changes the
* storage by exactly the given amount report about the one required and may
* fail.
*
* @author mey
*
* @param
* <Q>
* the type of {@link Quantity} stored
*/
public interface MutableStorage<Q extends Quantity> extends Storage<Q> {
/**
* Adds given amount to storage.
* Adds given amount to the storage. Use a negative value to remove from
* storage. The stored amount added may differ and can be rejected partly or
* entirely. This is reflected within the returned {@link ChangeResult}.
*
* @param amountToAdd
* @param amount
* the amount offered to the storage
* @return {@link ChangeResult}
*/
ChangeResult<Q> add(Amount<Q> amountToAdd);
ChangeResult<Q> add(Amount<Q> amount);
/**
* Stores given amount. Use a negative value to remove from storage. The
* amount added to the storage will be exactly like the given. A returned
* amount reflects the cost of storing the given amount and both may differ.
*
* @param amount
* the amount the storage is changed by
* @return the amount required to store the given amount or
* <code>null</code> if it is rejected
*/
Amount<Q> store(Amount<Q> amount);
/**
* Clears the storage.
......
package de.zmt.storage;
import static de.zmt.storage.LimitedTestStorage.*;
import static org.hamcrest.AmountIsCloseTo.amountCloseTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
......@@ -28,9 +31,9 @@ public class AbstractLimitedStoragePipelineTest implements Serializable {
// TEST STORE AMOUNT
private static final Amount<Dimensionless> CHANGE = Amount.valueOf(15.4, Unit.ONE);
private static final Amount<Dimensionless> STORED_IN = CHANGE.times(LimitedTestStorage.FACTOR_IN);
private static final Amount<Dimensionless> STORED_IN = CHANGE.times(FACTOR_IN);
// amount will exceed lower limit, so the maximum will be drained
private static final Amount<Dimensionless> DRAINED = STORED_IN.minus(LimitedTestStorage.LOWER_LIMIT);
private static final Amount<Dimensionless> DRAINED = STORED_IN.divide(FACTOR_OUT);
private long timePassed;
......@@ -41,7 +44,7 @@ public class AbstractLimitedStoragePipelineTest implements Serializable {
}
@Test
public void testWithLimits() {
public void addWithLimits() {
logger.info("Testing Pipeline with limits.");
StoragePipeline<Dimensionless> pipeline = new Pipeline(new LimitedTestStorage());
......@@ -49,44 +52,44 @@ public class AbstractLimitedStoragePipelineTest implements Serializable {
// add amount
logger.info("adding " + CHANGE);
Amount<Dimensionless> storedIn = pipeline.add(CHANGE).getStored();
assertEquals("Storage did not store correct amount: ", STORED_IN, storedIn);
assertThat("Storage did not store correct amount: ", storedIn, is(amountCloseTo(STORED_IN)));
// drain nothing
assertEquals("Could drain an unexpired amount: ", 0, pipeline.drainExpired().getExactValue());
assertThat("Could drain an unexpired amount: ", pipeline.drainExpired(), is(Amount.ZERO));
// time passes
timePassed++;
// drain element
// only approximate due to storage added for lower limit
Amount<Dimensionless> drainedAmount = pipeline.drainExpired();
logger.info("Drained " + drainedAmount + " from pipeline.");
assertTrue("Drained amount does not approximate returned value. expected: <" + DRAINED + "> but was:<"
+ drainedAmount + ">", drainedAmount.approximates(DRAINED));
// drain nothing again because of lower limit
assertThat("Could drain more than lower limit: ", pipeline.drainExpired(), is(Amount.ZERO));
pipeline.store(LOWER_LIMIT);
timePassed++;
assertThat("Could not drain the expected amount: ", pipeline.drainExpired(), is(amountCloseTo(DRAINED)));
logger.info("Final state of pipeline: " + pipeline);
assertEquals("Final state differs from initial although pipeline should be at lower limit in both.",
LimitedTestStorage.LOWER_LIMIT, pipeline.getAmount());
assertThat("Pipeline is not at lower limit: ", pipeline.getAmount(), is(amountCloseTo(LOWER_LIMIT)));
assertFalse("No content although amount up to lower limit is left.", pipeline.getContent().isEmpty());
}
@Test
public void testWithoutLimits() {
public void addWithoutLimits() {
logger.info("Testing Pipeline without limits.");
StoragePipeline<Dimensionless> pipeline = new Pipeline(new ConfigurableStorage<>(Unit.ONE));
// initialize
logger.info(pipeline.toString());
assertEquals("Pipeline not initialized to zero.", Amount.ZERO, pipeline.getAmount());
assertThat("Pipeline not initialized to zero.", pipeline.getAmount(), is(amountCloseTo(Amount.ZERO)));
// add amount
logger.info("adding " + CHANGE);
Amount<Dimensionless> storedIn = pipeline.add(CHANGE).getStored();
assertEquals("Storage did not store correct amount: ", CHANGE.times(1d), storedIn);
assertThat("Storage did not store correct amount: ", storedIn, is(amountCloseTo(CHANGE)));
// drain nothing
assertEquals("Could drain an unexpired amount: ", 0, pipeline.drainExpired().getExactValue());
assertThat("Could drain an unexpired amount: ", pipeline.drainExpired(), is(Amount.ZERO));
// time passes
timePassed++;
......@@ -94,12 +97,11 @@ public class AbstractLimitedStoragePipelineTest implements Serializable {
// drain element
// only approximate due to storage added for lower limit
Amount<Dimensionless> drainedAmount = pipeline.drainExpired();
assertTrue("Drained amount does not approximate returned value. expected: <" + CHANGE + "> but was:<"
+ drainedAmount + ">", drainedAmount.approximates(CHANGE));
assertThat("Drained amount does not approximate returned value: ", drainedAmount, is(amountCloseTo(CHANGE)));
}
@Test
public void testSerialization() throws IOException, ClassNotFoundException {
public void serialization() throws IOException, ClassNotFoundException {
logger.info("Testing Pipeline serialization.");
Pipeline pipeline = new Pipeline(new ConfigurableStorage<>(Unit.ONE));
......
package de.zmt.storage;
import static de.zmt.storage.LimitedTestStorage.*;
import static org.hamcrest.AmountIsCloseTo.amountCloseTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;