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
================================================
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 ` 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
================================================
./tests/
================================================
FILE: src/NXP/Classes/Calculator.php
================================================
*/
class Calculator
{
/**
* @todo PHP8: Use constructor property promotion -> public function __construct(private array $functions, private array $operators)
*
* @param array $functions
* @param array $operators
*/
public function __construct(private array $functions, private array $operators)
{
}
/**
* Calculate array of tokens in reverse polish notation
*
* @param Token[] $tokens
* @param array $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
================================================
function = $function;
$reflection = (new ReflectionFunction($function));
$this->isVariadic = $reflection->isVariadic();
$this->totalParamCount = $reflection->getNumberOfParameters();
$this->requiredParamCount = $reflection->getNumberOfRequiredParameters();
}
/**
* @param array $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
================================================
function = $function;
$reflection = new ReflectionFunction($function);
$this->places = $reflection->getNumberOfParameters();
}
/**
* @param array $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
================================================
*/
class Tokenizer
{
/** @var array */
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 $stack */
$stack = new SplStack();
/**
* @var SplStack $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
================================================
*/
class DivisionByZeroException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectBracketsException.php
================================================
*/
class IncorrectBracketsException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectExpressionException.php
================================================
*/
class IncorrectExpressionException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/IncorrectFunctionParameterException.php
================================================
*/
class MathExecutorException extends \Exception
{
}
================================================
FILE: src/NXP/Exception/UnknownFunctionException.php
================================================
*/
class UnknownFunctionException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/UnknownOperatorException.php
================================================
*/
class UnknownOperatorException extends MathExecutorException
{
}
================================================
FILE: src/NXP/Exception/UnknownVariableException.php
================================================
*/
class UnknownVariableException extends MathExecutorException
{
}
================================================
FILE: src/NXP/MathExecutor.php
================================================
*/
protected array $variables = [];
/**
* @var callable|null
*/
protected $onVarNotFound = null;
/**
* @var callable|null
*/
protected $onVarValidation = null;
/**
* @var Operator[]
*/
protected array $operators = [];
/**
* @var array
*/
protected array $functions = [];
/**
* @var array
*/
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
*/
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 $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 of operator class names
*/
public function getOperators() : array
{
return $this->operators;
}
/**
* Get all registered functions
*
* @return array 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
*/
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
*/
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
*/
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
*/
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
================================================
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>
*/
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>
*/
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>
*/
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>
*/
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
================================================