Initial commit

This commit is contained in:
Chris 2022-02-13 15:31:49 +01:00
commit a6e8d252b1
6 changed files with 445 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/*.phar
/vendor
/composer.lock

74
README.md Normal file
View 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
View 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
View 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
View 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
View 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;
}
}