WC_Eval_Math

Class WC_Eval_Math.

Defined (1)

The class is defined in the following location(s).

/includes/libraries/class-wc-eval-math.php  
  1. class WC_Eval_Math { 
  2.  
  3. /** 
  4. * Last error. 
  5. * @var string 
  6. */ 
  7. public static $last_error = null; 
  8.  
  9. /** 
  10. * Variables (and constants). 
  11. * @var array 
  12. */ 
  13. public static $v = array( 'e' => 2.71, 'pi' => 3.14 ); 
  14.  
  15. /** 
  16. * User-defined functions. 
  17. * @var array 
  18. */ 
  19. public static $f = array(); 
  20.  
  21. /** 
  22. * Constants. 
  23. * @var array 
  24. */ 
  25. public static $vb = array( 'e', 'pi' ); 
  26.  
  27. /** 
  28. * Built-in functions. 
  29. * @var array 
  30. */ 
  31. public static $fb = array(); 
  32.  
  33. /** 
  34. * Evaluate maths string. 
  35. * @param string $expr 
  36. * @return mixed 
  37. */ 
  38. public static function evaluate( $expr ) { 
  39. self::$last_error = null; 
  40. $expr = trim( $expr ); 
  41. if ( substr( $expr, -1, 1 ) == ';' ) $expr = substr( $expr, 0, strlen( $expr ) -1 ); // strip semicolons at the end 
  42. // =============== 
  43. // is it a variable assignment? 
  44. if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) { 
  45. if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant 
  46. return self::trigger( "cannot assign to constant '$matches[1]'" ); 
  47. if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) return false; // get the result and make sure it's good 
  48. self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array 
  49. return self::$v[ $matches[1] ]; // and return the resulting value 
  50. // =============== 
  51. // is it a function assignment? 
  52. } elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*, \s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) { 
  53. $fnn = $matches[1]; // get the function name 
  54. if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in 
  55. return self::trigger( "cannot redefine built-in function '$matches[1]()'" ); 
  56. $args = explode( ", ", preg_replace( "/\s+/", "", $matches[2] ) ); // get the arguments 
  57. if ( ( $stack = self::nfx( $matches[3] ) ) === false ) return false; // see if it can be converted to postfix 
  58. $stack_size = count( $stack ); 
  59. for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables 
  60. $token = $stack[ $i ]; 
  61. if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) { 
  62. if ( array_key_exists( $token, self::$v ) ) { 
  63. $stack[ $i ] = self::$v[ $token ]; 
  64. } else { 
  65. return self::trigger( "undefined variable '$token' in function definition" ); 
  66. self::$f[ $fnn ] = array( 'args' => $args, 'func' => $stack ); 
  67. return true; 
  68. // =============== 
  69. } else { 
  70. return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo 
  71.  
  72. /** 
  73. * Convert infix to postfix notation. 
  74. * @param string $expr 
  75. * @return string 
  76. */ 
  77. private static function nfx( $expr ) { 
  78.  
  79. $index = 0; 
  80. $stack = new WC_Eval_Math_Stack; 
  81. $output = array(); // postfix form of expression, to be passed to pfx() 
  82. $expr = trim( $expr ); 
  83.  
  84. $ops = array( '+', '-', '*', '/', '^', '_' ); 
  85. $ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1 ); // right-associative operator? 
  86. $ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2 ); // operator precedence 
  87.  
  88. $expecting_op = false; // we use this in syntax-checking the expression 
  89. // and determining when a - is a negation 
  90. if ( preg_match( "/[^\w\s+*^\/()\., -]/", $expr, $matches ) ) { // make sure the characters are all good 
  91. return self::trigger( "illegal character '{$matches[0]}'" ); 
  92.  
  93. while ( 1 ) { // 1 Infinite Loop ;) 
  94. $op = substr( $expr, $index, 1 ); // get the first character at the current index 
  95. // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand 
  96. $ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match ); 
  97. // =============== 
  98. if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus? 
  99. $stack->push( '_' ); // put a negation on the stack 
  100. $index++; 
  101. } elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack 
  102. return self::trigger( "illegal character '_'" ); // but not in the input expression 
  103. // =============== 
  104. } elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack? 
  105. if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parethesis? 
  106. $op = '*'; 
  107. $index--; // it's an implicit multiplication 
  108. // heart of the algorithm: 
  109. while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) { 
  110. $output[] = $stack->pop(); // pop stuff off the stack into the output 
  111. // many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail 
  112. $stack->push( $op ); // finally put OUR operator onto the stack 
  113. $index++; 
  114. $expecting_op = false; 
  115. // =============== 
  116. } elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis? 
  117. while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last ( 
  118. if ( is_null( $o2 ) ) { 
  119. return self::trigger( "unexpected ')'" ); 
  120. } else { 
  121. $output[] = $o2; 
  122. if ( preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { // did we just close a function? 
  123. $fnn = $matches[1]; // get the function name 
  124. $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) 
  125. $output[] = $stack->pop(); // pop the function and push onto the output 
  126. if ( in_array( $fnn, self::$fb ) ) { // check the argument count 
  127. if ( $arg_count > 1 ) 
  128. return self::trigger( "too many arguments ($arg_count given, 1 expected)" ); 
  129. } elseif ( array_key_exists( $fnn, self::$f ) ) { 
  130. if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) 
  131. return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . " expected)" ); 
  132. } else { // did we somehow push a non-function on the stack? this should never happen 
  133. return self::trigger( "internal error" ); 
  134. $index++; 
  135. // =============== 
  136. } elseif ( ', ' === $op and $expecting_op ) { // did we just finish a function argument? 
  137. while ( ( $o2 = $stack->pop() ) != '(' ) { 
  138. if ( is_null( $o2 ) ) { 
  139. return self::trigger( "unexpected ', '" ); // oops, never had a ( 
  140. } else { 
  141. $output[] = $o2; // pop the argument expression stuff and push onto the output 
  142. // make sure there was a function 
  143. if ( ! preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) 
  144. return self::trigger( "unexpected ', '" ); 
  145. $stack->push( $stack->pop() + 1 ); // increment the argument count 
  146. $stack->push( '(' ); // put the ( back on, we'll need to pop back to it again 
  147. $index++; 
  148. $expecting_op = false; 
  149. // =============== 
  150. } elseif ( '(' === $op and ! $expecting_op ) { 
  151. $stack->push( '(' ); // that was easy 
  152. $index++; 
  153. // =============== 
  154. } elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number? 
  155. $expecting_op = true; 
  156. $val = $match[1]; 
  157. if ( preg_match( "/^([A-Za-z]\w*)\($/", $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses... 
  158. if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func 
  159. $stack->push( $val ); 
  160. $stack->push( 1 ); 
  161. $stack->push( '(' ); 
  162. $expecting_op = false; 
  163. } else { // it's a var w/ implicit multiplication 
  164. $val = $matches[1]; 
  165. $output[] = $val; 
  166. } else { // it's a plain old var or num 
  167. $output[] = $val; 
  168. $index += strlen( $val ); 
  169. // =============== 
  170. } elseif ( ')' === $op ) { // miscellaneous error checking 
  171. return self::trigger( "unexpected ')'" ); 
  172. } elseif ( in_array( $op, $ops ) and ! $expecting_op ) { 
  173. return self::trigger( "unexpected operator '$op'" ); 
  174. } else { // I don't even want to know what you did to get here 
  175. return self::trigger( "an unexpected error occurred" ); 
  176. if ( strlen( $expr ) == $index ) { 
  177. if ( in_array( $op, $ops ) ) { // did we end with an operator? bad. 
  178. return self::trigger( "operator '$op' lacks operand" ); 
  179. } else { 
  180. break; 
  181. while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace 
  182. $index++; // into implicit multiplication if no operator is there) 
  183. while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output 
  184. if ( '(' === $op ) { 
  185. return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced 
  186. $output[] = $op; 
  187. return $output; 
  188.  
  189. /** 
  190. * Evaluate postfix notation. 
  191. * @param mixed $tokens 
  192. * @param array $vars 
  193. * @return mixed 
  194. */ 
  195. private static function pfx( $tokens, $vars = array() ) { 
  196. if ( false == $tokens ) { 
  197. return false; 
  198. $stack = new WC_Eval_Math_Stack; 
  199.  
  200. foreach ( $tokens as $token ) { // nice and easy 
  201. // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on 
  202. if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) { 
  203. if ( is_null( $op2 = $stack->pop() ) ) return self::trigger( "internal error" ); 
  204. if ( is_null( $op1 = $stack->pop() ) ) return self::trigger( "internal error" ); 
  205. switch ( $token ) { 
  206. case '+': 
  207. $stack->push( $op1 + $op2 ); 
  208. break; 
  209. case '-': 
  210. $stack->push( $op1 - $op2 ); 
  211. break; 
  212. case '*': 
  213. $stack->push( $op1 * $op2 ); 
  214. break; 
  215. case '/': 
  216. if ( 0 == $op2 ) { 
  217. return self::trigger( 'division by zero' ); 
  218. $stack->push( $op1 / $op2 ); 
  219. break; 
  220. case '^': 
  221. $stack->push( pow( $op1, $op2 ) ); 
  222. break; 
  223. // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on 
  224. } elseif ( '_' === $token ) { 
  225. $stack->push( -1 * $stack->pop() ); 
  226. // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on 
  227. } elseif ( ! preg_match( "/^([a-z]\w*)\($/", $token, $matches ) ) { 
  228. if ( is_numeric( $token ) ) { 
  229. $stack->push( $token ); 
  230. } elseif ( array_key_exists( $token, self::$v ) ) { 
  231. $stack->push( self::$v[ $token ] ); 
  232. } elseif ( array_key_exists( $token, $vars ) ) { 
  233. $stack->push( $vars[ $token ] ); 
  234. } else { 
  235. return self::trigger( "undefined variable '$token'" ); 
  236. // when we're out of tokens, the stack should have a single element, the final result 
  237. if ( 1 != $stack->count ) { 
  238. return self::trigger( "internal error" ); 
  239. return $stack->pop(); 
  240.  
  241. /** 
  242. * Trigger an error, but nicely, if need be. 
  243. * @param string $msg 
  244. * @return bool 
  245. */ 
  246. private static function trigger( $msg ) { 
  247. self::$last_error = $msg; 
  248. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 
  249. echo "\nError found in:"; 
  250. self::debugPrintCallingFunction(); 
  251. trigger_error( $msg, E_USER_WARNING ); 
  252. return false; 
  253.  
  254. /** 
  255. * Prints the file name, function name, and 
  256. * line number which called your function 
  257. * (not this function, then one that called 
  258. * it to begin with) 
  259. */ 
  260. private static function debugPrintCallingFunction() { 
  261. $file = 'n/a'; 
  262. $func = 'n/a'; 
  263. $line = 'n/a'; 
  264. $debugTrace = debug_backtrace(); 
  265. if ( isset( $debugTrace[1] ) ) { 
  266. $file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a'; 
  267. $line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a'; 
  268. if ( isset( $debugTrace[2] ) ) $func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a'; 
  269. echo "\n$file, $func, $line\n";