Skip to content
Snippets Groups Projects
Commit a7e1e09b authored by Reinhold Kainhofer's avatar Reinhold Kainhofer
Browse files

Implement functions and unary operators

The shunting yard algorithm had to be severely extended (by using a temporary operand stack for the function arguments, and by storing the type of the previous token to distinguish unary operators from binary)
parent f632c04d
No related branches found
No related tags found
No related merge requests found
......@@ -37,6 +37,7 @@ VMSHIPMENT_RULES_UNKNOWN_VARIABLE="Unknown variable '%s' in rule '%s'"
VMSHIPMENT_RULES_UNKNOWN_OPERATOR="Unknown operator '%s' in shipment rule '%s'"
VMSHIPMENT_RULES_PARSE_MISSING_PAREN="Error during parsing expression '%s': Opening parenthesis cannot be found!"
VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED="Error during parsing expression '%s': A parenthesis was not closed properly!"
VMSHIPMENT_RULES_PARSE_FUNCTION_NOT_CLOSED="Error during parsing expression '%s': A function call was not closed properly!"
VMSHIPMENT_RULES_EVALUATE_NONNUMERIC="Encountered term '%s' during evaluation, that does not evaluate to a numeric value! (Full rule: '%s')"
VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR="Syntax error during evaluation, RPN is not well formed! (Full rule: '%s')"
......
......@@ -40,10 +40,12 @@ VMSHIPMENT_RULES_UNKNOWN_VARIABLE="Unknown variable '%s' in rule '%s'"
VMSHIPMENT_RULES_UNKNOWN_OPERATOR="Unknown operator '%s' in shipment rule '%s'"
VMSHIPMENT_RULES_PARSE_MISSING_PAREN="Error during parsing expression '%s': Opening parenthesis cannot be found!"
VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED="Error during parsing expression '%s': A parenthesis was not closed properly!"
VMSHIPMENT_RULES_PARSE_FUNCTION_NOT_CLOSED="Error during parsing expression '%s': A function call was not closed properly!"
VMSHIPMENT_RULES_EVALUATE_NONNUMERIC="Encountered term '%s' during evaluation, that does not evaluate to a numeric value! (Full rule: '%s')"
VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR="Syntax error during evaluation, RPN is not well formed! (Full rule: '%s')"
VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR="Unknown operator '%s' encountered during evaluation of rule '%s'."
VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION="Unknown function '%s' encountered during evaluation of rule '%s'."
VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR="Unknown error occurred during evaluation of rule '%s'."
VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL="Assignments are not allows inside expressions (rule given was '%s')"
VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE="Evaluation yields unknown value while evaluating rule part '%s'."
\ No newline at end of file
......@@ -28,6 +28,26 @@ if (!class_exists ('plgVmShipmentRules_Shipping_Base')) {
require (dirname(__FILE__).DS.'rules_shipping_base.php');
}
function print_array($obj) {
$res = "";
if (is_array($obj)) {
$res .= "array(";
$sep = "";
foreach ($obj as $e) {
$res .= $sep . print_array($e);
$sep = ", ";
}
$res .= ")";
} elseif (is_string($obj)) {
$res .= "\"$obj\"";
} else {
$res .= (string)$obj;
}
return $res;
}
/** Shipping costs according to general rules.
* Derived from the standard plugin, no need to change anything! The standard plugin already uses the advanced rules class defined below, if it can be found
*/
......@@ -85,6 +105,7 @@ class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_B
*/
class ShippingRule_Advanced extends ShippingRule {
var $operators = array(
".-" => 100, ".+" => 100,
"^" => 70,
"*" => 60, "/" => 60, "%" => 60,
"+" => 50, "-" => 50,
......@@ -94,7 +115,11 @@ class ShippingRule_Advanced extends ShippingRule {
" OR " => 20, "OR" => 20,
"=" => 10,
"(" => 0, ")" =>0 );
"(" => 0, ")" =>0
);
var $unary_ops = array(
"-" => ".-", "+" => ".+"
);
function __construct ($rule, $countries, $tax_id) {
parent::__construct ($rule, $countries, $tax_id);
......@@ -105,7 +130,7 @@ class ShippingRule_Advanced extends ShippingRule {
$str_re = '/("[^"]*"|\'[^\']*\')/';
$strings = preg_split($str_re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
// Then split all other parts of the expression at the operators
$op_re = ':\s*( OR |&&| AND |<=|=>|>=|=>|<>|!=|==|<|=|>|~|\+|-|\*|/|%|\(|\)|\^)\s*:';
$op_re = ':\s*( OR |&&| AND |<=|=>|>=|=>|<>|!=|==|<|=|>|~|\+|-|\*|/|%|\(|\)|\^|,)\s*:';
$atoms = array();
foreach ($strings as $s) {
if (preg_match($str_re, $s)) {
......@@ -115,30 +140,13 @@ class ShippingRule_Advanced extends ShippingRule {
$atoms = array_merge ($atoms, $newatoms);
}
}
// Finally handle the double meaning of -: After an operator and before an operand, it is NOT an operator, but belongs to the operand
$prevop = true;
$result = array();
$leftover = '';
foreach ($atoms as $a) {
if ($a == "-") {
$prev = end($result);
if (is_null ($prev) || (preg_match ($op_re, $prev) && $prev != ')')) {
$leftover = $a;
} else {
$result[] = $leftover.$a;
$leftover = '';
}
} else {
$result[] = $leftover.$a;
$leftover = '';
}
}
return $result;
return $atoms;
}
/** parse the mathematical expressions (following Knuth 1962):
/** parse the mathematical expressions using the Shunting Yard Algorithm by Dijkstra (with some extensions to allow arbitrary functions):
* First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators)
* TODO: Update this description to include unary operators and general function calls
* Then convert the infix notation into postfix (RPN), taking care of operator precedence
* 1) Initialize empty stack and empty result variable
* 2) Read infix expression from left to right, one atom at a time
......@@ -153,7 +161,10 @@ class ShippingRule_Advanced extends ShippingRule {
* 6b) push them to the result (not the opening parenthesis, of course)
* 7) At the end of the input, pop all operators from the stack and onto the result
*
* Afterwards, convert this RPN list into an expression tree to be evaluate
* Afterwards, convert this RPN list into an expression tree to be evaluated
*
* For the full algorithm, including function parsing, see Wikipedia:
* http://en.wikipedia.org/wiki/Shunting_yard_algorithm
*
*/
function parseRulePart($rulepart) {
......@@ -180,32 +191,104 @@ class ShippingRule_Advanced extends ShippingRule {
$is_assignment = false;
$stack = array (); // 1)
$rpn = array ();
$prev_token_operator = false;
$function_args = array();
$out_stack = array();
foreach ($atoms as $a) { // 2)
if (!isset($this->operators[$a])) { // 3) Operand
array_push ($rpn, $a);
} elseif ($a == "(") { // 5) parenthesis
array_push ($stack, $a);
if ($a == ",") { // A function argument separator
// pop-and-apply all operators back to the left function paren
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
if ($op != "FUNCTION(") {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
$this_func = array_pop($function_args);
// Add current output stack as argument, reset temporary output stack
if (!empty($out_stack)) $this_func[] = $out_stack;
$function_args[] = $this_func;
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and !$prev_token_operator) { // 5) parenthesis after operand -> FUNCTION CALL
array_push ($stack, "FUNCTION(");
// retrieve function name from RPN list (remove last entry from operand stack!)
$function = array_pop ($out_stack);
$new_stack = array();
// Set up function call data structure on function_args stack:
$function_args[] = array(/* old operand stack: */$out_stack, $function);
// Use a the temporary operand stack until the closing paren restores the previous operand stack again
$out_stack = array();
$prev_token_operator = true;
} elseif ($a == "(" and $prev_token_operator) { // 5) real parenthesis
$stack[] = $a;
$prev_token_operator = true;
} elseif ($a == ")") { // 6) parenthesis
do {
$op=array_pop($stack); // 6a)
if (!is_null($op) && ($op != "(")) {
array_push ($rpn, $op); // 6b)
} else {
if ($op != "(") {
// If no ( can be found, the expression is wrong!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error');
if ($op == "(") {
break; // We have found the opening parenthesis
} elseif ($op =="FUNCTION(") { // Function call
// Remove function info from the functions stack; Format is array(PREVIOUS_OPERAND_STACK, FUNCTION, ARGS...)
$this_func = array_pop ($function_args);
// Append last argument (if not empty)
if (!empty($out_stack)) $this_func[] = $out_stack;
// restore old output/operand stack
$out_stack = array_shift($this_func);
// Function name is the next entry
$function = array_shift($this_func);
// All other entries are function arguments, so append them to the current operand stack
foreach ($this_func as $a) {
foreach ($a as $aa) {
$out_stack[] = $aa;
}
}
$out_stack[] = array("FUNCTION", $function, count($this_func));
break; // We have found the opening parenthesis
} elseif (!is_null($op)) {
$out_stack[]=$op; // 6b) "normal" operators
} else {
// no ( and no operator, so the expression is wrong!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error');
break;
}
} while (true);
} else { // 4) operators
// For operators, pop operators from the stack until you reach an opening parenthesis,
$prev_token_operator = false;
} elseif (isset($this->unary_ops[$a]) && $prev_token_operator) { // 4) UNARY operators
// Unary and binary operators need to be handled differently:
// Unary operators must only pop other unary operators, never any binary operator
$unary_op = $this->unary_ops[$a];
// For unary operators, pop other unary operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// Remove all other unary operators:
if (in_array ($op, $this->unary_ops)) {
array_push ($out_stack, $op);
} else {
// No unary operator -> add it back to stack, exit loop
array_push ($stack, $op);
break;
}
} while (0);
array_push ($stack, $unary_op); // 4b)
$prev_token_operator = true;
} elseif (isset($this->operators[$a])) { // 4) BINARY operators
$prec = $this->operators[$a];
$is_condition |= in_array($a, $condition_ops);
$is_assignment |= ($a == "=");
// For operators, pop operators from the stack until you reach an opening parenthesis,
// an operator of lower precedence, or a right associative symbol of equal precedence.
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// The only right-associative operator is =, which we allow at most once!
......@@ -218,10 +301,15 @@ class ShippingRule_Advanced extends ShippingRule {
array_push ($stack, $op); // 4b)
break;
} else {
array_push ($rpn, $op);
array_push ($out_stack, $op);
}
} while (0);
array_push ($stack, $a); // 4b)
$prev_token_operator = true;
} else { // 3) Everything else is an Operand
$out_stack[] = $a;
$prev_token_operator = false;
}
}
// Finally, pop all operators from the stack and append them to the result
......@@ -230,11 +318,15 @@ class ShippingRule_Advanced extends ShippingRule {
if ($op == "(") {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error');
} else {
array_push ($rpn, $op);
array_push ($out_stack, $op);
}
}
if (!empty($function_args)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_FUNCTION_NOT_CLOSED', $rulepart), 'error');
}
/** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure, according to Knuth:
/** Now, turn the RPN into an expression tree (i.e. "evaluate" it into a tree structure), according to Knuth:
* 1) Initialize an empty stack
* 2) Read the RPN from left to right
* 3) If operand, push it onto the stack
......@@ -248,11 +340,31 @@ class ShippingRule_Advanced extends ShippingRule {
*/
$stack=array(); // 1)
foreach ($rpn as $e) { // 2)
if (!isset($this->operators[$e])) { // 3)
// Operand => push onto stack
array_push ($stack, $e);
} else { // 4)
foreach ($out_stack as $e) { // 2)
if (is_array($e) && $e[0]=="FUNCTION") { // A function call (#args is saved as $e[2], so remove that number of operands from the stack)
$function = $e[1];
$argc = $e[2];
$args = array();
for ($i = $argc; $i > 0; $i--) {
$a = array_pop($stack);
array_unshift($args, $a);
}
array_unshift($args, $function);
array_unshift($args, "FUNCTION");
$stack[] = $args;
} elseif (in_array($e, $this->unary_ops)) { // 4) unary operators
// Operator => apply to the last value on the stack
if (count($stack)<1) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o1 = array_pop($stack);
// Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison,
// insert the arguments to the existing comparison instead of creating a new one
$op = array ($e, $o1); // 4b)
array_push ($stack, $op); // 4c)
} elseif (isset($this->operators[$e])) { // 4) binary operators
// Operator => apply to the last two values on the stack
if (count($stack)<2) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
......@@ -276,7 +388,11 @@ class ShippingRule_Advanced extends ShippingRule {
$op = array ($e, $o1, $o2); // 4b)
}
array_push ($stack, $op); // 4c)
} else { // 3)
// Operand => push onto stack
array_push ($stack, $e);
}
}
// 5a)
if (count($stack) != 1) {
......@@ -307,3 +423,20 @@ class ShippingRule_Advanced extends ShippingRule {
}
// No closing tag
/*$rule = new ShippingRule_Advanced("", array(), 0);
$rp="a+b+(-1+d)";
$rp="1+maxx(a,b)";
$rp="f(1,2,+)";
$rp="1+year()";
$rp = "1+max( 1,2,3,4,5) + min(9,10,101)";
$rp = "abs(0-1.9999)";
$rp="max(1,min(0,5,7), 9)";
// $rp="f()";
print_r($rule->tokenize_expression($rp));
$rule->parseRulePart($rp);
print_r($rule->conditions);
print_r($rule->shipping);
print_r($rule->evaluateTerm($rule->shipping, array()));
*/
\ No newline at end of file
......@@ -704,6 +704,40 @@ class ShippingRule {
return true;
}
function evaluateFunction ($function, $args) {
$func = strtolower($function);
// Functions with no argument:
if (count($args) == 0) {
switch ($func) {
case "second": return getdate()["seconds"]; break;
case "minute": return getdate()["minutes"]; break;
case "hour": return getdate()["hours"]; break;
case "day": return getdate()["mday"]; break;
case "weekday":return getdate()["wday"]; break;
case "month": return getdate()["mon"]; break;
case "year": return getdate()["year"]; break;
case "yearday":return getdate()["yday"]; break;
}
}
// Functions with exactly one argument:
if (count($args) == 1) {
switch ($func) {
case "round": return round($args[0]); break;
case "ceil": return ceil ($args[0]); break;
case "floor": return floor($args[0]); break;
case "abs": return abs($args[0]); break;
}
}
// Functions with variable number of args
switch ($func) {
case "max": return max($args); break;
case "min": return min($args); break;
}
// No known function matches => print an error, return 0
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION', $function, $this->rulestring), 'error');
return 0;
}
function evaluateTerm ($expr, $vals) {
if (is_null($expr)) {
return $expr;
......@@ -724,17 +758,24 @@ class ShippingRule {
$op = array_shift($expr);
$args = array();
$evaluate = true;
if ($op == "FUNCTION") {
$evaluate = false;
}
foreach ($expr as $e) {
$term = $evaluate ? ($this->evaluateTerm($e, $vals)) : $e;
if ($op == 'comparison') {
// For comparisons, we only evaluate every other term (the operators are NOT evaluated!)
$evaluate = !$evaluate;
}
if ($op == "FUNCTION") {
$evaluate = true;
}
if (is_null($term)) return null;
$args[] = $term;
}
$res = false;
switch ($op) {
// Logical operators:
case 'OR':
case ' OR ': foreach ($args as $a) { $res = ($res || $a); }; break;
case '&&':
......@@ -742,6 +783,7 @@ class ShippingRule {
case ' AND ': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break;
case 'in': $needle = array_shift($args); $res = in_array($needle, $args); break;
// Comparisons:
case '<':
case '<=':
case '=<':
......@@ -756,6 +798,11 @@ class ShippingRule {
case 'comparison':
$res = $this->evaluateComparison($args, $vals); break;
// Unary operators:
case '.-': $res = -$args[0]; break;
case '.+': $res = $args[0]; break;
// Binary operators
case "+": $res = ($args[0] + $args[1]); break;
case "-": $res = ($args[0] - $args[1]); break;
case "*": $res = ($args[0] * $args[1]); break;
......@@ -763,12 +810,8 @@ class ShippingRule {
case "%": $res = (fmod($args[0], $args[1])); break;
case "^": $res = ($args[0] ^ $args[1]); break;
# TODO: Document these functions:
case "round": $res = round($args[0]); break;
case "ceil": $res = ceil($args[0]); break;
case "floor": $res = floor($args[0]); break;
case "max": $res = max($args); break;
case "min": $res = min($args); break;
// Functions:
case "FUNCTION": $func = array_shift($args); $res = $this->evaluateFunction($func, $args); break;
default: $res = false;
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment