1<?php
2/**
3 * RRDGraph Plugin: Simple RPN parser for rules.
4 *
5 * @author Daniel Goß <developer@flashsystems.de>
6 * @license MIT
7 */
8
9/**
10 * Simple RPN parser used for rrdgraph rules.
11 * @author dgoss
12 *
13 */
14class RPNComputer {
15    /**
16     * Contains the constants that are passed to addConst.
17     * @var Array
18     */
19    private $constants = array ();
20
21    /**
22     * Defines a  constant that can be used within an RPN expression.
23     * @param String $name Name of the constant.
24     * @param Multi $value The value to use.
25     * @throws Exception
26     */
27    public function addConst($name, $value) {
28        $name = strtolower(trim($name));
29
30        if (strspn($name, "abcdefghijklmnopqrstuvwxyz_.") != strlen($name)) throw new Exception("Invalid variable name");
31
32        if ($value === null)
33            unset($this->constants[$name]);
34        else
35            $this->constants[$name] = $value;
36    }
37
38    /**
39     * Checks if the given Value contains only numbers and optionally a sign.
40     * @param String $v The value to check.
41     * @return boolean Returns true if the string only contains numbers.
42     */
43    private function is_integer($v) {
44        return (strspn($v, "0123456789-") == strlen($v));
45    }
46
47    /**
48     * Compares any PHP variable in a sensfull manner.
49     * If a string contains only digits, it is compared as a number. If it contains anlything else, it is compared as a string.
50     * @param Multi $a The first value.
51     * @param Multi $b The second value.
52     * @return Returns 0 if $a and $b are equal. -1 if $a is less than $b and 1 if $a is more than $b.
53     */
54    private function compare($a, $b) {
55        //-- Comapring anything to null return false
56        if (is_null($a)) return false;
57        if (is_null($b)) return false;
58
59        //-- Convert boolean values into integer values 0 and 1
60        if (is_bool($a)) $a = $a?1:0;
61        if (is_bool($b)) $b = $b?1:0;
62
63        //-- If both values are numeric, their content is compared
64        if (ctype_digit($a) && ctype_digit($b)) {
65            //-- Convert a and b to integer or float and then compare them.
66            $a = $this->is_integer($a)?intval($a):floatval($a);
67            $b = $this->is_integer($b)?intval($b):floatval($b);
68
69            if ($a < $b)
70                return - 1;
71            else if ($a > $b)
72                return 1;
73            else
74                return 0;
75        } else {
76            return strcasecmp(strval($a), strval($b));
77        }
78    }
79
80    /**
81     * Processes a RPN expression in rrdtool style. The only supported operators are |, &, >, <, =
82     * @param String $expression RPN expression.
83     * @throws Exception An exception is thrown if the RPN expression could not be parsed.
84     * @return mixed Returns the result of the RPN computation.
85     */
86    public function compute($expression) {
87        $stack = array ();
88
89        foreach (explode(",", $expression) as $part) {
90            switch (trim($part)) {
91            case '|' :
92                if (count($stack) < 2) throw new Exception("RPN stack underflow"); //FIXME: Position
93
94
95                $b = array_pop($stack);
96                $a = array_pop($stack);
97                $r = ($a || $b);
98                array_push($stack, $r);
99                break;
100
101            case '&' :
102                if (count($stack) < 2) throw new Exception("RPN stack underflow"); //FIXME: Position
103
104
105                $b = array_pop($stack);
106                $a = array_pop($stack);
107                $r = ($a && $b);
108                array_push($stack, $r);
109                break;
110
111            case '>' :
112                if (count($stack) < 2) throw new Exception("RPN stack underflow"); //FIXME: Position
113
114
115                $b = array_pop($stack);
116                $a = array_pop($stack);
117                $r = ($this->compare($a, $b) > 0);
118                array_push($stack, $r);
119                break;
120
121            case '<' :
122                if (count($stack) < 2) throw new Exception("RPN stack underflow"); //FIXME: Position
123
124
125                $b = array_pop($stack);
126                $a = array_pop($stack);
127                $r = ($this->compare($a, $b) < 0);
128
129                array_push($stack, $r);
130                break;
131
132            case '=' :
133                if (count($stack) < 2) throw new Exception("RPN stack underflow"); //FIXME: Position
134
135
136                $b = array_pop($stack);
137                $a = array_pop($stack);
138                $r = ($this->compare($a, $b) == 0);
139
140                array_push($stack, $r);
141                break;
142
143            //-- Variable or Value
144            default :
145                $v = strtolower(trim($part));
146
147                if (array_key_exists($v, $this->constants)) {
148                    array_push($stack, $this->constants[$v]);
149                } else {
150                    array_push($stack, $v);
151                }
152            }
153        }
154
155        if (count($stack) > 1) throw new Exception("Unused parameters on RPN stack.");
156
157        return array_pop($stack);
158    }
159}