Skip to content

Instantly share code, notes, and snippets.

@justisr
Last active February 24, 2017 14:23
Show Gist options
  • Save justisr/f4f037283905a2894926be7836247066 to your computer and use it in GitHub Desktop.
Save justisr/f4f037283905a2894926be7836247066 to your computer and use it in GitHub Desktop.
Evaluate algebraic equations from a string
// 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