<classpathentry kind="lib" path="resources"/>
<classpathentry kind="lib" path="lib/jspf.core-1.0.2.jar" sourcepath="/home/sampo/Projects/lib/jspf/documentation/api"/>
<classpathentry kind="lib" path="lib/opencsv-2.3.jar"/>
+ <classpathentry kind="lib" path="lib/exp4j-0.2.9.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
<zipfileset src="lib/jfreechart-1.0.13.jar" />
<zipfileset src="lib/iText-5.0.2.jar" />
<zipfileset src="lib/opencsv-2.3.jar" />
+ <zipfileset src="lib/exp4j-0.2.9.jar" />
</jar>
</target>
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.ExpressionParser;
+import net.sf.openrocket.util.InvalidExpressionException;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.Invalidator;
import net.sf.openrocket.util.MathUtil;
import net.sf.openrocket.util.MemoryManagement;
import net.sf.openrocket.util.Reflection;
import net.sf.openrocket.util.StateChangeListener;
-import net.sf.openrocket.util.exp4j.Calculable;
-import net.sf.openrocket.util.exp4j.ExpressionBuilder;
-import net.sf.openrocket.util.exp4j.UnknownFunctionException;
-import net.sf.openrocket.util.exp4j.UnparsableExpressionException;
/**
public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable {
private static final LogHelper log = Application.getLogger();
-
-
+
+
public static final DoubleModel ZERO = new DoubleModel(0);
-
+
//////////// JSpinner Model ////////////
-
+
/**
* Model suitable for JSpinner.
* Note: Previously used using JSpinner.NumberEditor and extended SpinnerNumberModel
* to be entered so that fractional units and expressions can be used.
*/
public class ValueSpinnerModel extends AbstractSpinnerModel implements Invalidatable {
-
+
+ private ExpressionParser parser = new ExpressionParser();
+
@Override
public Object getValue() {
return currentUnit.toString(DoubleModel.this.getValue());
}
-
+
@Override
public void setValue(Object value) {
if (firing > 0) {
Number num = Double.NaN;
// Set num if possible
- if ( value instanceof Number ) {
- num = (Number)value;
+ if (value instanceof Number) {
+ num = (Number) value;
}
- else if ( value instanceof String ) {
+ else if (value instanceof String) {
try {
- String newValString = (String)value;
- ExpressionBuilder builder=new ExpressionBuilder(newValString);
- Calculable calc=builder.build();
- num = calc.calculate();
+ String newValString = (String) value;
+ num = parser.parse(newValString);
+ } catch (InvalidExpressionException e) {
+ // Ignore
}
- catch ( java.lang.NumberFormatException e ) {
- } catch (UnknownFunctionException e) {
- } catch (UnparsableExpressionException e) {
- } catch (java.util.EmptyStackException e) {
- }
}
-
+
// Update the doublemodel with the new number or return to the last number if not possible
- if ( ((Double)num).isNaN() ) {
- DoubleModel.this.setValue( lastValue );
+ if (((Double) num).isNaN()) {
+ DoubleModel.this.setValue(lastValue);
log.user("SpinnerModel could not set value for " + DoubleModel.this.toString() + ". Could not convert " + value.toString());
- }
- else {
+ }
+ else {
double newValue = num.doubleValue();
double converted = currentUnit.fromUnit(newValue);
-
+
log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" converted=" + converted);
DoubleModel.this.setValue(converted);
}
// Force a refresh if text doesn't match up exactly with the stored value
- if ( ! ((Double)lastValue).toString().equals( this.getValue().toString() ) ) {
+ if (!((Double) lastValue).toString().equals(this.getValue().toString())) {
DoubleModel.this.fireStateChanged();
- log.debug("SpinnerModel "+DoubleModel.this.toString()+" refresh forced because string did not match actual value.");
+ log.debug("SpinnerModel " + DoubleModel.this.toString() + " refresh forced because string did not match actual value.");
}
}
-
+
@Override
public Object getNextValue() {
double d = currentUnit.toUnit(DoubleModel.this.getValue());
d = max;
return d;
}
-
+
@Override
public Object getPreviousValue() {
double d = currentUnit.toUnit(DoubleModel.this.getValue());
d = min;
return d;
}
-
+
@Override
public void addChangeListener(ChangeListener l) {
DoubleModel.this.addChangeListener(l);
}
-
+
@Override
public void removeChangeListener(ChangeListener l) {
DoubleModel.this.removeChangeListener(l);
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
}
-
+
/**
* Returns a new SpinnerModel with the same base as the DoubleModel.
* The values given to the JSpinner are in the currently selected units.
public SpinnerModel getSpinnerModel() {
return new ValueSpinnerModel();
}
-
+
//////////// JSlider model ////////////
-
+
private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable {
private static final int MAX = 1000;
-
+
/*
* Use linear scale value = linear1 * x + linear0 when x < linearPosition
* Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
*/
private final boolean islinear;
-
+
// Linear in range x <= linearPosition
private final double linearPosition;
-
+
// May be changing DoubleModels when using linear model
private final DoubleModel min, mid, max;
-
+
// Linear multiplier and constant
//private final double linear1;
//private final double linear0;
-
+
// Non-linear multiplier, exponent and constant
private double quad2, quad1, quad0;
-
+
public ValueSliderModel(DoubleModel min, DoubleModel max) {
this.islinear = true;
linearPosition = 1.0;
-
+
this.min = min;
this.mid = max; // Never use exponential scale
this.max = max;
-
+
min.addChangeListener(this);
max.addChangeListener(this);
-
+
quad2 = quad1 = quad0 = 0; // Not used
}
-
-
-
+
+
+
/**
* Generate a linear model from min to max.
*/
public ValueSliderModel(double min, double max) {
this.islinear = true;
linearPosition = 1.0;
-
+
this.min = new DoubleModel(min);
this.mid = new DoubleModel(max); // Never use exponential scale
this.max = new DoubleModel(max);
-
+
quad2 = quad1 = quad0 = 0; // Not used
}
-
+
public ValueSliderModel(double min, double mid, double max) {
this(min, 0.5, mid, max);
}
-
+
public ValueSliderModel(double min, double mid, DoubleModel max) {
this(min, 0.5, mid, max);
}
-
+
/*
* v(x) = mul * x^exp + add
*
* v(1) = mul + add = max
* v'(pos) = mul*exp * pos^(exp-1) = linearMul
*/
- public ValueSliderModel(double min, double pos, double mid, double max ) {
+ public ValueSliderModel(double min, double pos, double mid, double max) {
this(min, pos, mid, new DoubleModel(max));
}
+
public ValueSliderModel(double min, double pos, double mid, DoubleModel max) {
this.min = new DoubleModel(min);
this.mid = new DoubleModel(mid);
this.max = max;
-
+
this.islinear = false;
-
+
max.addChangeListener(this);
-
+
linearPosition = pos;
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
if (!(min < mid && mid <= max.getValue() && 0 < pos && pos < 1)) {
throw new IllegalArgumentException("Bad arguments for ValueSliderModel " +
"min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos);
}
-
+
updateExponentialParameters();
-
+
}
-
+
private void updateExponentialParameters() {
double pos = this.linearPosition;
double minValue = this.min.getValue();
quad1 = (delta + 2 * (midValue - maxValue) * pos - delta * pos * pos) / pow2(pos - 1);
quad0 = (midValue - (2 * midValue + delta) * pos + (maxValue + delta) * pos * pos) / pow2(pos - 1);
}
-
+
private double pow2(double x) {
return x * x;
}
-
+
@Override
public int getValue() {
double value = DoubleModel.this.getValue();
return 0;
if (value >= max.getValue())
return MAX;
-
+
double x;
if (value <= mid.getValue()) {
// Use linear scale
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue());
} else {
// Use quadratic scale
}
return (int) (x * MAX);
}
-
-
+
+
@Override
public void setValue(int newValue) {
if (firing > 0) {
" value=" + newValue + ", currently firing events");
return;
}
-
+
double x = (double) newValue / MAX;
double scaledValue;
-
+
if (x <= linearPosition) {
// Use linear scale
//linear0 = min;
//linear1 = (mid-min)/pos;
-
+
scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue();
} else {
// Use quadratic scale
scaledValue = quad2 * x * x + quad1 * x + quad0;
}
-
+
double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue)));
log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue +
" scaledValue=" + scaledValue + " converted=" + converted);
DoubleModel.this.setValue(converted);
}
-
-
+
+
// Static get-methods
private boolean isAdjusting;
-
+
@Override
public int getExtent() {
return 0;
}
-
+
@Override
public int getMaximum() {
return MAX;
}
-
+
@Override
public int getMinimum() {
return 0;
}
-
+
@Override
public boolean getValueIsAdjusting() {
return isAdjusting;
}
-
+
// Ignore set-values
@Override
public void setExtent(int newExtent) {
}
-
+
@Override
public void setMaximum(int newMaximum) {
}
-
+
@Override
public void setMinimum(int newMinimum) {
}
-
+
@Override
public void setValueIsAdjusting(boolean b) {
isAdjusting = b;
}
-
+
@Override
public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
setValueIsAdjusting(adjusting);
setValue(value);
}
-
+
// Pass change listeners to the underlying model
@Override
public void addChangeListener(ChangeListener l) {
DoubleModel.this.addChangeListener(l);
}
-
+
@Override
public void removeChangeListener(ChangeListener l) {
DoubleModel.this.removeChangeListener(l);
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
-
+
@Override
public void stateChanged(EventObject e) {
// Min or max range has changed.
- if ( !islinear ) {
- double midValue = (max.getValue() - min.getValue()) /3.0;
+ if (!islinear) {
+ double midValue = (max.getValue() - min.getValue()) / 3.0;
mid.setValue(midValue);
updateExponentialParameters();
}
fireStateChanged();
}
}
-
-
+
+
public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
return new ValueSliderModel(min, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double max) {
return new ValueSliderModel(min, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double mid, double max) {
return new ValueSliderModel(min, mid, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double mid, DoubleModel max) {
return new ValueSliderModel(min, mid, max);
}
-
+
public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
return new ValueSliderModel(min, pos, mid, max);
}
-
-
-
-
-
+
+
+
+
+
//////////// Action model ////////////
-
+
private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable {
private boolean oldValue = false;
-
+
public AutomaticActionModel() {
oldValue = isAutomatic();
addChangeListener(this);
}
-
-
+
+
@Override
public boolean isEnabled() {
return isAutomaticAvailable();
}
-
+
@Override
public Object getValue(String key) {
if (key.equals(Action.SELECTED_KEY)) {
}
return super.getValue(key);
}
-
+
@Override
public void putValue(String key, Object value) {
if (firing > 0) {
super.putValue(key, value);
}
}
-
+
// Implement a wrapper to the ChangeListeners
ArrayList<PropertyChangeListener> propertyChangeListeners =
new ArrayList<PropertyChangeListener>();
-
+
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertyChangeListeners.add(listener);
DoubleModel.this.addChangeListener(this);
}
-
+
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
propertyChangeListeners.remove(listener);
if (propertyChangeListeners.isEmpty())
DoubleModel.this.removeChangeListener(this);
}
-
+
// If the value has changed, generate an event to the listeners
@Override
public void stateChanged(EventObject e) {
((PropertyChangeListener) l[i]).propertyChange(event);
}
}
-
+
@Override
public void actionPerformed(ActionEvent e) {
// Setting performed in putValue
}
-
+
@Override
public void invalidate() {
DoubleModel.this.invalidate();
}
}
-
+
/**
* Returns a new Action corresponding to the changes of the automatic setting
* property of the value model. This may be used directly with e.g. check buttons.
public Action getAutomaticAction() {
return new AutomaticActionModel();
}
-
-
-
-
-
+
+
+
+
+
//////////// Main model /////////////
-
+
/*
* The main model handles all values in SI units, i.e. no conversion is made within the model.
*/
-
+
private final ChangeSource source;
private final String valueName;
private final double multiplier;
-
+
private final Method getMethod;
private final Method setMethod;
-
+
private final Method getAutoMethod;
private final Method setAutoMethod;
-
+
private final ArrayList<EventListener> listeners = new ArrayList<EventListener>();
-
+
private final UnitGroup units;
private Unit currentUnit;
-
+
private final double minValue;
private double maxValue;
-
+
private String toString = null;
-
-
+
+
private int firing = 0; // >0 when model itself is sending events
-
-
+
+
// Used to differentiate changes in valueName and other changes in the component:
private double lastValue = 0;
private boolean lastAutomatic = false;
-
+
private Invalidator invalidator = new Invalidator(this);
-
-
+
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value) {
this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value, UnitGroup unit) {
this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
public DoubleModel(double value, UnitGroup unit, double min) {
this(value, unit, min, Double.POSITIVE_INFINITY);
}
-
+
/**
* Generate a DoubleModel that contains an internal double value.
*
this.lastValue = value;
this.minValue = min;
this.maxValue = max;
-
+
source = null;
valueName = "Constant value";
multiplier = 1;
-
+
getMethod = setMethod = null;
getAutoMethod = setAutoMethod = null;
units = unit;
currentUnit = units.getDefaultUnit();
}
-
-
+
+
/**
* Generates a new DoubleModel that changes the values of the specified component.
* The double value is read and written using the methods "get"/"set" + valueName.
this.source = source;
this.valueName = valueName;
this.multiplier = multiplier;
-
+
this.units = unit;
currentUnit = units.getDefaultUnit();
-
+
this.minValue = min;
this.maxValue = max;
-
+
try {
getMethod = source.getClass().getMethod("get" + valueName);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("get method for value '" + valueName +
"' not present in class " + source.getClass().getCanonicalName());
}
-
+
Method s = null;
try {
s = source.getClass().getMethod("set" + valueName, double.class);
} catch (NoSuchMethodException e1) {
} // Ignore
setMethod = s;
-
+
// Automatic selection methods
-
+
Method set = null, get = null;
-
+
try {
get = source.getClass().getMethod("is" + valueName + "Automatic");
set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class);
} catch (NoSuchMethodException e) {
} // ignore
-
+
if (set != null && get != null) {
getAutoMethod = get;
setAutoMethod = set;
getAutoMethod = null;
setAutoMethod = null;
}
-
+
}
-
+
public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
double min) {
this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
double min, double max) {
this(source, valueName, 1.0, unit, min, max);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE,
Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double min) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY);
}
-
+
public DoubleModel(ChangeSource source, String valueName, double min, double max) {
this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max);
}
-
-
-
+
+
+
/**
* Returns the value of the variable (in SI units).
*/
public double getValue() {
if (getMethod == null) // Constant value
return lastValue;
-
+
try {
return (Double) getMethod.invoke(source) * multiplier;
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Sets the value of the variable.
* @param v New value for parameter in SI units.
*/
public void setValue(double v) {
checkState(true);
-
+
log.debug("Setting value " + v + " for " + this);
if (setMethod == null) {
if (getMethod != null) {
fireStateChanged();
return;
}
-
+
try {
setMethod.invoke(source, v / multiplier);
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Returns whether setting the value automatically is available.
*/
public boolean isAutomaticAvailable() {
return (getAutoMethod != null) && (setAutoMethod != null);
}
-
+
/**
* Returns whether the value is currently being set automatically.
* Returns false if automatic setting is not available at all.
public boolean isAutomatic() {
if (getAutoMethod == null)
return false;
-
+
try {
return (Boolean) getAutoMethod.invoke(source);
} catch (IllegalArgumentException e) {
throw Reflection.handleWrappedException(e);
}
}
-
+
/**
* Sets whether the value should be set automatically. Simply fires a
* state change event if automatic setting is not available.
*/
public void setAutomatic(boolean auto) {
checkState(true);
-
+
if (setAutoMethod == null) {
log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available");
fireStateChanged(); // in case something is out-of-sync
return;
}
-
+
log.debug("Setting automatic to " + auto + " for " + this);
lastAutomatic = auto;
try {
throw Reflection.handleWrappedException(e);
}
}
-
-
+
+
/**
* Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
* @return The most recently set unit.
public Unit getCurrentUnit() {
return currentUnit;
}
-
+
/**
* Sets the current Unit. The unit must be one of those included in the UnitGroup.
* @param u The unit to set active.
currentUnit = u;
fireStateChanged();
}
-
-
+
+
/**
* Returns the UnitGroup associated with the parameter value.
*
public UnitGroup getUnitGroup() {
return units;
}
-
-
-
+
+
+
/**
* Add a listener to the model. Adds the model as a listener to the value source if this
* is the first listener.
@Override
public void addChangeListener(EventListener l) {
checkState(true);
-
+
if (listeners.isEmpty()) {
if (source != null) {
source.addChangeListener(this);
lastAutomatic = isAutomatic();
}
}
-
+
listeners.add(l);
log.verbose(this + " adding listener (total " + listeners.size() + "): " + l);
}
-
+
/**
* Remove a listener from the model. Removes the model from being a listener to the Component
* if this was the last listener of the model.
@Override
public void removeChangeListener(EventListener l) {
checkState(false);
-
+
listeners.remove(l);
if (listeners.isEmpty() && source != null) {
source.removeChangeListener(this);
}
log.verbose(this + " removing listener (total " + listeners.size() + "): " + l);
}
-
-
+
+
/**
* Invalidates this model by removing all listeners and removing this from
* listening to the source. After invalidation no listeners can be added to this
public void invalidate() {
log.verbose("Invalidating " + this);
invalidator.invalidate();
-
+
if (!listeners.isEmpty()) {
log.warn("Invalidating " + this + " while still having listeners " + listeners);
}
}
MemoryManagement.collectable(this);
}
-
-
+
+
private void checkState(boolean error) {
invalidator.check(error);
}
-
-
+
+
@Override
protected void finalize() throws Throwable {
super.finalize();
log.warn(this + " being garbage-collected while having listeners " + listeners);
}
};
-
-
+
+
/**
* Fire a ChangeEvent to all listeners.
*/
protected void fireStateChanged() {
checkState(true);
-
+
EventObject event = new EventObject(this);
ChangeEvent cevent = new ChangeEvent(this);
firing++;
// Copy the list before iterating to prevent concurrent modification exceptions.
EventListener[] ls = listeners.toArray(new EventListener[0]);
for (EventListener l : ls) {
- if ( l instanceof StateChangeListener ) {
- ((StateChangeListener)l).stateChanged(event);
- } else if ( l instanceof ChangeListener ) {
- ((ChangeListener)l).stateChanged(cevent);
+ if (l instanceof StateChangeListener) {
+ ((StateChangeListener) l).stateChanged(event);
+ } else if (l instanceof ChangeListener) {
+ ((ChangeListener) l).stateChanged(cevent);
}
}
firing--;
}
-
+
/**
* Called when the component changes. Checks whether the modeled value has changed, and if
* it has, updates lastValue and generates ChangeEvents for all listeners of the model.
@Override
public void stateChanged(EventObject e) {
checkState(true);
-
+
double v = getValue();
boolean b = isAutomatic();
if (lastValue == v && lastAutomatic == b)
lastAutomatic = b;
fireStateChanged();
}
-
-
+
+
/**
* Explain the DoubleModel as a String.
*/
--- /dev/null
+package net.sf.openrocket.util;
+
+import de.congrace.exp4j.Calculable;
+import de.congrace.exp4j.ExpressionBuilder;
+import de.congrace.exp4j.UnknownFunctionException;
+import de.congrace.exp4j.UnparsableExpressionException;
+
+public class ExpressionParser {
+
+
+ public double parse(String expression) throws InvalidExpressionException {
+ try {
+ ExpressionBuilder builder = new ExpressionBuilder(modify(expression));
+ Calculable calc = builder.build();
+ return calc.calculate();
+ } catch (java.lang.NumberFormatException e) {
+ throw new InvalidExpressionException("Invalid expression: " + expression, e);
+ } catch (UnknownFunctionException e) {
+ throw new InvalidExpressionException("Invalid expression: " + expression, e);
+ } catch (UnparsableExpressionException e) {
+ throw new InvalidExpressionException("Invalid expression: " + expression, e);
+ } catch (java.util.EmptyStackException e) {
+ throw new InvalidExpressionException("Invalid expression: " + expression, e);
+ }
+ }
+
+ private String modify(String exp) throws InvalidExpressionException {
+ exp = exp.replaceAll("(\\d+)\\s+(\\d+)\\s*/\\s*(\\d+)", "($1+$2/$3)");
+ exp = exp.replace(',', '.');
+ // Disallow spaces between numbers - default is to remove spaces!
+ if (exp.matches(".*[0-9.]\\s+[0-9.].*")) {
+ throw new InvalidExpressionException("Invalid expression: " + exp);
+ }
+ return exp;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+/**
+ * Exception indicating an invalid expression.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class InvalidExpressionException extends Exception {
+
+ public InvalidExpressionException(String message) {
+ super(message);
+ }
+
+ public InvalidExpressionException(Throwable cause) {
+ super(cause);
+ }
+
+ public InvalidExpressionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.text.NumberFormat;
-import java.util.List;
-
-/**
- * Abstract base class for mathematical expressions
- *
- * @author fas@congrace.de
- */
-abstract class AbstractExpression {
- private final String expression;
- private final Token[] tokens;
- private final String[] variableNames;
- private final NumberFormat numberFormat = NumberFormat.getInstance();
-
- /**
- * Construct a new {@link AbstractExpression}
- *
- * @param expression
- * the mathematical expression to be used
- * @param tokens
- * the {@link Token}s in the expression
- * @param variableNames
- * an array of variable names which are used in the expression
- */
- AbstractExpression(String expression, Token[] tokens, String[] variableNames) {
- this.expression = expression;
- this.tokens = tokens;
- this.variableNames = variableNames;
- }
-
- /**
- * get the mathematical expression {@link String}
- *
- * @return the expression
- */
- public String getExpression() {
- return expression;
- }
-
- /**
- * get the used {@link NumberFormat}
- *
- * @return the used {@link NumberFormat}
- */
- public NumberFormat getNumberFormat() {
- return numberFormat;
- }
-
- /**
- * get the {@link Token}s
- *
- * @return the array of {@link Token}s
- */
- Token[] getTokens() {
- return tokens;
- }
-
- /**
- * get the variable names
- *
- * @return the {@link List} of variable names
- */
- String[] getVariableNames() {
- return variableNames;
- }
-
-}
+++ /dev/null
-package net.sf.openrocket.util.exp4j;
-
-/**
- * This is the basic result class of the exp4j {@link ExpressionBuilder}
- *
- * @author ruckus
- *
- */
-public interface Calculable {
- /**
- * calculate the result of the expression
- *
- * @return the result of the calculation
- */
- public double calculate();
-
- /**
- * return the expression in reverse polish postfix notation
- *
- * @return the expression used to construct this {@link Calculable}
- */
- public String getExpression();
-
- /**
- * set a variable value for the calculation
- *
- * @param name
- * the variable name
- * @param value
- * the value of the variable
- */
- public void setVariable(String name, double value);
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Map;
-import java.util.Stack;
-
-abstract class CalculationToken extends Token {
-
- CalculationToken(String value) {
- super(value);
- }
-
- abstract void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues);
-
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-/**
- * Simple commandline interpreter for mathematical expressions the interpreter
- * takes a mathematical expressions as a {@link String} argument, evaluates it
- * and prints out the result.
- *
- *
- * <pre>
- * java de.congrace.exp4j.CommandlineInterpreter "2 * log(2.2223) - ((2-3.221) * 14.232^2)"
- * > 248.91042049521056
- * </pre>
- *
- * @author fas@congrace.de
- *
- */
-public class CommandlineInterpreter {
- private static void calculateExpression(String string) {
- try {
- final PostfixExpression pe = PostfixExpression.fromInfix(string);
- System.out.println(pe.calculate());
- } catch (UnparsableExpressionException e) {
- e.printStackTrace();
- } catch (UnknownFunctionException e) {
- e.printStackTrace();
- }
- }
-
- public static void main(String[] args) {
- if (args.length != 1) {
- printUsage();
- } else {
- calculateExpression(args[0]);
- }
- }
-
- private static void printUsage() {
- final StringBuilder usage = new StringBuilder();
- usage.append("Commandline Expression Parser\n\n").append("Example: ").append("\n").append("java -jar exp4j.jar \"2.12 * log(23) * (12 - 4)\"\n\n")
- .append("written by fas@congrace.de");
- System.err.println(usage.toString());
- }
-}
+++ /dev/null
-package net.sf.openrocket.util.exp4j;
-import java.util.Map;
-import java.util.Stack;
-
-import net.sf.openrocket.util.exp4j.FunctionToken.Function;
-
-/**
- * this classed is used to create custom functions for exp4j<br/>
- * <br/>
- * <b>Example</b><br/>
- * <code><pre>{@code
- * CustomFunction fooFunc = new CustomFunction("foo") {
- * public double applyFunction(double value) {
- * return value*Math.E;
- * }
- * };
- * double varX=12d;
- * Calculable calc = new ExpressionBuilder("foo(x)").withCustomFunction(fooFunc).withVariable("x",varX).build();
- * assertTrue(calc.calculate() == Math.E * varX);
- * }</pre></code>
- *
- * @author ruckus
- *
- */
-public abstract class CustomFunction extends CalculationToken {
- private int argc=1;
-
- /**
- * create a new single value input CustomFunction with a set name
- *
- * @param value
- * the name of the function (e.g. foo)
- */
- protected CustomFunction(String value) throws InvalidCustomFunctionException{
- super(value);
- for (Function f:Function.values()) {
- if (value.equalsIgnoreCase(f.toString())){
- throw new InvalidCustomFunctionException(value + " is already reserved as a function name");
- }
- }
- }
-
- /**
- * create a new single value input CustomFunction with a set name
- *
- * @param value
- * the name of the function (e.g. foo)
- */
- protected CustomFunction(String value,int argumentCount) throws InvalidCustomFunctionException{
- super(value);
- this.argc=argumentCount;
- for (Function f:Function.values()) {
- if (value.equalsIgnoreCase(f.toString())){
- throw new InvalidCustomFunctionException(value + " is already reserved as a function name");
- }
- }
- }
-
- /**
- * apply the function to a value
- *
- * @param values
- * the values to which the function should be applied.
- * @return the function value
- */
- public abstract double applyFunction(double[] values);
-
- @Override
- void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
- double[] args=new double[argc];
- for (int i=0;i<argc;i++) {
- args[i]=stack.pop();
- }
- stack.push(this.applyFunction(args));
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- operatorStack.push(this);
- }
- public int getArgumentCount() {
- return argc;
- }
-}
+++ /dev/null
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
-/**
- * This is Builder implementation for the exp4j API used to create a Calculable
- * instance for the user
- *
- * @author ruckus
- *
- */
-public class ExpressionBuilder {
- private final Map<String, Double> variables = new LinkedHashMap<String, Double>();
- private final Set<CustomFunction> customFunctions = new HashSet<CustomFunction>();
-
- private String expression;
-
- /**
- * Create a new ExpressionBuilder
- *
- * @param expression
- * the expression to evaluate
- */
- public ExpressionBuilder(String expression) {
- this.expression = expression;
- }
-
- /**
- * build a new {@link Calculable} from the expression using the supplied
- * variables
- *
- * @return the {@link Calculable} which can be used to evaluate the
- * expression
- * @throws UnknownFunctionException
- * when an unrecognized function name is used in the expression
- * @throws UnparsableExpressionException
- * if the expression could not be parsed
- */
- public Calculable build() throws UnknownFunctionException, UnparsableExpressionException {
- if (expression.indexOf('=') == -1 && !variables.isEmpty()) {
-
- // User supplied an expression without leading "f(...)="
- // so we just append the user function to a proper "f()="
- // for PostfixExpression.fromInfix()
- StringBuilder function = new StringBuilder("f(");
- for (String var : variables.keySet()) {
- function.append(var).append(',');
- }
- expression = function.deleteCharAt(function.length() - 1).toString() + ")=" + expression;
- }
- // create the PostfixExpression and return it as a Calculable
- PostfixExpression delegate = PostfixExpression.fromInfix(expression, customFunctions);
- for (String var : variables.keySet()) {
- if (variables.get(var) != null) {
- delegate.setVariable(var, variables.get(var));
- }
- for (CustomFunction custom:customFunctions){
- if (custom.getValue().equals(var)){
- throw new UnparsableExpressionException("variable '" + var + "' cannot have the same name as a custom function " + custom.getValue());
- }
- }
- }
- return delegate;
- }
-
- /**
- * add a custom function instance for the evaluator to recognize
- *
- * @param function
- * the {@link CustomFunction} to add
- * @return the {@link ExpressionBuilder} instance
- */
- public ExpressionBuilder withCustomFunction(CustomFunction function) {
- customFunctions.add(function);
- return this;
- }
-
- public ExpressionBuilder withCustomFunctions(Collection<CustomFunction> functions) {
- customFunctions.addAll(functions);
- return this;
- }
-
- /**
- * set the value for a variable
- *
- * @param variableName
- * the variable name e.g. "x"
- * @param value
- * the value e.g. 2.32d
- * @return the {@link ExpressionBuilder} instance
- */
- public ExpressionBuilder withVariable(String variableName, double value) {
- variables.put(variableName, value);
- return this;
- }
-
- /**
- * set the variables names used in the expression without setting their
- * values
- *
- * @param variableNames
- * vararg {@link String} of the variable names used in the
- * expression
- * @return the ExpressionBuilder instance
- */
- public ExpressionBuilder withVariableNames(String... variableNames) {
- for (String variable : variableNames) {
- variables.put(variable, null);
- }
- return this;
- }
-
- /**
- * set the values for variables
- *
- * @param variableMap
- * a map of variable names to variable values
- * @return the {@link ExpressionBuilder} instance
- */
- public ExpressionBuilder withVariables(Map<String, Double> variableMap) {
- for (Entry<String, Double> v : variableMap.entrySet()) {
- variables.put(v.getKey(), v.getValue());
- }
- return this;
- }
-}
+++ /dev/null
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Stack;
-
-public class FunctionSeparatorToken extends Token{
- public FunctionSeparatorToken() {
- super(",");
- }
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- Token token;
- while (!((token=operatorStack.peek()) instanceof ParenthesisToken) && !token.getValue().equals("(")){
- output.append(operatorStack.pop().getValue()).append(" ");
- }
- }
-
-}
+++ /dev/null
-/*
-Copyright 2011 frank asseg
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Map;
-import java.util.Stack;
-
-/**
- * A {@link Token} for functions
- *
- * @author fas@congrace.de
- *
- */
-class FunctionToken extends CalculationToken {
- /**
- * the functionNames that can be used in an expression
- *
- * @author ruckus
- *
- */
- enum Function {
- ABS, ACOS, ASIN, ATAN, CBRT, CEIL, COS, COSH, EXP, EXPM1, FLOOR, LOG, SIN, SINH, SQRT, TAN, TANH
- }
-
- private Function function;
-
- /**
- * construct a new {@link FunctionToken}
- *
- * @param value
- * the name of the function
- * @throws UnknownFunctionException
- * if an unknown function name is encountered
- */
- FunctionToken(String value) throws UnknownFunctionException {
- super(value);
- try {
- function = Function.valueOf(value.toUpperCase());
- } catch (IllegalArgumentException e) {
- throw new UnknownFunctionException(value);
- }
- if (function == null) {
- throw new UnknownFunctionException(value);
- }
- }
-
- /**
- * apply a function to a value x
- *
- * @param x
- * the value the function should be applied to
- * @return the result of the function
- */
- double applyFunction(double x) {
- switch (function) {
- case ABS:
- return Math.abs(x);
- case ACOS:
- return Math.acos(x);
- case ASIN:
- return Math.asin(x);
- case ATAN:
- return Math.atan(x);
- case CBRT:
- return Math.cbrt(x);
- case CEIL:
- return Math.ceil(x);
- case COS:
- return Math.cos(x);
- case COSH:
- return Math.cosh(x);
- case EXP:
- return Math.exp(x);
- case EXPM1:
- return Math.expm1(x);
- case FLOOR:
- return Math.floor(x);
- case LOG:
- return Math.log(x);
- case SIN:
- return Math.sin(x);
- case SINH:
- return Math.sinh(x);
- case SQRT:
- return Math.sqrt(x);
- case TAN:
- return Math.tan(x);
- case TANH:
- return Math.tanh(x);
- default:
- return Double.NaN; // should not happen ;)
- }
- }
-
- /**
- * get the {@link Function}
- *
- * @return the correspoding {@link Function}
- */
- Function getFunction() {
- return function;
- }
-
- @Override
- void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
- stack.push(this.applyFunction(stack.pop()));
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- operatorStack.push(this);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Set;
-import java.util.Stack;
-
-/**
- * Translate a mathematical expression in human readable infix notation to a
- * Reverse Polish Notation (postfix) expression for easier parsing. by
- * implementing the shunting yard algorithm by dijkstra
- *
- * @author fas@congrace.de
- */
-class InfixTranslator {
-
- private static String substituteUnaryOperators(String expr) {
- final StringBuilder exprBuilder = new StringBuilder(expr.length());
- final char[] data = expr.toCharArray();
- char lastChar = ' ';
- for (int i = 0; i < expr.length(); i++) {
- if (exprBuilder.length() > 0) {
- lastChar = exprBuilder.charAt(exprBuilder.length() - 1);
- }
- final char c = data[i];
- switch (c) {
- case '+':
- if (i > 0 && lastChar != '(' && !(OperatorToken.isOperator(lastChar))) {
- exprBuilder.append(c);
- }
- break;
- case '-':
- if (i > 0 && lastChar != '(' && !(OperatorToken.isOperator(lastChar))) {
- exprBuilder.append(c);
- } else {
- exprBuilder.append('#');
- }
- break;
- default:
- if (!Character.isWhitespace(c)) {
- exprBuilder.append(c);
- }
- }
- }
- return exprBuilder.toString();
- }
-
- /**
- * Delegation method for simple expression without variables or custom
- * functions
- *
- * @param infixExpression
- * the infix expression to be translated
- * @return translated RNP postfix expression
- * @throws UnparsableExpressionException
- * when the expression is invalid
- * @throws UnknownFunctionException
- * when an unknown function has been used in the input.
- */
- static String toPostfixExpression(String infixExpression) throws UnparsableExpressionException, UnknownFunctionException {
- return toPostfixExpression(infixExpression, null, null);
- }
-
- /**
- * implement the shunting yard algorithm
- *
- * @param infixExpression
- * the human readable expression which should be translated to
- * RPN
- * @param variableNames
- * the variable names used in the expression
- * @param customFunctions
- * the CustomFunction implementations used
- * @return the expression in postfix format
- * @throws UnparsableExpressionException
- * if the expression could not be translated to RPN
- * @throws UnknownFunctionException
- * if an unknown function was encountered
- */
- static String toPostfixExpression(String infixExpression, String[] variableNames, Set<CustomFunction> customFunctions)
- throws UnparsableExpressionException, UnknownFunctionException {
- infixExpression = substituteUnaryOperators(infixExpression);
- final Token[] tokens = new Tokenizer(variableNames, customFunctions).tokenize(infixExpression);
- final StringBuilder output = new StringBuilder(tokens.length);
- final Stack<Token> operatorStack = new Stack<Token>();
- for (final Token token : tokens) {
- token.mutateStackForInfixTranslation(operatorStack, output);
- }
- // all tokens read, put the rest of the operations on the output;
- while (operatorStack.size() > 0) {
- output.append(operatorStack.pop().getValue()).append(" ");
- }
- return output.toString().trim();
- }
-}
+++ /dev/null
-package net.sf.openrocket.util.exp4j;
-
-public class InvalidCustomFunctionException extends Exception{
- private static final long serialVersionUID = 1L;
-
- public InvalidCustomFunctionException(String message) {
- super(message);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Map;
-import java.util.Stack;
-
-/**
- * A {@link Token} for Numbers
- *
- * @author fas@congrace.de
- *
- */
-class NumberToken extends CalculationToken {
-
- private final double doubleValue;
-
- /**
- * construct a new {@link NumberToken}
- *
- * @param value
- * the value of the number as a {@link String}
- */
- NumberToken(String value) {
- super(value);
- this.doubleValue = Double.parseDouble(value);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof NumberToken) {
- final NumberToken t = (NumberToken) obj;
- return t.getValue().equals(this.getValue());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return getValue().hashCode();
- }
-
- @Override
- void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
- stack.push(this.doubleValue);
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- output.append(this.getValue()).append(' ');
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Map;
-import java.util.Stack;
-
-/**
- * {@link Token} for Operations like +,-,*,/,% and ^
- *
- * @author fas@congrace.de
- */
-class OperatorToken extends CalculationToken {
-
- /**
- * the valid {@link Operation}s for the {@link OperatorToken}
- *
- * @author fas@congrace.de
- */
- enum Operation {
- ADDITION(1, true), SUBTRACTION(1, true), MULTIPLICATION(2, true), DIVISION(2, true), MODULO(2, true), EXPONENTIATION(3, false), UNARY_MINUS(4, false), UNARY_PLUS(
- 4, false);
- private final int precedence;
- private final boolean leftAssociative;
-
- private Operation(int precedence, boolean leftAssociative) {
- this.precedence = precedence;
- this.leftAssociative = leftAssociative;
- }
- }
-
- /**
- * return a corresponding {@link Operation} for a symbol
- *
- * @param c
- * the symbol of the operation
- * @return the corresponding {@link Operation}
- */
- static Operation getOperation(char c) {
- switch (c) {
- case '+':
- return Operation.ADDITION;
- case '-':
- return Operation.SUBTRACTION;
- case '*':
- return Operation.MULTIPLICATION;
- case '/':
- return Operation.DIVISION;
- case '^':
- return Operation.EXPONENTIATION;
- case '#':
- return Operation.UNARY_MINUS;
- case '%':
- return Operation.MODULO;
- default:
- return null;
- }
- }
-
- static boolean isOperator(char c) {
- return getOperation(c) != null;
- }
-
- private final Operation operation;
-
- /**
- * construct a new {@link OperatorToken}
- *
- * @param value
- * the symbol (e.g.: '+')
- * @param operation
- * the {@link Operation} of this {@link Token}
- */
- OperatorToken(String value, Operation operation) {
- super(value);
- this.operation = operation;
- }
-
- /**
- * apply the {@link Operation}
- *
- * @param values
- * the doubles to operate on
- * @return the result of the {@link Operation}
- */
- double applyOperation(double... values) {
- switch (operation) {
- case ADDITION:
- return values[0] + values[1];
- case SUBTRACTION:
- return values[0] - values[1];
- case MULTIPLICATION:
- return values[0] * values[1];
- case EXPONENTIATION:
- return Math.pow(values[0], values[1]);
- case DIVISION:
- return values[0] / values[1];
- case UNARY_MINUS:
- return -values[0];
- case UNARY_PLUS:
- return values[0];
- case MODULO:
- return values[0] % values[1];
- default:
- return 0;
- }
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof OperatorToken) {
- final OperatorToken t = (OperatorToken) obj;
- return t.getValue().equals(this.getValue());
- }
- return false;
- }
-
- int getOperandCount() {
- switch (operation) {
- case ADDITION:
- case SUBTRACTION:
- case MULTIPLICATION:
- case DIVISION:
- case EXPONENTIATION:
- case MODULO:
- return 2;
- case UNARY_MINUS:
- case UNARY_PLUS:
- return 1;
- default:
- return 0;
- }
- }
-
- /**
- * get the {@link Operation} of this {@link Token}
- *
- * @return the {@link Operation}
- */
- Operation getOperation() {
- return operation;
- }
-
- int getPrecedence() {
- return operation.precedence;
- }
-
- @Override
- public int hashCode() {
- return getValue().hashCode();
- }
-
- /**
- * check if the operation is left associative
- *
- * @return true if left associative, otherwise false
- */
- boolean isLeftAssociative() {
- return operation.leftAssociative;
- }
-
- @Override
- void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
- if (this.getOperandCount() == 2) {
- final double n2 = stack.pop();
- final double n1 = stack.pop();
- stack.push(this.applyOperation(n1, n2));
- } else if (this.getOperandCount() == 1) {
- final double n1 = stack.pop();
- stack.push(this.applyOperation(n1));
- }
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- Token before;
- while (!operatorStack.isEmpty() && (before = operatorStack.peek()) != null && (before instanceof OperatorToken || before instanceof FunctionToken)) {
- if (before instanceof FunctionToken) {
- operatorStack.pop();
- output.append(before.getValue()).append(" ");
- } else {
- final OperatorToken stackOperator = (OperatorToken) before;
- if (this.isLeftAssociative() && this.getPrecedence() <= stackOperator.getPrecedence()) {
- output.append(operatorStack.pop().getValue()).append(" ");
- } else if (!this.isLeftAssociative() && this.getPrecedence() < stackOperator.getPrecedence()) {
- output.append(operatorStack.pop().getValue()).append(" ");
- } else {
- break;
- }
- }
- }
- operatorStack.push(this);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Stack;
-
-/**
- * Token for parenthesis
- *
- * @author fas@congrace.de
- */
-class ParenthesisToken extends Token {
-
- ParenthesisToken(String value) {
- super(value);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof ParenthesisToken) {
- final ParenthesisToken t = (ParenthesisToken) obj;
- return t.getValue().equals(this.getValue());
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return getValue().hashCode();
- }
-
- /**
- * check the direction of the parenthesis
- *
- * @return true if it's a left parenthesis (open) false if it is a right
- * parenthesis (closed)
- */
- boolean isOpen() {
- return getValue().equals("(") || getValue().equals("[") || getValue().equals("{");
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- if (this.isOpen()) {
- operatorStack.push(this);
- } else {
- Token next;
- while ((next = operatorStack.peek()) instanceof OperatorToken || next instanceof FunctionToken || next instanceof CustomFunction
- || (next instanceof ParenthesisToken && !((ParenthesisToken) next).isOpen())) {
- output.append(operatorStack.pop().getValue()).append(" ");
- }
- operatorStack.pop();
- }
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.Stack;
-
-/**
- * Class for calculating values from a RPN postfix expression.<br/>
- * The default way to create a new instance of {@link PostfixExpression} is by
- * using the static factory method fromInfix()
- *
- * @author fas@congrace.de
- */
-public final class PostfixExpression extends AbstractExpression implements Calculable {
- /**
- * Factory method for creating {@link PostfixExpression}s from human
- * readable infix expressions
- *
- * @param expression
- * the infix expression to be used
- * @return an equivalent {@link PostfixExpression}
- * @throws UnparsableExpressionException
- * if the expression was invalid
- * @throws UnknownFunctionException
- * if an unknown function has been used
- * @deprecated please use {@link ExpressionBuilder} API
- */
- @Deprecated
- public static PostfixExpression fromInfix(String expression) throws UnparsableExpressionException, UnknownFunctionException {
- return fromInfix(expression, null);
- }
-
- /**
- * Factory method for creating {@link PostfixExpression}s from human
- * readable infix expressions
- *
- * @param expression
- * the infix expression to be used
- * @param customFunctions
- * the CustomFunction implementations used
- * @return an equivalent {@link PostfixExpression}
- * @throws UnparsableExpressionException
- * if the expression was invalid
- * @throws UnknownFunctionException
- * if an unknown function has been used
- * @deprecated please use {@link ExpressionBuilder}
- */
- @Deprecated
- public static PostfixExpression fromInfix(String expression, Set<CustomFunction> customFunctions) throws UnparsableExpressionException,
- UnknownFunctionException {
- String[] variables = null;
- int posStart, posEnd;
- if ((posStart = expression.indexOf('=')) > 0) {
- String functionDef = expression.substring(0, posStart);
- expression = expression.substring(posStart + 1);
- if ((posStart = functionDef.indexOf('(')) > 0 && (posEnd = functionDef.indexOf(')')) > 0) {
- variables = functionDef.substring(posStart + 1, posEnd).split(",");
- }
- }
- return new PostfixExpression(InfixTranslator.toPostfixExpression(expression, variables, customFunctions), variables, customFunctions);
- }
-
- private final Map<String, Double> variableValues = new HashMap<String, Double>();
-
- /**
- * Construct a new simple {@link PostfixExpression}
- *
- * @param expression
- * the postfix expression to be calculated
- * @param variableNames
- * the variable names in the expression
- * @param customFunctions
- * the CustomFunction implementations used
- * @throws UnparsableExpressionException
- * when expression is invalid
- * @throws UnknownFunctionException
- * when an unknown function has been used
- */
- private PostfixExpression(String expression, String[] variableNames, Set<CustomFunction> customFunctions) throws UnparsableExpressionException,
- UnknownFunctionException {
- super(expression, new Tokenizer(variableNames, customFunctions).tokenize(expression), variableNames);
- }
-
- /**
- * delegate the calculation of a simple expression without variables
- *
- * @return the result
- */
- public double calculate() {
- return calculate(null);
- }
-
- /**
- * calculate the result of the expression and substitute the variables by
- * their values beforehand
- *
- * @param values
- * the variable values to be substituted
- * @return the result of the calculation
- * @throws IllegalArgumentException
- * if the variables are invalid
- */
- public double calculate(double... values) throws IllegalArgumentException {
- if (getVariableNames() == null && values != null) {
- throw new IllegalArgumentException("there are no variables to set values");
- } else if (getVariableNames() != null && values == null && variableValues.isEmpty()) {
- throw new IllegalAccessError("variable values have to be set");
- } else if (values != null && values.length != getVariableNames().length) {
- throw new IllegalArgumentException("The are an unequal number of variables and arguments");
- }
- int i = 0;
- if (getVariableNames() != null && values != null) {
- for (double val : values) {
- variableValues.put(getVariableNames()[i++], val);
- }
- }
- final Stack<Double> stack = new Stack<Double>();
- for (final Token t : getTokens()) {
- ((CalculationToken) t).mutateStackForCalculation(stack, variableValues);
- }
- return stack.pop();
- }
-
- public void setVariable(String name, double value) {
- variableValues.put(name, value);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Stack;
-
-/**
- * Superclass for tokenized Strings
- *
- * @author fas@congrace.de
- */
-abstract class Token {
- private final String value;
-
- /**
- * construct a new {@link Token}
- *
- * @param value
- * the value of the {@link Token}
- */
- Token(String value) {
- super();
- this.value = value;
- }
-
- /**
- * get the value (String representation) of the token
- *
- * @return the value
- */
- String getValue() {
- return value;
- }
-
- abstract void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output);
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import net.sf.openrocket.util.exp4j.FunctionToken.Function;
-
-/**
- * Class for tokenizing mathematical expressions by breaking an expression up
- * into multiple different {@link Token}s
- *
- * @author fas@congrace.de
- */
-class Tokenizer {
- private String[] variableNames;
- private final Set<String> functionNames = new HashSet<String>();
- private final Set<CustomFunction> customFunctions;
-
- {
- functionNames.add("abs");
- functionNames.add("acos");
- functionNames.add("asin");
- functionNames.add("atan");
- functionNames.add("cbrt");
- functionNames.add("ceil");
- functionNames.add("cos");
- functionNames.add("cosh");
- functionNames.add("exp");
- functionNames.add("expm1");
- functionNames.add("floor");
- functionNames.add("log");
- functionNames.add("sin");
- functionNames.add("sinh");
- functionNames.add("sqrt");
- functionNames.add("tan");
- functionNames.add("tanh");
- }
-
- Tokenizer() {
- super();
- customFunctions = null;
- }
-
- /**
- * construct a new Tokenizer that recognizes variable names
- *
- * @param variableNames
- * the variable names in the expression
- * @throws IllegalArgumentException
- * if a variable has the name as a function
- * @param customFunctions
- * the CustomFunction implementations used if the variableNames
- * are not valid
- */
- Tokenizer(String[] variableNames, Set<CustomFunction> customFunctions) throws IllegalArgumentException {
- super();
- this.variableNames = variableNames;
- if (variableNames != null) {
- for (String varName : variableNames) {
- if (functionNames.contains(varName.toLowerCase())) {
- throw new IllegalArgumentException("Variable '" + varName + "' can not have the same name as a function");
- }
- }
- }
- this.customFunctions = customFunctions;
- }
-
- private Token getCustomFunctionToken(String name) throws UnknownFunctionException {
- for (CustomFunction func : customFunctions) {
- if (func.getValue().equals(name)) {
- return func;
- }
- }
- throw new UnknownFunctionException(name);
- }
-
- private boolean isCustomFunction(String name) {
- if (customFunctions == null) {
- return false;
- }
- for (CustomFunction func : customFunctions) {
- if (func.getValue().equals(name)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * check if a char is part of a number
- *
- * @param c
- * the char to be checked
- * @return true if the char is part of a number
- */
- private boolean isDigit(char c) {
- return Character.isDigit(c) || c == '.';
- }
-
- private boolean isFunction(String name) {
- for (Function fn : Function.values()) {
- if (fn.name().equals(name.toUpperCase())) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * check if a String is a variable name
- *
- * @param name
- * the variable name which is checked to be valid the char to be
- * checked
- * @return true if the char is a variable name (e.g. x)
- */
- private boolean isVariable(String name) {
- if (variableNames != null) {
- for (String var : variableNames) {
- if (name.equals(var)) {
- return true;
- }
- }
- }
- return false;
- }
-
- /**
- * tokenize an infix expression by breaking it up into different
- * {@link Token} that can represent operations,functions,numbers,
- * parenthesis or variables
- *
- * @param infix
- * the infix expression to be tokenized
- * @return the {@link Token}s representing the expression
- * @throws UnparsableExpressionException
- * when the expression is invalid
- * @throws UnknownFunctionException
- * when an unknown function name has been used.
- */
- Token[] tokenize(String infix) throws UnparsableExpressionException, UnknownFunctionException {
- final List<Token> tokens = new ArrayList<Token>();
- final char[] chars = infix.toCharArray();
- // iterate over the chars and fork on different types of input
- Token lastToken;
- for (int i = 0; i < chars.length; i++) {
- char c = chars[i];
- if (c == ' ')
- continue;
- if (isDigit(c)) {
- final StringBuilder valueBuilder = new StringBuilder(1);
- // handle the numbers of the expression
- valueBuilder.append(c);
- int numberLen = 1;
- while (chars.length > i + numberLen && isDigit(chars[i + numberLen])) {
- valueBuilder.append(chars[i + numberLen]);
- numberLen++;
- }
- i += numberLen - 1;
- lastToken = new NumberToken(valueBuilder.toString());
- } else if (Character.isLetter(c) || c == '_') {
- // can be a variable or function
- final StringBuilder nameBuilder = new StringBuilder();
- nameBuilder.append(c);
- int offset = 1;
- while (chars.length > i + offset && (Character.isLetter(chars[i + offset]) || Character.isDigit(chars[i + offset]) || chars[i + offset] == '_')) {
- nameBuilder.append(chars[i + offset++]);
- }
- String name = nameBuilder.toString();
- if (this.isVariable(name)) {
- // a variable
- i += offset - 1;
- lastToken = new VariableToken(name);
- } else if (this.isFunction(name)) {
- // might be a function
- i += offset - 1;
- lastToken = new FunctionToken(name);
- } else if (this.isCustomFunction(name)) {
- // a custom function
- i += offset - 1;
- lastToken = getCustomFunctionToken(name);
- } else {
- // an unknown symbol was encountered
- throw new UnparsableExpressionException(c, i);
- }
- }else if (c == ',') {
- // a function separator, hopefully
- lastToken=new FunctionSeparatorToken();
- } else if (OperatorToken.isOperator(c)) {
- lastToken = new OperatorToken(String.valueOf(c), OperatorToken.getOperation(c));
- } else if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}') {
- lastToken = new ParenthesisToken(String.valueOf(c));
- } else {
- // an unknown symbol was encountered
- throw new UnparsableExpressionException(c, i);
- }
- tokens.add(lastToken);
- }
- return tokens.toArray(new Token[tokens.size()]);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-/**
- * Exception for handling unknown Functions.
- *
- * @see FunctionToken
- * @author fas@congrace.de
- */
-public class UnknownFunctionException extends Exception {
- private static final long serialVersionUID = 1L;
-
- /**
- * construct a new {@link UnknownFunctionException}
- *
- * @param functionName
- * the function name which could not be found
- */
- public UnknownFunctionException(String functionName) {
- super("Unknown function: " + functionName);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-/**
- * Exception for invalid expressions
- *
- * @author fas@congrace.de
- */
-public class UnparsableExpressionException extends Exception {
- private static final long serialVersionUID = 1L;
-
- /**
- * construct a new {@link UnparsableExpressionException}
- *
- * @param c
- * the character which could not be parsed
- * @param pos
- * the position of the character in the expression
- */
- public UnparsableExpressionException(char c, int pos) {
- super("Unable to parse character at position " + pos + ": '" + String.valueOf(c) + "'");
- }
- /**
- * construct a new {@link UnparsableExpressionException}
- *
- * @param msg
- * the error message
- */
- public UnparsableExpressionException(String msg) {
- super(msg);
- }
-}
+++ /dev/null
-/*
- Copyright 2011 frank asseg
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
- */
-package net.sf.openrocket.util.exp4j;
-
-import java.util.Map;
-import java.util.Stack;
-
-/**
- * A {@link Token} for representing variables
- *
- * @author fas
- */
-class VariableToken extends CalculationToken {
- /**
- * construct a new {@link VariableToken}
- *
- * @param value
- * the value of the token
- */
- VariableToken(String value) {
- super(value);
- }
-
- @Override
- void mutateStackForCalculation(Stack<Double> stack, Map<String, Double> variableValues) {
- double value = variableValues.get(this.getValue());
- stack.push(value);
- }
-
- @Override
- void mutateStackForInfixTranslation(Stack<Token> operatorStack, StringBuilder output) {
- output.append(this.getValue()).append(" ");
- }
-}
--- /dev/null
+package net.sf.openrocket.util;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class ExpressionParserTest {
+
+ private static final double EPS = 1e-10;
+
+ private ExpressionParser parser = new ExpressionParser();
+
+ @Test
+ public void testPlainNumber() throws InvalidExpressionException {
+ assertEquals(1.0, parser.parse("1"), EPS);
+ assertEquals(1.0, parser.parse("\t 1 "), EPS);
+ assertEquals(0.9, parser.parse(".9"), EPS);
+ assertEquals(1.0, parser.parse("1."), EPS);
+ assertEquals(1.2, parser.parse("1.2"), EPS);
+ assertEquals(1.2, parser.parse("01.200"), EPS);
+ }
+
+
+ @Test
+ public void testDecimalComma() throws InvalidExpressionException {
+ assertEquals(1.0, parser.parse("1,"), EPS);
+ assertEquals(1.2, parser.parse("1,2"), EPS);
+ assertEquals(1.2, parser.parse("01,200"), EPS);
+ assertEquals(0.9, parser.parse(",9"), EPS);
+ }
+
+
+ @Test
+ public void testSimpleExpression() throws InvalidExpressionException {
+ assertEquals(3.0, parser.parse("1+2"), EPS);
+ assertEquals(6.0, parser.parse("1+2.5*2"), EPS);
+ assertEquals(7.0, parser.parse("(1+2.5) * 2"), EPS);
+ assertEquals(1.0 + 2.0 / 3.0, parser.parse("1+2/3"), EPS);
+ }
+
+ @Test
+ public void testFraction() throws InvalidExpressionException {
+ assertEquals(1.5, parser.parse("1 1/2"), EPS);
+ assertEquals(1.5, parser.parse(" 1 1 / 2"), EPS);
+ assertEquals(2.0 + 3.0 / 7.0, parser.parse("1 + 1 3/7"), EPS);
+ assertEquals(3.0, parser.parse("1 1/2 * 2"), EPS);
+ }
+
+ @Test
+ public void testInvalidExpression() {
+ expectInvalid("1+");
+ expectInvalid("1+2/");
+ expectInvalid("1 2");
+ expectInvalid("12 2.5");
+ expectInvalid("1 2.5/4");
+ expectInvalid("1. 2");
+ expectInvalid("1 .2");
+ }
+
+ private void expectInvalid(String exp) {
+ try {
+ double value = parser.parse(exp);
+ fail("Expression '" + exp + "' evaluated to " + value + ", expected failure");
+ } catch (InvalidExpressionException e) {
+ // expected
+ }
+ }
+}