diff --git a/java_console/models/src/com/autsia/bracer/test/BracerParserTest.java b/java_console/models/src/com/autsia/bracer/test/BracerParserTest.java index 58f455f491..08b0824246 100644 --- a/java_console/models/src/com/autsia/bracer/test/BracerParserTest.java +++ b/java_console/models/src/com/autsia/bracer/test/BracerParserTest.java @@ -16,15 +16,13 @@ package com.autsia.bracer.test; -import java.text.ParseException; -import java.util.Collection; - -import org.junit.Assert; +import com.autsia.bracer.BracerParser; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import com.autsia.bracer.BracerParser; +import java.text.ParseException; +import java.util.Collection; import static org.junit.Assert.assertEquals; @@ -58,6 +56,7 @@ public class BracerParserTest { @Test public void testBooleanConversion() throws ParseException { + assertParse("2 1 >", "2 > 1"); assertParse("rpm 0 >", "rpm > false"); assertParse("rpm 0 >", "(rpm > false)"); assertParse("rpm user0 > clt user2 > | vbatt user1 > |", "(rpm > user0) or (clt > user2) or (vbatt > user1)"); @@ -92,27 +91,41 @@ public class BracerParserTest { @Test public void testRusEfi() throws ParseException { - bracerParser.parse("(time_since_boot < 4) | (rpm > 0)"); - assertEquals("time_since_boot 4 < rpm 0 > |", bracerParser.getRusEfi()); + assertParse("time_since_boot 4 <", "(time_since_boot < 4)"); + assertParse("1 4 |", "1 | 4"); + assertParse("time_since_boot 4 < rpm 0 > |", "(time_since_boot < 4) | (rpm > 0)"); - bracerParser.parse("(fan and (coolant > fan_off_setting)) OR (coolant > fan_on_setting)"); - assertEquals("fan coolant fan_off_setting > & coolant fan_on_setting > OR", bracerParser.getRusEfi()); + assertParse("1 4 |", "1 or 4"); - bracerParser.parse("(time_since_boot <= 4) | (rpm > 0)"); - assertEquals("time_since_boot 4 <= rpm 0 > |", bracerParser.getRusEfi()); + assertParse("1 4 &", "1 & 4"); - bracerParser.parse("(time_since_boot <= 4) | (rpm > 0)"); - assertEquals("time_since_boot 4 <= rpm 0 > |", bracerParser.getRusEfi()); + assertParse("coolant fan_off_setting >", "(coolant > fan_off_setting)"); + assertParse("1 3 |", "(1) or (3)"); + assertParse("1 3 &", "1 and 3"); + assertParse("1 coolant fan_on_setting > |", "1 | (coolant > fan_on_setting)"); + assertParse("fan coolant fan_off_setting > & coolant fan_on_setting > OR", "(fan and (coolant > fan_off_setting)) OR (coolant > fan_on_setting)"); - bracerParser.parse("(time_since_boot <= 4) OR (rpm > 0)"); - assertEquals("time_since_boot 4 <= rpm 0 > OR", bracerParser.getRusEfi()); + assertParse("time_since_boot 4 <= rpm 0 > |", "(time_since_boot <= 4) | (rpm > 0)"); - bracerParser.parse("(self and (rpm > 4800)) OR (rpm > 5000)"); - assertEquals("self rpm 4800 > & rpm 5000 > OR", bracerParser.getRusEfi()); + assertParse("time_since_boot 4 <= rpm 0 > |", "(time_since_boot <= 4) | (rpm > 0)"); + + assertParse("time_since_boot 4 <= rpm 0 > OR", "(time_since_boot <= 4) OR (rpm > 0)"); + + assertParse("self rpm 4800 > & rpm 5000 > OR", "(self and (rpm > 4800)) OR (rpm > 5000)"); + } + + private void assertValue(String expectedRpn, String expression, double expectedValue) { + try { + assertEquals(Integer.toString((int) expectedValue), bracerParser.parse(expression).evaluate()); + } catch (ParseException e) { + throw new IllegalStateException(e); + } + assertEquals("RPN", expectedRpn, bracerParser.getRusEfi()); } @Test public void testBooleanNot2() throws Exception { + assertValue("0 !", "! false", 0); assertEquals("0", bracerParser.parse("! false").evaluate()); assertEquals("0", bracerParser.parse("! 0").evaluate()); assertEquals("0", bracerParser.parse("1 & not(false)").evaluate()); diff --git a/java_console/models/src/com/fathzer/soft/javaluator/AbstractEvaluator.java b/java_console/models/src/com/fathzer/soft/javaluator/AbstractEvaluator.java new file mode 100644 index 0000000000..64aaa6cf78 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/AbstractEvaluator.java @@ -0,0 +1,470 @@ +package com.fathzer.soft.javaluator; + +import sun.security.provider.certpath.CollectionCertStore; + +import java.util.*; + +/** An abstract evaluator, able to evaluate infix expressions. + *
Some standard evaluators are included in the library, you can define your own by subclassing this class. + *
This class is thread safe. + * @param The type of values handled by the evaluator + * @author Jean-Marc Astesana + * @see License information + */ +public abstract class AbstractEvaluator { + private final Tokenizer tokenizer; + private final Map functions; + private final Map> operators; + private final Map constants; + private final String functionArgumentSeparator; + private final Map functionBrackets; + private final Map expressionBrackets; + + public final Stack stackRPN = new Stack() { + @Override + public String push(String t) { + System.out.println("RPN push " + t); + return super.push(t); + } + + }; + + + /** Constructor. + * @param parameters The evaluator parameters. + *
Please note that there's no side effect between the evaluator and the parameters. + * So, changes made to the parameters after the call to this constructor are ignored by the instance. + */ + protected AbstractEvaluator(Parameters parameters) { + //TODO if constants, operators, functions are duplicated => error + final ArrayList tokenDelimitersBuilder = new ArrayList(); + this.functions = new HashMap(); + this.operators = new HashMap>(); + this.constants = new HashMap(); + this.functionBrackets = new HashMap(); + for (final BracketPair pair : parameters.getFunctionBrackets()) { + functionBrackets.put(pair.getOpen(), pair); + functionBrackets.put(pair.getClose(), pair); + tokenDelimitersBuilder.add(pair.getOpen()); + tokenDelimitersBuilder.add(pair.getClose()); + } + this.expressionBrackets = new HashMap(); + for (final BracketPair pair : parameters.getExpressionBrackets()) { + expressionBrackets.put(pair.getOpen(), pair); + expressionBrackets.put(pair.getClose(), pair); + tokenDelimitersBuilder.add(pair.getOpen()); + tokenDelimitersBuilder.add(pair.getClose()); + } + if (operators!=null) { + for (Operator ope : parameters.getOperators()) { + tokenDelimitersBuilder.add(ope.getSymbol()); + List known = this.operators.get(ope.getSymbol()); + if (known==null) { + known = new ArrayList(); + this.operators.put(ope.getSymbol(), known); + } + known.add(ope); + if (known.size()>1) { + validateHomonyms(known); + } + } + } + boolean needFunctionSeparator = false; + if (parameters.getFunctions()!=null) { + for (Function function : parameters.getFunctions()) { + this.functions.put(parameters.getTranslation(function.getName()), function); + if (function.getMaximumArgumentCount()>1) { + needFunctionSeparator = true; + } + } + } + if (parameters.getConstants()!=null) { + for (Constant constant : parameters.getConstants()) { + this.constants.put(parameters.getTranslation(constant.getName()), constant); + } + } + functionArgumentSeparator = parameters.getFunctionArgumentSeparator(); + if (needFunctionSeparator) { + tokenDelimitersBuilder.add(functionArgumentSeparator); + } + tokenizer = new Tokenizer(tokenDelimitersBuilder); + } + + /** Validates that homonym operators are valid. + *
Homonym operators are operators with the same name (like the unary - and the binary - operators) + *
This method is called when homonyms are passed to the constructor. + *
This default implementation only allows the case where there's two operators, one binary and one unary. + * Subclasses can override this method in order to accept others configurations. + * @param operators The operators to validate. + * @throws IllegalArgumentException if the homonyms are not compatibles. + * @see #guessOperator(Token, List) + */ + protected void validateHomonyms(List operators) { + if (operators.size()>2) { + throw new IllegalArgumentException(); + } + } + + /** When a token can be more than one operator (homonym operators), this method guesses the right operator. + *
A very common case is the - sign in arithmetic computation which can be an unary or a binary operator, depending + * on what was the previous token. + *
Warning: maybe the arguments of this function are not enough to deal with all the cases. + * So, this part of the evaluation is in alpha state (method may change in the future). + * @param previous The last parsed tokens (the previous token in the infix expression we are evaluating). + * @param candidates The candidate tokens. + * @return A token + * @see #validateHomonyms(List) + */ + protected Operator guessOperator(Token previous, List candidates) { + final int argCount = ((previous!=null) && (previous.isCloseBracket() || previous.isLiteral())) ? 2 : 1; + for (Operator operator : candidates) { + if (operator.getOperandCount()==argCount) { + return operator; + } + } + return null; + } + + @SuppressWarnings("unchecked") + private void output(Deque values, Token token, Object evaluationContext) { + if (token.isLiteral()) { // If the token is a literal, a constant, or a variable name + String literal = token.getLiteral(); + Constant ct = this.constants.get(literal); + T value = ct==null?null:evaluate(ct, evaluationContext); + if (value==null && evaluationContext!=null && (evaluationContext instanceof AbstractVariableSet)) { + value = ((AbstractVariableSet)evaluationContext).get(literal); + } + values.push(value!=null ? value : toValue(literal, evaluationContext)); + } else if (token.isOperator()) { + Operator operator = token.getOperator(); + stackRPN.push(operator.getRpnSymbol()); + values.push(evaluate(operator, getArguments(values, operator.getOperandCount()), evaluationContext)); + } else { + throw new IllegalArgumentException(); + } + } + + /** Evaluates a constant. + *
Subclasses that support constants must override this method. + * The default implementation throws a RuntimeException meaning that implementor forget to implement this method + * while creating a subclass that accepts constants. + * @param constant The constant + * @param evaluationContext The context of the evaluation + * @return The constant's value + */ + protected T evaluate(Constant constant, Object evaluationContext) { + throw new RuntimeException("evaluate(Constant) is not implemented for "+constant.getName()); + } + + /** Evaluates an operation. + *
Subclasses that support operators must override this method. + * The default implementation throws a RuntimeException meaning that implementor forget to implement this method + * while creating a subclass that accepts operators. + * @param operator The operator + * @param operands The operands + * @param evaluationContext The context of the evaluation + * @return The result of the operation + */ + protected T evaluate(Operator operator, Iterator operands, Object evaluationContext) { + throw new RuntimeException("evaluate(Operator, Iterator) is not implemented for "+operator.getSymbol()); + } + + /** Evaluates a function. + *
Subclasses that support functions must override this method. + * The default implementation throws a RuntimeException meaning that implementor forget to implement this method + * while creating a subclass that accepts functions. + * @param function The function + * @param arguments The function's arguments + * @param evaluationContext The context of the evaluation + * @return The result of the function + */ + protected T evaluate(Function function, Iterator arguments, Object evaluationContext) { + throw new RuntimeException("evaluate(Function, Iterator) is not implemented for "+function.getName()); + } + + private void doFunction(Deque values, Function function, int argCount, Object evaluationContext) { + System.out.println("doFunction " + function + " " + argCount); + + if (function.getMinimumArgumentCount()>argCount || function.getMaximumArgumentCount() getArguments(Deque values, int nb) { + // Be aware that arguments are in reverse order on the values stack. + // Don't forget to reorder them in the original order (the one they appear in the evaluated formula) + if (values.size() result = new LinkedList(); + for (int i = 0; i This context is an object that can contain useful dynamic data, for example the values of the variables + * used in the expression (Use an AbstractVariableSet to do that).
The context is not limited to variable values but + * can be used for any dynamic information. A good example is the BooleanSetEvaluator one. + * @return the result of the evaluation. + * @throws IllegalArgumentException if the expression is not correct. + * @see AbstractVariableSet + */ + public T evaluate(String expression, Object evaluationContext) { + stackRPN.clear(); + final Deque values = new ArrayDeque() { + @Override + public void push(T t) { + System.out.println("v push " + t); + super.push(t); + } + }; // values stack + final Deque stack = new ArrayDeque() { + @Override + public void push(Token t) { + System.out.println("fun push " + t); + super.push(t); + } + + }; // operator stack + final Deque previousValuesSize = functions.isEmpty()?null:new ArrayDeque(); + final Iterator tokens = tokenize(expression); + Token previous = null; + while (tokens.hasNext()) { + // read one token from the input stream + String strToken = tokens.next(); + final Token token = toToken(previous, strToken); + if (token.isOpenBracket()) { + // If the token is a left parenthesis, then push it onto the stack. + stack.push(token); + if (previous!=null && previous.isFunction()) { + if (!functionBrackets.containsKey(token.getBrackets().getOpen())) { + throw new IllegalArgumentException("Invalid bracket after function: "+strToken); + } + } else { + if (!expressionBrackets.containsKey(token.getBrackets().getOpen())) { + throw new IllegalArgumentException("Invalid bracket in expression: "+strToken); + } + } + } else if (token.isCloseBracket()) { + System.out.println("isCloseBracket"); + if (previous==null) { + throw new IllegalArgumentException("expression can't start with a close bracket"); + } + if (previous.isFunctionArgumentSeparator()) { + throw new IllegalArgumentException("argument is missing"); + } + BracketPair brackets = token.getBrackets(); + // If the token is a right parenthesis: + boolean openBracketFound = false; + // Until the token at the top of the stack is a left parenthesis, + // pop operators off the stack onto the output queue + while (!stack.isEmpty()) { + Token sc = stack.pop(); + if (sc.isOpenBracket()) { + if (sc.getBrackets().equals(brackets)) { + System.out.println("close>isOpenBracket"); + openBracketFound = true; + break; + } else { + throw new IllegalArgumentException("Invalid parenthesis match "+sc.getBrackets().getOpen()+brackets.getClose()); + } + } else { + output(values, sc, evaluationContext); + } + } + if (!openBracketFound) { + // If the stack runs out without finding a left parenthesis, then + // there are mismatched parentheses. + throw new IllegalArgumentException("Parentheses mismatched"); + } + if (!stack.isEmpty() && stack.peek().isFunction()) { + stackRPN.push(stack.peek().getLiteral()); + // If the token at the top of the stack is a function token, pop it + // onto the output queue. + int argCount = values.size()-previousValuesSize.pop(); + doFunction(values, (Function)stack.pop().getFunction(), argCount, evaluationContext); + } + } else if (token.isFunctionArgumentSeparator()) { + if (previous==null) { + throw new IllegalArgumentException("expression can't start with a function argument separator"); + } + // Verify that there was an argument before this separator + if (previous.isOpenBracket() || previous.isFunctionArgumentSeparator()) { + // The cases were operator miss an operand are detected elsewhere. + throw new IllegalArgumentException("argument is missing"); + } + // If the token is a function argument separator + boolean pe = false; + while (!stack.isEmpty()) { + if (stack.peek().isOpenBracket()) { + pe = true; + break; + } else { + // Until the token at the top of the stack is a left parenthesis, + // pop operators off the stack onto the output queue. + output(values, stack.pop(), evaluationContext); + } + } + if (!pe) { + // If no left parentheses are encountered, either the separator was misplaced + // or parentheses were mismatched. + throw new IllegalArgumentException("Separator or parentheses mismatched"); + } + } else if (token.isFunction()) { + // If the token is a function token, then push it onto the stack. + stack.push(token); + previousValuesSize.push(values.size()); + } else if (token.isOperator()) { + // If the token is an operator, op1, then: + while (!stack.isEmpty()) { + Token sc = stack.peek(); + // While there is an operator token, o2, at the top of the stack + // op1 is left-associative and its precedence is less than or equal + // to that of op2, + // or op1 has precedence less than that of op2, + // Let + and ^ be right associative. + // Correct transformation from 1^2+3 is 12^3+ + // The differing operator priority decides pop / push + // If 2 operators have equal priority then associativity decides. + if (sc.isOperator() + && ((token.getAssociativity().equals(Operator.Associativity.LEFT) && (token.getPrecedence() <= sc.getPrecedence())) || + (token.getPrecedence() < sc.getPrecedence()))) { + stackRPN.push(sc.getLiteral()); + // Pop o2 off the stack, onto the output queue; + output(values, stack.pop(), evaluationContext); + } else { + break; + } + } + // push op1 onto the stack. + stack.push(token); + } else { + // If the token is a number (identifier), a constant or a variable, then add its value to the output queue. + if ((previous!=null) && previous.isLiteral()) { + throw new IllegalArgumentException("A literal can't follow another literal"); + } + stackRPN.push(token.getBooleanHackedLiteral()); + output(values, token, evaluationContext); + } + previous = token; + } + // When there are no more tokens to read: + // While there are still operator tokens in the stack: + while (!stack.isEmpty()) { + Token sc = stack.pop(); + if (sc.isOpenBracket() || sc.isCloseBracket()) { + throw new IllegalArgumentException("Parentheses mismatched"); + } + if (sc.isOperator()) { +// stackRPN.push(sc.getOperator().getRpnSymbol()); + } + output(values, sc, evaluationContext); + } + if (values.size()!=1) { + throw new IllegalArgumentException(); + } + Collections.reverse(stackRPN); + + return values.pop(); + } + + public String getRusEfi() { + List list = new ArrayList<>(stackRPN); + ListIterator li = list.listIterator(list.size()); +// List reverse = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + while (li.hasPrevious()) { + if (sb.length() > 0) + sb.append(" "); + sb.append(li.previous()); + } +// String result = reverse.toString(); + return sb.toString(); + } + + + private Token toToken(Token previous, String token) { + if (token.equals(functionArgumentSeparator)) { + return Token.FUNCTION_ARG_SEPARATOR; + } else if (functions.containsKey(token)) { + return Token.buildFunction(functions.get(token)); + } else if (operators.containsKey(token)) { + List list = operators.get(token); + return (list.size()==1) ? Token.buildOperator(list.get(0)) : Token.buildOperator(guessOperator(previous, list)); + } else { + final BracketPair brackets = getBracketPair(token); + if (brackets!=null) { + if (brackets.getOpen().equals(token)) { + return Token.buildOpenToken(brackets); + } else { + return Token.buildCloseToken(brackets); + } + } else { + return Token.buildLiteral(token); + } + } + } + + private BracketPair getBracketPair(String token) { + BracketPair result = expressionBrackets.get(token); + return result==null ? functionBrackets.get(token) : result; + } + + /** Gets the operators supported by this evaluator. + * @return a collection of operators. + */ + public Collection getOperators() { + ArrayList result = new ArrayList(); + Collection> values = this.operators.values(); + for (List list : values) { + result.addAll(list); + } + return result; + } + + /** Gets the functions supported by this evaluator. + * @return a collection of functions. + */ + public Collection getFunctions() { + return this.functions.values(); + } + + /** Gets the constants supported by this evaluator. + * @return a collection of constants. + */ + public Collection getConstants() { + return this.constants.values(); + } + + /** Converts the evaluated expression into tokens. + *
Example: The result for the expression "-1+min(10,3)" is an iterator on "-", "1", "+", "min", "(", "10", ",", "3", ")". + *
By default, the operators symbols, the brackets and the function argument separator are used as delimiter in the string. + * @param expression The expression that is evaluated + * @return A string iterator. + */ + protected Iterator tokenize(String expression) { + return tokenizer.tokenize(expression); + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/AbstractVariableSet.java b/java_console/models/src/com/fathzer/soft/javaluator/AbstractVariableSet.java new file mode 100644 index 0000000000..5185dfabf0 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/AbstractVariableSet.java @@ -0,0 +1,16 @@ +package com.fathzer.soft.javaluator; + +/** An abstract variable set. + *
Javaluator supports expression that contains variables (for example sin(x)). + *
An AbstractVariableSet converts, during the expression evaluation, each variable to its value. + * @param The type of the values of the variable (the one handled by the evaluator). + * @author Jean-Marc Astesana + * @see License information + */ +public interface AbstractVariableSet { + /** Gets the value of a variable. + * @param variableName The name of a variable + * @return the variable's value or null if the variable is unknown + */ + public T get(String variableName); +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/BracketPair.java b/java_console/models/src/com/fathzer/soft/javaluator/BracketPair.java new file mode 100644 index 0000000000..f10df76408 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/BracketPair.java @@ -0,0 +1,48 @@ +package com.fathzer.soft.javaluator; + +/** A bracket pair. + * @author Jean-Marc Astesana + * @see License information + */ +public class BracketPair { + /** The parentheses pair: ().*/ + public static final BracketPair PARENTHESES = new BracketPair('(', ')'); + /** The square brackets pair: [].*/ + public static final BracketPair BRACKETS = new BracketPair('[', ']'); + /** The braces pair: {}.*/ + public static final BracketPair BRACES = new BracketPair('{', '}'); + /** The angle brackets pair: <>.*/ + public static final BracketPair ANGLES = new BracketPair('<', '>'); + + private String open; + private String close; + + /** Constructor. + * @param open The character used to open the brackets. + * @param close The character used to close the brackets. + */ + public BracketPair(char open, char close) { + super(); + this.open = new String(new char[]{open}); + this.close = new String(new char[]{close}); + } + + /** Gets the open bracket character. + * @return a char + */ + public String getOpen() { + return open; + } + + /** Gets the close bracket character. + * @return a char + */ + public String getClose() { + return close; + } + + @Override + public String toString() { + return open + close; + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Constant.java b/java_console/models/src/com/fathzer/soft/javaluator/Constant.java new file mode 100644 index 0000000000..85b2cc5db7 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Constant.java @@ -0,0 +1,31 @@ +package com.fathzer.soft.javaluator; + +/** + * A constant in an expression. + *
Some expressions needs constants. For instance it is impossible to perform trigonometric calculus without using pi. + * A constant allows you to use mnemonic in your expressions instead of the raw value of the constant. + *
A constant for pi would be defined by :
+ * Constant pi = new Constant("pi"); + *
With such a constant, you will be able to evaluate the expression "sin(pi/4)" + * @author Jean-Marc Astesana + * @see License information + * @see AbstractEvaluator#evaluate(Constant, Object) + */ +public class Constant { + private String name; + + /** Constructor + * @param name The mnemonic of the constant. + *
The name is used in expressions to identified the constants. + */ + public Constant(String name) { + this.name = name; + } + + /** Gets the mnemonic of the constant. + * @return the id + */ + public String getName() { + return name; + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/DoubleEvaluator.java b/java_console/models/src/com/fathzer/soft/javaluator/DoubleEvaluator.java new file mode 100644 index 0000000000..763b41e4e7 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/DoubleEvaluator.java @@ -0,0 +1,349 @@ +package com.fathzer.soft.javaluator; + +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Locale; + +/** An evaluator that is able to evaluate arithmetic expressions on real numbers. + *
Built-in operators:
    + *
  • +: Addition
  • + *
  • -: Subtraction
  • + *
  • -: Unary minus
  • + *
  • *: Multiplication
  • + *
  • /: Division
  • + *
  • ^: Exponentiation.
    Warning: Exponentiation is implemented using java.lang.Math.pow which has some limitations (please read oracle documentation about this method to known details).
    For example (-1)^(1/3) returns NaN.
  • + *
  • %: Modulo
  • + *
+ * Built-in functions:
    + *
  • abs: absolute value
  • + *
  • acos: arc cosine
  • + *
  • asin: arc sine
  • + *
  • atan: arc tangent
  • + *
  • average: average of arguments
  • + *
  • ceil: nearest upper integer
  • + *
  • cos: cosine
  • + *
  • cosh: hyperbolic cosine
  • + *
  • floor: nearest lower integer
  • + *
  • ln: natural logarithm (base e)
  • + *
  • log: base 10 logarithm
  • + *
  • max: maximum of arguments
  • + *
  • min: minimum of arguments
  • + *
  • round: nearest integer
  • + *
  • sin: sine
  • + *
  • sinh: hyperbolic sine
  • + *
  • sum: sum of arguments
  • + *
  • tan: tangent
  • + *
  • tanh: hyperbolic tangent
  • + *
  • random: pseudo-random number (between 0 and 1)
  • + *
+ * Built-in constants:
    + *
  • e: Base of natural algorithms
  • + *
  • pi: Ratio of the circumference of a circle to its diameter
  • + *
+ * @author Jean-Marc Astesana + * @see License information + */ +public class DoubleEvaluator extends AbstractEvaluator { + /** The order or operations (operator precedence) is not clearly defined, especially between the unary minus operator and exponentiation + * operator (see http://en.wikipedia.org/wiki/Order_of_operations). + * These constants define the operator precedence styles. + */ + public static enum Style { + /** The most commonly operator precedence, where the unary minus as a lower precedence than the exponentiation. + *
With this style, used by Google, Wolfram alpha, and many others, -2^2=-4. + */ + STANDARD, + } + + /** The true constant. */ + public final static Constant TRUE = new Constant("true"); + /** The false constant. */ + public final static Constant FALSE = new Constant("false"); + + /** Returns the smallest integer >= argument */ + public static final Function CEIL = new Function("ceil", 1); + /** Returns the largest integer <= argument */ + public static final Function FLOOR = new Function("floor", 1); + /** Returns the closest integer of a number */ + public static final Function ROUND = new Function("round", 1); + /** Returns the absolute value of a number */ + public static final Function ABS = new Function("abs", 1); + + /** Returns the trigonometric sine of an angle. The angle is expressed in radian.*/ + public static final Function SINE = new Function("sin", 1); + /** Returns the trigonometric cosine of an angle. The angle is expressed in radian.*/ + public static final Function COSINE = new Function("cos", 1); + /** Returns the trigonometric tangent of an angle. The angle is expressed in radian.*/ + public static final Function TANGENT = new Function("tan", 1); + /** Returns the trigonometric arc-cosine of an angle. The angle is expressed in radian.*/ + public static final Function ACOSINE = new Function("acos", 1); + /** Returns the trigonometric arc-sine of an angle. The angle is expressed in radian.*/ + public static final Function ASINE = new Function("asin", 1); + /** Returns the trigonometric arc-tangent of an angle. The angle is expressed in radian.*/ + public static final Function ATAN = new Function("atan", 1); + + /** Returns the hyperbolic sine of a number.*/ + public static final Function SINEH = new Function("sinh", 1); + /** Returns the hyperbolic cosine of a number.*/ + public static final Function COSINEH = new Function("cosh", 1); + /** Returns the hyperbolic tangent of a number.*/ + public static final Function TANGENTH = new Function("tanh", 1); + + /** Returns the minimum of n numbers (n>=1) */ + public static final Function MIN = new Function("min", 1, Integer.MAX_VALUE); + /** Returns the maximum of n numbers (n>=1) */ + public static final Function MAX = new Function("max", 1, Integer.MAX_VALUE); + /** Returns the sum of n numbers (n>=1) */ + public static final Function SUM = new Function("sum", 1, Integer.MAX_VALUE); + /** Returns the average of n numbers (n>=1) */ + public static final Function AVERAGE = new Function("avg", 1, Integer.MAX_VALUE); + + /** Returns the natural logarithm of a number */ + public static final Function LN = new Function("ln", 1); + /** Returns the decimal logarithm of a number */ + public static final Function LOG = new Function("log", 1); + + /** Returns a pseudo random number */ + public static final Function RANDOM = new Function("random", 0); + + /** The negate unary operator in the standard operator precedence.*/ + public static final Operator NEGATE = new Operator("-", 1, Operator.Associativity.RIGHT, 3, "negate"); + /** The negate unary operator in the Excel like operator precedence.*/ + public static final Operator NEGATE_HIGH = new Operator("-", 1, Operator.Associativity.RIGHT, 5); + + + public static final Operator MORE = new Operator(">", 2, Operator.Associativity.LEFT, 6); + public static final Operator MORE_EQ = new Operator(">=", 2, Operator.Associativity.LEFT, 6); + + public static final Operator LESS = new Operator("<", 2, Operator.Associativity.LEFT, 6); + public static final Operator LESS_EQ = new Operator("<=", 2, Operator.Associativity.LEFT, 6); + + public static final Operator OR2 = new Operator("or", 2, Operator.Associativity.LEFT, 12, "|"); + public static final Operator OR = new Operator("|", 2, Operator.Associativity.LEFT, 12); +// https://en.wikipedia.org/wiki/Order_of_operations + public static final Operator AND = new Operator("&", 2, Operator.Associativity.LEFT, 11); + public static final Operator AND2 = new Operator("and", 2, Operator.Associativity.LEFT, 11, "&"); + + /** The substraction operator.*/ + public static final Operator MINUS = new Operator("-", 2, Operator.Associativity.LEFT, 1); + /** The addition operator.*/ + public static final Operator PLUS = new Operator("+", 2, Operator.Associativity.LEFT, 1); + /** The multiplication operator.*/ + public static final Operator MULTIPLY = new Operator("*", 2, Operator.Associativity.LEFT, 2); + /** The division operator.*/ + public static final Operator DIVIDE = new Operator("/", 2, Operator.Associativity.LEFT, 2); + /** The exponentiation operator.*/ + public static final Operator EXPONENT = new Operator("^", 2, Operator.Associativity.LEFT, 4); + /** The modulo operator.*/ + public static final Operator MODULO = new Operator("%", 2, Operator.Associativity.LEFT, 2); + + /** The standard whole set of predefined operators */ + private static final Operator[] OPERATORS = new Operator[]{NEGATE, MORE, MORE_EQ, AND, AND2, OR, OR2, LESS, LESS_EQ, MINUS, PLUS, MULTIPLY, DIVIDE, EXPONENT, MODULO}; + + /** The whole set of predefined functions */ + private static final Function[] FUNCTIONS = new Function[]{SINE, COSINE, TANGENT, ASINE, ACOSINE, ATAN, SINEH, COSINEH, TANGENTH, MIN, MAX, SUM, AVERAGE, LN, LOG, ROUND, CEIL, FLOOR, ABS, RANDOM}; + /** The whole set of predefined constants */ + private static final Constant[] CONSTANTS = new Constant[]{TRUE, FALSE}; + + private static Parameters DEFAULT_PARAMETERS; + private static final ThreadLocal FORMATTER = new ThreadLocal() { + @Override + protected NumberFormat initialValue() { + return NumberFormat.getNumberInstance(Locale.US); + } + }; + + /** Gets a copy of DoubleEvaluator standard default parameters. + *
The returned parameters contains all the predefined operators, functions and constants. + *
Each call to this method create a new instance of Parameters. + * @return a Paramaters instance + * @see Style + */ + public static Parameters getDefaultParameters() { + return getDefaultParameters(Style.STANDARD); + } + + /** Gets a copy of DoubleEvaluator default parameters. + *
The returned parameters contains all the predefined operators, functions and constants. + *
Each call to this method create a new instance of Parameters. + * @return a Paramaters instance + */ + public static Parameters getDefaultParameters(Style style) { + Parameters result = new Parameters(); + result.addOperators(Arrays.asList(OPERATORS)); + result.addFunctions(Arrays.asList(FUNCTIONS)); + result.addConstants(Arrays.asList(CONSTANTS)); + result.addFunctionBracket(BracketPair.PARENTHESES); + result.addExpressionBracket(BracketPair.PARENTHESES); + return result; + } + + private static Parameters getParameters() { + if (DEFAULT_PARAMETERS == null) { + DEFAULT_PARAMETERS = getDefaultParameters(); + } + return DEFAULT_PARAMETERS; + } + + /** Constructor. + *
This default constructor builds an instance with all predefined operators, functions and constants. + */ + public DoubleEvaluator() { + this(getParameters()); + } + + /** Constructor. + *
This constructor can be used to reduce the set of supported operators, functions or constants, + * or to localize some function or constant's names. + * @param parameters The parameters of the evaluator. + */ + public DoubleEvaluator(Parameters parameters) { + super(parameters); + } + + @Override + protected Double toValue(String literal, Object evaluationContext) { + ParsePosition p = new ParsePosition(0); + Number result = FORMATTER.get().parse(literal, p); + if (p.getIndex()==0 || p.getIndex()!=literal.length()) { + return 666.0; +// throw new IllegalArgumentException(literal+" is not a number"); + } + return result.doubleValue(); + } + + /* (non-Javadoc) + * @see net.astesana.javaluator.AbstractEvaluator#evaluate(net.astesana.javaluator.Constant) + */ + @Override + protected Double evaluate(Constant constant, Object evaluationContext) { + if (TRUE.equals(constant)) { + return 0.0; + } else if (FALSE.equals(constant)) { + return 0.0; + } else { + return super.evaluate(constant, evaluationContext); + } + } + + /* (non-Javadoc) + * @see net.astesana.javaluator.AbstractEvaluator#evaluate(net.astesana.javaluator.Operator, java.util.Iterator) + */ + @Override + protected Double evaluate(Operator operator, Iterator operands, Object evaluationContext) { + if (NEGATE.equals(operator) || NEGATE_HIGH.equals(operator)) { + return -operands.next(); + } else if (AND.equals(operator) || AND2.equals(operator)) { + return boolean2double((operands.next() == 1.0 ) && (operands.next() == 1.0)); + } else if (OR.equals(operator) || OR2.equals(operator)) { + return boolean2double((operands.next() == 1.0 ) || (operands.next() == 1.0)); + } else if (MORE.equals(operator)) { + return boolean2double(operands.next() > operands.next()); + } else if (MORE_EQ.equals(operator)) { + return boolean2double(operands.next() >= operands.next()); + } else if (LESS.equals(operator)) { + return boolean2double(operands.next() < operands.next()); + } else if (LESS_EQ.equals(operator)) { + return boolean2double(operands.next() <= operands.next()); + } else if (MINUS.equals(operator)) { + return operands.next() - operands.next(); + } else if (PLUS.equals(operator)) { + return operands.next() + operands.next(); + } else if (MULTIPLY.equals(operator)) { + return operands.next() * operands.next(); + } else if (DIVIDE.equals(operator)) { + return operands.next() / operands.next(); + } else if (EXPONENT.equals(operator)) { + return Math.pow(operands.next(),operands.next()); + } else if (MODULO.equals(operator)) { + return operands.next() % operands.next(); + } else { + return super.evaluate(operator, operands, evaluationContext); + } + } + + private double boolean2double(boolean value) { + return value ? 1.0 : 0.0; + } + + /* (non-Javadoc) + * @see net.astesana.javaluator.AbstractEvaluator#evaluate(net.astesana.javaluator.Function, java.util.Iterator) + */ + @Override + protected Double evaluate(Function function, Iterator arguments, Object evaluationContext) { + Double result; + if (ABS.equals(function)) { + result = Math.abs(arguments.next()); + } else if (CEIL.equals(function)) { + result = Math.ceil(arguments.next()); + } else if (FLOOR.equals(function)) { + result = Math.floor(arguments.next()); + } else if (ROUND.equals(function)) { + Double arg = arguments.next(); + if (arg==Double.NEGATIVE_INFINITY || arg==Double.POSITIVE_INFINITY) { + result = arg; + } else { + result = (double) Math.round(arg); + } + } else if (SINEH.equals(function)) { + result = Math.sinh(arguments.next()); + } else if (COSINEH.equals(function)) { + result = Math.cosh(arguments.next()); + } else if (TANGENTH.equals(function)) { + result = Math.tanh(arguments.next()); + } else if (SINE.equals(function)) { + result = Math.sin(arguments.next()); + } else if (COSINE.equals(function)) { + result = Math.cos(arguments.next()); + } else if (TANGENT.equals(function)) { + result = Math.tan(arguments.next()); + } else if (ACOSINE.equals(function)) { + result = Math.acos(arguments.next()); + } else if (ASINE.equals(function)) { + result = Math.asin(arguments.next()); + } else if (ATAN.equals(function)) { + result = Math.atan(arguments.next()); + } else if (MIN.equals(function)) { + result = arguments.next(); + while (arguments.hasNext()) { + result = Math.min(result, arguments.next()); + } + } else if (MAX.equals(function)) { + result = arguments.next(); + while (arguments.hasNext()) { + result = Math.max(result, arguments.next()); + } + } else if (SUM.equals(function)) { + result = 0.; + while (arguments.hasNext()) { + result = result + arguments.next(); + } + } else if (AVERAGE.equals(function)) { + result = 0.; + int nb = 0; + while (arguments.hasNext()) { + result = result + arguments.next(); + nb++; + } + result = result/nb; + } else if (LN.equals(function)) { + result = Math.log(arguments.next()); + } else if (LOG.equals(function)) { + result = Math.log10(arguments.next()); + } else if (RANDOM.equals(function)) { + result = Math.random(); + } else { + result = super.evaluate(function, arguments, evaluationContext); + } + errIfNaN(result, function); + return result; + } + + private void errIfNaN(Double result, Function function) { + if (result.equals(Double.NaN)) { + throw new IllegalArgumentException("Invalid argument passed to "+function.getName()); + } + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Function.java b/java_console/models/src/com/fathzer/soft/javaluator/Function.java new file mode 100644 index 0000000000..7995c25ffb --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Function.java @@ -0,0 +1,71 @@ +package com.fathzer.soft.javaluator; + +/** A function. + * @author Jean-Marc Astesana + * @see License information + */ +public class Function { + private String name; + private int minArgumentCount; + private int maxArgumentCount; + + /** Constructor. + *
This constructor builds a function with a fixed arguments count. + * @param name The function's name + * @param argumentCount The function's argument count. + * @throws IllegalArgumentException if argumentCount is lower than 0 or if the function name is null or empty. + */ + public Function(String name, int argumentCount) { + this(name, argumentCount, argumentCount); + } + + /** Constructor. + *
This constructor builds a function with a variable arguments count. + *
For instance, a minimum function may have at least one argument. + * @param name The function's name + * @param minArgumentCount The function's minimum argument count. + * @param maxArgumentCount The function's maximum argument count (Integer.MAX_VALUE to specify no upper limit). + * @throws IllegalArgumentException if minArgumentCount is less than 0 or greater than maxArgumentCount or if the function name is null or empty. + */ + public Function(String name, int minArgumentCount, int maxArgumentCount) { + if ((minArgumentCount<0) || (minArgumentCount>maxArgumentCount)) { + throw new IllegalArgumentException("Invalid argument count"); + } + if (name==null || name.length()==0) { + throw new IllegalArgumentException("Invalid function name"); + } + this.name = name; + this.minArgumentCount = minArgumentCount; + this.maxArgumentCount = maxArgumentCount; + } + + /** Gets the function's name. + * @return the name of the function + */ + public String getName() { + return this.name; + } + + /** Gets the function's minimum argument count. + * @return an integer + */ + public int getMinimumArgumentCount() { + return this.minArgumentCount; + } + + /** Gets the function's maximum argument count. + * @return an integer + */ + public int getMaximumArgumentCount() { + return this.maxArgumentCount; + } + + @Override + public String toString() { + return "Function{" + + "name='" + name + '\'' + + ", minArgumentCount=" + minArgumentCount + + ", maxArgumentCount=" + maxArgumentCount + + '}'; + } +} \ No newline at end of file diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Operator.java b/java_console/models/src/com/fathzer/soft/javaluator/Operator.java new file mode 100644 index 0000000000..e3c654bbe5 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Operator.java @@ -0,0 +1,138 @@ +package com.fathzer.soft.javaluator; + +/** An operator. + * @author Jean-Marc Astesana + * @see License information + */ +public class Operator { + public final String rpnSymbol; + + /** An Operator's associativity. + */ + public enum Associativity { + /** Left associativity.*/ + LEFT, + /** Right associativity. */ + RIGHT, + /** No associativity.*/ + NONE + } + private String symbol; + private int precedence; + private int operandCount; + private Associativity associativity; + + /** Constructor. + * @param symbol The operator name (Currently, the name's length must be one character). + * @param operandCount The number of operands of the operator (must be 1 or 2). + * @param associativity true if operator is left associative + * @param precedence The precedence of the operator. + *
The precedence is the priority of the operator. An operator with an higher precedence will be executed before an operator with a lower precedence. + * Example : In "1+3*4" * has a higher precedence than +, so the expression is interpreted as 1+(3*4). + * @throws IllegalArgumentException if operandCount if not 1 or 2 or if associativity is none + * @throws NullPointerException if symbol or associativity are null + */ + public Operator(String symbol, int operandCount, Associativity associativity, int precedence) { + this(symbol, operandCount, associativity, precedence, null); + } + + public Operator(String symbol, int operandCount, Associativity associativity, int precedence, String rpnSymbol) { + this.rpnSymbol = rpnSymbol; + if (symbol==null || associativity==null) { + throw new NullPointerException(); + } + if (symbol.length()==0) { + throw new IllegalArgumentException("Operator symbol can't be null"); + } + if ((operandCount<1) || (operandCount>2)) { + throw new IllegalArgumentException("Only unary and binary operators are supported"); + } + if (Associativity.NONE.equals(associativity)) { + throw new IllegalArgumentException("None associativity operators are not supported"); + } + this.symbol = symbol; + this.operandCount = operandCount; + this.associativity = associativity; + this.precedence = precedence; + } + + /** Gets the operator's symbol. + * @return a String + */ + public String getSymbol() { + return this.symbol; + } + + + public String getRpnSymbol() { + if (rpnSymbol != null) + return rpnSymbol; + return symbol; + } + + + /** Gets the operator's operand count. + * @return an integer + */ + public int getOperandCount() { + return this.operandCount; + } + + /** Gets this operator's associativity. + * @return true if the operator is left associative. + * @see Operator's associativity in Wikipedia + */ + public Associativity getAssociativity() { + return this.associativity; + } + + /** Gets the operator's precedence. + * @return an integer + * @see Operator's associativity in Wikipedia + */ + public int getPrecedence() { + return this.precedence; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + operandCount; + result = prime * result + ((associativity == null) ? 0 : associativity.hashCode()); + result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); + result = prime * result + precedence; + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (obj instanceof Operator)) { + return false; + } + Operator other = (Operator) obj; + if ((operandCount != other.operandCount) || (associativity != other.associativity)) { + return false; + } + if (symbol == null) { + if (other.symbol != null) { + return false; + } + } else if (!symbol.equals(other.symbol)) { + return false; + } + if (precedence != other.precedence) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Parameters.java b/java_console/models/src/com/fathzer/soft/javaluator/Parameters.java new file mode 100644 index 0000000000..ade3feeafa --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Parameters.java @@ -0,0 +1,186 @@ +package com.fathzer.soft.javaluator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** The parameters of an evaluator. + *
An evaluator may have different parameters as the supported operators, the supported functions, etc ... + * @author Jean-Marc Astesana + * @see License information + */ +public class Parameters { + private String functionSeparator; + private final List operators; + private final List functions; + private final List constants; + private final Map translations; + private final List expressionBrackets; + private final List functionBrackets; + + /** Constructor. + *
This method builds an instance with no operator, no function, no constant, no translation and no bracket + *
Function argument separator is set to ','. + */ + public Parameters() { + this.operators = new ArrayList(); + this.functions = new ArrayList(); + this.constants = new ArrayList(); + this.translations = new HashMap(); + this.expressionBrackets = new ArrayList(); + this.functionBrackets = new ArrayList(); + setFunctionArgumentSeparator(','); + } + + /** Gets the supported operators. + * @return a Collection of operators. + */ + public Collection getOperators() { + return this.operators; + } + + /** Gets the supported functions. + * @return a Collection of functions. + */ + public Collection getFunctions() { + return this.functions; + } + + /** Gets the supported constants. + * @return a Collection of constants. + */ + public Collection getConstants() { + return this.constants; + } + + /** Gets the supported bracket pairs for expressions. + * @return a Collection of bracket pairs. + */ + public Collection getExpressionBrackets() { + return this.expressionBrackets; + } + + /** Gets the supported bracket pairs for functions. + * @return a Collection of bracket pairs. + */ + public Collection getFunctionBrackets() { + return this.functionBrackets; + } + + /** Adds operators to the supported ones. + * @param operators The operators to be added. + */ + public void addOperators(Collection operators) { + this.operators.addAll(operators); + } + + /** Adds an operator to the supported ones. + * @param operator The added operator + */ + public void add(Operator operator) { + this.operators.add(operator); + } + + /** Adds functions to the supported ones. + * @param functions The functions to be added. + */ + public void addFunctions(Collection functions) { + this.functions.addAll(functions); + } + + /** Adds a function to the supported ones. + * @param function The added function + */ + public void add(Function function) { + this.functions.add(function); + } + + /** Adds constants to the supported ones. + * @param constants The constants to be added. + */ + public void addConstants(Collection constants) { + this.constants.addAll(constants); + } + + /** Adds a constant to the supported ones. + * @param constant The added constant + */ + public void add(Constant constant) { + this.constants.add(constant); + } + + /** Adds a new bracket pair to the expression bracket list. + * @param pair A bracket pair + */ + public void addExpressionBracket(BracketPair pair) { + this.expressionBrackets.add(pair); + } + + /** Adds bracket pairs to the expression bracket list. + * @param brackets The brackets to be added. + */ + public void addExpressionBrackets(Collection brackets) { + this.expressionBrackets.addAll(brackets); + } + + /** Adds a new bracket pair to the function bracket list. + * @param pair A bracket pair + */ + public void addFunctionBracket(BracketPair pair) { + this.functionBrackets.add(pair); + } + + /** Adds bracket pairs to the function bracket list. + * @param brackets The brackets to be added. + */ + public void addFunctionBrackets(Collection brackets) { + this.functionBrackets.addAll(brackets); + } + + /** Sets the translated term for a function. + *
Using this method, you can localize the names of some built-in functions. For instance, + * for french people,you can use this method to use "somme" instead of "sum" with the SUM built-in + * function of DoubleEvaluator. + * @param function The function you want to translate the name + * @param translatedName The translated name + * @see DoubleEvaluator#SUM + */ + public void setTranslation(Function function, String translatedName) { + setTranslation(function.getName(), translatedName); + } + + /** Sets the translated term for a constant. + * @param constant The constant you want to translate the name + * @param translatedName The translated name + * @see #setTranslation(Function, String) + */ + public void setTranslation(Constant constant, String translatedName) { + setTranslation(constant.getName(), translatedName); + } + + private void setTranslation(String name, String translatedName) { + this.translations.put(name, translatedName); + } + + String getTranslation(String originalName) { + String translation = this.translations.get(originalName); + return translation==null?originalName:translation; + } + + /** Sets the function argument separator. + *
Its default value is ','. + * @param separator The new separator + */ + public void setFunctionArgumentSeparator(char separator) { + this.functionSeparator = new String(new char[]{separator}); + } + + /** Gets the function argument separator. + * @return a string + */ + public String getFunctionArgumentSeparator() { + return this.functionSeparator; + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Token.java b/java_console/models/src/com/fathzer/soft/javaluator/Token.java new file mode 100644 index 0000000000..709398c8de --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Token.java @@ -0,0 +1,142 @@ +package com.fathzer.soft.javaluator; + +/** A token. + *
When evaluating an expression, it is first split into tokens. + * These tokens can be operators, constants, etc ... + * @author Jean-Marc Astesana + * @see License information + */ +public class Token { + private enum Kind { + OPEN_BRACKET, + CLOSE_BRACKET, + FUNCTION_SEPARATOR, + FUNCTION, + OPERATOR, + LITERAL + } + static final Token FUNCTION_ARG_SEPARATOR = new Token(Kind.FUNCTION_SEPARATOR, null); + + private Kind kind; + private Object content; + + static Token buildLiteral(String literal) { + return new Token(Kind.LITERAL, literal); + } + + static Token buildOperator(Operator ope) { + return new Token(Kind.OPERATOR, ope); + } + + static Token buildFunction(Function function) { + return new Token(Kind.FUNCTION, function); + } + + static Token buildOpenToken(BracketPair pair) { + return new Token(Kind.OPEN_BRACKET, pair); + } + + static Token buildCloseToken(BracketPair pair) { + return new Token(Kind.CLOSE_BRACKET, pair); + } + + private Token(Kind kind, Object content) { + super(); + if ((kind.equals(Kind.OPERATOR) && !(content instanceof Operator)) || + (kind.equals(Kind.FUNCTION) && !(content instanceof Function)) || + (kind.equals(Kind.LITERAL) && !(content instanceof String))) { + throw new IllegalArgumentException(); + } + this.kind = kind; + this.content = content; + } + + BracketPair getBrackets() { + return (BracketPair) this.content; + } + + Operator getOperator() { + return (Operator) this.content; + } + + Function getFunction() { + return (Function) this.content; + } + + Kind getKind() { + return kind; + } + + /** Tests whether the token is an operator. + * @return true if the token is an operator + */ + public boolean isOperator() { + return kind.equals(Kind.OPERATOR); + } + + /** Tests whether the token is a function. + * @return true if the token is a function + */ + public boolean isFunction() { + return kind.equals(Kind.FUNCTION); + } + + /** Tests whether the token is an open bracket. + * @return true if the token is an open bracket + */ + public boolean isOpenBracket() { + return kind.equals(Kind.OPEN_BRACKET); + } + + /** Tests whether the token is a close bracket. + * @return true if the token is a close bracket + */ + public boolean isCloseBracket() { + return kind.equals(Kind.CLOSE_BRACKET); + } + + /** Tests whether the token is a function argument separator. + * @return true if the token is a function argument separator + */ + public boolean isFunctionArgumentSeparator() { + return kind.equals(Kind.FUNCTION_SEPARATOR); + } + + /** Tests whether the token is a literal or a constant or a variable name. + * @return true if the token is a literal, a constant or a variable name + */ + public boolean isLiteral() { + return kind.equals(Kind.LITERAL); + } + + Operator.Associativity getAssociativity() { + return getOperator().getAssociativity(); + } + + int getPrecedence() { + return getOperator().getPrecedence(); + } + + String getLiteral() { + if (!this.kind.equals(Kind.LITERAL)) { + throw new IllegalArgumentException(); + } + return (String)this.content; + } + + String getBooleanHackedLiteral() { + if ("true".equalsIgnoreCase((String) content)) + return "1"; + if ("false".equalsIgnoreCase((String) content)) + return "0"; + return getLiteral(); + } + + @Override + public String toString() { + return "Token{" + + "kind=" + kind + + ", content=" + content + + '}'; + } +} diff --git a/java_console/models/src/com/fathzer/soft/javaluator/Tokenizer.java b/java_console/models/src/com/fathzer/soft/javaluator/Tokenizer.java new file mode 100644 index 0000000000..4c673342d6 --- /dev/null +++ b/java_console/models/src/com/fathzer/soft/javaluator/Tokenizer.java @@ -0,0 +1,173 @@ +package com.fathzer.soft.javaluator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** A String tokenizer that accepts delimiters that are greater than one character. + * @author Jean-Marc Astesana + * @see License information + */ +public class Tokenizer { + private Pattern pattern; + private String tokenDelimiters; + private boolean trimTokens; + + /** Constructor. + *
By default, this tokenizer trims all the tokens. + * @param delimiters the delimiters of the tokenizer, usually, the operators symbols, the brackets and the function argument separator are used as delimiter in the string. + */ + public Tokenizer(List delimiters) { + if (onlyOneChar(delimiters)) { + StringBuilder builder = new StringBuilder(); + for (String delimiter : delimiters) { + builder.append(delimiter); + } + tokenDelimiters = builder.toString(); + } else { + this.pattern = delimitersToRegexp(delimiters); + } + trimTokens = true; + } + + /** Tests whether this tokens trims the tokens returned by {@link #tokenize(String)} method. + * @return true if tokens are trimmed. + */ + public boolean isTrimTokens() { + return trimTokens; + } + + /** Sets the trimTokens attribute. + * @param trimTokens true to have the tokens returned by {@link #tokenize(String)} method trimmed. + *
Note that empty tokens are always omitted by this class. + */ + public void setTrimTokens(boolean trimTokens) { + this.trimTokens = trimTokens; + } + + /** Tests whether a String list contains only 1 character length elements. + * @param delimiters The list to test + * @return true if it contains only one char length elements (or no elements) + */ + private boolean onlyOneChar(List delimiters) { + for (String delimiter : delimiters) { + if (delimiter.length()!=1) { + return false; + } + } + return true; + } + + private static Pattern delimitersToRegexp(List delimiters) { + // First, create a regular expression that match the union of the delimiters + // Be aware that, in case of delimiters containing others (example && and &), + // the longer may be before the shorter (&& should be before &) or the regexpr + // parser will recognize && as two &. + Collections.sort(delimiters, new Comparator() { + @Override + public int compare(String o1, String o2) { + return -o1.compareTo(o2); + } + }); + // Build a string that will contain the regular expression + StringBuilder result = new StringBuilder(); + result.append('('); + for (String delim : delimiters) { + // For each delimiter + if (result.length()!=1) { + // Add it to the union + result.append('|'); + } + // Quote the delimiter as it could contain some regexpr reserved characters + result.append("\\Q").append(delim).append("\\E"); + } + result.append(')'); + return Pattern.compile(result.toString()); + } + + private void addToTokens (List tokens, String token) { + if (trimTokens) { + token = token.trim(); + } + if (!token.isEmpty()) { + tokens.add(token); + } + } + + /** Converts a string into tokens. + *
Example: The result for the expression "-1+min(10,3)" evaluated for a DoubleEvaluator is an iterator on "-", "1", "+", "min", "(", "10", ",", "3", ")". + * @param string The string to be split into tokens + * @return The tokens + */ + public Iterator tokenize(String string) { + if (pattern!=null) { + List res = new ArrayList(); + Matcher m = pattern.matcher(string); + int pos = 0; + while (m.find()) { + // While there's a delimiter in the string + if (pos != m.start()) { + // If there's something between the current and the previous delimiter + // Add to the tokens list + addToTokens(res, string.substring(pos, m.start())); + } + addToTokens(res, m.group()); // add the delimiter + pos = m.end(); // Remember end of delimiter + } + if (pos != string.length()) { + // If it remains some characters in the string after last delimiter + addToTokens(res, string.substring(pos)); + } + // Return the result + return res.iterator(); + } else { + return new StringTokenizerIterator(new StringTokenizer(string, tokenDelimiters, true)); + } + } + + private class StringTokenizerIterator implements Iterator { + private StringTokenizer tokens; + /** Constructor. + * @param tokens The Stringtokenizer on which is based this instance. + */ + public StringTokenizerIterator(StringTokenizer tokens) { + this.tokens = tokens; + } + private String nextToken = null; + @Override + public boolean hasNext() { + return buildNextToken(); + } + @Override + public String next() { + if (!buildNextToken()) { + throw new NoSuchElementException(); + } + String token = nextToken; + nextToken = null; + return token; + } + private boolean buildNextToken() { + while ((nextToken == null) && tokens.hasMoreTokens()) { + nextToken = tokens.nextToken(); + if (trimTokens) { + nextToken = nextToken.trim(); + } + if (nextToken.isEmpty()) { + nextToken = null; + } + } + return nextToken!=null; + } + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/java_console/models/src/com/rusefi/test/ParserTest.java b/java_console/models/src/com/rusefi/test/ParserTest.java new file mode 100644 index 0000000000..ca121b405f --- /dev/null +++ b/java_console/models/src/com/rusefi/test/ParserTest.java @@ -0,0 +1,73 @@ +package com.rusefi.test; + +import com.fathzer.soft.javaluator.DoubleEvaluator; +import org.junit.Test; + +import java.text.ParseException; + +import static org.junit.Assert.assertEquals; + +public class ParserTest { + @Test + public void testBooleanConversion() throws ParseException { + assertParse("2 1 >", "2 > 1"); + assertParse("rpm 0 >", "rpm > false"); + assertParse("rpm 0 >", "(rpm > false)"); +// assertParse("rpm user0 > clt user2 > | vbatt user1 > |", "(rpm > user0) or (clt > user2) or (vbatt > user1)"); + } + + private void assertParse(String rpn, String expression) { + DoubleEvaluator evaluator = new DoubleEvaluator(); + evaluator.evaluate(expression.toLowerCase()); + + assertEquals(rpn, evaluator.getRusEfi()); + } + + + @Test + public void test() { + assertValue("2 3 6 ^ negate +", "2+-3^6", -727.0); + } + + @Test + public void testBooleanNot2() throws Exception { +// assertValue("0 !", "! false", 0); + } + + private void assertValue(String expectedRpn, String expression, double expectedValue) { + DoubleEvaluator evaluator = new DoubleEvaluator(); + assertEquals(expectedValue, evaluator.evaluate(expression), 0.001); + assertParse(expectedRpn, expression); + } + + @Test + public void testRusEfi() { + assertParse("1 4 |", "1 or 4"); + assertParse("1 4 |", "1 OR 4"); + + assertParse("time_since_boot 4 <", "(time_since_boot < 4)"); + assertParse("1 4 |", "1 | 4"); + assertParse("time_since_boot 4 < rpm 0 > |", "(time_since_boot < 4) | (rpm > 0)"); + + assertParse("1 4 |", "1 or 4"); + assertParse("1 4 |", "1 OR 4"); + + assertParse("1 4 &", "1 & 4"); + + assertParse("coolant fan_off_setting >", "(coolant > fan_off_setting)"); + assertParse("1 3 |", "1 OR 3"); + assertParse("1 3 &", "1 and 3"); + assertParse("1 coolant fan_on_setting > |", "1 OR (coolant > fan_on_setting)"); + assertParse("fan coolant fan_off_setting > & coolant fan_on_setting > |", "(fan and (coolant > fan_off_setting)) OR (coolant > fan_on_setting)"); + + assertParse("time_since_boot 4 <= rpm 0 > |", "(time_since_boot <= 4) | (rpm > 0)"); + + assertParse("time_since_boot 4 <= rpm 0 > |", "(time_since_boot <= 4) | (rpm > 0)"); + + assertParse("time_since_boot 4 <= rpm 0 > |", "(time_since_boot <= 4) OR (rpm > 0)"); + + assertParse("self rpm 4800 >= & rpm 5000 > |", "(self and (rpm >= 4800)) OR (rpm > 5000)"); + + + } +} \ No newline at end of file