| | <?php
class Expr {
var $s, $p, $n;
function calc($text) {
$this->s=$text; $this->p=0; $this->n=strlen($text);
$r=$this->L0();
$this->skipspace(); if (!$this->eos()) $this->wtf();
return $r;
}
function L0() {
$r=$this->L1();
for(;;) {
if ($this->chk('+')) $r=$r+$this->L1();
elseif ($this->chk('-')) $r=$r-$this->L1();
else break;
}
return $r;
}
function L1() {
$r=$this->L2();
for(;;) {
if ($this->chk('*')) $r=$r*$this->L2();
elseif ($this->chk('/')) $r=$r/$this->L2();
else break;
}
return $r;
}
function L2() {
if ($this->chk('(')) {
$r=$this->L0();
if (!$this->chk(')')) $this->error("no closing braket");
return $r;
}
return $this->L3();
}
function L3() {
if ($this->chk('-')) return -$this->L2();
if ($this->chk('+')) return $this->L2();
return $this->L4();
}
function L4() {
$this->skipspace(); $c=$this->peek();
if (self::is_digit($c) || $c=='-') return $this->number();
elseif (self::is_letter($c)) return $this->name();
else $this->wtf();
}
function number() {
$r=0; $sign=$this->chk('-');
$c=$this->peek(); if (!self::is_digit($c)) $this->error("invalid number");
while(self::is_digit($c)) {
$r=$r*10 + ($c-'0');
$this->p++; $c=$this->peek();
}
if ($this->chk('.')) {
$np=1.0; $c=$this->peek();
while(self::is_digit($c)) {
$np/=10;
$r+=($c-'0')*$np;
$this->p++; $c=$this->peek();
}
}
if ($this->chk('e') || $this->chk('E')) {
$ne=0; $ns=false;
$c=$this->peek();
if ($c=='-') { $ns=true; $this->p++; $c=$this->peek(); }
elseif($c=='+') { $this->p++; $c=$this->peek(); }
if (!self::is_digit($c)) $this->error("invalid number");
while(self::is_digit($c)) {
$ne=$ne*10 + ($c-'0');
$this->p++; $c=$this->peek();
}
if ($ns) $ne=-$ne;
$r*=pow(10.0,$ne);
}
return $sign ? -$r : $r;
}
var $names=[
'pi'=>3.1415926535897932,
'e' =>2.7182818284590452,
];
function init() {
$this->names['sin']=function($x) { return sin($x); };
$this->names['cos']=function($x) { return cos($x); };
$this->names['exp']=function($x) { return exp($x); };
$this->names['ln']=function($x) { return log($x); };
$this->names['pow']=function($x,$n) { return pow($x,$n); };
}
function name() {
if (preg_match('/\b([A-Za-z_][A-Za-z0-9_]*)/',$this->s,$m,0,$this->p)) {
$name=$m[1]; $len=strlen($name);
if (!isset($this->names[$name])) $this->error("undefined name $name",$len);
$this->p+=$len;
if ($this->chk('(')) return $this->func($name);
return $this->names[$name];
}
$this->error("name expected");
}
function func($name) {
$args=[];
if (!$this->chk(')')) for(;;) {
if ($this->eos()) $this->error("unexpected end");
array_push($args,$this->L0());
if ($this->chk(')')) break;
if (!$this->chk(',')) $this->wtf();
}
$fn=$this->names[$name];
$r=call_user_func_array($fn,$args);
return $r;
}
static function is_space($c) { return ord($c)>0 && $c<=' '; }
static function is_digit($c) { return $c>='0' && $c<='9'; }
static function is_letter($c) { return ($c>='a' && $c<='z') || ($c>='A' && $c<='Z') || ($c=='_'); }
function eos() { return $this->p>=$this->n; }
function peek() { return $this->p<$this->n ? $this->s[$this->p] : '\0'; }
function get() { return $this->p<$this->n ? $this->s[$this->p++] : '\0'; }
function skipspace() { while(self::is_space($this->peek())) $this->p++; }
function chk($c) {
$this->skipspace();
if ($this->peek()!=$c) return false;
$this->p++; return true;
}
function wtf() { $this->error("unexpected symbol ".$this->peek()); }
function error($msg,$len=1) {
$p=$this->p; $n=$this->n; $w=20;
$q=[ $p-$w, $p, $p+$len, $p+$len+$w ]; $qn=count($q);
foreach($q as &$x) if ($x<0) $x=0; elseif ($x>$n) $x=$n;
$ss=$q[0]==0 ? "" : "...";
for($i=1;$i<$qn;$i++) {
if ($i==2) $ss.='[??';
$ss.=substr($this->s,$q[$i-1],$q[$i]-$q[$i-1]);
if ($i==2) $ss.='??]';
}
if ($q[$qn-1]!=$n) $ss.="...";
throw new Exception($msg.": $ss\n");
}
}
function expr($s) {
$e=new Expr(); $e->init();
return $e->calc($s);
}
|