Last active
February 24, 2017 14:23
-
-
Save justisr/f4f037283905a2894926be7836247066 to your computer and use it in GitHub Desktop.
Evaluate algebraic equations from a string
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Created by Lawrence PC Dol. Released into the public domain. | |
// http://tech.dolhub.com | |
// | |
// Contributions by Carlos Gómez of Asturias, Spain, in the area of unary operators | |
// and right-to-left evaluations proved invaluable to implementing these features. | |
// Thanks Carlos! | |
// | |
// Source is licensed for any use, provided this copyright notice is retained. | |
// No warranty for any purpose whatsoever is implied or expressed. The author | |
// is not liable for any losses of any kind, direct or indirect, which result | |
// from the use of this software. | |
// | |
// Scientific notation support added by Justis Root. | |
import java.util.Collections; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.SortedMap; | |
import java.util.StringTokenizer; | |
import java.util.TreeMap; | |
import java.util.TreeSet; | |
/** | |
* Math Evaluator. Provides the ability to evaluate a String math expression, | |
* with support for pureFunctions, variables and standard math constants. | |
* <p> | |
* Supported Operators: | |
* | |
* <pre> | |
* Operator Precedence Unary Binding Description | |
* --------- ----------- -------------- ------------------------------------------------ | |
* '=' 99 / 99 RIGHT_SIDE Simple assignment (internal, used for the final operation) | |
* '^' 80 / 81 NO_SIDE Power | |
* '±' 60 / 60 RIGHT_SIDE Unary negation (internal, substituted for '-') | |
* '*' 40 / 40 NO_SIDE Multiple (conventional computer notation) | |
* '×' 40 / 40 NO_SIDE Multiple (because it's a Unicode world out there) | |
* '·' 40 / 40 NO_SIDE Multiple (because it's a Unicode world out there) | |
* '(' 40 / 40 NO_SIDE Multiply (implicit due to brackets, e.g "(a)(b)") | |
* '/' 40 / 40 NO_SIDE Divide (conventional computer notation) | |
* '÷' 40 / 40 NO_SIDE Divide (because it's a Unicode world out there) | |
* '%' 40 / 40 NO_SIDE Remainder | |
* '+' 20 / 20 NO_SIDE Add/unary-positive | |
* '-' 20 / 20 NO_SIDE Subtract/unary-negative | |
* </pre> | |
* <p> | |
* Predefined Constants: | |
* | |
* <pre> | |
* Name Description | |
* -------------------- ---------------------------------------------------------------- | |
* E The double value that is closer than any other to e, the base of the natural logarithms (2.718281828459045). | |
* Euler Euler's Constant (0.577215664901533). | |
* LN2 Log of 2 base e (0.693147180559945). | |
* LN10 Log of 10 base e (2.302585092994046). | |
* LOG2E Log of e base 2 (1.442695040888963). | |
* LOG10E Log of e base 10 (0.434294481903252). | |
* PHI The golden ratio (1.618033988749895). | |
* PI The double value that is closer than any other to pi, the ratio of the circumference of a circle to its diameter (3.141592653589793). | |
* </pre> | |
* <p> | |
* Supported Functions (see java.Math for detail and parameters): | |
* <ul> | |
* <li>abs | |
* <li>acos | |
* <li>asin | |
* <li>atan | |
* <li>cbrt | |
* <li>ceil | |
* <li>cos | |
* <li>cosh | |
* <li>exp | |
* <li>expm1 | |
* <li>floor | |
* <li>log | |
* <li>log10 | |
* <li>log1p | |
* <li>max | |
* <li>min | |
* <li>random | |
* <li>round | |
* <li>roundHE (maps to Math.rint) | |
* <li>signum | |
* <li>sin | |
* <li>sinh | |
* <li>sqrt | |
* <li>tan | |
* <li>tanh | |
* <li>toDegrees | |
* <li>toRadians | |
* <li>ulp | |
* </ul> | |
* <p> | |
* Threading Design : [x] Single Threaded [ ] Threadsafe [ ] Immutable [ ] | |
* Isolated | |
* | |
* @author Lawrence Dol | |
* @since Build 2008.0426.1016 | |
*/ | |
public class MathEval { | |
// ************************************************************************************************* | |
// INSTANCE PROPERTIES | |
// ************************************************************************************************* | |
private Operator[] operators; // operators in effect for this parser | |
private final SortedMap<String, Double> constants; // external constants | |
private final SortedMap<String, Double> variables; // external variables | |
private final SortedMap<String, FunctionHandler> pureFunctions; // external | |
// pureFunctions | |
private final SortedMap<String, FunctionHandler> impureFunctions; // external | |
// pureFunctions | |
private boolean relaxed; // allow variables to be undefined | |
private String separators; // cache of the operators, used for separators | |
// for getVariablesWithin() | |
private String expression; // expression being evaluated | |
private int offset; // used when returning from a higher precedence | |
// sub-expression evaluation | |
private boolean isConstant; // last expression evaluated is constant | |
// ************************************************************************************************* | |
// INSTANCE CREATE/DELETE | |
// ************************************************************************************************* | |
private static MathEval instance; | |
public static MathEval current() { | |
if (instance == null) return generate(); | |
return instance; | |
} | |
public static MathEval generate() { | |
return instance = new MathEval(); | |
} | |
/** | |
* Create a math evaluator. | |
*/ | |
public MathEval() { | |
operators = new Operator[256]; | |
DefaultImpl.registerOperators(this); | |
constants = new TreeMap<String, Double>(String.CASE_INSENSITIVE_ORDER); | |
variables = new TreeMap<String, Double>(String.CASE_INSENSITIVE_ORDER); | |
setConstant("E", Math.E); | |
setConstant("Euler", 0.577215664901533D); | |
setConstant("LN2", 0.693147180559945D); | |
setConstant("LN10", 2.302585092994046D); | |
setConstant("LOG2E", 1.442695040888963D); | |
setConstant("LOG10E", 0.434294481903252D); | |
setConstant("PHI", 1.618033988749895D); | |
setConstant("PI", Math.PI); | |
pureFunctions = new TreeMap<String, FunctionHandler>(String.CASE_INSENSITIVE_ORDER); | |
impureFunctions = new TreeMap<String, FunctionHandler>(String.CASE_INSENSITIVE_ORDER); | |
DefaultImpl.registerFunctions(this); | |
relaxed = false; | |
separators = null; | |
offset = 0; | |
isConstant = false; | |
} | |
/** | |
* Create a math evaluator with the same constants, variables, function | |
* handlers and relaxation setting as the supplied evaluator. | |
*/ | |
public MathEval(MathEval oth) { | |
super(); | |
operators = oth.operators; | |
constants = new TreeMap<String, Double>(String.CASE_INSENSITIVE_ORDER); | |
constants.putAll(oth.constants); | |
variables = new TreeMap<String, Double>(String.CASE_INSENSITIVE_ORDER); | |
variables.putAll(oth.variables); | |
pureFunctions = new TreeMap<String, FunctionHandler>(String.CASE_INSENSITIVE_ORDER); | |
impureFunctions = new TreeMap<String, FunctionHandler>(String.CASE_INSENSITIVE_ORDER); | |
pureFunctions.putAll(oth.pureFunctions); | |
impureFunctions.putAll(oth.impureFunctions); | |
relaxed = oth.relaxed; | |
separators = oth.separators; | |
offset = 0; | |
isConstant = false; | |
} | |
// ************************************************************************************************* | |
// INSTANCE METHODS - ACCESSORS | |
// ************************************************************************************************* | |
/** | |
* Set a named constant (constant names are not case-sensitive). Constants | |
* are like variables but are not cleared by clear(). Variables of the same | |
* name have precedence over constants. | |
*/ | |
public double getConstant(String nam) { | |
Double val = constants.get(nam); | |
return (val == null ? 0 : val.doubleValue()); | |
} | |
/** Gets an unmodifiable iterable of the constants in this evaluator. */ | |
public Iterable<Map.Entry<String, Double>> getConstants() { | |
return Collections.unmodifiableMap(constants).entrySet(); | |
} | |
/** | |
* Set a named constant (constants names are not case-sensitive). Constants | |
* are like variables but are not cleared by clear(). Variables of the same | |
* name have precedence over constants. | |
*/ | |
public MathEval setConstant(String nam, double val) { | |
return setConstant(nam, Double.valueOf(val)); | |
} | |
/** | |
* Set a named constant (constants names are not case-sensitive). Constants | |
* are like variables but are not cleared by clear(). Variables of the same | |
* name have precedence over constants. | |
*/ | |
public MathEval setConstant(String nam, Double val) { | |
if (constants.get(nam) != null) { | |
throw new IllegalArgumentException("Constants may not be redefined"); | |
} | |
validateName(nam); | |
constants.put(nam, val); | |
return this; | |
} | |
/** | |
* Set a custom operator, replacing any existing operator with the same | |
* symbol. Operators cannot be removed, only replaced. | |
*/ | |
public MathEval setOperator(Operator opr) { | |
if (opr.symbol >= operators.length) { // extend the array if necessary | |
Operator[] noa = new Operator[opr.symbol + (opr.symbol % 255) + 1]; // use | |
// allocation | |
// pages | |
// of | |
// 256 | |
System.arraycopy(operators, 0, noa, 0, operators.length); | |
operators = noa; | |
} | |
operators[opr.symbol] = opr; | |
return this; | |
} | |
/** | |
* Set a pure function handler for the specific named function, replacing | |
* any existing handler for the given name; if the handler is null the | |
* function handler is removed. | |
* <p> | |
* Pure functions have results which depend purely on their arguments; given | |
* constant arguments they will have a constant result. Impure functions are | |
* rare. | |
*/ | |
public MathEval setFunctionHandler(String nam, FunctionHandler hdl) { | |
return setFunctionHandler(nam, hdl, false); | |
} | |
/** | |
* Set a function handler for the specific named function optionally tagging | |
* the function as impure, replacing any existing handler for the given | |
* name; if the handler is null the function handler is removed. | |
* <p> | |
* Pure functions have results which depend purely on their arguments; given | |
* constant arguments they will have a constant result. Impure functions are | |
* rare. | |
*/ | |
public MathEval setFunctionHandler(String nam, FunctionHandler hdl, boolean impure) { | |
validateName(nam); | |
if (hdl == null) { | |
pureFunctions.remove(nam); | |
impureFunctions.remove(nam); | |
} else if (impure) { | |
pureFunctions.remove(nam); | |
impureFunctions.put(nam, hdl); | |
} else { | |
pureFunctions.put(nam, hdl); | |
impureFunctions.remove(nam); | |
} | |
return this; | |
} | |
/** Set a named variable (variables names are not case-sensitive). */ | |
public double getVariable(String nam) { | |
Double val = variables.get(nam); | |
return (val == null ? 0 : val.doubleValue()); | |
} | |
/** Gets an unmodifiable iterable of the variables in this evaluator. */ | |
public Iterable<Map.Entry<String, Double>> getVariables() { | |
return Collections.unmodifiableMap(variables).entrySet(); | |
} | |
/** Set a named variable (variables names are not case-sensitive). */ | |
public MathEval setVariable(String nam, double val) { | |
return setVariable(nam, Double.valueOf(val)); | |
} | |
/** | |
* Set a named variable (variables names are not case-sensitive). If the | |
* value is null, the variable is removed. | |
*/ | |
public MathEval setVariable(String nam, Double val) { | |
validateName(nam); | |
if (val == null) { | |
variables.remove(nam); | |
} else { | |
variables.put(nam, val); | |
} | |
return this; | |
} | |
/** Clear all variables (constants are not affected). */ | |
public MathEval clear() { | |
variables.clear(); | |
return this; | |
} | |
/** | |
* Clear all variables prefixed by the supplied string followed by a dot, | |
* such that they match "Prefix.xxx". | |
*/ | |
public MathEval clear(String pfx) { | |
variables.subMap((pfx + "."), (pfx + "." + Character.MAX_VALUE)).clear(); | |
return this; | |
} | |
/** | |
* Get whether a variable which is used in an expression is required to be | |
* explicitly set. If not explicitly set, the value 0.0 is assumed. | |
*/ | |
public boolean getVariableRequired() { | |
return relaxed; | |
} | |
/** | |
* Set whether a variable which is used in an expression is required to be | |
* explicitly set. If not explicitly set, the value 0.0 is assumed. | |
*/ | |
public MathEval setVariableRequired(boolean val) { | |
relaxed = (!val); | |
return this; | |
} | |
private void validateName(String nam) { | |
if (!Character.isLetter(nam.charAt(0))) { | |
throw new IllegalArgumentException("Names for constants, variables and functions must start with a letter"); | |
} | |
if (nam.indexOf('(') != -1 || nam.indexOf(')') != -1) { | |
throw new IllegalArgumentException( | |
"Names for constants, variables and functions may not contain a parenthesis"); | |
} | |
} | |
// ************************************************************************************************* | |
// INSTANCE METHODS - PUBLIC API | |
// ************************************************************************************************* | |
/** | |
* Evaluate this expression. | |
*/ | |
public double evaluate(String exp) throws NumberFormatException, ArithmeticException { | |
expression = exp; | |
isConstant = true; | |
offset = 0; | |
return _evaluate(0, (exp.length() - 1)); | |
} | |
/** | |
* Return whether the previous expression evaluated was constant (i.e. | |
* contained no variables). This is useful when optimizing to store the | |
* result instead of repeatedly evaluating a constant expression like "2+2". | |
*/ | |
public boolean previousExpressionConstant() { | |
return isConstant; | |
} | |
/** | |
* Return a set of the variables in the supplied expression. Note: | |
* Substitutions which are in the constant table are not included. | |
*/ | |
public Set<String> getVariablesWithin(String exp) { | |
Set<String> all = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); | |
String add = null; | |
if (separators == null) { | |
StringBuilder sep = new StringBuilder(10); | |
for (char chr = 0; chr < operators.length; chr++) { | |
if (operators[chr] != null && !operators[chr].internal) { | |
sep.append(chr); | |
} | |
} | |
sep.append("()"); | |
separators = sep.toString(); | |
} | |
for (StringTokenizer tkz = new StringTokenizer(exp, separators, true); tkz.hasMoreTokens();) { | |
String tkn = tkz.nextToken().trim(); | |
if (tkn.length() != 0 && Character.isLetter(tkn.charAt(0))) { | |
add = tkn; | |
} else if (tkn.length() == 1 && tkn.charAt(0) == '(') { | |
add = null; | |
} else if (add != null && !constants.containsKey(add)) { | |
all.add(add); | |
} | |
} | |
if (add != null && !constants.containsKey(add)) { | |
all.add(add); | |
} | |
return all; | |
} | |
// ************************************************************************************************* | |
// INSTANCE METHODS - PRIVATE IMPLEMENTATION | |
// ************************************************************************************************* | |
/** | |
* Evaluate a complete (sub-)expression. | |
* | |
* @param beg | |
* Inclusive begin offset for subexpression. | |
* @param end | |
* Inclusive end offset for subexpression. | |
*/ | |
private double _evaluate(int beg, int end) throws NumberFormatException, ArithmeticException { | |
return _evaluate(beg, end, 0.0, OPERAND, getOperator('=')); | |
} | |
/** | |
* Evaluate the next operand of an expression. | |
* | |
* @param beg | |
* Inclusive begin offset for subexpression. | |
* @param end | |
* Inclusive end offset for subexpression. | |
* @param pnd | |
* Pending operator (operator previous to this subexpression). | |
* @param lft | |
* Left-value with which to initialize this subexpression. | |
* @param cur | |
* Current operator (the operator for this subexpression). | |
*/ | |
@SuppressWarnings("unused") | |
private double _evaluate(int beg, int end, double lft, Operator pnd, Operator cur) | |
throws NumberFormatException, ArithmeticException { | |
Operator nxt = OPERAND; // next operator | |
int ofs; // current expression offset | |
for (ofs = beg; (ofs = skipWhitespace(expression, ofs, end)) <= end; ofs++) { | |
boolean fnc=false; | |
double rgt = Double.NaN; // next operand (right-value) to process | |
for (beg = ofs; ofs <= end; ofs++) { | |
char chr = expression.charAt(ofs); | |
if ((nxt = getOperator(chr)) != OPERAND) { | |
if (nxt.internal) { | |
nxt = OPERAND; | |
} // must kill operator to prevent spurious "Expression ends | |
// with a blank sub-expression" at end of function | |
else { | |
break; | |
} | |
} else if (chr == ')' || chr == ',') { // end of subexpression | |
// or function argument. | |
break; | |
} | |
} | |
EvaluateOperand: { | |
char ch0 = expression.charAt(beg); | |
boolean alp = Character.isLetter(ch0); | |
if (cur.unary != LEFT_SIDE) { | |
if (ch0 == '+') { | |
continue; | |
} // unary '+': no-op; i.e. +(-1) == -1 | |
if (ch0 == '-') { | |
nxt = getOperator('±'); | |
} // unary '-': right-binding, high precedence operation | |
// (different from subtract) | |
} | |
if (beg == ofs && (cur.unary == LEFT_SIDE || nxt.unary == RIGHT_SIDE)) { | |
rgt = Double.NaN; // left-binding unary operator; right | |
// value will not be used and should be | |
// blank | |
} else if (ch0 == '(') { | |
rgt = _evaluate(beg + 1, end); | |
ofs = skipWhitespace(expression, offset + 1, end); // skip | |
// past | |
// ')' | |
// and | |
// any | |
// following | |
// whitespace | |
nxt = (ofs <= end ? getOperator(expression.charAt(ofs)) : OPERAND); // modify | |
// next | |
// operator | |
} else if (alp && nxt.symbol == '(') { | |
rgt = doFunction(beg, end); | |
ofs = skipWhitespace(expression, offset + 1, end); // skip | |
// past | |
// ')' | |
// and | |
// any | |
// following | |
// whitespace | |
nxt = (ofs <= end ? getOperator(expression.charAt(ofs)) : OPERAND); // modify | |
// next | |
// operator | |
} else if (alp) { | |
rgt = doNamedVal(beg, (ofs - 1)); | |
//TODO: Custom stuff | |
//} else if (expression.substring(beg, ofs)) { | |
} else { | |
try { | |
if (stringOfsEq(expression, beg, "0x")) { | |
rgt = (double) Long.parseLong(expression.substring(beg + 2, ofs).trim(), 16); | |
} else { | |
rgt = Double.parseDouble(expression.substring(beg, ofs).trim()); | |
} | |
} catch (NumberFormatException thr) { | |
throw exception(beg, "Invalid numeric value \"" + expression.substring(beg, ofs).trim() + "\""); | |
} | |
} | |
} | |
if (opPrecedence(cur, LEFT_SIDE) < opPrecedence(nxt, RIGHT_SIDE)) { // correct | |
// even | |
// for | |
// last | |
// (non-operator) | |
// character, | |
// since | |
// non-operators | |
// have | |
// the | |
// artificial | |
// "precedence" | |
// zero | |
rgt = _evaluate((ofs + 1), end, rgt, cur, nxt); // from after | |
// operator to | |
// end of | |
// current | |
// subexpression | |
ofs = offset; // modify offset to after subexpression | |
nxt = (ofs <= end ? getOperator(expression.charAt(ofs)) : OPERAND); // modify | |
// next | |
// operator | |
} | |
lft = doOperation(beg, lft, cur, rgt); | |
cur = nxt; | |
if (opPrecedence(pnd, LEFT_SIDE) >= opPrecedence(cur, RIGHT_SIDE)) { | |
break; | |
} | |
if (cur.symbol == '(') { | |
ofs--; | |
} // operator omitted for implicit multiplication of subexpression | |
} | |
if (ofs > end && cur != OPERAND) { | |
if (cur.unary == LEFT_SIDE) { | |
lft = doOperation(beg, lft, cur, Double.NaN); | |
} else { | |
throw exception(ofs, "Expression ends with a blank operand after operator '" + nxt.symbol + "'"); | |
} | |
} | |
offset = ofs; | |
return lft; | |
} | |
private Operator getOperator(char chr) { | |
if (chr < operators.length) { | |
Operator opr = operators[chr]; | |
if (opr != null) { | |
return opr; | |
} | |
} | |
return OPERAND; | |
} | |
private int opPrecedence(Operator opr, int sid) { | |
if (opr == null) { | |
return Integer.MIN_VALUE; | |
} // not an operator | |
else if (opr.unary == NO_SIDE || opr.unary != sid) { | |
return (sid == LEFT_SIDE ? opr.precedenceL : opr.precedenceR); | |
} // operator is binary or is unary and bound to the operand on the | |
// other side | |
else { | |
return Integer.MAX_VALUE; | |
} // operator is unary and associates with the operand on this side | |
} | |
private double doOperation(int beg, double lft, Operator opr, double rgt) { | |
if (opr.unary != RIGHT_SIDE && Double.isNaN(lft)) { | |
throw exception(beg, "Mathematical NaN detected in right-operand"); | |
} | |
if (opr.unary != LEFT_SIDE && Double.isNaN(rgt)) { | |
throw exception(beg, "Mathematical NaN detected in left-operand"); | |
} | |
try { | |
return opr.handler.evaluateOperator(lft, opr.symbol, rgt); | |
} catch (ArithmeticException thr) { | |
throw exception(beg, "Mathematical expression \"" + expression + "\" failed to evaluate", thr); | |
} catch (UnsupportedOperationException thr) { | |
int tmp = beg; | |
while (tmp > 0 && getOperator(expression.charAt(tmp)) == null) { | |
tmp--; | |
} // set up for offset of the offending operator | |
throw exception(tmp, "Operator \"" + opr.symbol | |
+ "\" not handled by math engine (Programmer error: The list of operators is inconsistent within the engine)"); | |
} | |
} | |
private double doFunction(int beg, int end) { | |
int argbeg; | |
for (argbeg = beg; argbeg <= end && expression.charAt(argbeg) != '('; argbeg++) { | |
; | |
} | |
String fncnam = expression.substring(beg, argbeg).trim(); | |
ArgParser fncargs = new ArgParser(argbeg, end); | |
FunctionHandler fnchdl = null; | |
try { | |
if ((fnchdl = pureFunctions.get(fncnam)) != null) { | |
return fnchdl.evaluateFunction(fncnam, fncargs); | |
} else if ((fnchdl = impureFunctions.get(fncnam)) != null) { | |
isConstant = false; // impure functions cannot be guaranteed to | |
// be constant | |
return fnchdl.evaluateFunction(fncnam, fncargs); | |
} | |
fncargs = null; // suppress check for too many fncargs | |
} catch (ArithmeticException thr) { | |
fncargs = null; | |
throw thr; | |
} catch (NoSuchMethodError thr) { | |
fncargs = null; | |
throw exception(beg, "Function not supported in this JVM: \"" + fncnam + "\""); | |
} catch (UnsupportedOperationException thr) { | |
fncargs = null; | |
throw exception(beg, thr.getMessage()); | |
} catch (Throwable thr) { | |
fncargs = null; | |
throw exception(beg, "Unexpected exception parsing function arguments", thr); | |
} finally { | |
if (fncargs != null) { | |
if (fncargs.hasNext()) { | |
throw exception(fncargs.getIndex(), "Function has too many arguments"); | |
} | |
offset = fncargs.getIndex(); | |
} | |
} | |
throw exception(beg, "Function \"" + fncnam + "\" not recognized"); | |
} | |
private double doNamedVal(int beg, int end) { | |
while (beg < end && Character.isWhitespace(expression.charAt(end))) { | |
end--; | |
} // since a letter triggers a named value, this can never reduce to | |
// beg==end | |
String nam = expression.substring(beg, (end + 1)); | |
Double val; | |
if ((val = constants.get(nam)) != null) { | |
return val.doubleValue(); | |
} else if ((val = variables.get(nam)) != null) { | |
isConstant = false; | |
return val.doubleValue(); | |
} else if (relaxed) { | |
isConstant = false; | |
return 0.0; | |
} | |
throw exception(beg, "Unrecognized constant or variable \"" + nam + "\""); | |
} | |
private ArithmeticException exception(int ofs, String txt) { | |
return new ArithmeticException(txt + " at offset " + ofs + " in expression \"" + expression + "\""); | |
} | |
private ArithmeticException exception(int ofs, String txt, Throwable thr) { | |
return new ArithmeticException(txt + " at offset " + ofs + " in expression \"" + expression + "\"" + " (Cause: " | |
+ (thr.getMessage() != null ? thr.getMessage() : thr.toString()) + ")"); | |
} | |
private boolean stringOfsEq(String str, int ofs, String val) { | |
return str.regionMatches(true, ofs, val, 0, val.length()); | |
} | |
private int skipWhitespace(String exp, int ofs, int end) { | |
while (ofs <= end && Character.isWhitespace(exp.charAt(ofs))) { | |
ofs++; | |
} | |
return ofs; | |
} | |
// ************************************************************************************************* | |
// INSTANCE INNER CLASSES - FUNCTION ARGUMENT PARSER | |
// ************************************************************************************************* | |
/** | |
* An abstract parser for function arguments. | |
*/ | |
public final class ArgParser { | |
final int exEnd; | |
int index; | |
ArgParser(int excstr, int excend) { | |
exEnd = excend; | |
index = (excstr + 1); | |
index = skipWhitespace(expression, index, exEnd - 1); | |
} | |
/** | |
* Parse the next argument, throwing an exception if there are no more | |
* arguments. | |
* | |
* @throws ArithmeticException | |
* If there are no more arguments. | |
*/ | |
public double next() { | |
if (!hasNext()) { | |
throw exception(index, "Function has too few arguments"); | |
} | |
return _next(); | |
} | |
/** | |
* Parse the next argument, returning the supplied default if there are | |
* no more arguments. | |
*/ | |
public double next(double dft) { | |
if (!hasNext()) { | |
return dft; | |
} | |
return _next(); | |
} | |
private double _next() { | |
if (expression.charAt(index) == ',') { | |
index++; | |
} | |
double ret = _evaluate(index, exEnd); | |
index = offset; | |
return ret; | |
} | |
/** Test whether there is another argument to parse. */ | |
public boolean hasNext() { | |
return (expression.charAt(index) != ')'); | |
} | |
int getIndex() { | |
return index; | |
} | |
} | |
// ************************************************************************************************* | |
// STATIC NESTED CLASSES - OPERATOR | |
// ************************************************************************************************* | |
/** | |
* Operator Structure. | |
* <p> | |
* This class is immutable and threadsafe, but note that whether it can be | |
* used in multiple MathEval instances (as opposed to for multiple operators | |
* in one instance) depends on the threadsafety of the handler it contains. | |
*/ | |
static public final class Operator extends Object { | |
final char symbol; // parser symbol for this operator | |
final int precedenceL; // precedence when on the left | |
final int precedenceR; // precedence when on the right | |
final int unary; // unary operator binding: left, right, or neither | |
final boolean internal; // internal pseudo operator | |
final OperatorHandler handler; | |
/** | |
* Create a binary operator with the same precedence on the left and | |
* right. | |
*/ | |
public Operator(char sym, int prc, OperatorHandler hnd) { | |
this(sym, prc, prc, NO_SIDE, false, hnd); | |
} | |
/** | |
* Create an operator which may have different left and right precedence | |
* and/or may be unary. | |
* <p> | |
* Using different precedence for one side allows affinity binding such | |
* that consecutive operators are evaluated left to right. | |
* <p> | |
* Marking an operator as unary binds the precedence for the specified | |
* side such that it always has maximum precedence when considered from | |
* the opposite side. | |
*/ | |
public Operator(char sym, int prclft, int prcrgt, int unibnd, OperatorHandler hnd) { | |
this(sym, prclft, prcrgt, unibnd, false, hnd); | |
if (prclft < 0 || prclft > 99) { | |
throw new IllegalArgumentException("Operator precendence must be 0 - 99"); | |
} | |
if (prcrgt < 0 || prcrgt > 99) { | |
throw new IllegalArgumentException("Operator precendence must be 0 - 99"); | |
} | |
if (handler == null) { | |
throw new IllegalArgumentException("Operator handler is required"); | |
} | |
} | |
Operator(char sym, int prclft, int prcrgt, int unibnd, boolean intern, OperatorHandler hnd) { | |
symbol = sym; | |
precedenceL = prclft; | |
precedenceR = prcrgt; | |
unary = unibnd; | |
internal = intern; | |
handler = hnd; | |
} | |
public String toString() { | |
return ("MathOperator['" + symbol + "']"); | |
} | |
} | |
// ************************************************************************************************* | |
// STATIC NESTED CLASSES - OPERATION EVALUATOR INTERFACE | |
// ************************************************************************************************* | |
static public interface OperatorHandler { | |
public double evaluateOperator(double lft, char opr, double rgt) throws ArithmeticException; | |
} | |
// ************************************************************************************************* | |
// STATIC NESTED CLASSES - FUNCTION EVALUATOR INTERFACE | |
// ************************************************************************************************* | |
static public interface FunctionHandler { | |
public double evaluateFunction(String fncnam, ArgParser fncargs) throws ArithmeticException; | |
} | |
// ************************************************************************************************* | |
// STATIC NESTED CLASSES - DEFAULT OPERATOR/FUNCTION IMPLEMENTATION | |
// ************************************************************************************************* | |
/** | |
* An implementation of the default supported operations and functions. | |
*/ | |
static class DefaultImpl extends Object implements OperatorHandler, FunctionHandler { | |
private DefaultImpl() { | |
} | |
// To add/remove operators change evaluateOperator() and registration | |
public double evaluateOperator(double lft, char opr, double rgt) { | |
switch (opr) { | |
case '=': | |
return rgt; // simple assignment, used as the final operation, | |
// must be maximum precedence | |
case 'E': | |
return lft * Math.pow(10, rgt); // power | |
case '^': | |
return Math.pow(lft, rgt); // power | |
case '±': | |
return -rgt; // unary negation | |
case '*': | |
return lft * rgt; // multiply (classical) | |
case '×': | |
return lft * rgt; // multiply (because it's a Unicode world out | |
// there) | |
case '·': | |
return lft * rgt; // multiply (because it's a Unicode world out | |
// there) | |
case '(': | |
return lft * rgt; // multiply (implicit due to brackets, e.g | |
// "(a)(b)") | |
case '/': | |
return lft / rgt; // divide (classical computing) | |
case '÷': | |
return lft / rgt; // divide (because it's a Unicode world out | |
// there) | |
case '%': | |
return lft % rgt; // remainder | |
case '+': | |
return lft + rgt; // add/unary-positive | |
case '-': | |
return lft - rgt; // subtract/unary-negative | |
default: | |
throw new UnsupportedOperationException( | |
"MathEval internal operator setup is incorrect - internal operator \"" + opr | |
+ "\" not handled"); | |
} | |
} | |
// To add/remove functions change evaluateOperator() and registration | |
public double evaluateFunction(String fncnam, ArgParser fncargs) throws ArithmeticException { | |
switch (Character.toLowerCase(fncnam.charAt(0))) { | |
case 'a': { | |
if (fncnam.equalsIgnoreCase("abs")) { | |
return Math.abs(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("acos")) { | |
return Math.acos(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("asin")) { | |
return Math.asin(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("atan")) { | |
return Math.atan(fncargs.next()); | |
} | |
} | |
break; | |
case 'c': { | |
if (fncnam.equalsIgnoreCase("cbrt")) { | |
return Math.cbrt(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("ceil")) { | |
return Math.ceil(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("cos")) { | |
return Math.cos(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("cosh")) { | |
return Math.cosh(fncargs.next()); | |
} | |
} | |
break; | |
case 'e': { | |
if (fncnam.equalsIgnoreCase("exp")) { | |
return Math.exp(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("expm1")) { | |
return Math.expm1(fncargs.next()); | |
} | |
} | |
break; | |
case 'f': { | |
if (fncnam.equalsIgnoreCase("floor")) { | |
return Math.floor(fncargs.next()); | |
} | |
} | |
break; | |
case 'g': { | |
// if(fncnam.equalsIgnoreCase("getExponent" )) { return | |
// Math.getExponent(fncargs.next()); } needs Java 6 | |
} | |
break; | |
case 'l': { | |
if (fncnam.equalsIgnoreCase("log")) { | |
return Math.log(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("log10")) { | |
return Math.log10(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("log1p")) { | |
return Math.log1p(fncargs.next()); | |
} | |
} | |
break; | |
case 'm': { | |
if (fncnam.equalsIgnoreCase("max")) { | |
return Math.max(fncargs.next(), fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("min")) { | |
return Math.min(fncargs.next(), fncargs.next()); | |
} | |
} | |
break; | |
case 'n': { | |
// if(fncnam.equalsIgnoreCase("nextUp" )) { return Math.nextUp | |
// (fncargs.next()); } needs Java 6 | |
} | |
break; | |
case 'r': { | |
if (fncnam.equalsIgnoreCase("random")) { | |
return Math.random(); | |
} // impure | |
if (fncnam.equalsIgnoreCase("round")) { | |
return Math.round(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("roundHE")) { | |
return Math.rint(fncargs.next()); | |
} // round half-even | |
} | |
break; | |
case 's': { | |
if (fncnam.equalsIgnoreCase("signum")) { | |
return Math.signum(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("sin")) { | |
return Math.sin(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("sinh")) { | |
return Math.sinh(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("sqrt")) { | |
return Math.sqrt(fncargs.next()); | |
} | |
} | |
break; | |
case 't': { | |
if (fncnam.equalsIgnoreCase("tan")) { | |
return Math.tan(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("tanh")) { | |
return Math.tanh(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("toDegrees")) { | |
return Math.toDegrees(fncargs.next()); | |
} | |
if (fncnam.equalsIgnoreCase("toRadians")) { | |
return Math.toRadians(fncargs.next()); | |
} | |
} | |
break; | |
case 'u': { | |
if (fncnam.equalsIgnoreCase("ulp")) { | |
return Math.ulp(fncargs.next()); | |
} | |
} | |
break; | |
// no default | |
} | |
throw new UnsupportedOperationException( | |
"MathEval internal function setup is incorrect - internal function \"" + fncnam + "\" not handled"); | |
} | |
static final DefaultImpl INSTANCE = new DefaultImpl(); | |
static private final Operator OPR_EQU = new Operator('=', 99, 99, RIGHT_SIDE, true, DefaultImpl.INSTANCE); // simple | |
// assignment, | |
// used | |
// as | |
// the | |
// final | |
// operation, | |
// must | |
// be | |
// maximum | |
static private final Operator OPR_E = new Operator('E', 80, 81, NO_SIDE, true, DefaultImpl.INSTANCE); // power // precedence | |
static private final Operator OPR_PWR = new Operator('^', 80, 81, NO_SIDE, false, DefaultImpl.INSTANCE); // power | |
static private final Operator OPR_NEG = new Operator('±', 60, 60, RIGHT_SIDE, true, DefaultImpl.INSTANCE); // unary | |
// negation | |
static private final Operator OPR_MLT1 = new Operator('*', 40, DefaultImpl.INSTANCE); // multiply | |
// (classical) | |
static private final Operator OPR_MLT2 = new Operator('×', 40, DefaultImpl.INSTANCE); // multiply | |
// (because | |
// it's | |
// a | |
// Unicode | |
// world | |
// out | |
// there) | |
static private final Operator OPR_MLT3 = new Operator('·', 40, DefaultImpl.INSTANCE); // multiply | |
// (because | |
// it's | |
// a | |
// Unicode | |
// world | |
// out | |
// there) | |
static private final Operator OPR_BKT = new Operator('(', 40, DefaultImpl.INSTANCE); // multiply | |
// (implicit | |
// due | |
// to | |
// brackets, | |
// e.g | |
// "(a)(b)") | |
static private final Operator OPR_DIV1 = new Operator('/', 40, DefaultImpl.INSTANCE); // divide | |
// (classical | |
// computing) | |
static private final Operator OPR_DIV2 = new Operator('÷', 40, DefaultImpl.INSTANCE); // divide | |
// (because | |
// it's | |
// a | |
// Unicode | |
// world | |
// out | |
// there) | |
static private final Operator OPR_MOD = new Operator('%', 40, DefaultImpl.INSTANCE); // remainder | |
static private final Operator OPR_ADD = new Operator('+', 20, DefaultImpl.INSTANCE); // add/unary-positive | |
static private final Operator OPR_SUB = new Operator('-', 20, DefaultImpl.INSTANCE); // subtract/unary-negative | |
// To add/remove operators change evaluateOperator() and registration | |
static void registerOperators(MathEval tgt) { | |
tgt.setOperator(OPR_EQU); | |
tgt.setOperator(OPR_E); | |
tgt.setOperator(OPR_PWR); | |
tgt.setOperator(OPR_NEG); | |
tgt.setOperator(OPR_MLT1); | |
tgt.setOperator(OPR_MLT2); | |
tgt.setOperator(OPR_MLT3); | |
tgt.setOperator(OPR_BKT); | |
tgt.setOperator(OPR_DIV1); | |
tgt.setOperator(OPR_DIV2); | |
tgt.setOperator(OPR_MOD); | |
tgt.setOperator(OPR_ADD); | |
tgt.setOperator(OPR_SUB); | |
} | |
// To add/remove functions change evaluateOperator() and registration | |
static void registerFunctions(MathEval tgt) { | |
tgt.setFunctionHandler("abs", INSTANCE); | |
tgt.setFunctionHandler("acos", INSTANCE); | |
tgt.setFunctionHandler("asin", INSTANCE); | |
tgt.setFunctionHandler("atan", INSTANCE); | |
tgt.setFunctionHandler("cbrt", INSTANCE); | |
tgt.setFunctionHandler("ceil", INSTANCE); | |
tgt.setFunctionHandler("cos", INSTANCE); | |
tgt.setFunctionHandler("cosh", INSTANCE); | |
tgt.setFunctionHandler("exp", INSTANCE); | |
tgt.setFunctionHandler("expm1", INSTANCE); | |
tgt.setFunctionHandler("floor", INSTANCE); | |
// t.setFunctionHandler("getExponent" ,INSTANCE); // needs Java 6 | |
tgt.setFunctionHandler("log", INSTANCE); | |
tgt.setFunctionHandler("log10", INSTANCE); | |
tgt.setFunctionHandler("log1p", INSTANCE); | |
tgt.setFunctionHandler("max", INSTANCE); | |
tgt.setFunctionHandler("min", INSTANCE); | |
// t.setFunctionHandler("nextUp" ,INSTANCE); // needs Java 6 | |
tgt.setFunctionHandler("random", INSTANCE, true); // impure | |
tgt.setFunctionHandler("round", INSTANCE); | |
tgt.setFunctionHandler("roundHE", INSTANCE); // round half-even | |
tgt.setFunctionHandler("signum", INSTANCE); | |
tgt.setFunctionHandler("sin", INSTANCE); | |
tgt.setFunctionHandler("sinh", INSTANCE); | |
tgt.setFunctionHandler("sqrt", INSTANCE); | |
tgt.setFunctionHandler("tan", INSTANCE); | |
tgt.setFunctionHandler("tanh", INSTANCE); | |
tgt.setFunctionHandler("toDegrees", INSTANCE); | |
tgt.setFunctionHandler("toRadians", INSTANCE); | |
tgt.setFunctionHandler("ulp", INSTANCE); | |
} | |
} | |
// ************************************************************************************************* | |
// STATIC PROPERTIES | |
// ************************************************************************************************* | |
/** Operator/operand on on the left. */ | |
static public final int LEFT_SIDE = 'L'; | |
/** Operator/operand on on the right. */ | |
static public final int RIGHT_SIDE = 'R'; | |
/** Operator/operand side is immaterial. */ | |
static public final int NO_SIDE = 'B'; | |
/** Implementation for the default operators. */ | |
static public final OperatorHandler DFT_OPERATOR_HANDLER = DefaultImpl.INSTANCE; | |
/** Implementation for the default function (java.lang.Math). */ | |
static public final FunctionHandler DFT_FUNCTION_HANDLER = DefaultImpl.INSTANCE; | |
static private final Operator OPERAND = new Operator('\0', 0, 0, NO_SIDE, false, null); // special | |
// "non-operator" | |
// representing | |
// an | |
// operand | |
// character | |
// ************************************************************************************************* | |
// STATIC METHODS - UTILITY | |
// ************************************************************************************************* | |
} // END CLASS |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment