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(); } } }