EvalEx Documentation

repository·main·Indexed 22 days ago

https://github.com/ezylang/evalex

A lightweight Java expression evaluator for parsing and evaluating complex mathematical, boolean, string, date-time, and structured data expressions. Version 3.7.0 supports a variety of data types including BigDecimal, Instant, Duration, Lists, and Maps, featuring thread-safe evaluation via the .copy() method and lazy evaluation using EXPRESSION_NODE.

Tokens
22.5K
Snippets
114
Records
132
Agent score
77%

What's inside EvalEx

  1. Overview of EvalEx Data Types

    main

    EvalEx uses an EvaluationValue wrapper to store both a value and its corresponding data type. The following table maps EvalEx data types to their internal Java representations:

    Data TypeInternal Representation
    NUMBERjava.math.BigDecimal
    BOOLEANjava.lang.Boolean
    STRINGjava.lang.String
    DATE_TIMEjava.time.Instant
    DURATIONjava.time.Duration
    ARRAYjava.util.List
    STRUCTUREjava.util.Map
    EXPRESSION_NODEcom.ezylang.evalex.parser.ASTNode
    BINARYjava.lang.Object
    NULLnull
    UNDEFINEDnull (returned as "logical null")
  2. Evaluate expressions with Arrays and Structures

    main

    EvalEx supports complex data types:

    • Arrays: Can be passed as Java List objects or standard Java arrays. Supports multidimensional access.
    • Structures: Can be passed as Java Map objects.

    These can be nested to build arbitrary data structures (e.g., a List of Maps).

    // Array example
    Expression arrayExpr = new Expression("values[i-1] * factors[i-1]");
    EvaluationValue arrayResult = arrayExpr
        .with("values", List.of(2, 3, 4))
        .and("factors", new Object[] {2, 4, 6})
        .and("i", 1)
        .evaluate();
    
    // Structure example
    Map<String, Object> order = new HashMap<>();
    order.put("id", 12345);
    Map<String, Object> position = new HashMap<>();
    position.put("amount", 3);
    position.put("price", new BigDecimal("14.95"));
    order.put("positions", List.of(position));
    
    Expression structExpr = new Expression("order.positions[x].amount * order.positions[x].price")
        .with("order", order)
        .and("x", 0);
    
    BigDecimal result = structExpr.evaluate().getNumberValue();
  3. Define a custom function by implementing FunctionIfc

    main

    To create a custom function in EvalEx, you must implement the FunctionIfc interface. It is recommended to extend the AbstractFunction class to simplify implementation.

    Function definition involves two main parts:

    1. Defining logic and parameters: Create a class that implements the evaluate method. Use the @FunctionParameter annotation to define the expected arguments.
    2. Registering the function: Add the function to a function dictionary or an ExpressionConfiguration so it can be recognized during expression evaluation.

    The evaluate method is called during evaluation and receives the Expression context, the Token representing the function, and the EvaluationValue... parameterValues.

    @FunctionParameter(name = "value")
    @FunctionParameter(name = "scale")
    public class RoundFunction extends AbstractFunction {
      @Override
      public EvaluationValue evaluate(
          Expression expression, Token functionToken, EvaluationValue... parameterValues) {
    
        EvaluationValue value = parameterValues[0];
        EvaluationValue precision = parameterValues[1];
    
        return new EvaluationValue(
            value
                .getNumberValue()
                .setScale(
                    precision.getNumberValue().intValue(),
                    expression.getConfiguration().getMathContext().getRoundingMode()));
      }
    }
  4. How to implement a custom FunctionDictionaryIfc

    main

    EvalEx uses a FunctionDictionaryIfc to store and retrieve functions used in expressions. By default, it uses MapBasedFunctionDictionary, which is case-insensitive.

    To define your own dictionary, implement the FunctionDictionaryIfc interface. You must implement two core methods:

    1. addFunction(String functionName, FunctionIfc function): Adds a function to the dictionary. This is called when using .withAdditionalFunctions() on an ExpressionConfiguration.
    2. getFunction(String functionName): Retrieves the function implementation for a given name. Returns null if not found.

    Note: Function names passed to these methods are case-sensitive, matching how they appear in the expression.

    public interface FunctionDictionaryIfc {
    
      /**
       * Allows to add a function to the dictionary.
       * @param functionName The function name.
       * @param function The function implementation.
       */
      void addFunction(String functionName, FunctionIfc function);
    
      /**
       * Check if the dictionary has a function with that name.
       * @param functionName The function name to look for.
       * @return true if a function was found or false if not.
       */
      default boolean hasFunction(String functionName) {
        return getFunction(functionName) != null;
      }
    
      /**
       * Get the function definition for a function name.
       * @param functionName The name of the function.
       * @return The function definition or null if no function was found.
       */
      FunctionIfc getFunction(String functionName);
    }
  5. How Boolean conversion works in EvalEx

    main

    Booleans can be passed directly or result from operations. When STRING or NUMBER values are used in boolean expressions, they follow these conversion rules:

    • NUMBER: Evaluates to false if the value equals zero. All other values evaluate to true.
    • STRING: Evaluates to true if the string equals "true" (case-insensitive). All other values evaluate to false.

    Example of mixed-type boolean evaluation:

    Expression expression = new Expression("stringValue && numberValue")
        .with("stringValue", "True")
        .and("numberValue", 42);
  6. Thread-safe expression evaluation using copy()

    main

    To evaluate the same expression in multiple threads without re-parsing the string or causing race conditions, use the .copy() method. The copy shares the same syntax tree and configuration but allows for a unique set of variable values. Each thread should work with its own copy of the original expression.

    Expression expression = new Expression("a + b").with("a", 1).and("b", 2);
    Expression copiedExpression = expression.copy().with("a", 3).and("b", 4);
    
    EvaluationValue result = expression.evaluate();
    EvaluationValue copiedResult = copiedExpression.evaluate();
    
    System.out.println(result.getNumberValue()); // prints 3
    System.out.println(copiedResult.getNumberValue()); // prints 7
  7. Configure Lenient Mode for undeclared variables

    main

    Introduced in version 3.6.0, Lenient Mode allows for graceful evaluation of expressions containing undeclared variables or constants.

    Instead of throwing an exception, the engine returns a special type representing logical nulls (e.g., returning false where a boolean is expected). This is useful for permissive or lazy evaluation logic.

  8. Enable Lenient Mode for undeclared variables

    main

    Introduced in version 3.6.0, Lenient Mode prevents the evaluator from throwing exceptions when it encounters undeclared variables or constants. Instead of halting, it returns logical null values.

    Lenient Mode is disabled by default. To enable it, you must configure the property in the ExpressionConfiguration.

  9. Understand Precision, Scale, and Rounding in EvalEx

    main

    EvalEx uses java.math.BigDecimal for calculations and internal storage. This ensures high precision suitable for financial systems, unlike standard float or double types.

    • Precision: The total number of digits in the unscaled value (e.g., 123.456 has a precision of 6). The default precision in EvalEx is 68.
    • Scale: The number of digits after the decimal point (e.g., 123.456 has a scale of 3).
    • Rounding Mode: Determines how values are handled when they exceed the configured precision or scale. The default rounding mode is HALF_EVEN.
  10. Configure Data Accessors and Variable Storage

    main

    The Data Accessor is responsible for storing and retrieving variable values.

    • The default implementation is MapBasedDataAccessor, which uses a case-insensitive Map.
    • You can provide a custom supplier via .dataAccessorSupplier(Supplier<DataAccessor>). This is called whenever a new Expression is created, allowing each expression to own its own instance or share a space via a custom implementation.
  11. Enable or disable Implicit Multiplication

    main

    Implicit multiplication automatically inserts a multiplication operator in expressions like 2x, 2sin(x), or (a+b)(b+c).

    • Enabled (Default): 2(a+b) is expanded to 2*(a+b).
    • Note: It does not work for patterns like x(a+b), which the engine treats as a function call to x.

    Use .implicitMultiplicationAllowed(boolean) to toggle this feature.

  12. Working with Structures in EvalEx

    main

    Structures are stored internally as java.util.Map<String, EvaluationValue>. When passed as a variable, map entries are converted to EvaluationValue objects.

    Key features:

    • Nesting: Structures can form tree-like data structures by containing other structures.
    • Accessing keys with spaces: If a key contains spaces, wrap the key in double quotes within the expression (e.g., data."property name"). This also applies to array access on structure elements.
    • Combination: Arrays and structures can be combined arbitrarily.
    Map<String, Object> order = new HashMap<>();
    order.put("id", 12345);
    order.put("name", "Mary");
    
    Map<String, Object> position = new HashMap<>();
    position.put("article", 3114);
    position.put("amount", 3);
    position.put("price", new BigDecimal("14.95"));
    
    order.put("positions", List.of(position));
    
    Expression expression = new Expression("order.positions[x].amount * order.positions[x].price")
        .with("order", order)
        .and("x", 0);
    
    BigDecimal result = expression.evaluate().getNumberValue();
    System.out.println(result); // prints 44.85