Dentaku

repository·main·Indexed 21 days ago

https://github.com/rubysolo/dentaku

A safe mathematical and logical formula parser and evaluator for Ruby. Dentaku allows for run-time binding of variables and is designed to evaluate untrusted expressions without security risks. It features AST caching for performance, support for custom functions, dependency analysis, and a BulkExpressionSolver for resolving sets of interdependent formulas.

Tokens
5.2K
Snippets
22
Records
22
Agent score
71%

What's inside dentaku

  1. Basic usage of Dentaku::Calculator

    main

    To evaluate mathematical or logical expressions, instantiate a Dentaku::Calculator and use the evaluate method. You can pass variables directly to the evaluate method as a hash, or store them in the calculator's memory using store (or its alias bind).

    By default, variable names are case-sensitive. To enable case-insensitive mode, initialize the calculator with case_sensitive: true.

    # Basic evaluation
    calculator = Dentaku::Calculator.new
    calculator.evaluate('10 * 2') #=> 20
    
    # Evaluation with runtime variables
    calculator.evaluate('kiwi + 5', kiwi: 2) #=> 7
    
    # Case-sensitive mode
    calculator = Dentaku::Calculator.new(case_sensitive: true)
    calculator.evaluate('Kiwi + 5', Kiwi: -2, kiwi: 2) #=> 3
    
    # Using calculator memory
    calculator.store(peaches: 15)
    calculator.evaluate('peaches - 5') #=> 10
  2. Optimize performance with AST caching

    main

    Dentaku's parsing and tokenization can be slow. To improve performance for repeated evaluations of the same formula, enable AST (Abstract Syntax Tree) caching. This makes subsequent evaluations much faster (closer to 4x native Ruby speed) at the cost of increased memory usage for each unique formula.

    You can enable caching globally for the whole module or specifically for a single calculator instance.

    # Enable globally
    Dentaku.enable_ast_cache!
    
    # Enable for a specific instance
    calculator = Dentaku::Calculator.new(cache_ast: true)
  3. Handle errors in BulkExpressionSolver

    main

    When using solve (permissive mode), the error handler block receives the exception object. Dentaku attaches the name of the variable that caused the error to the exception via the assigned_to property. This allows you to identify which specific expression failed within your error handling logic.

    Supported error types include:

    • Dentaku::UnboundVariableError
    • Dentaku::ZeroDivisionError
    • Dentaku::ArgumentError
    • TSort::Cyclic (for circular dependencies)
    solver.solve do |ex| 
      puts "Error in variable: #{ex.assigned_to}"
      :undefined
    end
  4. Initialize a Dentaku::Calculator

    main

    To use Dentaku, instantiate a Dentaku::Calculator. You can configure several options during initialization to control how expressions are parsed and evaluated.

    Key options:

    • case_sensitive: (Boolean) If true, variable names and aliases are treated with case sensitivity. Defaults to false.
    • aliases: (Hash) A mapping of aliases to their actual variable names. If nil, it falls back to Dentaku.aliases.
    • nested_data_support: (Boolean) Enables support for accessing nested data structures. Defaults to true.
    • raw_date_literals: (Boolean) Controls how date literals are handled. Defaults to true.
    • ast_cache: (Hash) An optional hash used to cache Abstract Syntax Trees (AST) for performance.
    calculator = Dentaku::Calculator.new(
      case_sensitive: true,
      aliases: { 'total' => 'sum_of_all' },
      nested_data_support: true
    )
  5. Handle evaluation errors with evaluate! vs evaluate

    main

    The evaluate method returns nil if there is an error in the formula (such as an unbound variable). If you prefer to raise an exception when an error occurs, use evaluate! instead. This will raise a Dentaku::UnboundVariableError if a required variable is missing.

    # Returns nil on error
    calculator.evaluate('10 * x') #=> nil
    
    # Raises exception on error
    calculator.evaluate!('10 * x') #=> raises Dentaku::UnboundVariableError
  6. Add custom functions to Dentaku

    main

    You can extend Dentaku by adding custom functions at runtime using add_function or add_functions.

    When adding a function, you must provide:

    1. A name (symbol).
    2. A declared return type (one of :numeric, :integer, :array, or nil). This type is used at parse time to validate arithmetic operations.
    3. A lambda that accepts the arguments and returns the result.
    4. (Optional) volatile: true if the function performs I/O, reads external state, or is not pure. Volatile functions are skipped during dependency analysis, meaning all branches in IF or CASE statements will be treated as dependencies.
    # Adding a standard function
    c = Dentaku::Calculator.new
    c.add_function(:pow, :numeric, ->(mantissa, exponent) { mantissa ** exponent })
    c.evaluate('POW(3,2)') #=> 9
    
    # Adding a variadic function
    c.add_function(:max, :numeric, ->(*args) { args.max })
    c.evaluate('MAX(8,6,7,5,3,0,9)') #=> 9
    
    # Adding a volatile function (e.g., reading external state)
    c.add_function(:user_level, :numeric, -> { Current.user.level }, volatile: true)
  7. Resolve formula evaluation order with solve!

    main

    If you have a set of formulas where some variables depend on the results of others, use solve! to determine the correct evaluation order.

    Pass a hash of { eventual_variable_name: "expression" } to solve!. Dentaku will use TSort to resolve the dependencies.

    • solve!: Raises TSort::Cyclic if a circular dependency is found, or raises an exception if a formula cannot be evaluated (e.g., ZeroDivisionError).
    • solve: Returns the symbol :undefined for formulas that cannot be solved instead of raising an exception.
    calc = Dentaku::Calculator.new
    calc.store(monthly_income: 50)
    
    need_to_compute = {
      income_taxes: "annual_income / 5",
      annual_income: "monthly_income * 12"
    }
    
    calc.solve!(need_to_compute)
    #=> {annual_income: 600, income_taxes: 120}
  8. Analyze formula dependencies and identifiers

    main

    Dentaku provides two ways to inspect a formula's inputs:

    1. identifiers(expression): A purely syntactic check that returns every identifier the formula could reference, regardless of logic or branching. Use this for static validation.
    2. dependencies(expression, context): A resolution-aware check that reports identifiers still needed given a specific context. It can prune branches (like IF or AND/OR) if the condition is already satisfied by the context.

    Note: Context keys prefixed with __ (e.g., __evaluation_mode) are reserved for internal use.

    # Static list of all possible inputs
    calculator.identifiers('IF(x > 5, y, z)') #=> ["x", "y", "z"]
    
    # List of required inputs given current knowledge
    calculator.dependencies('IF(x > 5, y, z)', x: 7) #=> ["y"]
  9. Configure function aliases for multilingual support

    main

    You can define synonyms for built-in or custom functions using the aliases option. This is useful for supporting multiple languages or specific domain terminology.

    You can set aliases globally via Dentaku.aliases= or per-instance via the Dentaku::Calculator.new(aliases: ...) initializer for thread-safety.

    # Global aliases
    Dentaku.aliases = {
      round: ['rrrrround!', 'округлить']
    }
    Dentaku('rrrrround!(8.2)') #=> 8
    
    # Thread-safe instance aliases
    aliases = { round: ['rrrrround!'] }
    c = Dentaku::Calculator.new(aliases: aliases)
    c.evaluate('rrrrround!(8.2)') #=> 8
  10. Reference: Built-in Dentaku operators and functions

    main

    Dentaku includes a wide range of built-in mathematical, logical, and string functions.

    Math: +, -, *, /, %, ^, |, &, <<, >>
    Also: All functions from Ruby's Math module (SIN, COS, TAN, etc.)
    
    Comparison: <, >, <=, >=, <>, !=, =, 
    
    Logic: IF, AND, OR, XOR, NOT, SWITCH
    
    Numeric: MIN, MAX, SUM, AVG, COUNT, ROUND, ROUNDDOWN, ROUNDUP, ABS, INTERCEPT
    
    Selections: CASE
    
    String: LEFT, RIGHT, MID, LEN, FIND, SUBSTITUTE, CONCAT, CONTAINS
    
    Collection: MAP, FILTER, ALL, ANY, PLUCK
  11. Register custom functions in Dentaku

    main

    You can extend Dentaku's mathematical capabilities by registering custom functions using the FunctionRegistry#register method. This allows you to define new logic that can be called within Dentaku expressions.

    register method

    Use register to add a new function to the registry.

    Parameters:

    • name (String/Symbol): The name of the function as it will appear in expressions (case-insensitive).
    • type (Symbol): The return type of the function.
    • implementation (Proc/Method): The logic to execute. This should be a callable object (like a Proc) that accepts the function's arguments.
    • callback (Proc, optional): A callback to be executed.
    • volatile (Boolean, optional): Set to true if the function's result can change even with the same inputs (e.g., a random() function). Defaults to false.

    When a function is registered, Dentaku automatically determines the required arity (number of arguments) based on the implementation's parameter requirements.

    # Example of registering a custom 'square' function
    registry = Dentaku::AST::FunctionRegistry.new
    
    registry.register(
      :square,           # name
      :numeric,          # type
      ->(x) { x * x }   # implementation
    )
  12. Evaluate expressions with Dentaku.evaluate

    main

    Use Dentaku.evaluate to compute the result of a mathematical or logical expression. You can provide an optional data hash to supply variables used within the expression.

    If you need to raise errors for invalid expressions or math errors (like division by zero) instead of returning nil or specific error values, use Dentaku.evaluate!.

    # Basic evaluation
    result = Dentaku.evaluate("1 + 1")
    # => 2
    
    # Evaluation with data context
    result = Dentaku.evaluate("income - expenses", { "income" => 100, "expenses" => 40 })
    # => 60