From 25f74869c84d1e73eb6523fcb7d843c2aa4f0f83 Mon Sep 17 00:00:00 2001 From: Jeremy Wadhams Date: Thu, 27 Oct 2016 21:51:37 -0500 Subject: [PATCH] 'and' and 'or' are control structures, can't do depth-first recursion Now that the user can introduce side-effects, it's important that 'and' stops at the first falsy 'or' stops at the first truthy --- logic.js | 39 ++++++++++++------------ tests/tests.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/logic.js b/logic.js index 69cf4b0..aeff558 100644 --- a/logic.js +++ b/logic.js @@ -51,22 +51,6 @@ var jsonLogic = {}, "!!" : function(a){ return jsonLogic.truthy(a); }, "!" : function(a){ return !jsonLogic.truthy(a); }, "%" : function(a,b){ return a % b; }, - "and" : function(){ //Return first falsy, or last - for(var i=0 ; i < arguments.length ; i+=1){ - if( ! jsonLogic.truthy(arguments[i])){ - return arguments[i]; - } - } - return arguments[i-1]; - }, - "or" : function(){ //Return first truthy, or last - for(var i=0 ; i < arguments.length ; i+=1){ - if( jsonLogic.truthy(arguments[i])){ - return arguments[i]; - } - } - return arguments[i-1]; - }, "log" : function(a){ console.log(a); return a; }, "in" : function(a, b){ if(typeof b.indexOf === 'undefined') return false; @@ -173,12 +157,12 @@ jsonLogic.apply = function(logic, data){ var op = Object.keys(logic)[0], values = logic[op], - i; + i, current; //easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]} if( ! Array.isArray(values)){ values = [values]; } - // 'if' violates the normal rule of depth-first calculating consequents, let it manage recursion + // 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed. if(op === 'if' || op == '?:'){ /* 'if' should be called with a odd number of parameters, 3 or greater This works on the pattern: @@ -200,7 +184,24 @@ jsonLogic.apply = function(logic, data){ } if(values.length === i+1) return jsonLogic.apply(values[i], data); return null; - } + }else if(op === "and"){ //Return first falsy, or last + for(i=0 ; i < values.length ; i+=1){ + current = jsonLogic.apply(values[i], data); + if( ! jsonLogic.truthy(current)){ + return current; + } + } + return current; //Last + }else if(op === "or"){//Return first truthy, or last + for(i=0 ; i < values.length ; i+=1){ + current = jsonLogic.apply(values[i], data); + if( jsonLogic.truthy(current) ){ + return current; + } + } + return current; //Last + } + // Everyone else gets immediate depth-first recursion diff --git a/tests/tests.js b/tests/tests.js index 72fc050..8eda6f6 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -165,3 +165,83 @@ QUnit.test( "Expanding functionality with method", function( assert) { assert.equal(a.count, 42); //Happy state change }); + + +QUnit.test("Control structures don't use depth-first computation", function(assert){ + //Depth-first recursion was wasteful but not harmful until we added custom operations that could have side-effects. + + //If operations run the condition, if truthy, it runs and returns that consequent. + //Consequents of falsy conditions should not run. + //After one truthy condition, no other condition should run + var conditions = []; + var consequents = []; + jsonLogic.add_operation("push.if", function(v){ conditions.push(v); return v; }); + jsonLogic.add_operation("push.then", function(v){ consequents.push(v); return v; }); + jsonLogic.add_operation("push.else", function(v){ consequents.push(v); return v; }); + + jsonLogic.apply({"if":[ + {"push.if" : [true] }, + {"push.then":["first"]}, + {"push.if" : [false]}, + {"push.then":["second"]}, + {"push.else":["third"]} + ]}); + assert.deepEqual(conditions, [true]); + assert.deepEqual(consequents, ["first"]); + + conditions = []; + consequents = []; + jsonLogic.apply({"if":[ + {"push.if" : [false] }, + {"push.then":["first"]}, + {"push.if" : [true]}, + {"push.then":["second"]}, + {"push.else":["third"]} + ]}); + assert.deepEqual(conditions, [false,true]); + assert.deepEqual(consequents, ["second"]); + + conditions = []; + consequents = []; + jsonLogic.apply({"if":[ + {"push.if" : [false] }, + {"push.then":["first"]}, + {"push.if" : [false]}, + {"push.then":["second"]}, + {"push.else":["third"]} + ]}); + assert.deepEqual(conditions, [false,false]); + assert.deepEqual(consequents, ["third"]); + + + jsonLogic.add_operation("push", function(arg){ i.push(arg); return arg; }); + var i = []; + + i = []; + jsonLogic.apply({"and":[ {"push":[false]}, {"push":[false]} ]}); + assert.deepEqual(i, [false]); + i = []; + jsonLogic.apply({"and":[ {"push":[false]}, {"push":[true]} ]}); + assert.deepEqual(i, [false]); + i = []; + jsonLogic.apply({"and":[ {"push":[true]}, {"push":[false]} ]}); + assert.deepEqual(i, [true, false]); + i = []; + jsonLogic.apply({"and":[ {"push":[true]}, {"push":[true]} ]}); + assert.deepEqual(i, [true, true]); + + + i = []; + jsonLogic.apply({"or":[ {"push":[false]}, {"push":[false]} ]}); + assert.deepEqual(i, [false,false]); + i = []; + jsonLogic.apply({"or":[ {"push":[false]}, {"push":[true]} ]}); + assert.deepEqual(i, [false,true]); + i = []; + jsonLogic.apply({"or":[ {"push":[true]}, {"push":[false]} ]}); + assert.deepEqual(i, [true]); + i = []; + jsonLogic.apply({"or":[ {"push":[true]}, {"push":[true]} ]}); + assert.deepEqual(i, [true]); + +});