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

Version 2.0: Implement OR, coupon variables, prices w/o tax, better parsing, etc.

parent 6ba51b36
No related branches found
No related tags found
No related merge requests found
BASE=rules_shipping
BASE_ADV=rules_shipping_advanced
PLUGINTYPE=vmshipment
VERSION=1.1.0
VERSION=2.0
PLUGINFILES=$(BASE).php $(BASE).script.php $(BASE).xml index.html
PLUGINFILES_ADV=$(BASE_ADV).php $(BASE).php $(BASE_ADV).script.php $(BASE_ADV).xml index.html
PLUGINFILES=$(BASE).php $(BASE)_base.php $(BASE).script.php $(BASE).xml index.html
PLUGINFILES_ADV=$(BASE_ADV).php $(BASE)_base.php $(BASE_ADV).script.php $(BASE_ADV).xml index.html
TRANSLATIONS=$(call wildcard,language/*/*.plg_$(PLUGINTYPE)_$(BASE).*ini)
TRANSLATIONS_ADV=$(subst $(BASE),$(BASE_ADV),$(TRANSLATIONS))
......
......@@ -60,4 +60,6 @@ VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED="Error during parsing expression '%s': A
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_ERROR="Unknown error occurred during evaluation of rule '%s'."
\ No newline at end of file
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
File added
File added
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8" ?>
<install version="1.5" type="plugin" group="vmshipment" method="upgrade">
<name>VMSHIPMENT_RULES</name>
<creationDate>2013-01-12</creationDate>
<creationDate>2013-02-08</creationDate>
<author>Reinhold Kainhofer</author>
<authorUrl>http://www.kainhofer.com</authorUrl>
<copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright>
<license>GPL v3+</license>
<version>1.1.0</version>
<version>2.0.0</version>
<description>VMSHIPMENT_RULES_DESC</description>
<files>
<filename plugin="rules_shipping">rules_shipping.php</filename>
<filename>rules_shipping_base.php</filename>
<folder>language</folder>
<folder>elements</folder>
</files>
......
......@@ -25,28 +25,98 @@ defined ('_JEXEC') or die('Restricted access');
if (!class_exists ('vmPSPlugin')) {
require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php');
}
if (!class_exists ('plgVmShipmentRules_Shipping')) {
require (dirname(__FILE__).DS.'rules_shipping.php');
if (!class_exists ('plgVmShipmentRules_Shipping_Base')) {
require (dirname(__FILE__).DS.'rules_shipping_base.php');
}
/** 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
*/
class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping {
// function __construct (& $subject, $config) {
// parent::__construct ($subject, $config);
// }
class plgVmShipmentRules_Shipping_Advanced extends plgVmShipmentRules_Shipping_Base {
function __construct (& $subject, $config) {
parent::__construct ($subject, $config);
}
protected function createMethodRule ($r, $countries, $tax) {
return new ShippingRule_Advanced ($r, $countries, $tax);
}
/** Allow child classes to add additional variables for the rules
*/
protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) {
$values['coupon'] = $cart->couponCode;
}
}
// is_comparison: if the expression $o is a (possibly chained) comparison consisting of operators in $ops,
// return the last term of hte comparison; null otherwise
function is_comparison ($o, $ops) {
if (!is_array($o))
return null;
if (in_array($o[0], $ops))
return $o[2];
$o2comp = is_comparison($o[2], $ops);
if (trim($o[0])=='AND' && !is_null (is_comparison($o[1], $ops)) && !is_null($o2comp))
return $o2comp;
return null;
}
/** Extend the shipping rules by allowing arbitrary mathematical expressions
*/
class ShippingRule_Advanced extends ShippingRule {
var $operators = array("+"=>10, "-"=>10, "*"=>20, "/"=>20, "%"=>30, "^"=>50, "("=>100, ")"=>100);
var $operators = array(
"^" => 70,
"*" => 60, "/" => 60, "%" => 60,
"+" => 50, "-" => 50,
"<" => 40, "<=" => 40, ">" => 40, ">=" => 40, "=>" => 40, "=<" => 40,
"==" => 40, "!=" => 40, "<>" => 40,
"&" => 21,
" OR " => 20,
"=" => 10,
"(" => 0, ")" =>0 );
function __construct ($rule, $countries, $tax_id) {
parent::__construct ($rule, $countries, $tax_id);
}
function tokenize_expression ($expression) {
// First, extract all strings, delimited by ":
$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 |<=|=>|>=|=>|<>|!=|==|<|=|>|\+|-|\*|/|%|\(|\)|\^)\s*:';
$atoms = array();
foreach ($strings as $s) {
if (preg_match($str_re, $s)) {
$atoms[] = $s;
} else {
$newatoms = preg_split($op_re, $s, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
$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;
}
/** parse the mathematical expressions (following Knuth 1962):
* First parse the string into an array of tokens (operators and operands) by a simple regexp with known operators as separators)
......@@ -63,50 +133,74 @@ class ShippingRule_Advanced extends ShippingRule {
* 6a) Pop operators from stack until opening parenthesis is found
* 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
* Store this RPN list for later evaluation
*
* Afterwards, convert this RPN list into an expression tree to be evaluate
*
*/
function parseShippingTerm ($expr) {
$op_re = ':\s*(\+|-|\*|/|%|\(|\)|\^)\s*:';
$atoms = preg_split($op_re, strtolower($expr), -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
function parseRulePart($rulepart) {
/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
$rulepart = trim($rulepart);
if (empty($rulepart)) return;
// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
if (preg_match('/^\s*(name)\s*=\s*"?(.*)"?\s*$/i', $rulepart, $matches)) {
$this->handleAssignment ($matches[1], $matches[2], $rulepart);
return;
}
// Split at all operators:
$atoms = $this->tokenize_expression ($rulepart);
// Any of these indicate a comparison and thus a condition:
$comparison_ops = array('<', '<=', '=<', '<>', '!=', '==', '>', '>=', '=>');
$is_comparison = false;
$is_assignment = false;
if (count($atoms)==1) return $atoms[0];
// Operators,including precedence
$stack = array (); // 1)
$result = array ();
$rpn = array ();
foreach ($atoms as $a) { // 2)
if (!isset($this->operators[$a])) { // 3)
array_push ($result, $a);
} elseif ($a == "(") { // 5)
if (!isset($this->operators[$a])) { // 3) Operand
array_push ($rpn, $a);
} elseif ($a == "(") { // 5) parenthesis
array_push ($stack, $a);
} elseif ($a == ")") { // 6)
} elseif ($a == ")") { // 6) parenthesis
do {
$op=array_pop($stack); // 6a)
if (!empty($op) && $op != "(") {
array_push ($result, $op); // 6b)
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', $expr), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_MISSING_PAREN', $rulepart), 'error');
}
break; // We have found the opening parenthesis
}
} while (0);
} else { // 4
} while (true);
} else { // 4) operators
// 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.
$prec = $this->operators[$a];
$is_comparison |= in_array($a, $comparison_ops);
$is_assignment |= ($a == "=");
while (count($stack)>0) { // 4a)
$op = array_pop ($stack);
// Ignore the right-associative symbols of equal precedence for now...
// The only right-associative operator is =, which we allow at most once!
if ($op == "(") {
// add it back to the stack!
array_push ($stack, $op);
break;
} elseif ($this->operators[$op]<$prec) {
// We found an operator with lower precedence, add it back to the stack!
array_push ($stack, $op); // 4b)
break;
} else {
array_push ($result, $op);
array_push ($rpn, $op);
}
} while (0);
array_push ($stack, $a); // 4b)
......@@ -116,27 +210,13 @@ class ShippingRule_Advanced extends ShippingRule {
while ($op=array_pop($stack)) {
// Opening parentheses should not be found on the stack any more. That would mean a closing paren is missing!
if ($op == "(") {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $expr), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_PARSE_PAREN_NOT_CLOSED', $rulepart), 'error');
} else {
array_push ($result, $op);
array_push ($rpn, $op);
}
}
/* In the advanced version, shipping cost can be given as a full mathematical expression */
return $result;
}
function evaluateTerm ($expr, $vals) {
if (!is_array($expr)) {
// Normal expression, no complex mathematical formula in RPN
if (!is_numeric($expr) && isset($vals[$expr])) $expr=$vals[$expr];
if (!is_numeric($expr)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $expr, $this->rulestring), 'error');
return 0;
}
return $expr;
}
/** Evaluate the RPN, 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
......@@ -148,49 +228,58 @@ class ShippingRule_Advanced extends ShippingRule {
* 5) At the end of the RPN, pop the result from the stack.
* 5a) The stack should now be empty (otherwise, ERROR, invalid syntax)
*/
$stack=array(); // 1)
foreach ($expr as $e) { // 2)
foreach ($rpn as $e) { // 2)
if (!isset($this->operators[$e])) { // 3)
// Operand => push onto stack
if (!is_numeric($e) && isset($vals[$e])) $e = $vals[$e];
if (!is_numeric($e)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error');
$e = 0;
}
array_push ($stack, $e);
} else { // 4)
// Operator => apply to the last two values on the stack
if (count($stack)<2) { // 4d)
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $this->rulestring), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_SYNTAXERROR', $rulepart), 'error');
array_push($stack, 0);
continue;
}
$o2 = array_pop($stack); // 4a)
$o1 = array_pop($stack);
$res = 0;
switch ($e) { // 4b)
case "+": $res = $o1+$o2; break;
case "-": $res = $o1-$o2; break;
case "*": $res = $o1*$o2; break;
case "/": $res = $o1/$o2; break;
case "%": $res = $o1%$o2; break;
case "^": $res = $o1^$o1; break;
default:
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_OPERATOR', $e, $this->rulestring), 'error');
$res = 0;
// TODO: Special-case chained comparisons: if e is a comparison, and operator(o1) is also a comparison,
// insert AND(o1, e(arg2(o1), o2)) instead of e(o1, o1)
// is_comparison nicely returns the last term of the (possibly chained) comparison, null otherwise
$o1comp = is_comparison($o1, $comparison_ops);
if (in_array ($e, $comparison_ops) && !is_null($o1comp) ) {
$op = array ('AND', $o1, array($e, $o1comp, $o2));
} else {
$op = array ($e, $o1, $o2); // 4b)
}
array_push($stack, $res); // 4c)
array_push ($stack, $op); // 4c)
}
}
// 5a)
if (count($stack) != 1) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error');
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $rulepart), 'error');
$stack = array (0);
}
$res = array_pop($stack); // 5)
return $res;
if ($is_comparison) { // Comparisons are conditions
$this->conditions[] = $res;
} elseif ($is_assignment) {
if ($res[0]=='=') {
$this->handleAssignment ($res[1], $res[2], $rulepart);
} else {
// Assignment has to be top-level!
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_ASSIGNMENT_TOPLEVEL', $rulepart), 'error');
}
} else {
// Terms without comparisons or assignments are shipping cost expressions
$this->shipping = $res;
$this->includes_tax = False;
}
}
}
// No closing tag
<?xml version="1.0" encoding="UTF-8" ?>
<install version="1.5" type="plugin" group="vmshipment" method="upgrade">
<name>VMSHIPMENT_RULES_ADV</name>
<creationDate>2013-01-16</creationDate>
<creationDate>2013-02-08</creationDate>
<author>Reinhold Kainhofer</author>
<authorUrl>http://www.kainhofer.com</authorUrl>
<copyright>Copyright (C) 2013, Reinhold Kainhofer</copyright>
<license>GPL v3+</license>
<version>1.1.0</version>
<version>2.0.0</version>
<description>VMSHIPMENT_RULES_ADV_DESC</description>
<files>
<filename plugin="rules_shipping_advanced">rules_shipping_advanced.php</filename>
<filename>rules_shipping.php</filename>
<filename>rules_shipping_base.php</filename>
<folder>language</folder>
<folder>elements</folder>
</files>
......
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment