Skip to content

Commit 25f7486

Browse files
committed
'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
1 parent 9f6f776 commit 25f7486

File tree

2 files changed

+100
-19
lines changed

2 files changed

+100
-19
lines changed

logic.js

+20-19
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,6 @@ var jsonLogic = {},
5151
"!!" : function(a){ return jsonLogic.truthy(a); },
5252
"!" : function(a){ return !jsonLogic.truthy(a); },
5353
"%" : function(a,b){ return a % b; },
54-
"and" : function(){ //Return first falsy, or last
55-
for(var i=0 ; i < arguments.length ; i+=1){
56-
if( ! jsonLogic.truthy(arguments[i])){
57-
return arguments[i];
58-
}
59-
}
60-
return arguments[i-1];
61-
},
62-
"or" : function(){ //Return first truthy, or last
63-
for(var i=0 ; i < arguments.length ; i+=1){
64-
if( jsonLogic.truthy(arguments[i])){
65-
return arguments[i];
66-
}
67-
}
68-
return arguments[i-1];
69-
},
7054
"log" : function(a){ console.log(a); return a; },
7155
"in" : function(a, b){
7256
if(typeof b.indexOf === 'undefined') return false;
@@ -173,12 +157,12 @@ jsonLogic.apply = function(logic, data){
173157

174158
var op = Object.keys(logic)[0],
175159
values = logic[op],
176-
i;
160+
i, current;
177161

178162
//easy syntax for unary operators, like {"var" : "x"} instead of strict {"var" : ["x"]}
179163
if( ! Array.isArray(values)){ values = [values]; }
180164

181-
// 'if' violates the normal rule of depth-first calculating consequents, let it manage recursion
165+
// 'if', 'and', and 'or' violate the normal rule of depth-first calculating consequents, let each manage recursion as needed.
182166
if(op === 'if' || op == '?:'){
183167
/* 'if' should be called with a odd number of parameters, 3 or greater
184168
This works on the pattern:
@@ -200,7 +184,24 @@ jsonLogic.apply = function(logic, data){
200184
}
201185
if(values.length === i+1) return jsonLogic.apply(values[i], data);
202186
return null;
203-
}
187+
}else if(op === "and"){ //Return first falsy, or last
188+
for(i=0 ; i < values.length ; i+=1){
189+
current = jsonLogic.apply(values[i], data);
190+
if( ! jsonLogic.truthy(current)){
191+
return current;
192+
}
193+
}
194+
return current; //Last
195+
}else if(op === "or"){//Return first truthy, or last
196+
for(i=0 ; i < values.length ; i+=1){
197+
current = jsonLogic.apply(values[i], data);
198+
if( jsonLogic.truthy(current) ){
199+
return current;
200+
}
201+
}
202+
return current; //Last
203+
}
204+
204205

205206

206207
// Everyone else gets immediate depth-first recursion

tests/tests.js

+80
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,83 @@ QUnit.test( "Expanding functionality with method", function( assert) {
165165
assert.equal(a.count, 42); //Happy state change
166166

167167
});
168+
169+
170+
QUnit.test("Control structures don't use depth-first computation", function(assert){
171+
//Depth-first recursion was wasteful but not harmful until we added custom operations that could have side-effects.
172+
173+
//If operations run the condition, if truthy, it runs and returns that consequent.
174+
//Consequents of falsy conditions should not run.
175+
//After one truthy condition, no other condition should run
176+
var conditions = [];
177+
var consequents = [];
178+
jsonLogic.add_operation("push.if", function(v){ conditions.push(v); return v; });
179+
jsonLogic.add_operation("push.then", function(v){ consequents.push(v); return v; });
180+
jsonLogic.add_operation("push.else", function(v){ consequents.push(v); return v; });
181+
182+
jsonLogic.apply({"if":[
183+
{"push.if" : [true] },
184+
{"push.then":["first"]},
185+
{"push.if" : [false]},
186+
{"push.then":["second"]},
187+
{"push.else":["third"]}
188+
]});
189+
assert.deepEqual(conditions, [true]);
190+
assert.deepEqual(consequents, ["first"]);
191+
192+
conditions = [];
193+
consequents = [];
194+
jsonLogic.apply({"if":[
195+
{"push.if" : [false] },
196+
{"push.then":["first"]},
197+
{"push.if" : [true]},
198+
{"push.then":["second"]},
199+
{"push.else":["third"]}
200+
]});
201+
assert.deepEqual(conditions, [false,true]);
202+
assert.deepEqual(consequents, ["second"]);
203+
204+
conditions = [];
205+
consequents = [];
206+
jsonLogic.apply({"if":[
207+
{"push.if" : [false] },
208+
{"push.then":["first"]},
209+
{"push.if" : [false]},
210+
{"push.then":["second"]},
211+
{"push.else":["third"]}
212+
]});
213+
assert.deepEqual(conditions, [false,false]);
214+
assert.deepEqual(consequents, ["third"]);
215+
216+
217+
jsonLogic.add_operation("push", function(arg){ i.push(arg); return arg; });
218+
var i = [];
219+
220+
i = [];
221+
jsonLogic.apply({"and":[ {"push":[false]}, {"push":[false]} ]});
222+
assert.deepEqual(i, [false]);
223+
i = [];
224+
jsonLogic.apply({"and":[ {"push":[false]}, {"push":[true]} ]});
225+
assert.deepEqual(i, [false]);
226+
i = [];
227+
jsonLogic.apply({"and":[ {"push":[true]}, {"push":[false]} ]});
228+
assert.deepEqual(i, [true, false]);
229+
i = [];
230+
jsonLogic.apply({"and":[ {"push":[true]}, {"push":[true]} ]});
231+
assert.deepEqual(i, [true, true]);
232+
233+
234+
i = [];
235+
jsonLogic.apply({"or":[ {"push":[false]}, {"push":[false]} ]});
236+
assert.deepEqual(i, [false,false]);
237+
i = [];
238+
jsonLogic.apply({"or":[ {"push":[false]}, {"push":[true]} ]});
239+
assert.deepEqual(i, [false,true]);
240+
i = [];
241+
jsonLogic.apply({"or":[ {"push":[true]}, {"push":[false]} ]});
242+
assert.deepEqual(i, [true]);
243+
i = [];
244+
jsonLogic.apply({"or":[ {"push":[true]}, {"push":[true]} ]});
245+
assert.deepEqual(i, [true]);
246+
247+
});

0 commit comments

Comments
 (0)