/** * * Copyright 2015 Patrick Ahlbrecht * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.onyxbits.jbee; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.ParseException; import java.util.HashMap; import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeoutException; /** * The frontend class of JBEE and potentially the only one the average * application developer will have to deal with. In the most basic use case, * evaluating an expression is a simple matter of: * <p> * * <pre> * <code>System.out.println(new Evaluator().evaluate("3,000 * 4.0 + 0xFF"))</code> * </pre> * <p> * This will parse expressions using the number format of the default locale. A * more sophisticated example would be: * <p> * * <pre> * <code> * DecimalFormat df = (DecimalFormat)DecimalFormat.getInstance(Locale.GERMANY); * Evaluator e = new Evaluator(df, new FunctionCatalogMathLib()); * System.out.println(e.evaluate("3.000 * 4,0 + 0xFF - pow(2;4)")); * </code> * </pre> * <p> * This example will force the german locale for number parsing and also bind a * basic catalog of functions (by default, the function catalog is empty). Note * that functions use the semicolon instead of the comma as a list separator. * This is a necessity since the comma and full stop character may both appear * in decimal numbers. * * @author patrick * */ public final class Evaluator { private DecimalFormat format; private MathLib mathLib; private long timeout; /** * Standard constructor, using the {@link DecimalFormat} of the default * {@link Locale}, an instance of {@link DefaultMathLib} and no timeout. */ public Evaluator() { this(new DecimalFormat(), new DefaultMathLib()); } /** * Construct an Evaluator with a custom {@link DecimalFormat}, {@link MathLib} * and no timeout. * * @param format * the object to use for parsing decimal numbers. * @param mathLib * custom {@link MathLib} */ public Evaluator(DecimalFormat format, MathLib mathLib) { this.format = format; this.mathLib = mathLib; if (format == null || mathLib == null) { throw new NullPointerException(); } } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced in an * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, BigDecimal value) { mathLib.map(name, value); return this; } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, byte value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, short value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, int value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, long value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, double value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced inan * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, String value) { return map(name, new BigDecimal(value)); } /** * Bind a number to a symbolic name, so it can be referenced in an expression. * * @param name * the symbolic name by which the value may be referenced in an * expression. * @param value * number to associate with the symbol or null to clear it. * @return this reference for method chaining. */ public Evaluator map(String name, float value) { return map(name, new BigDecimal(value)); } /** * Bind a collection of numbers to symbols. * * @param all * the symbols * @return this reference for method chaining. */ public Evaluator mapAll(HashMap<String, BigDecimal> all) { Set<String> keys = all.keySet(); for (String key : keys) { map(key, all.get(key)); } return this; } /** * Clear all mapped variables. */ public void clearMappings() { mathLib.clearMappings(); } /** * Convenience method for creating a symbol/value mapping from a source * string. A single mapping in the source takes the format * <code>symbol=number</code>. Mappings may be separated by semicolon, * linebreak, semicolon and linebreak or any amount of spaces. Furthermore, * the source may contain c-style line end comments, for example: * * <pre> * <code> * // This is legal * name1=1 name2=2 * // This is legal as well * name3 = 3; name4 = 4; * // No need to be consistent * name5=5 name6=6; * </code> * </pre> * * Unlike the other map methods, this method will check for the symbol name to * be legal. * * @param src * string to parse * @return symbol value mapping * @throws ParseException * if src cannot be parsed. */ public HashMap<String, BigDecimal> createMapping(String src) throws ParseException { DeclarationParser p = new DeclarationParser(new Lexer(format, src)); p.yyparse(); return p.declarations; } /** * Check if a value is bound. * * @param name * name of a variable * @return it's value or null if not mapped. */ public BigDecimal valueOf(String name) { return mathLib.onLookup(name); } /** * Evaluate an expression, throw on error. This method may be called * repeatedly with different expression. Note that the throws clause only * lists the exceptions that can occur in the default implementation. Almost * all aspects of the parser can be overriden through a custom {@link MathLib} * which may or may not introduce additional/different exception. Therefore, a * try/catch block around this method should always include a general clause * to catch {@link RuntimeException}. * * @param expression * the expression to evaluate * @return the number the expression evaluates to. * @throws ArithmeticException * if the expression won't evaluate (e.g. division by zero, syntax * error, ...) * @throws NotDefinedException * if the expression references a function or variable that is not * defined. * @throws IllegalArgumentException * if the expression won't tokenize properly. * @throws TimeoutException * if evaluation takes longer than the timeout. */ public BigDecimal evaluateOrThrow(String expression) throws ArithmeticException, NotDefinedException, IllegalArgumentException, TimeoutException { Lexer l = new Lexer(format, expression); ExpressionParser p = new ExpressionParser(mathLib, l); if (timeout > 0) { BackgroundRunner runner = new BackgroundRunner(p); Thread t = new Thread(runner); t.start(); try { t.join(timeout); if (!runner.finished) { throw new TimeoutException(); } if (runner.error != null) { throw runner.error; } } catch (InterruptedException e) { throw new TimeoutException(); } } else { p.yyparse(); } return p.yyval.nval; } /** * Evaluate an expression, silently fail on error. This method may be called * repeatedly with different expression. * * @param expression * the expression to evaluate * @return The number the expression evaluates to or null on error. */ public BigDecimal evaluate(String expression) { try { return evaluateOrThrow(expression); } catch (Exception e) { e.printStackTrace(); return null; } } /** * How long evaluation may take at most. * * @param timeout * maximum execution time in milliseconds. A value of 0 means 'no * timeout'. */ public void setTimeout(long timeout) { if (timeout < 0) throw new IllegalArgumentException("timeout cannot be negative"); this.timeout = timeout; } /** * Query how long (in milliseconds) the evaluate methods will block at most. * * @return number of milliseconds or 0 for no timeout. */ public long getTimeout() { return timeout; } }