Skip to content

Commit

Permalink
Adding new helpers, so the default import is an object, not a single …
Browse files Browse the repository at this point in the history
…function. Working on complex conditionals.
  • Loading branch information
jwadhams committed Feb 25, 2016
1 parent 7f3db14 commit 8c0037c
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 77 deletions.
2 changes: 1 addition & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ gulp.task('test', function(cb) {
});

gulp.task('watch', function() {
gulp.watch('**/*.js', ['test']);
gulp.watch(['**/*.js', 'tests/tests.json'], ['test']);
});
237 changes: 169 additions & 68 deletions logic.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,192 @@
(function(global) {
/*
Using a Universal Module Loader that should be browser, require, and AMD friendly
http://ricostacruz.com/cheatsheets/umdjs.html
*/
;(function (root, factory) {

if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.jsonLogic = factory();
}

}(this, function () {
'use strict';
/*globals console:false */

if (!Array.isArray) {
if ( ! Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

global.jsonLogic = function(tests, data){
if( ! Array.unique){
Array.prototype.unique = function() {
var a = [];
for (var i=0, l=this.length; i<l; i++){
if (a.indexOf(this[i]) === -1){
a.push(this[i]);
}
}
return a;
};
}

var jsonLogic = {},
operations = {
"==" : function(a,b){ return a == b; },
"===" : function(a,b){ return a === b; },
"!=" : function(a,b){ return a != b; },
"!==" : function(a,b){ return a !== b; },
">" : function(a,b){ return a > b; },
">=" : function(a,b){ return a >= b; },
"<" : function(a,b,c){
return (c === undefined) ? a < b : (a < b) && (b < c);
},
"<=" : function(a,b,c){
return (c === undefined) ? a <= b : (a <= b) && (b <= c);
},
"!" : function(a){ return !a; },
"%" : function(a,b){ return a % b; },
"and" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; });
},
"or" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){ return a || b; });
},
"log" : function(a){ console.log(a); return a; },
"in" : function(a, b){
if(typeof b.indexOf === 'undefined') return false;
return (b.indexOf(a) !== -1);
},
"cat" : function(){
return Array.prototype.join.call(arguments, "");
},
"+" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){
return parseFloat(a,10) + parseFloat(b, 10);
}, 0);
},
"*" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){
return parseFloat(a,10) * parseFloat(b, 10);
});
},
"-" : function(a,b){ if(b === undefined){return -a;}else{return a - b;} },
"/" : function(a,b){ if(b === undefined){return a;}else{return a / b;} },
"min" : function(){ return Math.min.apply(this,arguments); },
"max" : function(){ return Math.max.apply(this,arguments); }
};

jsonLogic.is_logic = function(logic){
return (logic !== null && typeof logic === "object" && ! Array.isArray(logic) );
};

/*
This helper will defer to the JsonLogic spec as a tie-breaker when different language interpreters define different behavior for the truthiness of primitives. E.g., PHP considers empty arrays to be falsy, but Javascript considers them to be truthy. JsonLogic, as an ecosystem, needs one consistent answer.
Literal | JS | PHP | JsonLogic
--------+-------+-------+---------------
[] | true | false | false
"0" | true | false | true
*/
jsonLogic.truthy = function(value){
if(Array.isArray(value) && value.length === 0){ return false; }
return !! value;
};

jsonLogic.apply = function(logic, data){
//You've recursed to a primitive, stop!
if(tests === null || typeof tests !== "object" || Array.isArray(tests) ){
return tests;
if( ! jsonLogic.is_logic(logic) ){
return logic;
}

data = data || {};

var op = Object.keys(tests)[0],
values = tests[op],
operations = {
"==" : function(a,b){ return a == b; },
"===" : function(a,b){ return a === b; },
"!=" : function(a,b){ return a != b; },
"!==" : function(a,b){ return a !== b; },
">" : function(a,b){ return a > b; },
">=" : function(a,b){ return a >= b; },
"<" : function(a,b,c){
return (c === undefined) ? a < b : (a < b) && (b < c);
},
"<=" : function(a,b,c){
return (c === undefined) ? a <= b : (a <= b) && (b <= c);
},
"!" : function(a){ return !a; },
"%" : function(a,b){ return a % b; },
"and" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){ return a && b; });
},
"or" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){ return a || b; });
},
"?:" : function(a,b,c){ return a ? b : c; },
"log" : function(a){ console.log(a); return a; },
"in" : function(a, b){
if(typeof b.indexOf === 'undefined') return false;
return (b.indexOf(a) !== -1);
},
"var" : function(a, not_found){
if(not_found === undefined) not_found = null;
var sub_props = String(a).split(".");
for(var i = 0 ; i < sub_props.length ; i++){
//Descending into data
data = data[ sub_props[i] ];
if(data === undefined){ return not_found; }
}
return data;
},
"cat" : function(){
return Array.prototype.join.call(arguments, "");
},
"+" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){
return parseFloat(a,10) + parseFloat(b, 10);
});
},
"*" : function(){
return Array.prototype.reduce.call(arguments, function(a,b){
return parseFloat(a,10) * parseFloat(b, 10);
});
},
"-" : function(a,b){ if(b === undefined){return -a;}else{return a - b;} },
"/" : function(a,b){ if(b === undefined){return a;}else{return a / b;} },
"min" : function(){ return Math.min.apply(this,arguments); },
"max" : function(){ return Math.max.apply(this,arguments); }
};
var op = Object.keys(logic)[0],
values = logic[op];

//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(op === 'if' || op == '?:'){
/* 'if' should be called with a odd number of parameters, 3 or greater
This works on the pattern:
if( 0 ){ 1 }else{ 2 };
if( 0 ){ 1 }else if( 2 ){ 3 }else{ 4 };
if( 0 ){ 1 }else if( 2 ){ 3 }else if( 4 ){ 5 }else{ 6 };
The implementation is:
given two+ parameters,
shift off the first two values.
If the first evaluates truthy, evaluate and return the second
If the first evaluates falsy, start again with the remaining parameters.
given one parameter, evaluate and return it.
given 0 parameters, return NULL
*/
while(values.length >= 2){
var conditional = jsonLogic.apply(values.shift(), data),
consequent = values.shift();

if( jsonLogic.truthy(conditional) ){
return jsonLogic.apply(consequent, data);
}
}

if(values.length === 1) return jsonLogic.apply(values[0], data);
return null;
}


// Everyone else gets immediate depth-first recursion
values = values.map(function(val){ return jsonLogic.apply(val, data); });

// 'var' needs access to data, only available in this scope
if(op === "var"){
var not_found = values[1] || null,
sub_props = String(values[0]).split(".");
for(var i = 0 ; i < sub_props.length ; i++){
//Descending into data
data = data[ sub_props[i] ];
if(data === undefined){ return not_found; }
}
return data;
}

if(undefined === operations[op]){
throw new Error("Unrecognized operation " + op );
}

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

//Recursion!
values = values.map(function(val){ return jsonLogic(val, data); });
};

return operations[op].apply({}, values);
jsonLogic.uses_data = function(logic){
var collection = [];

if( jsonLogic.is_logic(logic) ){
var op = Object.keys(logic)[0],
values = logic[op];

if( ! Array.isArray(values)){ values = [values]; }

if(op === "var"){
//This doesn't cover the case where the arg to var is itself a rule.
collection.push(values[0]);
}else{
//Recursion!
values.map(function(val){
collection.push.apply(collection, jsonLogic.uses_data(val) );
});
}
}

return collection.unique();
};

}(this));
return jsonLogic;

}));
2 changes: 1 addition & 1 deletion play.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ <h1>Test JsonLogic in your Browser</h1>
data = $("#data").val() === "" ? null : JSON.parse($("#data").val()),
output;
try{
output = jsonLogic(rule, data);
output = jsonLogic.apply(rule, data);
console.log(output);
$("#message-compute").fadeOut();
$("#output").text( JSON.stringify(output, null, 4) );
Expand Down
10 changes: 3 additions & 7 deletions tests/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,21 @@ QUnit.test( "Shared JsonLogic.com tests ", function( assert ){
//Only waiting on the request() is async
stop();

/*
var fs = require('fs');
fs.readFile('tests.json', 'utf8', function (error, body) {
var response = { statusCode : 200 };
*/
/*
var request = require('request');
request('http://jsonlogic.com/tests.json', function (error, response, body) {
if (error || response.statusCode != 200) {
console.log("Failed to load tests from JsonLogic.com:", error, response.statusCode);
start();
return;
}

*/
try{
tests = JSON.parse(body);
}catch(e){
console.log("Trouble parsing shared test: ", body);
start();
return;
throw new Error("Trouble parsing shared test: " + e.message);
}

console.log("Including "+tests.length+" shared tests from JsonLogic.com");
Expand Down
65 changes: 65 additions & 0 deletions tests/tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
[
"Too few args",
[{"if":[]}, null, null],
[{"if":[true]}, null, true],
[{"if":[false]}, null, false],
[{"if":["apple"]}, null, "apple"],

"Simple if/then/else cases",
[{"if":[true, "apple"]}, null, "apple"],
[{"if":[false, "apple"]}, null, null],
[{"if":[true, "apple", "banana"]}, null, "apple"],
[{"if":[false, "apple", "banana"]}, null, "banana"],

"Empty arrays are falsey",
[{"if":[ [], "apple", "banana"]}, null, "banana"],
[{"if":[ [1], "apple", "banana"]}, null, "apple"],
[{"if":[ [1,2,3,4], "apple", "banana"]}, null, "apple"],

"Empty strings are falsey, all other strings are truthy",
[{"if":[ "", "apple", "banana"]}, null, "banana"],
[{"if":[ "zucchini", "apple", "banana"]}, null, "apple"],
[{"if":[ "0", "apple", "banana"]}, null, "apple"],

"You can cast a string to numeric with a unary + ",
[{"===":[0,"0"]}, null, false],
[{"===":[0,{"+":"0"}]}, null, true],
[{"if":[ {"+":"0"}, "apple", "banana"]}, null, "banana"],
[{"if":[ {"+":"1"}, "apple", "banana"]}, null, "apple"],

"Zero is falsy, all other numbers are truthy",
[{"if":[ 0, "apple", "banana"]}, null, "banana"],
[{"if":[ 1, "apple", "banana"]}, null, "apple"],
[{"if":[ 3.1416, "apple", "banana"]}, null, "apple"],
[{"if":[ -1, "apple", "banana"]}, null, "apple"],

"If the conditional is logic, it gets evaluated",
[{"if":[ {">":[2,1]}, "apple", "banana"]}, null, "apple"],
[{"if":[ {">":[1,2]}, "apple", "banana"]}, null, "banana"],

"If the consequents are logic, they get evaluated",
[{"if":[ true, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "apple"],
[{"if":[ false, {"cat":["ap","ple"]}, {"cat":["ba","na","na"]} ]}, null, "banana"],

"If/then/elseif/then cases",
[{"if":[true, "apple", true, "banana"]}, null, "apple"],
[{"if":[true, "apple", false, "banana"]}, null, "apple"],
[{"if":[false, "apple", true, "banana"]}, null, "banana"],
[{"if":[false, "apple", false, "banana"]}, null, null],

[{"if":[true, "apple", true, "banana", "carrot"]}, null, "apple"],
[{"if":[true, "apple", false, "banana", "carrot"]}, null, "apple"],
[{"if":[false, "apple", true, "banana", "carrot"]}, null, "banana"],
[{"if":[false, "apple", false, "banana", "carrot"]}, null, "carrot"],

[{"if":[false, "apple", false, "banana", false, "carrot"]}, null, null],
[{"if":[false, "apple", false, "banana", false, "carrot", "date"]}, null, "date"],
[{"if":[false, "apple", false, "banana", true, "carrot", "date"]}, null, "carrot"],
[{"if":[false, "apple", true, "banana", false, "carrot", "date"]}, null, "banana"],
[{"if":[false, "apple", true, "banana", true, "carrot", "date"]}, null, "banana"],
[{"if":[true, "apple", false, "banana", false, "carrot", "date"]}, null, "apple"],
[{"if":[true, "apple", false, "banana", true, "carrot", "date"]}, null, "apple"],
[{"if":[true, "apple", true, "banana", false, "carrot", "date"]}, null, "apple"],
[{"if":[true, "apple", true, "banana", true, "carrot", "date"]}, null, "apple"]

]

0 comments on commit 8c0037c

Please sign in to comment.