Repository: neonxp/MathExecutor
Branch: master
Commit: 235deeb11083
Files: 28
Total size: 108.8 KB
Directory structure:
gitextract_y5i3bttq/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── code-of-conduct.md
├── code-of-conduct.ru.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│ └── NXP/
│ ├── Classes/
│ │ ├── Calculator.php
│ │ ├── CustomFunction.php
│ │ ├── Operator.php
│ │ ├── Token.php
│ │ └── Tokenizer.php
│ ├── Exception/
│ │ ├── DivisionByZeroException.php
│ │ ├── IncorrectBracketsException.php
│ │ ├── IncorrectExpressionException.php
│ │ ├── IncorrectFunctionParameterException.php
│ │ ├── IncorrectNumberOfFunctionParametersException.php
│ │ ├── MathExecutorException.php
│ │ ├── UnknownFunctionException.php
│ │ ├── UnknownOperatorException.php
│ │ └── UnknownVariableException.php
│ └── MathExecutor.php
└── tests/
├── MathTest.php
└── bootstrap.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
* text=auto
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on: [push, pull_request]
jobs:
php-tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
php: [8.4, 8.3, 8.2, 8.1]
dependency-version: [prefer-stable]
os: [ubuntu-latest, windows-latest]
name: ${{ matrix.os }} - PHP${{ matrix.php }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, bcmath, intl
ini-values: precision=16
coverage: none
- name: Install dependencies
run: |
composer install --no-interaction
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
run: vendor/bin/phpunit
================================================
FILE: .gitignore
================================================
vendor/
.idea/
composer.lock
.phpunit.cache
.phpunit.result.cache
.vscode
.php-cs-fixer.cache
================================================
FILE: .php-cs-fixer.php
================================================
<?php
$config = new PhpCsFixer\Config();
$config->setUnsupportedPhpVersionAllowed(true);
$config
->setRiskyAllowed(true)
->setIndent(" ")
->setRules([
// Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one.
'align_multiline_comment' => ['comment_type'=>'all_multiline'],
// Each element of an array must be indented exactly once.
'array_indentation' => true,
// Converts simple usages of `array_push($x, $y);` to `$x[] = $y;`.
'array_push' => true,
// PHP arrays should be declared using the configured syntax.
'array_syntax' => ['syntax'=>'short'],
// Converts backtick operators to `shell_exec` calls.
'backtick_to_shell_exec' => true,
// Binary operators should be surrounded by space as configured.
'binary_operator_spaces' => true,
// There MUST be one blank line after the namespace declaration.
'blank_line_after_namespace' => true,
// Ensure there is no code on the same line as the PHP open tag and it is followed by a blank line.
'blank_line_after_opening_tag' => true,
// An empty line feed must precede any configured statement.
'blank_line_before_statement' => ['statements'=>['break','case','continue','declare','default','exit','do','exit','for','foreach','goto','if','return','switch','throw','try','while','yield']],
// A single space or none should be between cast and variable.
'cast_spaces' => ['space'=>'none'],
// Class, trait and interface elements must be separated with one or none blank line.
'class_attributes_separation' => true,
// Whitespace around the keywords of a class, trait or interfaces definition should be one space.
'class_definition' => true,
// Namespace must not contain spacing, comments or PHPDoc.
'clean_namespace' => true,
// Using `isset($var) &&` multiple times should be done in one call.
'combine_consecutive_issets' => true,
// Calling `unset` on multiple items should be done in one call.
'combine_consecutive_unsets' => true,
// Replace multiple nested calls of `dirname` by only one call with second `$level` parameter. Requires PHP >= 7.0.
'combine_nested_dirname' => true,
// Comments with annotation should be docblock when used on structural elements.
'comment_to_phpdoc' => true,
// Remove extra spaces in a nullable typehint.
'compact_nullable_type_declaration' => true,
// Concatenation should be spaced according configuration.
'concat_space' => ['spacing'=>'one'],
// The PHP constants `true`, `false`, and `null` MUST be written using the correct casing.
'constant_case' => true,
// Equal sign in declare statement should be surrounded by spaces or not following configuration.
'declare_equal_normalize' => ['space'=>'single'],
// Replaces `dirname(__FILE__)` expression with equivalent `__DIR__` constant.
'dir_constant' => true,
// The keyword `elseif` should be used instead of `else if` so that all control keywords look like single words.
'elseif' => true,
// PHP code MUST use only UTF-8 without BOM (remove BOM).
'encoding' => true,
// Replace deprecated `ereg` regular expression functions with `preg`.
'ereg_to_preg' => true,
// Add curly braces to indirect variables to make them clear to understand. Requires PHP >= 7.0.
'explicit_indirect_variable' => true,
// Converts implicit variables into explicit ones in double-quoted strings or heredoc syntax.
'explicit_string_variable' => true,
// Order the flags in `fopen` calls, `b` and `t` must be last.
'fopen_flag_order' => true,
// PHP code must use the long `<?php` tags or short-echo `<?=` tags and not other tag variations.
'full_opening_tag' => true,
// Spaces should be properly placed in a function declaration.
'function_declaration' => ['closure_function_spacing'=>'none', 'closure_fn_spacing'=>'none'],
// Replace core functions calls returning constants with the constants.
'function_to_constant' => true,
// Ensure single space between function's argument and its typehint.
'type_declaration_spaces' => true,
// Renames PHPDoc tags.
'general_phpdoc_tag_rename' => true,
// Function `implode` must be called with 2 arguments in the documented order.
'implode_call' => true,
// Include/Require and file path should be divided with a single space. File path should not be placed under brackets.
'include' => true,
// Code MUST use configured indentation type.
'indentation_type' => true,
// Replaces `is_null($var)` expression with `null === $var`.
'is_null' => true,
// All PHP files must use same line ending.
'line_ending' => true,
// Ensure there is no code on the same line as the PHP open tag.
'linebreak_after_opening_tag' => true,
// List (`array` destructuring) assignment should be declared using the configured syntax. Requires PHP >= 7.1.
'list_syntax' => ['syntax'=>'short'],
// Use `&&` and `||` logical operators instead of `and` and `or`.
'logical_operators' => true,
// Cast should be written in lower case.
'lowercase_cast' => true,
// PHP keywords MUST be in lower case.
'lowercase_keywords' => true,
// Class static references `self`, `static` and `parent` MUST be in lower case.
'lowercase_static_reference' => true,
// Magic constants should be referred to using the correct casing.
'magic_constant_casing' => true,
// Magic method definitions and calls must be using the correct casing.
'magic_method_casing' => true,
// In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line.
'method_argument_space' => true,
// Method chaining MUST be properly indented. Method chaining with different levels of indentation is not supported.
'method_chaining_indentation' => true,
// Replaces `intval`, `floatval`, `doubleval`, `strval` and `boolval` function calls with according type casting operator.
'modernize_types_casting' => true,
// Forbid multi-line whitespace before the closing semicolon or move the semicolon to the new line for chained calls.
'multiline_whitespace_before_semicolons' => true,
// Function defined by PHP should be called using the correct casing.
'native_function_casing' => true,
// Add leading `\` before function invocation to speed up resolving.
'native_function_invocation' => ['include'=>['@all','trans']],
// Native type hints for functions should use the correct case.
'native_type_declaration_casing' => true,
// All instances created with new keyword must be followed by parentheses.
'new_with_parentheses' => true,
// Master functions shall be used instead of aliases.
'no_alias_functions' => true,
// Master language constructs shall be used instead of aliases.
'no_alias_language_construct_call' => true,
// Replace control structure alternative syntax to use braces.
'no_alternative_syntax' => true,
// There should not be blank lines between docblock and the documented element.
'no_blank_lines_after_phpdoc' => true,
// There must be a comment when fall-through is intentional in a non-empty case body.
'no_break_comment' => ['comment_text'=>'Intentionally fall through'],
// The closing `? >` tag MUST be omitted from files containing only PHP.
'no_closing_tag' => true,
// There should not be any empty comments.
'no_empty_comment' => true,
// There should not be empty PHPDoc blocks.
'no_empty_phpdoc' => true,
// Remove useless (semicolon) statements.
'no_empty_statement' => true,
// Replace accidental usage of homoglyphs (non ascii characters) in names.
'no_homoglyph_names' => true,
// Remove leading slashes in `use` clauses.
'no_leading_import_slash' => true,
// The namespace declaration line shouldn't contain leading whitespace.
'no_leading_namespace_whitespace' => true,
// Either language construct `print` or `echo` should be used.
'no_mixed_echo_print' => true,
// Operator `=>` should not be surrounded by multi-line whitespaces.
'no_multiline_whitespace_around_double_arrow' => true,
// Convert PHP4-style constructors to `__construct`.
'no_php4_constructor' => true,
// Short cast `bool` using double exclamation mark should not be used.
'no_short_bool_cast' => true,
// When making a method or function call, there MUST NOT be a space between the method or function name and the opening parenthesis.
'no_spaces_after_function_name' => true,
// Removes `@param`, `@return` and `@var` tags that don't provide any useful information.
'no_superfluous_phpdoc_tags' => true,
// Remove trailing whitespace at the end of non-blank lines.
'no_trailing_whitespace' => true,
// There MUST be no trailing spaces inside comment or PHPDoc.
'no_trailing_whitespace_in_comment' => true,
// Removes unneeded parentheses around control statements.
'no_unneeded_control_parentheses' => true,
// Removes unneeded braces that are superfluous and aren't part of a control structure's body.
'no_unneeded_braces' => true,
// A `final` class must not have `final` methods and `private` methods must not be `final`.
'no_unneeded_final_method' => true,
// In function arguments there must not be arguments with default values before non-default ones.
'no_unreachable_default_argument_value' => true,
// Variables must be set `null` instead of using `(unset)` casting.
'no_unset_cast' => true,
// Properties should be set to `null` instead of using `unset`.
'no_unset_on_property' => true,
// Unused `use` statements must be removed.
'no_unused_imports' => true,
// There should not be useless `else` cases.
'no_useless_else' => true,
// There should not be an empty `return` statement at the end of a function.
'no_useless_return' => true,
// There must be no `sprintf` calls with only the first argument.
'no_useless_sprintf' => true,
// In array declaration, there MUST NOT be a whitespace before each comma.
'no_whitespace_before_comma_in_array' => true,
// Remove trailing whitespace at the end of blank lines.
'no_whitespace_in_blank_line' => true,
// Remove Zero-width space (ZWSP), Non-breaking space (NBSP) and other invisible unicode symbols.
'non_printable_character' => ['use_escape_sequences_in_strings'=>true],
// Array index should always be written by using square braces.
'normalize_index_brace' => true,
// Logical NOT operators (`!`) should have one trailing whitespace.
'not_operator_with_successor_space' => true,
// Adds or removes `?` before type declarations for parameters with a default `null` value.
'nullable_type_declaration_for_default_null_value' => true,
// There should not be space before or after object operators `->` and `?->`.
'object_operator_without_whitespace' => true,
// Orders the elements of classes/interfaces/traits.
'ordered_class_elements' => ['order'=>['use_trait','constant_public','constant_protected','constant_private','property_public','property_protected','property_private','construct','destruct','magic','phpunit','method_public','method_protected','method_private']],
// Ordering `use` statements.
'ordered_imports' => true,
// Orders the interfaces in an `implements` or `interface extends` clause.
'ordered_interfaces' => true,
// Trait `use` statements must be sorted alphabetically.
'ordered_traits' => true,
// Classy that does not inherit must not have `@inheritdoc` tags.
'phpdoc_no_useless_inheritdoc' => true,
// Annotations in PHPDoc should be ordered so that `@param` annotations come first, then `@throws` annotations, then `@return` annotations.
'phpdoc_order' => true,
// The type of `@return` annotations of methods returning a reference to itself must the configured one.
'phpdoc_return_self_reference' => true,
// Scalar types should always be written in the same form. `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`.
'phpdoc_scalar' => true,
// Fixes casing of PHPDoc tags.
'phpdoc_tag_casing' => true,
// Converts `protected` variables and methods to `private` where possible.
'protected_to_private' => true,
// Classes must be in a path that matches their namespace, be at least one namespace deep and the class name should match the file name.
'psr_autoloading' => true,
// There should be one or no space before colon, and one space after it in return type declarations, according to configuration.
'return_type_declaration' => ['space_before'=>'one'],
// Instructions must be terminated with a semicolon.
'semicolon_after_instruction' => true,
// Cast shall be used, not `settype`.
'set_type_to_cast' => true,
// Cast `(boolean)` and `(integer)` should be written as `(bool)` and `(int)`, `(double)` and `(real)` as `(float)`, `(binary)` as `(string)`.
'short_scalar_cast' => true,
// Converts explicit variables in double-quoted strings and heredoc syntax from simple to complex format (`${` to `{$`).
'simple_to_complex_string_variable' => true,
// Simplify `if` control structures that return the boolean result of their condition.
'simplified_if_return' => true,
// A return statement wishing to return `void` should not return `null`.
'simplified_null_return' => true,
// A PHP file without end tag must always end with a single empty line feed.
'single_blank_line_at_eof' => true,
// There should be exactly one blank line before a namespace declaration.
'blank_lines_before_namespace' => ['max_line_breaks' => 2, 'min_line_breaks' => 2],
// There MUST NOT be more than one property or constant declared per statement.
'single_class_element_per_statement' => true,
// There MUST be one use keyword per declaration.
'single_import_per_statement' => true,
// Each namespace use MUST go on its own line and there MUST be one blank line after the use statements block.
'single_line_after_imports' => true,
// Single-line comments and multi-line comments with only one line of actual content should use the `//` syntax.
'single_line_comment_style' => true,
// Convert double quotes to single quotes for simple strings.
'single_quote' => true,
// Each trait `use` must be done as single statement.
'single_trait_insert_per_statement' => true,
// There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis.
'spaces_inside_parentheses' => false,
// Replace all `<>` with `!=`.
'standardize_not_equals' => true,
// Lambdas not (indirect) referencing `$this` must be declared `static`.
'static_lambda' => true,
// All multi-line strings must use correct line ending.
'string_line_ending' => true,
// A case should be followed by a colon and not a semicolon.
'switch_case_semicolon_to_colon' => true,
// Removes extra spaces between colon and case value.
'switch_case_space' => true,
// Switch case must not be ended with `continue` but with `break`.
'switch_continue_to_break' => true,
// Standardize spaces around ternary operator.
'ternary_operator_spaces' => true,
// Use the Elvis operator `?:` where possible.
'ternary_to_elvis_operator' => true,
// Use `null` coalescing operator `??` where possible. Requires PHP >= 7.0.
'ternary_to_null_coalescing' => true,
// Arrays should be formatted like function/method arguments, without leading or trailing single line space.
'trim_array_spaces' => true,
// Unary operators should be placed adjacent to their operands.
'unary_operator_spaces' => true,
'modifier_keywords' => ['elements' => ['const', 'method', 'property']],
// Add `void` return type to functions with missing or empty return statements, but priority is given to `@return` annotations. Requires PHP >= 7.1.
'void_return' => true,
// In array declaration, there MUST be a whitespace after each comma.
'whitespace_after_comma_in_array' => true,
// Write conditions in Yoda style (`true`), non-Yoda style (`['equal' => false, 'identical' => false, 'less_and_greater' => false]`) or ignore those conditions (`null`) based on configuration.
'yoda_style' => true,
]);
return $config->setFinder(PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
);
================================================
FILE: LICENSE
================================================
Copyright (c) Alexander Kiryukhin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# MathExecutor [](https://github.com/neonxp/MathExecutor/actions?query=workflow%3ATests) 
# A simple and extensible math expressions calculator
## Features:
* Built in support for +, -, *, /, % and power (^) operators
* Parentheses () and arrays [] are fully supported
* Logical operators (==, !=, <, <, >=, <=, &&, ||, !)
* Built in support for most PHP math functions
* Support for BCMath Arbitrary Precision Math
* Support for variable number of function parameters and optional function parameters
* Conditional If logic
* Support for user defined operators
* Support for user defined functions
* Support for math on user defined objects
* Dynamic variable resolution (delayed computation)
* Unlimited variable name lengths
* String support, as function parameters or as evaluated as a number by PHP
* Exceptions on divide by zero, or treat as zero
* Unary Plus and Minus (e.g. +3 or -sin(12))
* Pi ($pi) and Euler's number ($e) support to 11 decimal places
* Easily extensible
## Install via Composer:
```
composer require nxp/math-executor
```
## Sample usage:
```php
use NXP\MathExecutor;
$executor = new MathExecutor();
echo $executor->execute('1 + 2 * (2 - (4+10))^2 + sin(10)');
```
## Functions:
Default functions:
* abs
* acos (arccos)
* acosh
* arccos
* arccosec
* arccot
* arccotan
* arccsc (arccosec)
* arcctg (arccot, arccotan)
* arcsec
* arcsin
* arctan
* arctg
* array
* asin (arcsin)
* atan (atn, arctan, arctg)
* atan2
* atanh
* atn
* avg
* bindec
* ceil
* cos
* cosec
* cosec (csc)
* cosh
* cot
* cotan
* cotg
* csc
* ctg (cot, cotan, cotg, ctn)
* ctn
* decbin
* dechex
* decoct
* deg2rad
* exp
* expm1
* floor
* fmod
* hexdec
* hypot
* if
* intdiv
* lg
* ln
* log (ln)
* log10 (lg)
* log1p
* max
* median
* min
* octdec
* pi
* pow
* rad2deg
* round
* sec
* sin
* sinh
* sqrt
* tan (tn, tg)
* tanh
* tg
* tn
Add custom function to executor:
```php
$executor->addFunction('concat', function($arg1, $arg2) {return $arg1 . $arg2;});
```
Optional parameters:
```php
$executor->addFunction('round', function($num, int $precision = 0) {return round($num, $precision);});
$executor->execute('round(17.119)'); // 17
$executor->execute('round(17.119, 2)'); // 17.12
```
Variable number of parameters:
```php
$executor->addFunction('average', function(...$args) {return array_sum($args) / count($args);});
$executor->execute('average(1,3)'); // 2
$executor->execute('average(1, 3, 4, 8)'); // 4
```
## Operators:
Default operators: `+ - * / % ^`
Add custom float modulo operator to executor:
```php
use NXP\Classes\Operator;
$executor->addOperator(new Operator(
'%', // Operator sign
false, // Is right associated operator
180, // Operator priority
function ($op1, $op2)
{
return fmod($op1, $op2);
}
));
```
## Logical operators:
Logical operators (==, !=, <, <, >=, <=, &&, ||, !) are supported, but logically they can only return true (1) or false (0). In order to leverage them, use the built in **if** function:
```
if($a > $b, $a - $b, $b - $a)
```
You can think of the **if** function as prototyped like:
```
function if($condition, $returnIfTrue, $returnIfFalse)
```
## Variables:
Variables can be prefixed with the dollar sign ($) for PHP compatibility, but is not required.
Default variables:
```
$pi = 3.14159265359
$e = 2.71828182846
```
You can add your own variables to executor:
```php
$executor->setVar('var1', 0.15)->setVar('var2', 0.22);
echo $executor->execute("$var1 + var2");
```
Arrays are also supported (as variables, as func params or can be returned in user defined funcs):
```php
$executor->setVar('monthly_salaries', [1800, 1900, 1200, 1600]);
echo $executor->execute("avg(monthly_salaries) * min([1.1, 1.3])");
```
By default, variables must be scalar values (int, float, bool or string) or array. If you would like to support another type, use **setVarValidationHandler**
```php
$executor->setVarValidationHandler(function (string $name, $variable) {
// allow all scalars, array and null
if (is_scalar($variable) || is_array($variable) || $variable === null) {
return;
}
// Allow variables of type DateTime, but not others
if (! $variable instanceof \DateTime) {
throw new MathExecutorException("Invalid variable type");
}
});
```
You can dynamically define variables at run time. If a variable has a high computation cost, but might not be used, then you can define an undefined variable handler. It will only get called when the variable is used, rather than having to always set it initially.
```php
$executor->setVarNotFoundHandler(
function ($varName) {
if ($varName == 'trans') {
return transmogrify();
}
return null;
}
);
```
## Floating Point BCMath Support
By default, `MathExecutor` uses PHP floating point math, but if you need a fixed precision, call **useBCMath()**. Precision defaults to 2 decimal points, or pass the required number.
`WARNING`: Functions may return a PHP floating point number. By doing the basic math functions on the results, you will get back a fixed number of decimal points. Use a plus sign in front of any stand alone function to return the proper number of decimal places.
## Division By Zero Support:
Division by zero throws a `\NXP\Exception\DivisionByZeroException` by default
```php
try {
echo $executor->execute('1/0');
} catch (DivisionByZeroException $e) {
echo $e->getMessage();
}
```
Or call setDivisionByZeroIsZero
```php
echo $executor->setDivisionByZeroIsZero()->execute('1/0');
```
If you want another behavior, you can override division operator:
```php
$executor->addOperator(new Operator("/", false, 180, function($a, $b) {
if ($b == 0) {
return null;
}
return $a / $b;
});
echo $executor->execute('1/0');
```
## String Support:
Expressions can contain double or single quoted strings that are evaluated the same way as PHP evaluates strings as numbers. You can also pass strings to functions.
```php
echo $executor->execute("1 + '2.5' * '.5' + myFunction('category')");
```
To use reverse solidus character (\) in strings, or to use single quote character (') in a single quoted string, or to use double quote character (") in a double quoted string, you must prepend reverse solidus character (\).
```php
echo $executor->execute("countArticleSentences('My Best Article\'s Title')");
```
## Extending MathExecutor
You can add operators, functions and variables with the public methods in MathExecutor, but if you need to do more serious modifications to base behaviors, the easiest way to extend MathExecutor is to redefine the following methods in your derived class:
* defaultOperators
* defaultFunctions
* defaultVars
This will allow you to remove functions and operators if needed, or implement different types more simply.
Also note that you can replace an existing default operator by adding a new operator with the same regular expression string. For example if you just need to redefine TokenPlus, you can just add a new operator with the same regex string, in this case '\\+'.
## Documentation
Full class documentation via [PHPFUI/InstaDoc](http://phpfui.com/?n=NXP&c=MathExecutor)
## Future Enhancements
This package will continue to track currently supported versions of PHP.
================================================
FILE: code-of-conduct.md
================================================
# Code of conduct
We don't care who you are IRL. Be professional and responsible.
If you are a good person, we are happy for you.
If you are an asshole to us, we will be assholes in relation to you.
> Do to no one what you yourself dislike
================================================
FILE: code-of-conduct.ru.md
================================================
# Кодекс поведения
Нам всё равно, кто ты в реальной жизни, просто будь профессионалом и всё будет хорошо.
Если ты будешь вести себя как сволочь - не удивляйся, что и к тебе будут относиться так же.
================================================
FILE: composer.json
================================================
{
"name": "nxp/math-executor",
"description": "Simple math expressions calculator",
"minimum-stability": "stable",
"keywords": [
"math",
"parser",
"expression",
"calculator",
"formula",
"mathmatics"
],
"homepage": "http://github.com/NeonXP/MathExecutor",
"license": "MIT",
"authors": [
{
"name": "Alexander 'NeonXP' Kiryukhin",
"email": "a.kiryukhin@mail.ru"
},
{
"name": "Bruce Wells",
"email": "brucekwells@gmail.com"
}
],
"scripts": {
"test": "vendor/bin/phpunit --colors=always",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage"
},
"require": {
"php": ">=8.0 <8.6"
},
"require-dev": {
"phpunit/phpunit": ">=10.0",
"friendsofphp/php-cs-fixer": "*",
"phpstan/phpstan": "*"
},
"autoload": {
"psr-4": {
"NXP\\": "src/NXP"
}
},
"autoload-dev": {
"psr-4": {
"NXP\\Tests\\": "tests/"
}
}
}
================================================
FILE: phpstan.neon.dist
================================================
parameters:
level: 6
errorFormat: raw
editorUrl: '%%file%% %%line%% %%column%%: %%error%%'
paths:
- src
- tests
================================================
FILE: phpunit.xml.dist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
colors="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="./tests/bootstrap.php"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerErrors="true"
displayDetailsOnTestsThatTriggerNotices="true"
displayDetailsOnTestsThatTriggerWarnings="true"
backupStaticProperties="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd">
<testsuites>
<testsuite name="Math Executor tests">
<directory>./tests/</directory>
</testsuite>
</testsuites>
</phpunit>
================================================
FILE: src/NXP/Classes/Calculator.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Classes;
use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\UnknownFunctionException;
use NXP\Exception\UnknownOperatorException;
use NXP\Exception\UnknownVariableException;
/**
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class Calculator
{
/**
* @todo PHP8: Use constructor property promotion -> public function __construct(private array $functions, private array $operators)
*
* @param array<string, CustomFunction> $functions
* @param array<Operator> $operators
*/
public function __construct(private array $functions, private array $operators)
{
}
/**
* Calculate array of tokens in reverse polish notation
*
* @param Token[] $tokens
* @param array<string, float|string> $variables
*
* @throws UnknownVariableException
* @throws IncorrectExpressionException
* @return int|float|string|null
*/
public function calculate(array $tokens, array $variables, ?callable $onVarNotFound = null)
{
/** @var Token[] $stack */
$stack = [];
foreach ($tokens as $token) {
if (Token::Literal === $token->type || Token::String === $token->type) {
$stack[] = $token;
} elseif (Token::Variable === $token->type) {
$variable = $token->value;
$value = null;
if (\array_key_exists($variable, $variables)) {
$value = $variables[$variable];
} elseif ($onVarNotFound) {
$value = \call_user_func($onVarNotFound, $variable);
} else {
throw new UnknownVariableException($variable);
}
$stack[] = new Token(Token::Literal, $value, $variable);
} elseif (Token::Function === $token->type) {
if (! \array_key_exists($token->value, $this->functions)) {
throw new UnknownFunctionException($token->value);
}
$stack[] = $this->functions[$token->value]->execute($stack, $token->paramCount);
} elseif (Token::Operator === $token->type) {
if (! \array_key_exists($token->value, $this->operators)) {
throw new UnknownOperatorException($token->value);
}
$stack[] = $this->operators[$token->value]->execute($stack);
}
}
$result = \array_pop($stack);
if (null === $result || ! empty($stack)) {
throw new IncorrectExpressionException('Stack must be empty');
}
return $result->value;
}
}
================================================
FILE: src/NXP/Classes/CustomFunction.php
================================================
<?php
namespace NXP\Classes;
use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use ReflectionException;
use ReflectionFunction;
class CustomFunction
{
/**
* @var callable $function
*/
public $function;
private bool $isVariadic;
private int $totalParamCount;
private int $requiredParamCount;
/**
* CustomFunction constructor.
*
* @throws ReflectionException
*/
public function __construct(public string $name, callable $function)
{
$this->function = $function;
$reflection = (new ReflectionFunction($function));
$this->isVariadic = $reflection->isVariadic();
$this->totalParamCount = $reflection->getNumberOfParameters();
$this->requiredParamCount = $reflection->getNumberOfRequiredParameters();
}
/**
* @param array<Token> $stack
*
* @throws IncorrectNumberOfFunctionParametersException
*/
public function execute(array &$stack, int $paramCountInStack) : Token
{
if ($paramCountInStack < $this->requiredParamCount) {
throw new IncorrectNumberOfFunctionParametersException($this->name);
}
if ($paramCountInStack > $this->totalParamCount && ! $this->isVariadic) {
throw new IncorrectNumberOfFunctionParametersException($this->name);
}
$args = [];
if ($paramCountInStack > 0) {
for ($i = 0; $i < $paramCountInStack; $i++) {
\array_unshift($args, \array_pop($stack)->value);
}
}
$result = \call_user_func_array($this->function, $args);
return new Token(Token::Literal, $result);
}
}
================================================
FILE: src/NXP/Classes/Operator.php
================================================
<?php
namespace NXP\Classes;
use NXP\Exception\IncorrectExpressionException;
use ReflectionFunction;
class Operator
{
/**
* @var callable
*/
public $function;
public int $places = 0;
/**
* Operator constructor.
*/
public function __construct(public string $operator, public bool $isRightAssoc, public int $priority, callable $function)
{
$this->function = $function;
$reflection = new ReflectionFunction($function);
$this->places = $reflection->getNumberOfParameters();
}
/**
* @param array<Token> $stack
*
* @throws IncorrectExpressionException
*/
public function execute(array &$stack) : Token
{
if (\count($stack) < $this->places) {
throw new IncorrectExpressionException();
}
$args = [];
for ($i = 0; $i < $this->places; $i++) {
\array_unshift($args, \array_pop($stack)->value);
}
$result = \call_user_func_array($this->function, $args);
return new Token(Token::Literal, $result);
}
}
================================================
FILE: src/NXP/Classes/Token.php
================================================
<?php
namespace NXP\Classes;
class Token
{
public const Literal = 'literal';
public const Variable = 'variable';
public const Operator = 'operator';
public const LeftParenthesis = 'LP';
public const RightParenthesis = 'RP';
public const Function = 'function';
public const ParamSeparator = 'separator';
public const String = 'string';
public const Space = 'space';
public ?int $paramCount = null;//to store function parameter count in stack
/**
* Token constructor.
*
*/
public function __construct(public string $type, public mixed $value, public ?string $name = null)
{
}
}
================================================
FILE: src/NXP/Classes/Tokenizer.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Classes;
use NXP\Exception\IncorrectBracketsException;
use NXP\Exception\UnknownOperatorException;
use RuntimeException;
use SplStack;
/**
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class Tokenizer
{
/** @var array<Token> */
public array $tokens = [];
private string $numberBuffer = '';
private string $stringBuffer = '';
private bool $allowNegative = true;
private bool $inSingleQuotedString = false;
private bool $inDoubleQuotedString = false;
/**
* Tokenizer constructor.
* @param Operator[] $operators
*/
public function __construct(private string $input, private array $operators)
{
}
public function tokenize() : self
{
$isLastCharEscape = false;
foreach (\str_split($this->input) as $ch) {
switch (true) {
case $this->inSingleQuotedString:
if ('\\' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
} else {
$isLastCharEscape = true;
}
continue 2;
} elseif ("'" === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= "'";
$isLastCharEscape = false;
} else {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inSingleQuotedString = false;
$this->stringBuffer = '';
}
continue 2;
}
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
}
$this->stringBuffer .= $ch;
continue 2;
case $this->inDoubleQuotedString:
if ('\\' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
} else {
$isLastCharEscape = true;
}
continue 2;
} elseif ('"' === $ch) {
if ($isLastCharEscape) {
$this->stringBuffer .= '"';
$isLastCharEscape = false;
} else {
$this->tokens[] = new Token(Token::String, $this->stringBuffer);
$this->inDoubleQuotedString = false;
$this->stringBuffer = '';
}
continue 2;
}
if ($isLastCharEscape) {
$this->stringBuffer .= '\\';
$isLastCharEscape = false;
}
$this->stringBuffer .= $ch;
continue 2;
case '[' === $ch:
$this->tokens[] = new Token(Token::Function, 'array');
$this->allowNegative = true;
$this->tokens[] = new Token(Token::LeftParenthesis, '');
continue 2;
case ' ' == $ch || "\n" == $ch || "\r" == $ch || "\t" == $ch:
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
$this->tokens[] = new Token(Token::Space, '');
continue 2;
case $this->isNumber($ch):
if ('' != $this->stringBuffer) {
$this->stringBuffer .= $ch;
continue 2;
}
$this->numberBuffer .= $ch;
$this->allowNegative = false;
break;
/** @noinspection PhpMissingBreakStatementInspection */
case 'e' === \strtolower($ch):
if (\strlen($this->numberBuffer) && \str_contains($this->numberBuffer, '.')) {
$this->numberBuffer .= 'e';
$this->allowNegative = false;
break;
}
// no break
// Intentionally fall through
case $this->isAlpha($ch):
if (\strlen($this->numberBuffer)) {
$this->emptyNumberBufferAsLiteral();
$this->tokens[] = new Token(Token::Operator, '*');
}
$this->allowNegative = false;
$this->stringBuffer .= $ch;
break;
case '"' == $ch:
$this->inDoubleQuotedString = true;
continue 2;
case "'" == $ch:
$this->inSingleQuotedString = true;
continue 2;
case $this->isDot($ch):
$this->numberBuffer .= $ch;
$this->allowNegative = false;
break;
case $this->isLP($ch):
if ('' != $this->stringBuffer) {
$this->tokens[] = new Token(Token::Function, $this->stringBuffer);
$this->stringBuffer = '';
} elseif (\strlen($this->numberBuffer)) {
$this->emptyNumberBufferAsLiteral();
$this->tokens[] = new Token(Token::Operator, '*');
}
$this->allowNegative = true;
$this->tokens[] = new Token(Token::LeftParenthesis, '');
break;
case $this->isRP($ch) || ']' === $ch :
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
$this->allowNegative = false;
$this->tokens[] = new Token(Token::RightParenthesis, '');
break;
case $this->isComma($ch):
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
$this->allowNegative = true;
$this->tokens[] = new Token(Token::ParamSeparator, '');
break;
default:
// special case for unary operations
if ('-' == $ch || '+' == $ch) {
if ($this->allowNegative) {
$this->allowNegative = false;
$this->tokens[] = new Token(Token::Operator, '-' == $ch ? 'uNeg' : 'uPos');
continue 2;
}
// could be in exponent, in which case negative should be added to the numberBuffer
if ($this->numberBuffer && 'e' == $this->numberBuffer[\strlen($this->numberBuffer) - 1]) {
$this->numberBuffer .= $ch;
continue 2;
}
}
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
if ('$' != $ch) {
if (\count($this->tokens) > 0) {
if (Token::Operator === $this->tokens[\count($this->tokens) - 1]->type) {
$this->tokens[\count($this->tokens) - 1]->value .= $ch;
} else {
$this->tokens[] = new Token(Token::Operator, $ch);
}
} else {
$this->tokens[] = new Token(Token::Operator, $ch);
}
}
$this->allowNegative = true;
}
}
$this->emptyNumberBufferAsLiteral();
$this->emptyStrBufferAsVariable();
return $this;
}
/**
* @throws IncorrectBracketsException
* @throws UnknownOperatorException
* @return Token[] Array of tokens in revers polish notation
*/
public function buildReversePolishNotation() : array
{
$tokens = [];
/** @var SplStack<Token> $stack */
$stack = new SplStack();
/**
* @var SplStack<int> $paramCounter
*/
$paramCounter = new SplStack();
foreach ($this->tokens as $token) {
switch ($token->type) {
case Token::Literal:
case Token::Variable:
case Token::String:
$tokens[] = $token;
if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) {
$paramCounter->push($paramCounter->pop() + 1);
}
break;
case Token::Function:
if ($paramCounter->count() > 0 && 0 === $paramCounter->top()) {
$paramCounter->push($paramCounter->pop() + 1);
}
$stack->push($token);
$paramCounter->push(0);
break;
case Token::LeftParenthesis:
$stack->push($token);
break;
case Token::ParamSeparator:
while (Token::LeftParenthesis !== $stack->top()->type) {
if (0 === $stack->count()) {
throw new IncorrectBracketsException();
}
$tokens[] = $stack->pop();
}
$paramCounter->push($paramCounter->pop() + 1);
break;
case Token::Operator:
if (! \array_key_exists($token->value, $this->operators)) {
throw new UnknownOperatorException($token->value);
}
$op1 = $this->operators[$token->value];
while ($stack->count() > 0 && Token::Operator === $stack->top()->type) {
if (! \array_key_exists($stack->top()->value, $this->operators)) {
throw new UnknownOperatorException($stack->top()->value);
}
$op2 = $this->operators[$stack->top()->value];
if ($op2->priority >= $op1->priority) {
$tokens[] = $stack->pop();
continue;
}
break;
}
$stack->push($token);
break;
case Token::RightParenthesis:
while (true) {
try {
$ctoken = $stack->pop();
if (Token::LeftParenthesis === $ctoken->type) {
break;
}
$tokens[] = $ctoken;
} catch (RuntimeException) {
throw new IncorrectBracketsException();
}
}
if ($stack->count() > 0 && Token::Function == $stack->top()->type) {
/**
* @var Token $f
*/
$f = $stack->pop();
$f->paramCount = $paramCounter->pop();
$tokens[] = $f;
}
break;
case Token::Space:
//do nothing
}
}
while (0 !== $stack->count()) {
if (Token::LeftParenthesis === $stack->top()->type || Token::RightParenthesis === $stack->top()->type) {
throw new IncorrectBracketsException();
}
if (Token::Space === $stack->top()->type) {
$stack->pop();
continue;
}
$tokens[] = $stack->pop();
}
return $tokens;
}
private function isNumber(string $ch) : bool
{
return $ch >= '0' && $ch <= '9';
}
private function isAlpha(string $ch) : bool
{
return $ch >= 'a' && $ch <= 'z' || $ch >= 'A' && $ch <= 'Z' || '_' == $ch;
}
private function emptyNumberBufferAsLiteral() : void
{
if (\strlen($this->numberBuffer)) {
$this->tokens[] = new Token(Token::Literal, $this->numberBuffer);
$this->numberBuffer = '';
}
}
private function isDot(string $ch) : bool
{
return '.' == $ch;
}
private function isLP(string $ch) : bool
{
return '(' == $ch;
}
private function isRP(string $ch) : bool
{
return ')' == $ch;
}
private function emptyStrBufferAsVariable() : void
{
if ('' != $this->stringBuffer) {
$this->tokens[] = new Token(Token::Variable, $this->stringBuffer);
$this->stringBuffer = '';
}
}
private function isComma(string $ch) : bool
{
return ',' == $ch;
}
}
================================================
FILE: src/NXP/Exception/DivisionByZeroException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Vitaliy Zhuk <zhuk2205@gmail.com>
*/
class DivisionByZeroException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectBracketsException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class IncorrectBracketsException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectExpressionException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Vitaliy Zhuk <zhuk2205@gmail.com>
*/
class IncorrectExpressionException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectFunctionParameterException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
class IncorrectFunctionParameterException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectNumberOfFunctionParametersException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
class IncorrectNumberOfFunctionParametersException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/MathExecutorException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Vitaliy Zhuk <zhuk2205@gmail.com>
*/
class MathExecutorException extends \Exception
{
}
================================================
FILE: src/NXP/Exception/UnknownFunctionException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Vitaliy Zhuk <zhuk2205@gmail.com>
*/
class UnknownFunctionException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/UnknownOperatorException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Vitaliy Zhuk <zhuk2205@gmail.com>
*/
class UnknownOperatorException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/UnknownVariableException.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Exception;
/**
* @author Alexander Kiryukhin <a.kiryukhin@mail.ru>
*/
class UnknownVariableException extends MathExecutorException
{
}
================================================
FILE: src/NXP/MathExecutor.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP;
use NXP\Classes\Calculator;
use NXP\Classes\CustomFunction;
use NXP\Classes\Operator;
use NXP\Classes\Token;
use NXP\Classes\Tokenizer;
use NXP\Exception\DivisionByZeroException;
use NXP\Exception\MathExecutorException;
use NXP\Exception\UnknownVariableException;
use ReflectionException;
/**
* Class MathExecutor
* @package NXP
*/
class MathExecutor
{
/**
* Available variables
*
* @var array<string, float|string>
*/
protected array $variables = [];
/**
* @var callable|null
*/
protected $onVarNotFound = null;
/**
* @var callable|null
*/
protected $onVarValidation = null;
/**
* @var Operator[]
*/
protected array $operators = [];
/**
* @var array<string, CustomFunction>
*/
protected array $functions = [];
/**
* @var array<string, Token[]>
*/
protected array $cache = [];
/**
* Base math operators
*/
public function __construct()
{
$this->addDefaults();
}
public function __clone()
{
$this->addDefaults();
}
/**
* Add operator to executor
*
*/
public function addOperator(Operator $operator) : self
{
$this->operators[$operator->operator] = $operator;
return $this;
}
/**
* Execute expression
*
* @throws Exception\IncorrectExpressionException
* @throws Exception\UnknownOperatorException
* @throws UnknownVariableException
* @throws Exception\IncorrectBracketsException
* @return int|float|string|null
*/
public function execute(string $expression, bool $cache = true)
{
$cacheKey = $expression;
if (! \array_key_exists($cacheKey, $this->cache)) {
$tokens = (new Tokenizer($expression, $this->operators))->tokenize()->buildReversePolishNotation();
if ($cache) {
$this->cache[$cacheKey] = $tokens;
}
} else {
$tokens = $this->cache[$cacheKey];
}
$calculator = new Calculator($this->functions, $this->operators);
return $calculator->calculate($tokens, $this->variables, $this->onVarNotFound);
}
/**
* Add function to executor
*
* @param string $name Name of function
* @param callable|null $function Function
*
* @throws ReflectionException
* @throws Exception\IncorrectNumberOfFunctionParametersException
*/
public function addFunction(string $name, ?callable $function = null) : self
{
$this->functions[$name] = new CustomFunction($name, $function);
return $this;
}
/**
* Get all vars
*
* @return array<string, float|string>
*/
public function getVars() : array
{
return $this->variables;
}
/**
* Get a specific var
*
* @throws UnknownVariableException if VarNotFoundHandler is not set
*/
public function getVar(string $variable) : mixed
{
if (! \array_key_exists($variable, $this->variables)) {
if ($this->onVarNotFound) {
return \call_user_func($this->onVarNotFound, $variable);
}
throw new UnknownVariableException("Variable ({$variable}) not set");
}
return $this->variables[$variable];
}
/**
* Add variable to executor. To set a custom validator use setVarValidationHandler.
*
* @throws MathExecutorException if the value is invalid based on the default or custom validator
*/
public function setVar(string $variable, mixed $value) : self
{
if ($this->onVarValidation) {
\call_user_func($this->onVarValidation, $variable, $value);
}
$this->variables[$variable] = $value;
return $this;
}
/**
* Test to see if a variable exists
*
*/
public function varExists(string $variable) : bool
{
return \array_key_exists($variable, $this->variables);
}
/**
* Add variables to executor
*
* @param array<string, float|int|string> $variables
* @param bool $clear Clear previous variables
* @throws \Exception
*/
public function setVars(array $variables, bool $clear = true) : self
{
if ($clear) {
$this->removeVars();
}
foreach ($variables as $name => $value) {
$this->setVar($name, $value);
}
return $this;
}
/**
* Define a method that will be invoked when a variable is not found.
* The first parameter will be the variable name, and the returned value will be used as the variable value.
*
*
*/
public function setVarNotFoundHandler(callable $handler) : self
{
$this->onVarNotFound = $handler;
return $this;
}
/**
* Define a validation method that will be invoked when a variable is set using setVar.
* The first parameter will be the variable name, and the second will be the variable value.
* Set to null to disable validation.
*
* @param ?callable $handler throws a MathExecutorException in case of an invalid variable
*
*/
public function setVarValidationHandler(?callable $handler) : self
{
$this->onVarValidation = $handler;
return $this;
}
/**
* Remove variable from executor
*
*/
public function removeVar(string $variable) : self
{
unset($this->variables[$variable]);
return $this;
}
/**
* Remove all variables and the variable not found handler
*/
public function removeVars() : self
{
$this->variables = [];
$this->onVarNotFound = null;
return $this;
}
/**
* Get all registered operators to executor
*
* @return array<Operator> of operator class names
*/
public function getOperators() : array
{
return $this->operators;
}
/**
* Get all registered functions
*
* @return array<string, CustomFunction> containing callback and places indexed by
* function name
*/
public function getFunctions() : array
{
return $this->functions;
}
/**
* Remove a specific operator
*/
public function removeOperator(string $operator) : self
{
unset($this->operators[$operator]);
return $this;
}
/**
* Set division by zero returns zero instead of throwing DivisionByZeroException
*/
public function setDivisionByZeroIsZero() : self
{
$this->addOperator(new Operator('/', false, 180, static fn($a, $b) => 0 == $b ? 0 : $a / $b));
return $this;
}
/**
* Get cache array with tokens
* @return array<string, Token[]>
*/
public function getCache() : array
{
return $this->cache;
}
/**
* Clear token's cache
*/
public function clearCache() : self
{
$this->cache = [];
return $this;
}
public function useBCMath(int $scale = 2) : self
{
\bcscale($scale);
$this->addOperator(new Operator('+', false, 170, static fn($a, $b) => \bcadd("{$a}", "{$b}")));
$this->addOperator(new Operator('-', false, 170, static fn($a, $b) => \bcsub("{$a}", "{$b}")));
$this->addOperator(new Operator('uNeg', false, 200, static fn($a) => \bcsub('0.0', "{$a}")));
$this->addOperator(new Operator('*', false, 180, static fn($a, $b) => \bcmul("{$a}", "{$b}")));
$this->addOperator(new Operator('/', false, 180, static function($a, $b) {
/** @todo PHP8: Use throw as expression -> static fn($a, $b) => 0 == $b ? throw new DivisionByZeroException() : $a / $b */
if (0 == $b) {
throw new DivisionByZeroException();
}
return \bcdiv("{$a}", "{$b}");
}));
$this->addOperator(new Operator('^', true, 220, static fn($a, $b) => \bcpow("{$a}", "{$b}")));
$this->addOperator(new Operator('%', false, 180, static fn($a, $b) => \bcmod("{$a}", "{$b}")));
return $this;
}
/**
* Set default operands and functions
* @throws ReflectionException
*/
protected function addDefaults() : self
{
foreach ($this->defaultOperators() as $name => $operator) {
[$callable, $priority, $isRightAssoc] = $operator;
$this->addOperator(new Operator($name, $isRightAssoc, $priority, $callable));
}
foreach ($this->defaultFunctions() as $name => $callable) {
$this->addFunction($name, $callable);
}
$this->onVarValidation = [$this, 'defaultVarValidation'];
$this->variables = $this->defaultVars();
return $this;
}
/**
* Get the default operators
*
* @return array<string, array{callable, int, bool}>
*/
protected function defaultOperators() : array
{
return [
'+' => [static fn($a, $b) => $a + $b, 170, false],
'-' => [static fn($a, $b) => $a - $b, 170, false],
// unary positive token
'uPos' => [static fn($a) => $a, 200, false],
// unary minus token
'uNeg' => [static fn($a) => 0 - $a, 200, false],
'*' => [static fn($a, $b) => $a * $b, 180, false],
'/' => [
static function($a, $b) {
/** @todo PHP8: Use throw as expression -> static fn($a, $b) => 0 == $b ? throw new DivisionByZeroException() : $a / $b */
if (0 == $b) {
throw new DivisionByZeroException();
}
return $a / $b;
},
180,
false
],
'^' => [static fn($a, $b) => $a ** $b, 220, true],
'%' => [static fn($a, $b) => $a % $b, 180, false],
'&&' => [static fn($a, $b) => $a && $b, 100, false],
'||' => [static fn($a, $b) => $a || $b, 90, false],
'==' => [static fn($a, $b) => \is_string($a) || \is_string($b) ? 0 == \strcmp((string)$a, (string)$b) : $a == $b, 140, false],
'!=' => [static fn($a, $b) => \is_string($a) || \is_string($b) ? 0 != \strcmp((string)$a, (string)$b) : $a != $b, 140, false],
'>=' => [static fn($a, $b) => $a >= $b, 150, false],
'>' => [static fn($a, $b) => $a > $b, 150, false],
'<=' => [static fn($a, $b) => $a <= $b, 150, false],
'<' => [static fn($a, $b) => $a < $b, 150, false],
'!' => [static fn($a) => ! $a, 190, false],
];
}
/**
* Gets the default functions as an array. Key is function name
* and value is the function as a closure.
*
* @return array<callable>
*/
protected function defaultFunctions() : array
{
return [
'abs' => static fn($arg) => \abs($arg),
'acos' => static fn($arg) => \acos($arg),
'acosh' => static fn($arg) => \acosh($arg),
'arcsin' => static fn($arg) => \asin($arg),
'arcctg' => static fn($arg) => M_PI / 2 - \atan($arg),
'arccot' => static fn($arg) => M_PI / 2 - \atan($arg),
'arccotan' => static fn($arg) => M_PI / 2 - \atan($arg),
'arcsec' => static fn($arg) => \acos(1 / $arg),
'arccosec' => static fn($arg) => \asin(1 / $arg),
'arccsc' => static fn($arg) => \asin(1 / $arg),
'arccos' => static fn($arg) => \acos($arg),
'arctan' => static fn($arg) => \atan($arg),
'arctg' => static fn($arg) => \atan($arg),
'array' => static fn(...$args) => $args,
'asin' => static fn($arg) => \asin($arg),
'atan' => static fn($arg) => \atan($arg),
'atan2' => static fn($arg1, $arg2) => \atan2($arg1, $arg2),
'atanh' => static fn($arg) => \atanh($arg),
'atn' => static fn($arg) => \atan($arg),
'avg' => static function($arg1, ...$args) {
if (\is_array($arg1)) {
if (0 === \count($arg1)) {
throw new \InvalidArgumentException('avg() must have at least one argument!');
}
return \array_sum($arg1) / \count($arg1);
}
$args = [$arg1, ...$args];
return \array_sum($args) / \count($args);
},
'bindec' => static fn($arg) => \bindec($arg),
'ceil' => static fn($arg) => \ceil($arg),
'cos' => static fn($arg) => \cos($arg),
'cosec' => static fn($arg) => 1 / \sin($arg),
'csc' => static fn($arg) => 1 / \sin($arg),
'cosh' => static fn($arg) => \cosh($arg),
'ctg' => static fn($arg) => \cos($arg) / \sin($arg),
'cot' => static fn($arg) => \cos($arg) / \sin($arg),
'cotan' => static fn($arg) => \cos($arg) / \sin($arg),
'cotg' => static fn($arg) => \cos($arg) / \sin($arg),
'ctn' => static fn($arg) => \cos($arg) / \sin($arg),
'decbin' => static fn($arg) => \decbin($arg),
'dechex' => static fn($arg) => \dechex($arg),
'decoct' => static fn($arg) => \decoct($arg),
'deg2rad' => static fn($arg) => \deg2rad($arg),
'exp' => static fn($arg) => \exp($arg),
'expm1' => static fn($arg) => \expm1($arg),
'floor' => static fn($arg) => \floor($arg),
'fmod' => static fn($arg1, $arg2) => \fmod($arg1, $arg2),
'hexdec' => static fn($arg) => \hexdec($arg),
'hypot' => static fn($arg1, $arg2) => \hypot($arg1, $arg2),
'if' => function($expr, $trueval, $falseval) {
if (true === $expr || false === $expr) {
$exres = $expr;
} else {
$exres = $this->execute($expr);
}
if ($exres) {
return $this->execute($trueval);
}
return $this->execute($falseval);
},
'intdiv' => static fn($arg1, $arg2) => \intdiv($arg1, $arg2),
'ln' => static fn($arg1, $arg2 = M_E) => \log($arg1, $arg2),
'lg' => static fn($arg) => \log10($arg),
'log' => static fn($arg1, $arg2 = M_E) => \log($arg1, $arg2),
'log10' => static fn($arg) => \log10($arg),
'log1p' => static fn($arg) => \log1p($arg),
'max' => static function($arg1, ...$args) {
if (\is_array($arg1) && 0 === \count($arg1)) {
throw new \InvalidArgumentException('max() must have at least one argument!');
}
return \max(\is_array($arg1) ? $arg1 : [$arg1, ...$args]);
},
'median' => static function($arg1, ...$args) {
if (\is_array($arg1)) {
if (0 === \count($arg1)) {
throw new \InvalidArgumentException('Array must contain at least one element!');
}
$finalArgs = $arg1;
} else {
$finalArgs = [$arg1, ...$args];
}
$count = \count($finalArgs);
\sort($finalArgs);
$index = (int)\floor($count / 2);
return ($count & 1) ? $finalArgs[$index] : ($finalArgs[$index - 1] + $finalArgs[$index]) / 2;
},
'min' => static function($arg1, ...$args) {
if (\is_array($arg1) && 0 === \count($arg1)) {
throw new \InvalidArgumentException('min() must have at least one argument!');
}
return \min(\is_array($arg1) ? $arg1 : [$arg1, ...$args]);
},
'octdec' => static fn($arg) => \octdec($arg),
'pi' => static fn() => M_PI,
'pow' => static fn($arg1, $arg2) => $arg1 ** $arg2,
'rad2deg' => static fn($arg) => \rad2deg($arg),
'round' => static fn($num, int $precision = 0) => \round($num, $precision),
'sin' => static fn($arg) => \sin($arg),
'sinh' => static fn($arg) => \sinh($arg),
'sec' => static fn($arg) => 1 / \cos($arg),
'sqrt' => static fn($arg) => \sqrt($arg),
'tan' => static fn($arg) => \tan($arg),
'tanh' => static fn($arg) => \tanh($arg),
'tn' => static fn($arg) => \tan($arg),
'tg' => static fn($arg) => \tan($arg)
];
}
/**
* Returns the default variables names as key/value pairs
*
* @return array<string, float>
*/
protected function defaultVars() : array
{
return [
'pi' => 3.14159265359,
'e' => 2.71828182846
];
}
/**
* Default variable validation, ensures that the value is a scalar or array.
* @throws MathExecutorException if the value is not a scalar
*/
protected function defaultVarValidation(string $variable, mixed $value) : void
{
if (! \is_scalar($value) && ! \is_array($value) && null !== $value) {
$type = \gettype($value);
throw new MathExecutorException("Variable ({$variable}) type ({$type}) is not scalar or array!");
}
}
}
================================================
FILE: tests/MathTest.php
================================================
<?php
/**
* This file is part of the MathExecutor package
*
* (c) Alexander Kiryukhin
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/
namespace NXP\Tests;
use Exception;
use NXP\Exception\DivisionByZeroException;
use NXP\Exception\IncorrectExpressionException;
use NXP\Exception\IncorrectNumberOfFunctionParametersException;
use NXP\Exception\MathExecutorException;
use NXP\Exception\UnknownFunctionException;
use NXP\Exception\UnknownVariableException;
use NXP\MathExecutor;
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('providerExpressions')]
public function testCalculating(string $expression) : void
{
$calculator = new MathExecutor();
/** @var float $phpResult */
$phpResult = 0.0;
eval('$phpResult = ' . $expression . ';');
try {
$result = $calculator->execute($expression);
} catch (Exception $e) {
$this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression));
}
$this->assertEquals($phpResult, $result, "Expression was: {$expression}");
}
/**
* Expressions data provider
*
* Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval.
* The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
* something more complex and not a simple mathmatical expression.
*
* @return array<array<string>>
*/
public static function providerExpressions()
{
return [
['-5'],
['-5+10'],
['4-5'],
['4 -5'],
['(4*2)-5'],
['(4*2) - 5'],
['4*-5'],
['4 * -5'],
['+5'],
['+(3+2)'],
['+(+3+2)'],
['+(-3+2)'],
['-5'],
['-(-5)'],
['-(+5)'],
['+(-5)'],
['+(+5)'],
['-(3+2)'],
['-(-3+-2)'],
['abs(1.5)'],
['acos(0.15)'],
['acosh(1.5)'],
['asin(0.15)'],
['atan(0.15)'],
['atan2(1.5, 3.5)'],
['atanh(0.15)'],
['bindec("10101")'],
['ceil(1.5)'],
['cos(1.5)'],
['cosh(1.5)'],
['decbin("15")'],
['dechex("15")'],
['decoct("15")'],
['deg2rad(1.5)'],
['exp(1.5)'],
['expm1(1.5)'],
['floor(1.5)'],
['fmod(1.5, 3.5)'],
['hexdec("abcdef")'],
['hypot(1.5, 3.5)'],
['intdiv(10, 2)'],
['log(1.5)'],
['log(1.5, 3)'],
['log10(1.5)'],
['log1p(1.5)'],
['max(1.5, 3.5)'],
['min(1.5, 3.5)'],
['octdec("15")'],
['pi()'],
['pow(1.5, 3.5)'],
['rad2deg(1.5)'],
['round(1.5)'],
['sin(1.5)'],
['sin(12)'],
['+sin(12)'],
['-sin(12)'],
['sinh(1.5)'],
['sqrt(1.5)'],
['tan(1.5)'],
['tanh(1.5)'],
['0.1 + 0.2'],
['0.1 + 0.2 - 0.3'],
['1 + 2'],
['0.1 - 0.2'],
['1 - 2'],
['0.1 * 2'],
['1 * 2'],
['0.1 / 0.2'],
['1 / 2'],
['2 * 2 + 3 * 3'],
['2 * 2 / 3 * 3'],
['2 / 2 / 3 / 3'],
['2 / 2 * 3 / 3'],
['2 / 2 * 3 * 3'],
['1 + 0.6 - 3 * 2 / 50'],
['(5 + 3) * -1'],
['-2- 2*2'],
['2- 2*2'],
['2-(2*2)'],
['(2- 2)*2'],
['2 + 2*2'],
['2+ 2*2'],
['2+2*2'],
['(2+2)*2'],
['(2 + 2)*-2'],
['(2+-2)*2'],
['1 + 2 * 3 / (min(1, 5) + 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) - 2 + 5)'],
['1 + 2 * 3 / (min(1, 5) * 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 * 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 / 1)'],
['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)'],
['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'],
['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)'],
['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'],
['(1 + 2) * 3 / (3 / min(1, 5) / 2 + 1)'],
['sin(10) * cos(50) / min(10, 20/2)'],
['sin(10) * cos(50) / min(10, (20/2))'],
['sin(10) * cos(50) / min(10, (max(10,20)/2))'],
['100500 * 3.5e5'],
['100500 * 3.5e-5'],
['100500 * 3.5E5'],
['100500 * 3.5E-5'],
['1 + "2" / 3'],
["1.5 + '2.5' / 4"],
['1.5 + "2.5" * ".5"'],
['-1 + -2'],
['-1+-2'],
['-1- -2'],
['-1/-2'],
['-1*-2'],
['(1+2+3+4-5)*7/100'],
['(-1+2+3+4- 5)*7/100'],
['(1+2+3+4- 5)*7/100'],
['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'],
['1 && 0'],
['1 && 0 && 1'],
['1 || 0'],
['1 && 0 || 1'],
['5 == 3'],
['5 == 5'],
['5 != 3'],
['5 != 5'],
['5 > 3'],
['3 > 5'],
['3 >= 5'],
['3 >= 3'],
['3 < 5'],
['5 < 3'],
['3 <= 5'],
['5 <= 5'],
['10 < 9 || 4 > (2+1)'],
['10 < 9 || 4 > (-2+1)'],
['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'],
['1 + 5 == 3 + 1'],
['1 + 5 == 5 + 1'],
['1 + 5 != 3 + 1'],
['1 + 5 != 5 + 1'],
['1 + 5 > 3 + 1'],
['1 + 3 > 5 + 1'],
['1 + 3 >= 5 + 1'],
['1 + 3 >= 3 + 1'],
['1 + 3 < 5 + 1'],
['1 + 5 < 3 + 1'],
['1 + 3 <= 5 + 1'],
['1 + 5 <= 5 + 1'],
['(-4)'],
['(-4 + 5)'],
['(3 * 1)'],
['(-3 * -1)'],
['1 + (-3 * -1)'],
['1 + ( -3 * 1)'],
['1 + (3 *-1)'],
['1 - 0'],
['1-0'],
['-(1.5)'],
['-log(4)'],
['0-acosh(1.5)'],
['-acosh(1.5)'],
['-(-4)'],
['-(-4 + 5)'],
['-(3 * 1)'],
['-(-3 * -1)'],
['-1 + (-3 * -1)'],
['-1 + ( -3 * 1)'],
['-1 + (3 *-1)'],
['-1 - 0'],
['-1-0'],
['-(4*2)-5'],
['-(4*-2)-5'],
['-(-4*2) - 5'],
['-4*-5'],
['max(1,2,4.9,3)'],
['min(1,2,4.9,3)'],
['max([1,2,4.9,3])'],
['min([1,2,4.9,3])'],
['4 % 4'],
['7 % 4'],
['99 % 4'],
['123 % 7'],
['!(1||0)'],
['!(1&&0)'],
['!(1)'],
['!(0)'],
['! 1'],
['! 0'],
['!1'],
['!0'],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('bcMathExpressions')]
public function testBCMathCalculating(string $expression, string $expected = '') : void
{
$calculator = new MathExecutor();
$calculator->useBCMath();
if ('' === $expected)
{
$expected = $expression;
}
/** @var float $phpResult */
$phpResult = 0.0;
eval('$phpResult = ' . $expected . ';');
try {
$result = $calculator->execute($expression);
} catch (Exception $e) {
$this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression));
}
$this->assertEquals($phpResult, $result, "Expression was: {$expression}");
}
/**
* Expressions data provider
*
* Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP with eval.
* The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
* something more complex and not a simple mathmatical expression.
*
* @return array<array<string>>
*/
public static function bcMathExpressions()
{
return [
['-5'],
['-5+10'],
['4-5'],
['4 -5'],
['(4*2)-5'],
['(4*2) - 5'],
['4*-5'],
['4 * -5'],
['+5'],
['+(3+2)'],
['+(+3+2)'],
['+(-3+2)'],
['-5'],
['-(-5)'],
['-(+5)'],
['+(-5)'],
['+(+5)'],
['-(3+2)'],
['-(-3+-2)'],
['abs(1.5)'],
['acos(0.15)'],
['acosh(1.5)'],
['asin(0.15)'],
['atan(0.15)'],
['atan2(1.5, 3.5)'],
['atanh(0.15)'],
['bindec("10101")'],
['ceil(1.5)'],
['cos(1.5)'],
['cosh(1.5)'],
['decbin("15")'],
['dechex("15")'],
['decoct("15")'],
['deg2rad(1.5)'],
['exp(1.5)'],
['expm1(1.5)'],
['floor(1.5)'],
['fmod(1.5, 3.5)'],
['hexdec("abcdef")'],
['hypot(1.5, 3.5)'],
['intdiv(10, 2)'],
['log(1.5)'],
['log10(1.5)'],
['log1p(1.5)'],
['max(1.5, 3.5)'],
['min(1.5, 3.5)'],
['octdec("15")'],
['pi()'],
['pow(1.5, 3.5)'],
['rad2deg(1.5)'],
['round(1.5)'],
['sin(1.5)'],
['sin(12)'],
['+sin(12)'],
['-sin(12)', '0.53'],
['sinh(1.5)'],
['sqrt(1.5)'],
['tan(1.5)'],
['tanh(1.5)'],
['0.1 + 0.2', '0.30'],
['0.1 + 0.2 - 0.3', '0.00'],
['1 + 2'],
['0.1 - 0.2'],
['1 - 2'],
['0.1 * 2'],
['1 * 2'],
['0.1 / 0.2'],
['1 / 2'],
['2 * 2 + 3 * 3'],
['2 * 2 / 3 * 3', '3.99'],
['2 / 2 / 3 / 3', '0.11'],
['2 / 2 * 3 / 3'],
['2 / 2 * 3 * 3'],
['1 + 0.6 - 3 * 2 / 50'],
['(5 + 3) * -1'],
['-2- 2*2'],
['2- 2*2'],
['2-(2*2)'],
['(2- 2)*2'],
['2 + 2*2'],
['2+ 2*2'],
['2+2*2'],
['(2+2)*2'],
['(2 + 2)*-2'],
['(2+-2)*2'],
['1 + 2 * 3 / (min(1, 5) + 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) - 2 + 5)'],
['1 + 2 * 3 / (min(1, 5) * 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 + 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 * 1)'],
['1 + 2 * 3 / (min(1, 5) / 2 / 1)'],
['1 + 2 * 3 / (3 + min(1, 5) + 2 + 1)', '1.85'],
['1 + 2 * 3 / (3 - min(1, 5) - 2 + 1)'],
['1 + 2 * 3 / (3 * min(1, 5) * 2 + 1)', '1.85'],
['1 + 2 * 3 / (3 / min(1, 5) / 2 + 1)'],
['(1 + 2) * 3 / (3 / min(1, 5) / 2 + 1)'],
['sin(10) * cos(50) / min(10, 20/2)', '-0.05'],
['sin(10) * cos(50) / min(10, (20/2))', '-0.05'],
['sin(10) * cos(50) / min(10, (max(10,20)/2))', '-0.05'],
['1 + "2" / 3', '1.66'],
["1.5 + '2.5' / 4", '2.12'],
['1.5 + "2.5" * ".5"'],
['-1 + -2'],
['-1+-2'],
['-1- -2'],
['-1/-2'],
['-1*-2'],
['(1+2+3+4-5)*7/100'],
['(-1+2+3+4- 5)*7/100'],
['(1+2+3+4- 5)*7/100'],
['( 1 + 2 + 3 + 4 - 5 ) * 7 / 100'],
['1 && 0'],
['1 && 0 && 1'],
['1 || 0'],
['1 && 0 || 1'],
['5 == 3'],
['5 == 5'],
['5 != 3'],
['5 != 5'],
['5 > 3'],
['3 > 5'],
['3 >= 5'],
['3 >= 3'],
['3 < 5'],
['5 < 3'],
['3 <= 5'],
['5 <= 5'],
['10 < 9 || 4 > (2+1)'],
['10 < 9 || 4 > (-2+1)'],
['10 < 9 || 4 > (2+1) && 5 == 5 || 4 != 6 || 3 >= 4 || 3 <= 7'],
['1 + 5 == 3 + 1'],
['1 + 5 == 5 + 1'],
['1 + 5 != 3 + 1'],
['1 + 5 != 5 + 1'],
['1 + 5 > 3 + 1'],
['1 + 3 > 5 + 1'],
['1 + 3 >= 5 + 1'],
['1 + 3 >= 3 + 1'],
['1 + 3 < 5 + 1'],
['1 + 5 < 3 + 1'],
['1 + 3 <= 5 + 1'],
['1 + 5 <= 5 + 1'],
['(-4)'],
['(-4 + 5)'],
['(3 * 1)'],
['(-3 * -1)'],
['1 + (-3 * -1)'],
['1 + ( -3 * 1)'],
['1 + (3 *-1)'],
['1 - 0'],
['1-0'],
['-(1.5)'],
['-log(4)', '-1.38'],
['0-acosh(1.5)', '-0.96'],
['-acosh(1.5)', '-0.96'],
['-(-4)'],
['-(-4 + 5)'],
['-(3 * 1)'],
['-(-3 * -1)'],
['-1 + (-3 * -1)'],
['-1 + ( -3 * 1)'],
['-1 + (3 *-1)'],
['-1 - 0'],
['-1-0'],
['-(4*2)-5'],
['-(4*-2)-5'],
['-(-4*2) - 5'],
['-4*-5'],
['max(1,2,4.9,3)'],
['min(1,2,4.9,3)'],
['max([1,2,4.9,3])'],
['min([1,2,4.9,3])'],
['4 % 4'],
['7 % 4'],
['99 % 4'],
['123 % 7'],
['!(1||0)'],
['!(1&&0)'],
['!(1)'],
['!(0)'],
['! 1'],
['! 0'],
['!1'],
['!0'],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('incorrectExpressions')]
public function testIncorrectExpressionException(string $expression) : void
{
$calculator = new MathExecutor();
$calculator->setVars(['a' => 12, 'b' => 24]);
$this->expectException(IncorrectExpressionException::class);
$calculator->execute($expression);
}
/**
* Incorrect Expressions data provider
*
* These expressions should not pass validation
*
* @return array<array<string>>
*/
public static function incorrectExpressions()
{
return [
['1 * + '],
[' 2 3'],
['2 3 '],
[' 2 4 3 '],
['$a $b'],
['$a [3, 4, 5]'],
['$a (3 + 4)'],
['$a "string"'],
['5 "string"'],
['"string" $a'],
['$a round(12.345)'],
['round(12.345) $a'],
['4 round(12.345)'],
['round(12.345) 4'],
];
}
public function testUnknownFunctionException() : void
{
$calculator = new MathExecutor();
$this->expectException(UnknownFunctionException::class);
$calculator->execute('1 * fred("wilma") + 3');
}
public function testZeroDivision() : void
{
$calculator = new MathExecutor();
$calculator->setDivisionByZeroIsZero();
$this->assertEquals(0, $calculator->execute('10 / 0'));
}
public function testUnaryOperators() : void
{
$calculator = new MathExecutor();
$this->assertEquals(5, $calculator->execute('+5'));
$this->assertEquals(5, $calculator->execute('+(3+2)'));
$this->assertEquals(-5, $calculator->execute('-5'));
$this->assertEquals(5, $calculator->execute('-(-5)'));
$this->assertEquals(-5, $calculator->execute('+(-5)'));
$this->assertEquals(-5, $calculator->execute('-(3+2)'));
}
public function testZeroDivisionException() : void
{
$calculator = new MathExecutor();
$this->expectException(DivisionByZeroException::class);
$calculator->execute('10 / 0');
$calculator->setVar('one', 1)->setVar('zero', 0);
$this->assertEquals(0.0, $calculator->execute('$one / $zero'));
}
public function testVariableIncorrectExpressionException() : void
{
$calculator = new MathExecutor();
$calculator->setVar('four', 4);
$this->assertEquals(4, $calculator->execute('$four'));
$this->expectException(IncorrectExpressionException::class);
$this->assertEquals(0.0, $calculator->execute('$'));
$this->assertEquals(0.0, $calculator->execute('$ + $four'));
}
public function testNotVariableOperator() : void
{
$calculator = new MathExecutor();
$calculator->setVar('one', 1);
$calculator->setVar('zero', 0);
$this->assertEquals(false, $calculator->execute('! $one'));
$this->assertEquals(false, $calculator->execute('!$one'));
$this->assertEquals(false, $calculator->execute('! ($one)'));
$this->assertEquals(false, $calculator->execute('!($one)'));
$this->assertEquals(true, $calculator->execute('! $zero'));
$this->assertEquals(true, $calculator->execute('!$zero'));
$this->assertEquals(true, $calculator->execute('! ($zero)'));
$this->assertEquals(true, $calculator->execute('!($zero)'));
}
public function testExponentiation() : void
{
$calculator = new MathExecutor();
$this->assertEquals(100, $calculator->execute('10 ^ 2'));
}
public function testStringEscape() : void
{
$calculator = new MathExecutor();
$this->assertEquals("test\string", $calculator->execute('"test\string"'));
$this->assertEquals("\\test\string\\", $calculator->execute('"\test\string\\\\"'));
$this->assertEquals('\test\string\\', $calculator->execute('"\test\string\\\\"'));
$this->assertEquals('test\\\\string', $calculator->execute('"test\\\\\\\\string"'));
$this->assertEquals('test"string', $calculator->execute('"test\"string"'));
$this->assertEquals('test""string', $calculator->execute('"test\"\"string"'));
$this->assertEquals('"teststring', $calculator->execute('"\"teststring"'));
$this->assertEquals('teststring"', $calculator->execute('"teststring\""'));
$this->assertEquals("test'string", $calculator->execute("'test\'string'"));
$this->assertEquals("test''string", $calculator->execute("'test\'\'string'"));
$this->assertEquals("'teststring", $calculator->execute("'\'teststring'"));
$this->assertEquals("teststring'", $calculator->execute("'teststring\''"));
$calculator->addFunction('concat', static fn($arg1, $arg2) => $arg1 . $arg2);
$this->assertEquals('test"ing', $calculator->execute('concat("test\"","ing")'));
$this->assertEquals("test'ing", $calculator->execute("concat('test\'','ing')"));
}
public function testArrays() : void
{
$calculator = new MathExecutor();
$this->assertEquals([1, 5, 2], $calculator->execute('array(1, 5, 2)'));
$this->assertEquals([1, 5, 2], $calculator->execute('[1, 5, 2]'));
$this->assertEquals(\max([1, 5, 2]), $calculator->execute('max([1, 5, 2])'));
$this->assertEquals(\max([1, 5, 2]), $calculator->execute('max(array(1, 5, 2))'));
$calculator->addFunction('arr_with_max_elements', static function($arg1, ...$args) {
$args = \is_array($arg1) ? $arg1 : [$arg1, ...$args];
\usort($args, static fn($arr1, $arr2) => (\is_countable($arr2) ? \count($arr2) : 0) <=> \count($arr1));
return $args[0];
});
$this->assertEquals([3, 3, 3], $calculator->execute('arr_with_max_elements([[1],array(2,2),[3,3,3]])'));
}
public function testFunctionParameterOrder() : void
{
$calculator = new MathExecutor();
$calculator->addFunction('concat', static fn($arg1, $arg2) => $arg1 . $arg2);
$this->assertEquals('testing', $calculator->execute('concat("test","ing")'));
$this->assertEquals('testing', $calculator->execute("concat('test','ing')"));
}
public function testFunction() : void
{
$calculator = new MathExecutor();
$calculator->addFunction('round', static fn($arg) => \round($arg));
$this->assertEquals(\round(100 / 30), $calculator->execute('round(100/30)'));
}
public function testFunctionUnlimitedParameters() : void
{
$calculator = new MathExecutor();
$calculator->addFunction('give_me_an_array', static fn() => [5, 3, 7, 9, 8]);
$this->assertEquals(6.4, $calculator->execute('avg(give_me_an_array())'));
$this->assertEquals(10, $calculator->execute('avg(12,8,15,5)'));
$this->assertEquals(3, $calculator->execute('min(give_me_an_array())'));
$this->assertEquals(1, $calculator->execute('min(1,2,3)'));
$this->assertEquals(9, $calculator->execute('max(give_me_an_array())'));
$this->assertEquals(3, $calculator->execute('max(1,2,3)'));
$this->assertEquals(7, $calculator->execute('median(give_me_an_array())'));
$this->assertEquals(4, $calculator->execute('median(1,3,5,7)'));
$calculator->setVar('monthly_salaries', [100, 200, 300]);
$this->assertEquals([100, 200, 300], $calculator->execute('$monthly_salaries'));
$this->assertEquals(200, $calculator->execute('avg($monthly_salaries)'));
$this->assertEquals(\min([100, 200, 300]), $calculator->execute('min($monthly_salaries)'));
$this->assertEquals(\max([100, 200, 300]), $calculator->execute('max($monthly_salaries)'));
$this->assertEquals(200, $calculator->execute('median($monthly_salaries)'));
}
public function testFunctionOptionalParameters() : void
{
$calculator = new MathExecutor();
$calculator->addFunction('round', static fn($num, $precision = 0) => \round($num, $precision));
$this->assertEquals(\round(11.176), $calculator->execute('round(11.176)'));
$this->assertEquals(\round(11.176, 2), $calculator->execute('round(11.176,2)'));
}
public function testFunctionIncorrectNumberOfParameters() : void
{
$calculator = new MathExecutor();
$this->expectException(IncorrectNumberOfFunctionParametersException::class);
$calculator->addFunction('myfunc', static fn($arg1, $arg2) => $arg1 + $arg2);
$calculator->execute('myfunc(1)');
}
public function testFunctionIncorrectNumberOfParametersTooMany() : void
{
$calculator = new MathExecutor();
$this->expectException(IncorrectNumberOfFunctionParametersException::class);
$calculator->addFunction('myfunc', static fn($arg1, $arg2) => $arg1 + $arg2);
$calculator->execute('myfunc(1,2,3)');
}
public function testFunctionIf() : void
{
$calculator = new MathExecutor();
$this->assertEquals(
30,
$calculator->execute(
'if(100 > 99, 30, 0)'
),
'Expression failed: if(100 > 99, 30, 0)'
);
$this->assertEquals(
0,
$calculator->execute(
'if(100 < 99, 30, 0)'
),
'Expression failed: if(100 < 99, 30, 0)'
);
$this->assertEquals(
30,
$calculator->execute(
'if(98 < 99 && sin(1) < 1, 30, 0)'
),
'Expression failed: if(98 < 99 && sin(1) < 1, 30, 0)'
);
$this->assertEquals(
40,
$calculator->execute(
'if(98 < 99 && sin(1) < 1, max(30, 40), 0)'
),
'Expression failed: if(98 < 99 && sin(1) < 1, max(30, 40), 0)'
);
$this->assertEquals(
40,
$calculator->execute(
'if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)'
),
'Expression failed: if(98 < 99 && sin(1) < 1, if(10 > 5, max(30, 40), 1), 0)'
);
$this->assertEquals(
20,
$calculator->execute(
'if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))'
),
'Expression failed: if(98 < 99 && sin(1) > 1, if(10 > 5, max(30, 40), 1), if(4 <= 4, 20, 21))'
);
$this->assertEquals(
\cos(2),
$calculator->execute(
'if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))'
),
'Expression failed: if(98 < 99 && sin(1) >= 1, max(30, 40), cos(2))'
);
$this->assertEquals(
\cos(2),
$calculator->execute(
'if(cos(2), cos(2), 0)'
),
'Expression failed: if(cos(2), cos(2), 0)'
);
$trx_amount = 100000;
$calculator->setVar('trx_amount', $trx_amount);
$this->assertEquals($trx_amount, $calculator->execute('$trx_amount'));
$this->assertEquals(
$trx_amount * 0.03,
$calculator->execute(
'if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)'
),
'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, $trx_amount * 0.03)'
);
$this->assertEquals(
$trx_amount * 0.03,
$calculator->execute(
'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
),
'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
);
$trx_amount = 39000;
$calculator->setVar('trx_amount', $trx_amount);
$this->assertEquals(
$trx_amount * 0.06,
$calculator->execute(
'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
),
'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
);
$trx_amount = 59000;
$calculator->setVar('trx_amount', $trx_amount);
$this->assertEquals(
$trx_amount * 0.05,
$calculator->execute(
'if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
),
'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06, if($trx_amount < 60000, $trx_amount * 0.05, $trx_amount * 0.03))'
);
$this->expectException(IncorrectNumberOfFunctionParametersException::class);
$this->assertEquals(
0.0,
$calculator->execute(
'if($trx_amount < 40000, $trx_amount * 0.06)'
),
'Expression failed: if($trx_amount < 40000, $trx_amount * 0.06)'
);
}
public function testVariables() : void
{
$calculator = new MathExecutor();
$this->assertEquals(3.14159265359, $calculator->execute('$pi'));
$this->assertEquals(3.14159265359, $calculator->execute('pi'));
$this->assertEquals(2.71828182846, $calculator->execute('$e'));
$this->assertEquals(2.71828182846, $calculator->execute('e'));
$calculator->setVars([
'trx_amount' => 100000.01,
'ten' => 10,
'nine' => 9,
'eight' => 8,
'seven' => 7,
'six' => 6,
'five' => 5,
'four' => 4,
'three' => 3,
'two' => 2,
'one' => 1,
'zero' => 0,
]);
$this->assertEquals(100000.01, $calculator->execute('$trx_amount'));
$this->assertEquals(10 - 9, $calculator->execute('$ten - $nine'));
$this->assertEquals(9 - 10, $calculator->execute('$nine - $ten'));
$this->assertEquals(10 + 9, $calculator->execute('$ten + $nine'));
$this->assertEquals(10 * 9, $calculator->execute('$ten * $nine'));
$this->assertEquals(10 / 9, $calculator->execute('$ten / $nine'));
$this->assertEquals(10 / (9 / 5), $calculator->execute('$ten / ($nine / $five)'));
// test variables without leading $
$this->assertEquals(100000.01, $calculator->execute('trx_amount'));
$this->assertEquals(10 - 9, $calculator->execute('ten - nine'));
$this->assertEquals(9 - 10, $calculator->execute('nine - ten'));
$this->assertEquals(10 + 9, $calculator->execute('ten + nine'));
$this->assertEquals(10 * 9, $calculator->execute('ten * nine'));
$this->assertEquals(10 / 9, $calculator->execute('ten / nine'));
$this->assertEquals(10 / (9 / 5), $calculator->execute('ten / (nine / five)'));
}
public function testEvaluateFunctionParameters() : void
{
$calculator = new MathExecutor();
$calculator->addFunction(
'round',
static fn($value, $decimals) => \round($value, $decimals)
);
$expression = 'round(100 * 1.111111, 2)';
$phpResult = 0;
eval('$phpResult = ' . $expression . ';');
$this->assertEquals($phpResult, $calculator->execute($expression));
$expression = 'round((100*0.04)+(((100*1.02)+0.5)*1.28),2)';
eval('$phpResult = ' . $expression . ';');
$this->assertEquals($phpResult, $calculator->execute($expression));
}
public function testFunctionsWithQuotes() : void
{
$calculator = new MathExecutor();
$calculator->addFunction('concat', static fn($first, $second) => $first . $second);
$this->assertEquals('testing', $calculator->execute('concat("test", "ing")'));
$this->assertEquals('testing', $calculator->execute("concat('test', 'ing')"));
}
public function testQuotes() : void
{
$calculator = new MathExecutor();
$testString = 'some, long. arg; with: different-separators!';
$calculator->addFunction(
'test',
function($arg) use ($testString) {
$this->assertEquals($testString, $arg);
return 0;
}
);
$calculator->execute('test("' . $testString . '")'); // single quotes
$calculator->execute("test('" . $testString . "')"); // double quotes
}
public function testBeginWithBracketAndMinus() : void
{
$calculator = new MathExecutor();
$this->assertEquals(-4, $calculator->execute('(-4)'));
$this->assertEquals(1, $calculator->execute('(-4 + 5)'));
}
public function testStringComparison() : void
{
$calculator = new MathExecutor();
$this->assertEquals(true, $calculator->execute('"a" == \'a\''));
$this->assertEquals(true, $calculator->execute('"hello world" == "hello world"'));
$this->assertEquals(false, $calculator->execute('"hello world" == "hola mundo"'));
$this->assertEquals(true, $calculator->execute('"hello world" != "hola mundo"'));
$this->assertEquals(true, $calculator->execute('"a" < "b"'));
$this->assertEquals(false, $calculator->execute('"a" > "b"'));
$this->assertEquals(true, $calculator->execute('"a" <= "b"'));
$this->assertEquals(false, $calculator->execute('"a" >= "b"'));
$this->assertEquals(true, $calculator->execute('"A" != "a"'));
}
public function testVarStringComparison() : void
{
$calculator = new MathExecutor();
$calculator->setVar('var', 97);
$this->assertEquals(false, $calculator->execute('97 == "a"'));
$this->assertEquals(false, $calculator->execute('$var == "a"'));
$calculator->setVar('var', 'a');
$this->assertEquals(true, $calculator->execute('$var == "a"'));
}
public function testOnVarNotFound() : void
{
$calculator = new MathExecutor();
$calculator->setVarNotFoundHandler(
static function($varName) {
if ('undefined' == $varName) {
return 3;
}
}
);
$this->assertEquals(15, $calculator->execute('5 * undefined'));
$this->assertEquals(3, $calculator->getVar('undefined'));
$this->assertNull($calculator->getVar('Lucy'));
}
public function testGetVarException() : void
{
$calculator = new MathExecutor();
$this->expectException(UnknownVariableException::class);
$this->assertNull($calculator->getVar('Lucy'));
}
public function testMinusZero() : void
{
$calculator = new MathExecutor();
$this->assertEquals(1, $calculator->execute('1 - 0'));
$this->assertEquals(1, $calculator->execute('1-0'));
}
public function testScientificNotation() : void
{
$calculator = new MathExecutor();
$this->assertEquals(1.5e9, $calculator->execute('1.5e9'));
$this->assertEquals(1.5e-9, $calculator->execute('1.5e-9'));
$this->assertEquals(1.5e+9, $calculator->execute('1.5e+9'));
}
public function testNullReturnType() : void
{
$calculator = new MathExecutor();
$calculator->setVar('nullValue', null);
$this->assertEquals(null, $calculator->execute('nullValue'));
}
public function testGetFunctionsReturnsArray() : void
{
$calculator = new MathExecutor();
$this->assertIsArray($calculator->getFunctions()); // @phpstan-ignore-line
}
public function testGetFunctionsReturnsFunctions() : void
{
$calculator = new MathExecutor();
$this->assertGreaterThan(40, \count($calculator->getFunctions()));
}
public function testGetVarsReturnsArray() : void
{
$calculator = new MathExecutor();
$this->assertIsArray($calculator->getVars()); // @phpstan-ignore-line
}
public function testGetVarsReturnsCount() : void
{
$calculator = new MathExecutor();
$this->assertGreaterThan(1, \count($calculator->getVars()));
}
public function testUndefinedVarThrowsExecption() : void
{
$calculator = new MathExecutor();
$this->assertGreaterThan(1, \count($calculator->getVars()));
$this->expectException(UnknownVariableException::class);
$calculator->execute('5 * undefined');
}
public function testSetVarsAcceptsAllScalars() : void
{
$calculator = new MathExecutor();
$calculator->setVar('boolTrue', true);
$calculator->setVar('boolFalse', false);
$calculator->setVar('int', 1);
$calculator->setVar('null', null);
$calculator->setVar('float', 1.1);
$calculator->setVar('string', 'string');
$this->assertCount(8, $calculator->getVars());
$this->assertEquals(true, $calculator->getVar('boolTrue'));
$this->assertEquals(false, $calculator->getVar('boolFalse'));
$this->assertEquals(1, $calculator->getVar('int'));
$this->assertEquals(null, $calculator->getVar('null'));
$this->assertEquals(1.1, $calculator->getVar('float'));
$this->assertEquals('string', $calculator->getVar('string'));
$this->expectException(MathExecutorException::class);
$calculator->setVar('validVar', new \DateTime());
}
public function testSetVarsDoesNotAcceptObject() : void
{
$calculator = new MathExecutor();
$this->expectException(MathExecutorException::class);
$calculator->setVar('object', $this);
}
public function testSetVarsDoesNotAcceptResource() : void
{
$calculator = new MathExecutor();
$this->expectException(MathExecutorException::class);
$calculator->setVar('resource', \tmpfile());
}
public function testSetCustomVarValidator() : void
{
$calculator = new MathExecutor();
$calculator->setVarValidationHandler(static function(string $name, $variable) : void {
// allow all scalars and null
if (\is_scalar($variable) || null === $variable) {
return;
}
// Allow variables of type DateTime, but not others
if (! $variable instanceof \DateTime) {
throw new MathExecutorException('Invalid variable type');
}
});
$calculator->setVar('validFloat', 0.0);
$calculator->setVar('validInt', 0);
$calculator->setVar('validTrue', true);
$calculator->setVar('validFalse', false);
$calculator->setVar('validString', 'string');
$calculator->setVar('validNull', null);
$calculator->setVar('validDateTime', new \DateTime());
$this->expectException(MathExecutorException::class);
$calculator->setVar('validVar', $this);
}
public function testSetCustomVarNameValidator() : void
{
$calculator = new MathExecutor();
$calculator->setVarValidationHandler(static function(string $name, $variable) : void {
// don't allow variable names with the word invalid in them
if (\str_contains($name, 'invalid')) {
throw new MathExecutorException('Invalid variable name');
}
});
$calculator->setVar('validFloat', 0.0);
$calculator->setVar('validInt', 0);
$calculator->setVar('validTrue', true);
$calculator->setVar('validFalse', false);
$calculator->setVar('validString', 'string');
$calculator->setVar('validNull', null);
$calculator->setVar('validDateTime', new \DateTime());
$this->expectException(MathExecutorException::class);
$calculator->setVar('invalidVar', 12);
}
public function testVarExists() : void
{
$calculator = new MathExecutor();
$varName = 'Eythel';
$calculator->setVar($varName, 1);
$this->assertTrue($calculator->varExists($varName));
$this->assertFalse($calculator->varExists('Lucy'));
}
#[\PHPUnit\Framework\Attributes\DataProvider('providerExpressionValues')]
public function testCalculatingValues(string $expression, mixed $value) : void
{
$calculator = new MathExecutor();
try {
$result = $calculator->execute($expression);
} catch (Exception $e) {
$this->fail(\sprintf('Exception: %s (%s:%d), expression was: %s', $e::class, $e->getFile(), $e->getLine(), $expression));
}
$this->assertEquals($value, $result, "{$expression} did not evaluate to {$value}");
}
/**
* Expressions data provider
*
* Most tests can go in here. The idea is that each expression will be evaluated by MathExecutor and by PHP directly.
* The results should be the same. If they are not, then the test fails. No need to add extra test unless you are doing
* something more complex and not a simple mathmatical expression.
*
* @return array<array<mixed>>
*/
public static function providerExpressionValues()
{
return [
['arccos(0.5)', \acos(0.5)],
['arccosec(4)', \asin(1 / 4)],
['arccot(3)', M_PI / 2 - \atan(3)],
['arccotan(4)', M_PI / 2 - \atan(4)],
['arccsc(4)', \asin(1 / 4)],
['arcctg(3)', M_PI / 2 - \atan(3)],
['arcsec(4)', \acos(1 / 4)],
['arcsin(0.5)', \asin(0.5)],
['arctan(0.5)', \atan(0.5)],
['arctan(4)', \atan(4)],
['arctg(0.5)', \atan(0.5)],
['cosec(12)', 1 / \sin(12)],
['cosec(4)', 1 / \sin(4)],
['cosh(12)', \cosh(12)],
['cot(12)', \cos(12) / \sin(12)],
['cotan(12)', \cos(12) / \sin(12)],
['cotan(4)', \cos(4) / \sin(4)],
['cotg(3)', \cos(3) / \sin(3)],
['csc(4)', 1 / \sin(4)],
['ctg(4)', \cos(4) / \sin(4)],
['ctn(4)', \cos(4) / \sin(4)],
['decbin(10)', \decbin(10)],
['lg(2)', \log10(2)],
['ln(2)', \log(2)],
['ln(2, 5)', \log(2, 5)],
['sec(4)', 1 / \cos(4)],
['tg(4)', \tan(4)],
];
}
public function testCache() : void
{
$calculator = new MathExecutor();
$this->assertEquals(256, $calculator->execute('2 ^ 8')); // second arg $cache is true by default
$this->assertIsArray($calculator->getCache()); // @phpstan-ignore-line
$this->assertCount(1, $calculator->getCache());
$this->assertEquals(512, $calculator->execute('2 ^ 9', true));
$this->assertCount(2, $calculator->getCache());
$this->assertEquals(1024, $calculator->execute('2 ^ 10', false));
$this->assertCount(2, $calculator->getCache());
$calculator->clearCache();
$this->assertIsArray($calculator->getCache());
$this->assertCount(0, $calculator->getCache());
$this->assertEquals(2048, $calculator->execute('2 ^ 11', false));
$this->assertCount(0, $calculator->getCache());
}
public function testUnsupportedOperands() : void
{
if (\version_compare(PHP_VERSION, '8.0') >= 0) { /** @phpstan-ignore-line */
$calculator = new MathExecutor();
$calculator->setVar('stringVar', 'string');
$calculator->setVar('intVar', 1);
$this->expectException(\TypeError::class);
$calculator->execute('stringVar + intVar');
} else {
$this->expectNotToPerformAssertions();
}
}
}
================================================
FILE: tests/bootstrap.php
================================================
<?php
$vendorDir = __DIR__ . '/../../..';
if (\file_exists($file = $vendorDir . '/autoload.php')) {
require_once $file;
} elseif (\file_exists($file = './vendor/autoload.php')) {
require_once $file;
} else {
throw new \RuntimeException('Not found composer autoload');
}
gitextract_y5i3bttq/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── tests.yml
├── .gitignore
├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── code-of-conduct.md
├── code-of-conduct.ru.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── src/
│ └── NXP/
│ ├── Classes/
│ │ ├── Calculator.php
│ │ ├── CustomFunction.php
│ │ ├── Operator.php
│ │ ├── Token.php
│ │ └── Tokenizer.php
│ ├── Exception/
│ │ ├── DivisionByZeroException.php
│ │ ├── IncorrectBracketsException.php
│ │ ├── IncorrectExpressionException.php
│ │ ├── IncorrectFunctionParameterException.php
│ │ ├── IncorrectNumberOfFunctionParametersException.php
│ │ ├── MathExecutorException.php
│ │ ├── UnknownFunctionException.php
│ │ ├── UnknownOperatorException.php
│ │ └── UnknownVariableException.php
│ └── MathExecutor.php
└── tests/
├── MathTest.php
└── bootstrap.php
SYMBOL INDEX (109 symbols across 16 files)
FILE: src/NXP/Classes/Calculator.php
class Calculator (line 22) | class Calculator
method __construct (line 30) | public function __construct(private array $functions, private array $o...
method calculate (line 44) | public function calculate(array $tokens, array $variables, ?callable $...
FILE: src/NXP/Classes/CustomFunction.php
class CustomFunction (line 9) | class CustomFunction
method __construct (line 27) | public function __construct(public string $name, callable $function)
method execute (line 42) | public function execute(array &$stack, int $paramCountInStack) : Token
FILE: src/NXP/Classes/Operator.php
class Operator (line 8) | class Operator
method __construct (line 20) | public function __construct(public string $operator, public bool $isRi...
method execute (line 32) | public function execute(array &$stack) : Token
FILE: src/NXP/Classes/Token.php
class Token (line 5) | class Token
method __construct (line 31) | public function __construct(public string $type, public mixed $value, ...
FILE: src/NXP/Classes/Tokenizer.php
class Tokenizer (line 22) | class Tokenizer
method __construct (line 41) | public function __construct(private string $input, private array $oper...
method tokenize (line 45) | public function tokenize() : self
method buildReversePolishNotation (line 249) | public function buildReversePolishNotation() : array
method isNumber (line 367) | private function isNumber(string $ch) : bool
method isAlpha (line 372) | private function isAlpha(string $ch) : bool
method emptyNumberBufferAsLiteral (line 377) | private function emptyNumberBufferAsLiteral() : void
method isDot (line 385) | private function isDot(string $ch) : bool
method isLP (line 390) | private function isLP(string $ch) : bool
method isRP (line 395) | private function isRP(string $ch) : bool
method emptyStrBufferAsVariable (line 400) | private function emptyStrBufferAsVariable() : void
method isComma (line 408) | private function isComma(string $ch) : bool
FILE: src/NXP/Exception/DivisionByZeroException.php
class DivisionByZeroException (line 17) | class DivisionByZeroException extends MathExecutorException
FILE: src/NXP/Exception/IncorrectBracketsException.php
class IncorrectBracketsException (line 17) | class IncorrectBracketsException extends MathExecutorException
FILE: src/NXP/Exception/IncorrectExpressionException.php
class IncorrectExpressionException (line 17) | class IncorrectExpressionException extends MathExecutorException
FILE: src/NXP/Exception/IncorrectFunctionParameterException.php
class IncorrectFunctionParameterException (line 14) | class IncorrectFunctionParameterException extends MathExecutorException
FILE: src/NXP/Exception/IncorrectNumberOfFunctionParametersException.php
class IncorrectNumberOfFunctionParametersException (line 14) | class IncorrectNumberOfFunctionParametersException extends MathExecutorE...
FILE: src/NXP/Exception/MathExecutorException.php
class MathExecutorException (line 17) | class MathExecutorException extends \Exception
FILE: src/NXP/Exception/UnknownFunctionException.php
class UnknownFunctionException (line 17) | class UnknownFunctionException extends MathExecutorException
FILE: src/NXP/Exception/UnknownOperatorException.php
class UnknownOperatorException (line 17) | class UnknownOperatorException extends MathExecutorException
FILE: src/NXP/Exception/UnknownVariableException.php
class UnknownVariableException (line 17) | class UnknownVariableException extends MathExecutorException
FILE: src/NXP/MathExecutor.php
class MathExecutor (line 28) | class MathExecutor
method __construct (line 65) | public function __construct()
method __clone (line 70) | public function __clone()
method addOperator (line 79) | public function addOperator(Operator $operator) : self
method execute (line 95) | public function execute(string $expression, bool $cache = true)
method addFunction (line 123) | public function addFunction(string $name, ?callable $function = null) ...
method getVars (line 135) | public function getVars() : array
method getVar (line 145) | public function getVar(string $variable) : mixed
method setVar (line 163) | public function setVar(string $variable, mixed $value) : self
method varExists (line 178) | public function varExists(string $variable) : bool
method setVars (line 190) | public function setVars(array $variables, bool $clear = true) : self
method setVarNotFoundHandler (line 209) | public function setVarNotFoundHandler(callable $handler) : self
method setVarValidationHandler (line 224) | public function setVarValidationHandler(?callable $handler) : self
method removeVar (line 235) | public function removeVar(string $variable) : self
method removeVars (line 245) | public function removeVars() : self
method getOperators (line 258) | public function getOperators() : array
method getFunctions (line 269) | public function getFunctions() : array
method removeOperator (line 277) | public function removeOperator(string $operator) : self
method setDivisionByZeroIsZero (line 287) | public function setDivisionByZeroIsZero() : self
method getCache (line 298) | public function getCache() : array
method clearCache (line 306) | public function clearCache() : self
method useBCMath (line 313) | public function useBCMath(int $scale = 2) : self
method addDefaults (line 338) | protected function addDefaults() : self
method defaultOperators (line 360) | protected function defaultOperators() : array
method defaultFunctions (line 402) | protected function defaultFunctions() : array
method defaultVars (line 529) | protected function defaultVars() : array
method defaultVarValidation (line 541) | protected function defaultVarValidation(string $variable, mixed $value...
FILE: tests/MathTest.php
class MathTest (line 24) | class MathTest extends TestCase
method testCalculating (line 26) | #[\PHPUnit\Framework\Attributes\DataProvider('providerExpressions')]
method providerExpressions (line 52) | public static function providerExpressions()
method testBCMathCalculating (line 270) | #[\PHPUnit\Framework\Attributes\DataProvider('bcMathExpressions')]
method bcMathExpressions (line 302) | public static function bcMathExpressions()
method testIncorrectExpressionException (line 514) | #[\PHPUnit\Framework\Attributes\DataProvider('incorrectExpressions')]
method incorrectExpressions (line 530) | public static function incorrectExpressions()
method testUnknownFunctionException (line 550) | public function testUnknownFunctionException() : void
method testZeroDivision (line 557) | public function testZeroDivision() : void
method testUnaryOperators (line 564) | public function testUnaryOperators() : void
method testZeroDivisionException (line 575) | public function testZeroDivisionException() : void
method testVariableIncorrectExpressionException (line 584) | public function testVariableIncorrectExpressionException() : void
method testNotVariableOperator (line 594) | public function testNotVariableOperator() : void
method testExponentiation (line 609) | public function testExponentiation() : void
method testStringEscape (line 615) | public function testStringEscape() : void
method testArrays (line 636) | public function testArrays() : void
method testFunctionParameterOrder (line 652) | public function testFunctionParameterOrder() : void
method testFunction (line 661) | public function testFunction() : void
method testFunctionUnlimitedParameters (line 668) | public function testFunctionUnlimitedParameters() : void
method testFunctionOptionalParameters (line 688) | public function testFunctionOptionalParameters() : void
method testFunctionIncorrectNumberOfParameters (line 696) | public function testFunctionIncorrectNumberOfParameters() : void
method testFunctionIncorrectNumberOfParametersTooMany (line 704) | public function testFunctionIncorrectNumberOfParametersTooMany() : void
method testFunctionIf (line 712) | public function testFunctionIf() : void
method testVariables (line 816) | public function testVariables() : void
method testEvaluateFunctionParameters (line 855) | public function testEvaluateFunctionParameters() : void
method testFunctionsWithQuotes (line 871) | public function testFunctionsWithQuotes() : void
method testQuotes (line 879) | public function testQuotes() : void
method testBeginWithBracketAndMinus (line 895) | public function testBeginWithBracketAndMinus() : void
method testStringComparison (line 902) | public function testStringComparison() : void
method testVarStringComparison (line 916) | public function testVarStringComparison() : void
method testOnVarNotFound (line 926) | public function testOnVarNotFound() : void
method testGetVarException (line 941) | public function testGetVarException() : void
method testMinusZero (line 948) | public function testMinusZero() : void
method testScientificNotation (line 955) | public function testScientificNotation() : void
method testNullReturnType (line 963) | public function testNullReturnType() : void
method testGetFunctionsReturnsArray (line 970) | public function testGetFunctionsReturnsArray() : void
method testGetFunctionsReturnsFunctions (line 976) | public function testGetFunctionsReturnsFunctions() : void
method testGetVarsReturnsArray (line 982) | public function testGetVarsReturnsArray() : void
method testGetVarsReturnsCount (line 988) | public function testGetVarsReturnsCount() : void
method testUndefinedVarThrowsExecption (line 994) | public function testUndefinedVarThrowsExecption() : void
method testSetVarsAcceptsAllScalars (line 1002) | public function testSetVarsAcceptsAllScalars() : void
method testSetVarsDoesNotAcceptObject (line 1023) | public function testSetVarsDoesNotAcceptObject() : void
method testSetVarsDoesNotAcceptResource (line 1030) | public function testSetVarsDoesNotAcceptResource() : void
method testSetCustomVarValidator (line 1037) | public function testSetCustomVarValidator() : void
method testSetCustomVarNameValidator (line 1064) | public function testSetCustomVarNameValidator() : void
method testVarExists (line 1086) | public function testVarExists() : void
method testCalculatingValues (line 1095) | #[\PHPUnit\Framework\Attributes\DataProvider('providerExpressionValues')]
method providerExpressionValues (line 1117) | public static function providerExpressionValues()
method testCache (line 1150) | public function testCache() : void
method testUnsupportedOperands (line 1172) | public function testUnsupportedOperands() : void
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (117K chars).
[
{
"path": ".gitattributes",
"chars": 13,
"preview": "* text=auto\n\n"
},
{
"path": ".github/workflows/tests.yml",
"chars": 918,
"preview": "name: Tests\n\non: [push, pull_request]\n\njobs:\n php-tests:\n runs-on: ${{ matrix.os }}\n strategy:\n fail-fast: f"
},
{
"path": ".gitignore",
"chars": 95,
"preview": "vendor/\n.idea/\ncomposer.lock\n.phpunit.cache\n.phpunit.result.cache\n.vscode\n.php-cs-fixer.cache\n\n"
},
{
"path": ".php-cs-fixer.php",
"chars": 17928,
"preview": "<?php\n\n$config = new PhpCsFixer\\Config();\n$config->setUnsupportedPhpVersionAllowed(true);\n\n$config\n ->setRiskyAllowed"
},
{
"path": "LICENSE",
"chars": 1057,
"preview": "Copyright (c) Alexander Kiryukhin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this "
},
{
"path": "README.md",
"chars": 7430,
"preview": "# MathExecutor [](https://github.com/neonxp/Ma"
},
{
"path": "code-of-conduct.md",
"chars": 243,
"preview": "# Code of conduct\n\nWe don't care who you are IRL. Be professional and responsible.\n\nIf you are a good person, we are hap"
},
{
"path": "code-of-conduct.ru.md",
"chars": 200,
"preview": "# Кодекс поведения\n\nНам всё равно, кто ты в реальной жизни, просто будь профессионалом и всё будет хорошо.\n\nЕсли ты буде"
},
{
"path": "composer.json",
"chars": 956,
"preview": "{\n \"name\": \"nxp/math-executor\",\n \"description\": \"Simple math expressions calculator\",\n \"minimum-stability\": \"stable\","
},
{
"path": "phpstan.neon.dist",
"chars": 120,
"preview": "parameters:\n\tlevel: 6\n\terrorFormat: raw\n\teditorUrl: '%%file%% %%line%% %%column%%: %%error%%'\n\tpaths:\n\t\t- src\n\t\t- tests\n"
},
{
"path": "phpunit.xml.dist",
"chars": 681,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n backupGlobals=\"f"
},
{
"path": "src/NXP/Classes/Calculator.php",
"chars": 2903,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Classes/CustomFunction.php",
"chars": 1684,
"preview": "<?php\n\nnamespace NXP\\Classes;\n\nuse NXP\\Exception\\IncorrectNumberOfFunctionParametersException;\nuse ReflectionException;\n"
},
{
"path": "src/NXP/Classes/Operator.php",
"chars": 1086,
"preview": "<?php\n\nnamespace NXP\\Classes;\n\nuse NXP\\Exception\\IncorrectExpressionException;\nuse ReflectionFunction;\n\nclass Operator\n{"
},
{
"path": "src/NXP/Classes/Token.php",
"chars": 659,
"preview": "<?php\n\nnamespace NXP\\Classes;\n\nclass Token\n{\n public const Literal = 'literal';\n\n public const Variable = 'variabl"
},
{
"path": "src/NXP/Classes/Tokenizer.php",
"chars": 13734,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/DivisionByZeroException.php",
"chars": 367,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/IncorrectBracketsException.php",
"chars": 378,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/IncorrectExpressionException.php",
"chars": 372,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/IncorrectFunctionParameterException.php",
"chars": 326,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/IncorrectNumberOfFunctionParametersException.php",
"chars": 335,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/MathExecutorException.php",
"chars": 354,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/UnknownFunctionException.php",
"chars": 368,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/UnknownOperatorException.php",
"chars": 368,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/Exception/UnknownVariableException.php",
"chars": 376,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "src/NXP/MathExecutor.php",
"chars": 17337,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "tests/MathTest.php",
"chars": 40801,
"preview": "<?php\n\n/**\n * This file is part of the MathExecutor package\n *\n * (c) Alexander Kiryukhin\n *\n * For the full copyright a"
},
{
"path": "tests/bootstrap.php",
"chars": 284,
"preview": "<?php\n\n$vendorDir = __DIR__ . '/../../..';\n\nif (\\file_exists($file = $vendorDir . '/autoload.php')) {\n require_once $"
}
]
About this extraction
This page contains the full source code of the neonxp/MathExecutor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (108.8 KB), approximately 30.0k tokens, and a symbol index with 109 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.