Advanced Use Cases#

Lazy Calculations#

Calculator vs LazyCalculator#

The default calculator has some advantages over the lazy calculator:

  • Works faster

  • Non-recursive calculation, not limited by call stack

  • Easier to debug missing variables and functions

Still, lazy calculator is a must if your variables may be missing or you need to guard against error conditions.

Note

Lazy functions and operators can still be used in the default calculator, just their arguments will be pre-calculated.

LazyArgument#

Arokettu\ArithmeticParser\Argument\LazyArgument

LazyArgument is an interface that wraps inner calculations. In LazyCalculator the inner calculations are not performed unless you call getValue(). In Calculator it’s just a wrapper for the float value that would be passed normally.

Partial Execution#

The most useful application of lazy calculator is partial execution:

<?php

use Arokettu\ArithmeticParser\Calculator;
use Arokettu\ArithmeticParser\LazyCalculator;

var_dump(LazyCalculator::evaluate('if (true(), 123, 1 / 0)')); // 123
var_dump(Calculator::evaluate('if (true(), 123, 1 / 0)')); // DivisionByZeroError

var_dump(LazyCalculator::evaluate('a or b', a: 1)); // 1 (true)
var_dump(Calculator::evaluate('a or b', a: 1)); // UndefinedVariableException

Error Handling#

Since getValue() actually wraps calculation, lazy functions and operators can also be used to detect errors in their subtrees.

Let’s create a custom optional operator value? and a custom default operator value ?? default:

<?php

use Arokettu\ArithmeticParser\Argument\LazyArgument;
use Arokettu\ArithmeticParser\Calculator;
use Arokettu\ArithmeticParser\Config;
use Arokettu\ArithmeticParser\Exceptions\UndefinedVariableException;
use Arokettu\ArithmeticParser\LazyCalculator;

$config = Config::default();

$config->addOperator(new Config\UnaryOperator(
    '?',
    function (LazyArgument $a) {
        try {
            return $a->getValue(); // if a value is defined, return it
        } catch (UndefinedVariableException) {
            return 0; // fall back to zero
        }
    },
    lazy: true,
));
$config->addOperator(new Config\BinaryOperator(
    '??',
    function (LazyArgument $a, LazyArgument $b) {
        try {
            return $a->getValue(); // if a value is defined, return it
        } catch (UndefinedVariableException) {
            return $b->getValue(); // fall back to the second argument
        }
    },
    1_000_000, // top priority
    lazy: true,
));

var_dump(LazyCalculator::evaluate('log(a, b ?? e())', $config, a: 1024, b: 2)); // 10
var_dump(LazyCalculator::evaluate('log(a, b ?? e())', $config, a: 1024)); // 6.9314...
// default config equivalent:
var_dump(LazyCalculator::evaluate('log(a, if(defined(b), b, e()))', a: 1024)); // 6.9314...

// Default calculator will still accept a lazy operation
var_dump(Calculator::evaluate('log(a, b ?? e())', $config, a: 1024, b: 2)); // 10
// but the actual fallback won't work
var_dump(Calculator::evaluate('log(a, b ?? e())', $config, a: 1024)); // UndefinedVariableException

// Lazy unary was created specifically because of error handling possibility
var_dump(LazyCalculator::evaluate('a? + b? + c?', $config)); // 0
var_dump(LazyCalculator::evaluate('a? + b? + c?', $config, a: 1, c: 3)); // 4
var_dump(LazyCalculator::evaluate('a? + b? + c?', $config, a: 1, b: 2, c: 3)); // 6

// Default calculator will still accept a lazy operation
var_dump(Calculator::evaluate('a? + b? + c?', $config, a: 1, b: 2, c: 3)); // 6
// but the actual optional won't work
var_dump(Calculator::evaluate('a? + b? + c?', $config)); // UndefinedVariableException

Dynamic Functions#

Warning

Calling parser and calculator with a different config objects is not supported unless only functions were added.

Since, unlike operators, functions are not resolved by the parser, you can dynamically add missing functions before the actual calculation:

<?php

declare(strict_types=1);

use Arokettu\ArithmeticParser\Calculator;
use Arokettu\ArithmeticParser\Config;
use Arokettu\ArithmeticParser\Parser;
use Arokettu\ArithmeticParser\Validator;

$config = Config::default();

$parser = new Parser($config);
$parsed = $parser->parse('log2(2048) + log3(27)');

$warnings = Validator::validate($parsed, $config, []);

foreach ($warnings as $w) {
    // find the warning about missing functions
    if ($w instanceof Validator\MissingFunctionsWarning) {
        foreach ($w->missingFunctions as $f) {
            // add logarithm function based on base value in the name
            if (str_starts_with($f->normalizedName, 'LOG')) {
                $base = \intval(substr($f->normalizedName, 3));
                $config->addFunctionFromCallable($f->name, fn ($a) => log($a, $base));
            }
        }
        break;
    }
}

var_dump((new Calculator($parsed->operations, $config))->calc()); // 14