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; package de.zmt.storage;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.PriorityQueue; import java.util.PriorityQueue;
...@@ -85,21 +86,34 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity> ...@@ -85,21 +86,34 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
@Override @Override
public Amount<Q> drainExpired() { public Amount<Q> drainExpired() {
Amount<Q> returnAmount = AmountUtil.zero(sum.getAmount()); 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) { while (true) {
DelayedStorage<Q> head = queue.poll(); DelayedStorage<Q> head = queue.poll();
if (head != null) { if (head != null) {
Amount<Q> amount = head.getAmount(); Amount<Q> amount = head.getAmount();
// subtract amount of this storage from sum // 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 // if amount could be subtracted:
returnAmount = returnAmount.minus(changeResult.getStored()); if (required != null) {
// sum the amount received from storage (it is negative)
returnAmount = returnAmount.minus(required);
} else {
holdBack.add(head);
}
} else { } else {
// no expired elements // no expired elements
break; break;
} }
} }
queue.addAll(holdBack);
// clear to prevent ever increasing numeric error // clear to prevent ever increasing numeric error
if (queue.isEmpty()) { if (queue.isEmpty()) {
clear(); clear();
...@@ -115,22 +129,39 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity> ...@@ -115,22 +129,39 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
/** /**
* @throws IllegalArgumentException * @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 @Override
public ChangeResult<Q> add(Amount<Q> amountToAdd) { public Amount<Q> store(Amount<Q> amount) {
if (amountToAdd.getEstimatedValue() < 0) { Amount<Q> required = sum.store(amount);
throw new IllegalArgumentException("amountToAdd must be positive."); 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 // do not add storage for zero amounts, e.g. storage is already at limit
if (result.getStored().getEstimatedValue() > 0) { if (amount.getEstimatedValue() > 0) {
queue.offer(createDelayedStorage(result.getStored())); queue.add(createDelayedStorage(amount));
} }
return result;
} }
@Override @Override
...@@ -171,18 +202,18 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity> ...@@ -171,18 +202,18 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
* *
*/ */
public static abstract class DelayedStorage<Q extends Quantity> extends BaseStorage<Q> implements Delayed { public static abstract class DelayedStorage<Q extends Quantity> extends BaseStorage<Q> implements Delayed {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public DelayedStorage(Amount<Q> amount) { public DelayedStorage(Amount<Q> amount) {
this.setAmount(amount); super(amount);
} }
@Override @Override
public int compareTo(Delayed o) { public int compareTo(Delayed o) {
// from TimerQueue#DelayedTimer // from TimerQueue#DelayedTimer
long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS); long diff = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (diff == 0) ? 0 : ((diff < 0) ? -1 : 1); return (diff == 0) ? 0 : ((diff < 0) ? -1 : 1);
} }
} }
/** /**
...@@ -221,7 +252,7 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity> ...@@ -221,7 +252,7 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
public Storage<Q> getSum() { public Storage<Q> getSum() {
return sum; return sum;
} }
public Collection<? extends Storage<Q>> getContent() { public Collection<? extends Storage<Q>> getContent() {
return AbstractLimitedStoragePipeline.this.getContent(); return AbstractLimitedStoragePipeline.this.getContent();
} }
...@@ -229,5 +260,11 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity> ...@@ -229,5 +260,11 @@ public abstract class AbstractLimitedStoragePipeline<Q extends Quantity>
public int getContentSize() { public int getContentSize() {
return getContent().size(); 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; package de.zmt.storage;
import javax.measure.quantity.Quantity; import javax.measure.quantity.Quantity;
import javax.measure.unit.Unit;
import org.jscience.physics.amount.Amount; import org.jscience.physics.amount.Amount;
import sim.util.Valuable; import sim.util.Valuable;
/** /**
* Basic implementation of {@link Storage}. * Basic implementation of {@link Storage}. Stores an amount without error.
* *
* @author mey * @author mey
* *
* @param * @param
* <Q> * <Q>
* type of {@link Quantity} * the type of {@link Quantity}
*/ */
public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable { public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable {
private static final long serialVersionUID = 1L; 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 @Override
public Amount<Q> getAmount() { public Amount<Q> getAmount() {
return amount; return Amount.valueOf(value, unit);
} }
protected void setAmount(Amount<Q> amount) { 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 @Override
...@@ -36,6 +67,6 @@ public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable { ...@@ -36,6 +67,6 @@ public class BaseStorage<Q extends Quantity> implements Storage<Q>, Valuable {
@Override @Override
public double doubleValue() { public double doubleValue() {
return amount.getEstimatedValue(); return value;
} }
} }
\ No newline at end of file
...@@ -14,41 +14,32 @@ import sim.util.Valuable; ...@@ -14,41 +14,32 @@ import sim.util.Valuable;
* A {@link MutableStorage} that rejects any amount exceeding its limits. Apart * A {@link MutableStorage} that rejects any amount exceeding its limits. Apart
* from that, there are factors for incoming and outgoing amounts, simulating * from that, there are factors for incoming and outgoing amounts, simulating
* losses and gains during exchange. * 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 * @author mey
* *
* @param * @param
* <Q> * <Q>
* the type of {@link Quantity}
*/ */
public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> implements LimitedStorage<Q>, Proxiable { public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> implements LimitedStorage<Q>, Proxiable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final int DIRECTION_UPPER = 1; private static final int DIRECTION_UPPER = 1;
private static final int DIRECTION_LOWER = -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 * @param unit
*/ */
public ConfigurableStorage(Unit<Q> unit) { public ConfigurableStorage(Unit<Q> unit) {
this(unit, false); super(0, unit);
}
/**
* 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));
} }
/** @return True if storage is at its lower limit. */ /** @return True if storage is at its lower limit. */
...@@ -125,65 +116,106 @@ public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> impl ...@@ -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 * Adds given amount to the storage without exceeding the limits. If the
* remove from storage. If the amount is positive, stored amount will be * amount is positive, factor in is applied, otherwise factor out.
* decreased by loss factor, otherwise the removed amount increases.
* <p> * <p>
* <b>NOTE:</b> The stored amount includes the factor while the rejected * <b>NOTE:</b> The stored amount includes the factor while the rejected
* will not:<br> * will not:<br>
* {@code stored + rejected != amountToAdd * factor} if {@code factor != 1}. * {@code stored + rejected != amountToAdd * factor} if {@code factor != 1}.
* *
* @param amountToAdd * @param amount
* @return {@link de.zmt.storage.MutableStorage.ChangeResult} including the * @return {@link de.zmt.storage.MutableStorage.ChangeResult} including the
* amount actually added / removed from storage, and the rejected * amount actually added / removed from storage, and the rejected
* one. * one
* *
*/ */
@Override @Override
public ChangeResult<Q> add(Amount<Q> amountToAdd) { public ChangeResult<Q> add(Amount<Q> amount) {
double estimatedValue = amountToAdd.getEstimatedValue(); double value = amount.doubleValue(getUnit());
if (estimatedValue == 0) {
if (value == 0) {
// nothing added // nothing added
return new ChangeResult<>(AmountUtil.zero(amountToAdd), amountToAdd); return new ChangeResult<>(AmountUtil.zero(amount), amount);
} }
boolean positive = estimatedValue > 0; ChangeData data = new ChangeData(value);
Amount<Q> limit = positive ? getUpperLimit() : getLowerLimit(); double productValue = value * data.factor;
int direction = positive ? DIRECTION_UPPER : DIRECTION_LOWER; double rejectedValue = checkCapacity(data, productValue);
double factor = positive ? getFactorIn() : getFactorOut();
if (atLimit(limit, direction)) { // if limit exceeded, return rejected amount without the factor
// if already at limit, return full amount if (rejectedValue != 0) {
return new ChangeResult<>(AmountUtil.zero(amountToAdd), amountToAdd); 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) { return new ChangeResult<>(createAmount(storedValue), createAmount(rejectedValue));
Amount<Q> capacityLeft = limit.minus(getAmount()); }
Amount<Q> rejectedAmount = productAmount.minus(capacityLeft);
// limit exceeded, return rejected amount without the factor /**
if (rejectedAmount.getEstimatedValue() > 0 == positive) { * Stores exactly the given amount. If it exceeds a limit, <code>null</code>
Amount<Q> storedAmount = capacityLeft; * will be returned. The returned amount includes the factor applied to
setAmount(limit); * incoming or outgoing amounts. If passed to {@link #add(Amount)}, the
// remove the factor * stored amount would be equal to the given amount.
rejectedAmount = rejectedAmount.divide(factor); */
return new ChangeResult<>(storedAmount, rejectedAmount); @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 ChangeData data = new ChangeData(value);
Amount<Q> storedAmount = productAmount; if (checkCapacity(data, value) != 0) {
Amount<Q> rejectedAmount = AmountUtil.zero(getAmount()); // amount exceeds capacity, cannot be stored
setAmount(getAmount().plus(storedAmount)); return null;
}
if (!storeError) { // limit not exceeded or not set: change is accepted
// clean error setValue(getValue() + value);
setAmount(Amount.valueOf(getAmount().getEstimatedValue(), getAmount().getUnit())); 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. */ /** 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 ...@@ -210,6 +242,20 @@ public class ConfigurableStorage<Q extends Quantity> extends BaseStorage<Q> impl
return new MyPropertiesProxy(); 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 class MyPropertiesProxy {
public Valuable getAmount() { public Valuable getAmount() {
return ValuableAmountAdapter.wrap(ConfigurableStorage.this.getAmount()); return ValuableAmountAdapter.wrap(ConfigurableStorage.this.getAmount());
......
...@@ -5,21 +5,41 @@ import javax.measure.quantity.Quantity; ...@@ -5,21 +5,41 @@ import javax.measure.quantity.Quantity;
import org.jscience.physics.amount.Amount; 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 * @author mey
* *
* @param * @param
* <Q> * <Q>
* the type of {@link Quantity} stored
*/ */
public interface MutableStorage<Q extends Quantity> extends Storage<Q> { 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} * @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. * Clears the storage.
......