Initial commit
This commit is contained in:
commit
a6e8d252b1
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/*.phar
|
||||||
|
/vendor
|
||||||
|
/composer.lock
|
74
README.md
Normal file
74
README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# PHP Calc
|
||||||
|
|
||||||
|
The CLI calculator you never knew you needed. Also usable as a library!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
Call with an expression to evaluate:
|
||||||
|
|
||||||
|
$ calc "42*atan(pi/2)"
|
||||||
|
42.163162517863
|
||||||
|
|
||||||
|
Or call without arguments to go interactive:
|
||||||
|
|
||||||
|
$ calc
|
||||||
|
~ 42*atan(pi/2)
|
||||||
|
42.163162517863
|
||||||
|
|
||||||
|
### Library
|
||||||
|
|
||||||
|
As simple as falling off a rock:
|
||||||
|
|
||||||
|
use NoccyLabs\PhpCalc\Calculator;
|
||||||
|
|
||||||
|
$calc = new Calculator();
|
||||||
|
$result = $calc->evaluate("42 * atan(pi/2)");
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Supported operations:
|
||||||
|
|
||||||
|
* `+` addition
|
||||||
|
* `-` subtraction
|
||||||
|
* `*` multiplication
|
||||||
|
* `/` division
|
||||||
|
* `^` exponent/raise to power
|
||||||
|
|
||||||
|
Supported functions:
|
||||||
|
|
||||||
|
* abs
|
||||||
|
* acos
|
||||||
|
* acosh
|
||||||
|
* asin
|
||||||
|
* asin
|
||||||
|
* atan
|
||||||
|
* atanh
|
||||||
|
* ceil
|
||||||
|
* cos
|
||||||
|
* cosh
|
||||||
|
* floor
|
||||||
|
* sin
|
||||||
|
* sinh
|
||||||
|
* log
|
||||||
|
* round
|
||||||
|
* sqrt
|
||||||
|
* tan
|
||||||
|
* tanh
|
||||||
|
|
||||||
|
Constants:
|
||||||
|
|
||||||
|
* `pi`
|
||||||
|
|
||||||
|
## Useful tips
|
||||||
|
|
||||||
|
* You can set the precision using the `_p` variable. If the expression is passed on the
|
||||||
|
command line, prepend the variable followed by a semicolon, such as `_p=2;pi` for the
|
||||||
|
result *3.14*.
|
||||||
|
|
||||||
|
## Bugs and known issues
|
||||||
|
|
||||||
|
* Negative numbers and subtraction should be surrounded by whitespace to parse properly.
|
||||||
|
Passing `2-1` will result in `2` and `-1`, returning *2* and a warning. Passing
|
||||||
|
`2 - 1` will return `1` as expected, as will `2- 1`.
|
6
bin/calc
Executable file
6
bin/calc
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__."/../vendor/autoload.php";
|
||||||
|
|
||||||
|
NoccyLabs\PhpCalc\Calculator::main();
|
26
composer.json
Normal file
26
composer.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "noccylabs/php-calc",
|
||||||
|
"description": "The calculator you never knew you needed",
|
||||||
|
"type": "application",
|
||||||
|
"license": "GPL-3.0-or-later",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"NoccyLabs\\PhpCalc\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Christopher Vagnetoft",
|
||||||
|
"email": "cvagnetoft@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"require": {},
|
||||||
|
"bin": [
|
||||||
|
"bin/calc"
|
||||||
|
],
|
||||||
|
"extra": {
|
||||||
|
"phar": {
|
||||||
|
"output": "calc.phar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
294
src/Calculator.php
Normal file
294
src/Calculator.php
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\PhpCalc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Yo dawg, we heard u like calculators, so we added a calculator to your fancy calculator"
|
||||||
|
*
|
||||||
|
* This is a fully sandboxed calculator that can be used to resolve fairly complex mathematical
|
||||||
|
* expressions to a result. It handles paranthesis, mathematical functions and the most common
|
||||||
|
* operations.
|
||||||
|
*
|
||||||
|
* It should in theory be safe to throw user-provided input to the evaluate() method, as the
|
||||||
|
* functions are checked against a whitelist, and all evaluation is done virtually. It is at
|
||||||
|
* least safer than eval(), and simpler than f.ex. ExpressionLanguage if all you need is math!
|
||||||
|
*
|
||||||
|
* @license GPL V3 or later
|
||||||
|
* @copyright (c) 2022, NoccyLabs
|
||||||
|
*/
|
||||||
|
class Calculator
|
||||||
|
{
|
||||||
|
|
||||||
|
private $vars = [];
|
||||||
|
|
||||||
|
private $consts = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->consts['pi'] = pi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
while (true) {
|
||||||
|
$in = readline("\001\e[92m\002~\001\e[0m\002 ");
|
||||||
|
if (trim($in) === '') continue;
|
||||||
|
|
||||||
|
readline_add_history(trim($in));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ret = $this->evaluate($in);
|
||||||
|
$this->printResult($ret, true);
|
||||||
|
} catch (\Throwable $t) {
|
||||||
|
printf("\e[33;1m %s\e[0m\n", $t->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function printResult($ret, bool $color=false)
|
||||||
|
{
|
||||||
|
if ($ret !== null) {
|
||||||
|
$_p = $this->vars['_p']??null;
|
||||||
|
if (ctype_digit($_p)) {
|
||||||
|
$out = sprintf("%.{$_p}f", $ret);
|
||||||
|
} else {
|
||||||
|
$out = $ret;
|
||||||
|
}
|
||||||
|
echo $color ? " \e[92;1m{$out}\e[0m\n" : "{$out}\n";
|
||||||
|
} else {
|
||||||
|
echo $color ? " \e[90m#null\e[0m\n" : "#null\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluate a string and return the result.
|
||||||
|
*
|
||||||
|
* @param string The expression
|
||||||
|
* @return int|float|null The result
|
||||||
|
*/
|
||||||
|
public function evaluate(string $expr)
|
||||||
|
{
|
||||||
|
$toks = $this->parse($expr);
|
||||||
|
if (!count($toks)) return;
|
||||||
|
|
||||||
|
$tokens = new TokenList($toks);
|
||||||
|
$ret = $this->evalTokens($tokens);
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a built-in math function
|
||||||
|
*
|
||||||
|
* @param string The function name
|
||||||
|
* @param int|float Parameter value
|
||||||
|
*/
|
||||||
|
private function evalFunc(string $func, $val)
|
||||||
|
{
|
||||||
|
if (in_array($func, [
|
||||||
|
'abs', 'acos', 'acosh', 'asin', 'asinh',
|
||||||
|
'atan', 'atanh', 'ceil', 'cos', 'cosh',
|
||||||
|
'floor', 'sin', 'sinh', 'log', 'round',
|
||||||
|
'sqrt', 'tan', 'tanh'
|
||||||
|
])) {
|
||||||
|
return call_user_func($func, $val);
|
||||||
|
}
|
||||||
|
throw new \RuntimeException("Bad function {$func}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively evaluate parsed tokens.
|
||||||
|
*
|
||||||
|
* Functions and paranthesized statements will be replaced by recursive calls t
|
||||||
|
* evalTokens or evalFunc.
|
||||||
|
*
|
||||||
|
* @param TokenList The tokens to evaluate
|
||||||
|
* @return int|float|null The result
|
||||||
|
*/
|
||||||
|
private function evalTokens(TokenList $toks)
|
||||||
|
{
|
||||||
|
$buf = [];
|
||||||
|
$assign = null;
|
||||||
|
|
||||||
|
while (($tok = $toks->read()) !== null) {
|
||||||
|
if (str_ends_with($tok, '(')) {
|
||||||
|
$val = $this->evalTokens($toks);
|
||||||
|
$fn = rtrim($tok, '(');
|
||||||
|
if ($fn) {
|
||||||
|
//printf("feval : %s(%f)\n", $fn, $val);
|
||||||
|
$val = $this->evalFunc($fn, $val);
|
||||||
|
}
|
||||||
|
array_push($buf, $val);
|
||||||
|
} elseif ($tok == ')') {
|
||||||
|
break;
|
||||||
|
} elseif ($tok == ';') {
|
||||||
|
if (!$assign) {
|
||||||
|
//fprintf(STDERR, "warning: Use of semicolon without assignment\n");
|
||||||
|
}
|
||||||
|
$val = $this->flatten($buf);
|
||||||
|
if ($assign) {
|
||||||
|
//printf("store : %s←%f\n", $assign, $val);
|
||||||
|
$this->vars[$assign] = $val;
|
||||||
|
$assign = null;
|
||||||
|
}
|
||||||
|
$buf = [];
|
||||||
|
} elseif ($tok == '=') {
|
||||||
|
$assign = array_pop($buf);
|
||||||
|
// printf("assign : %s (buf: %s)\n", $assign, join(" ", $buf));
|
||||||
|
} else {
|
||||||
|
if (array_key_exists($tok, $this->consts)) {
|
||||||
|
$tok = $this->consts[$tok];
|
||||||
|
} elseif (array_key_exists($tok, $this->vars)) {
|
||||||
|
//printf("subst : %s→%f\n", $tok, $this->vars[$tok]);
|
||||||
|
$tok = $this->vars[$tok];
|
||||||
|
}
|
||||||
|
array_push($buf, $tok);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ret = $this->flatten($buf);
|
||||||
|
|
||||||
|
if ($assign) {
|
||||||
|
//printf("%s=%f", $assign, $ret);
|
||||||
|
$this->vars[$assign] = $ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ret;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a buffer and performs the requested operations on it until a single value remains.
|
||||||
|
*
|
||||||
|
* @param array The buffer of tokens to evaluate
|
||||||
|
* @return int|float|null
|
||||||
|
*/
|
||||||
|
private function flatten(array $buf)
|
||||||
|
{
|
||||||
|
$sta = $buf;
|
||||||
|
$out = [];
|
||||||
|
|
||||||
|
if (count($buf) == 1) {
|
||||||
|
return array_shift($buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
//printf("flatten: %s\n", join(' ', $sta));
|
||||||
|
|
||||||
|
for ($p = 0; $p < count($sta); $p++) {
|
||||||
|
$t = $sta[$p];
|
||||||
|
switch ($t) {
|
||||||
|
case '^':
|
||||||
|
$a = array_pop($out); $b = $sta[++$p];
|
||||||
|
$t = pow($a, $b);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
array_push($out, $t);
|
||||||
|
}
|
||||||
|
//if ($sta != $buf) printf(" : %s\n", join(' ', $sta));
|
||||||
|
$sta = $out;
|
||||||
|
$out = [];
|
||||||
|
if (count($sta) == 1) {
|
||||||
|
return array_shift($sta);
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($p = 0; $p < count($sta); $p++) {
|
||||||
|
$t = $sta[$p];
|
||||||
|
switch ($t) {
|
||||||
|
case '*':
|
||||||
|
$a = array_pop($out); $b = $sta[++$p];
|
||||||
|
$t = $a * $b;
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
$a = array_pop($out); $b = $sta[++$p];
|
||||||
|
$t = $a / $b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
array_push($out, $t);
|
||||||
|
}
|
||||||
|
//if ($sta != $buf) printf(" : %s\n", join(' ', $sta));
|
||||||
|
$sta = $out;
|
||||||
|
$out = [];
|
||||||
|
if (count($sta) == 1) {
|
||||||
|
return array_shift($sta);
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($p = 0; $p < count($sta); $p++) {
|
||||||
|
$t = $sta[$p];
|
||||||
|
switch ($t) {
|
||||||
|
case '+':
|
||||||
|
$a = array_pop($out); $b = $sta[++$p];
|
||||||
|
$t = $a + $b;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
$a = array_pop($out); $b = $sta[++$p];
|
||||||
|
$t = $a - $b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
array_push($out, $t);
|
||||||
|
}
|
||||||
|
//if ($sta != $buf) printf(" : %s\n", join(' ', $sta));
|
||||||
|
$sta = $out;
|
||||||
|
$out = [];
|
||||||
|
|
||||||
|
if (count($sta) > 1) {
|
||||||
|
fprintf(STDERR, "warning: Buffer not empty after flattening, check your expression (contents: %s)\n", join(" ", $sta));
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_shift($sta);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an input expression using regular expressions.
|
||||||
|
*
|
||||||
|
* Each supported token has its own regex patten.
|
||||||
|
*
|
||||||
|
* @param string The expression to parse
|
||||||
|
* @return array The parsed tokens
|
||||||
|
*/
|
||||||
|
public function parse(string $expr): array
|
||||||
|
{
|
||||||
|
$expr = trim($expr);
|
||||||
|
$src = $expr;
|
||||||
|
$toks = [];
|
||||||
|
while (preg_match('/^('.
|
||||||
|
'[a-z]+\('. // sin(
|
||||||
|
'|[a-z_]+'. // my_var
|
||||||
|
'|\('. // (
|
||||||
|
'|\)'. // )
|
||||||
|
'|(-)?\d+\.\d+'. // 3.14
|
||||||
|
'|(-)?\d+'. // 42
|
||||||
|
'|='. // =
|
||||||
|
'|[\+\-\*\/\^]'. // + - * / ^
|
||||||
|
'|\;'. // ;
|
||||||
|
')/', $expr, $m)) {
|
||||||
|
$toks[] = $m[0];
|
||||||
|
$expr = trim(substr($expr, strlen($m[0])));
|
||||||
|
}
|
||||||
|
if (strlen($expr) > 0) {
|
||||||
|
printf("err: %s\n %s\n", $src, str_repeat(" ",strlen($src)-strlen($expr))."^");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return $toks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function main()
|
||||||
|
{
|
||||||
|
// call on run() or evaluate()
|
||||||
|
|
||||||
|
$calc = new Calculator();
|
||||||
|
|
||||||
|
$args = array_slice($GLOBALS['argv'], 1);
|
||||||
|
if (count($args) > 0) {
|
||||||
|
try {
|
||||||
|
$ret = $calc->evaluate($args[0]);
|
||||||
|
$calc->printResult($ret);
|
||||||
|
} catch (\Throwable $t) {
|
||||||
|
fprintf(STDERR, "error: %s\n", $t->getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$calc->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
42
src/TokenList.php
Normal file
42
src/TokenList.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace NoccyLabs\PhpCalc;
|
||||||
|
/**
|
||||||
|
* Helper class to iterate over a list of tokens
|
||||||
|
*
|
||||||
|
* @license GPL V3 or later
|
||||||
|
* @copyright (c) 2022, NoccyLabs
|
||||||
|
*/
|
||||||
|
class TokenList
|
||||||
|
{
|
||||||
|
|
||||||
|
private array $toks;
|
||||||
|
|
||||||
|
private int $ptr = 0;
|
||||||
|
|
||||||
|
public function __construct(array $toks)
|
||||||
|
{
|
||||||
|
$this->toks = $toks;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function current(): ?string
|
||||||
|
{
|
||||||
|
return ($this->ptr >= count($this->toks) ? null : $this->toks[$this->ptr]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function read(): ?string
|
||||||
|
{
|
||||||
|
$v = $this->current();
|
||||||
|
$this->next();
|
||||||
|
return $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next(): ?string
|
||||||
|
{
|
||||||
|
if ($this->ptr < count($this->toks)) {
|
||||||
|
$this->ptr++;
|
||||||
|
return ($this->ptr < count($this->toks) ? $this->toks[$this->ptr] : null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user