UNPKG

spahql

Version:

A query language and data model for deep Javascript object structures.

528 lines (485 loc) 19.9 kB
/** * class SpahQL.DataHelper * * This is a singleton helper dedicated to deep-merging complex JSON structures and returning both * the merged data and a digest of modified paths within the structure. **/ // Dependencies SpahQL_classCreate("SpahQL.DataHelper", { /** * SpahQL.DataHelper.compare(original, delta[, atPath]) -> Object * * Compares two objects at the given path (defaulting to "/") and returns a hash of differences between the two, * keyed by path. The hash has array values, each including a SpahQL modification symbol, the original data value * and the delta data value. **/ "compare": function(original, delta, atPath) { var pathStack = (atPath=="/")? "" : atPath; var modifications = {}; var oType = this.objectType(original); var dType = this.objectType(delta); var oIsSimple = (oType != "object" && oType != "array"); var dIsSimple = (dType != "object" && dType != "array"); // Escale if(this.eq(original, delta)) { // Items are equivalent return modifications; } else if(oIsSimple && dIsSimple) { // Compare simple -> simple var m = this.modificationSymbol(delta, original); if(m) modifications[atPath] = [m, original, delta]; } if(!dIsSimple) { // New value is complex - we'll run all subkeys against the keys on the original, if they exist for(var dK in delta) { // For each key, get modifications for this tree level and merge. var dMods = this.compare(((oIsSimple)? undefined : original[dK]), delta[dK], pathStack+"/"+dK); for(mK in dMods) { modifications[mK] = dMods[mK]; } } } if(!oIsSimple) { // Original value is complex - we'll run all subkeys against keys on the delta, if they exist // All keys in complex are nullified recursively for(var oK in original) { var oMods = this.compare(original[oK], ((dIsSimple)? undefined : delta[oK]), pathStack+"/"+oK); for(var mK in oMods) { modifications[mK] = oMods[mK]; } } } // Register working path as modified if we got this far and didn't register anything for the mod path. if(!modifications[atPath]) { var mSym = this.modificationSymbol(delta, original); if(mSym) modifications[atPath] = [mSym, original, delta]; } return modifications; }, /** * SpahQL.DataHelper.modificationSymbol(delta, target) -> String symbol * * Determines whether the change between two objects, assuming content inequality, is a "+" (addition), "-" (nullification) or "~" (alteration). **/ "modificationSymbol": function(delta, target) { if(this.objectType(target) == "null") return "+"; else if(this.objectType(delta) == "null") return "-"; else if(delta != target) return "~"; }, /** * SpahQL.DataHelper.objectType(obj) -> String type * * Extends the core typeof(obj) function by adding types "array" and "null". **/ "objectType": function(obj) { if(obj == null || obj == undefined) return "null"; if(typeof(obj) == "object") { return (Object.prototype.toString.call(obj) == "[object Array]") ? "array" : "object"; } else { return typeof(obj); } }, /** * SpahQL.DataHelper.eq(obj1, obj2[, objN]) -> Boolean equality result * * Determines content equality of two or more objects. Booleans, null values, numbers and strings are compared using * the <code>SpahQL.DataHelper.objectType</code> method and the built-in <code>==</code> operator, but arrays * and hashes are traversed recursively and have their values compared. **/ "eq": function() { var aP, aI, aT; aP = arguments[0]; for(aI=1; aI<arguments.length; aI++) { var a=arguments[aI]; // Determine a and aP equal var t = this.objectType(aP); if(t != this.objectType(a)) return false; if(t == "array") { if(a.length != aP.length) return false; for(var i=0; i<a.length; i++) { if(!this.eq(a[i], aP[i])) return false; } } else if(t == "object") { if(Object.keys(a).length != Object.keys(aP).length) return false; for(var k in a) { if(!this.eq(a[k], aP[k])) return false; } } else if(a != aP) { return false; } aP = a; } return true; }, /** * SpahQL.DataHelper.hashKeys(hash) -> Array of keys * - hash (Object): The hash being exploded * * Retrieves all keys found in an associative array and returns them as an array * without the corresponding values. **/ "hashKeys": function(hash) { var keys = Object.keys(hash) return keys.sort(); }, /** * SpahQL.DataHelper.hashValues(hash) -> Array of values * - hash (Object): The hash being exploded * * Retrieves all values found in an associative array and returns them as an array * without keys. Uniqueness is not enforced. **/ "hashValues": function(hash) { var a = []; for(var k in hash) a.push(hash[k]); return a; }, /** * SpahQL.DataHelper.mathGte(left, right) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * * Compares two objects of any type under the rules of Spah comparisons, returning true if the left value is * greater than or equal to to the right value. **/ "mathGte": function(left, right) { return this.mathCompare(left, right, function(a,b) { return a >= b; }); }, /** * SpahQL.DataHelper.mathGt(left, right) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * * Compares two objects of any type under the rules of Spah comparisons, returning true if the left value is * greater than to the right value. **/ "mathGt": function(left, right) { return this.mathCompare(left, right, function(a,b) { return a > b; }); }, /** * SpahQL.DataHelper.mathLte(left, right) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * * Compares two objects of any type under the rules of Spah comparisons, returning true if the left value is * less than or equal to the right value. **/ "mathLte": function(left, right) { return this.mathCompare(left, right, function(a,b) { return a <= b; }); }, /** * SpahQL.DataHelper.mathLt(left, right) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * * Compares two objects of any type under the rules of Spah comparisons, returning true if the left value is * less than the right value. **/ "mathLt": function(left, right) { return this.mathCompare(left, right, function(a,b) { return a < b; }); }, /** * SpahQL.DataHelper.mathCompare(left, right, callback) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * - callback (Function): A callback function which will be evaluating the mathematical comparison. * * Compares two objects of any type under the rules of Spah comparisons - both objects must be the same type, * and no type coercion will take place. The given callback function should accept two values as an argument and return the comparison result. * * Mostly used as a refactoring tool to wrap the global math comparison rules. **/ "mathCompare": function(left, right, callback) { var leftType = this.objectType(left); var rightType = this.objectType(right); if(leftType == rightType && (leftType == "number" || leftType == "string")) { return callback.apply(this, [left, right]); } return false; }, /** * SpahQL.DataHelper.eqRough(left, right) -> Boolean result * - left (Object, Array, Boolean, String, Number, null): The value at the left-hand side of the comparator * - right (Object, Array, Boolean, String, Number, null): The value at the right-hand side of the comparator * * Compares two objects under the rules of rough equality. See readme for details. **/ "eqRough": function(left, right) { var leftType = this.objectType(left); var rightType = this.objectType(right); if(leftType != rightType) { return false; } else { switch(leftType) { case "string": return this.eqStringRough(left, right); break; case "number": return this.eqNumberRough(left, right); break; case "array": return this.eqArrayRough(left, right); break; case "object": return this.eqHashRough(left, right); break; case "boolean": return this.eqBooleanRough(left, right); break; default: return false; } } }, /** * SpahQL.DataHelper.eqStringRough(left, right) -> Boolean result * - left (String): The value at the left-hand side of the comparator * - right (String): The value at the right-hand side of the comparator * * Compares two strings under the rules of rough equality. The right-hand value is parsed as a RegExp * and a result of true is returned if the left value matches it. **/ "eqStringRough": function(left, right) { return (left.match(new RegExp(right, "g"))); }, /** * SpahQL.DataHelper.eqNumberRough(left, right) -> Boolean result * - left (Number): The value at the left-hand side of the comparator * - right (Number): The value at the right-hand side of the comparator * * Compares two numbers for equality using integer accuracy only. **/ "eqNumberRough": function(left, right) { return (Math.floor(left) == Math.floor(right)); }, /** * SpahQL.DataHelper.eqArrayRough(left, right) -> Boolean result * - left (Array): The value at the left-hand side of the comparator * - right (Array): The value at the right-hand side of the comparator * * Compares two arrays under the rules of rough equality. A result of true * is returned if the arrays are joint sets, containing any two equal values. **/ "eqArrayRough": function(left, right) { return this.jointSet(left, right); }, /** * SpahQL.DataHelper.eqHashRough(left, right) -> Boolean result * - left (Object): The value at the left-hand side of the comparator * - right (Object): The value at the right-hand side of the comparator * * Compares two objects under the rules of rough equality. A result of true * is returned if the hashes are joint sets, containing any two equal values at the same key. **/ "eqHashRough": function(left, right) { for(var k in left) { if(right[k] && this.eq(left[k], right[k])) return true; } return false; }, /** * SpahQL.DataHelper.eqBooleanRough(left, right) -> Boolean result * - left (Boolean, null): The value at the left-hand side of the comparator * - right (Boolean, null): The value at the right-hand side of the comparator * * Compares two boolean-type objects under the rules of rough equality. A result of true * is returned if both values are truthy or if both values evaluate to false. **/ "eqBooleanRough": function(left, right) { return ((left && right) || (!left && !right)); }, /** * SpahQL.DataHelper.eqSetStrict(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets under the rules of strict equality. A result of true * is returned if both sets have a 1:1 relationship of values. The values * do not have to appear in the same order. **/ "eqSetStrict": function(set1, set2) { if(set1.length != set2.length) return false; var foundIndexMap = []; for(var i=0; i < set1.length; i++) { var val = set1[i]; for(var j=0; j < set2.length; j++) { // Search for equality values in the second set var candidate = set2[j]; if(this.eq(val, candidate) && (foundIndexMap.indexOf(j) < 0)) { foundIndexMap.push(j); } } } return (foundIndexMap.length == set1.length); }, /** * SpahQL.DataHelper.eqSetRough(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets under the rules of rough equality. A result of true * is returned if any value in the left-hand set is roughly equal to any * value in the right-hand set. **/ "eqSetRough": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.eqRough(a,b); }); }, /** * SpahQL.DataHelper.jointSet(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets and returns a result of true if any value in the left-hand * set is strictly equal to any value from the right-hand set. **/ "jointSet": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.eq(a,b); }); }, /** * SpahQL.DataHelper.gteSet(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets and returns a result of true if any value in the left-hand * set is greater than or equal to any value from the right-hand set. **/ "gteSet": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.mathGte(a,b) }); }, /** * SpahQL.DataHelper.lteSet(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets and returns a result of true if any value in the left-hand * set is less than or equal to any value from the right-hand set. **/ "lteSet": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.mathLte(a,b) }); }, /** * SpahQL.DataHelper.gtSet(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets and returns a result of true if any value in the left-hand * set is greater than any value from the right-hand set. **/ "gtSet": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.mathGt(a,b) }); }, /** * SpahQL.DataHelper.ltSet(set1, set2) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * * Compares two sets and returns a result of true if any value in the left-hand * set is less than any value from the right-hand set. **/ "ltSet": function(set1, set2) { return this.jointSetWithCallback(set1, set2, function(a,b) { return this.mathLt(a,b) }); }, /** * SpahQL.DataHelper.jointSetWithCallback(set1, set2, callback) -> Boolean result * - set1 (Array): The value at the left-hand side of the comparator * - set2 (Array): The value at the right-hand side of the comparator * - callback (Function): A function to be used for comparing the values. Should accept two values as arguments. * * Iterates over both sets such that every combination of values from the two is passed to the callback function * for comparison. If the callback function at any point returns true, the method exits and returns true. Once * all combinations have been exhausted and no matches are found, false will be returned. * * Mostly used to refactor the various joint set methods (jointSet, eqSetRough, gteSet, gtSet, ltSet, lteSet to name a few). **/ "jointSetWithCallback": function(set1, set2, callback) { for(var i=0; i < set2.length; i++) { for(var j=0; j < set1.length; j++) { if(callback.apply(this, [set1[j], set2[i]])) return true; } } return false; }, /** * SpahQL.DataHelper.superSet(superset, subset) -> Boolean result * - superset (Array): The value being asserted as a superset of the 'subset' argument. * - subset (Array): The value being asserted as a subset of the 'superset' argument. * * Compares two sets and returns a result of true if every value in the given subset * has a corresponding equal in the superset. Order of values within the sets is not enforced. **/ "superSet": function(superset, subset) { var foundIndexMap = []; isubset: for(var i=0; i < subset.length; i++) { var subVal = subset[i]; isuperset: for(var j=0; j < superset.length; j++) { var superVal = superset[j]; if((foundIndexMap.indexOf(j) == -1) && this.eq(subVal, superVal)) { foundIndexMap.push(j); break isuperset; } } } return (subset.length == foundIndexMap.length); }, /** * SpahQL.DataHelper.truthySet(set) -> Boolean result * - set (Array): The value being asserted as a "truthy" set. * * Asserts that a set may be considered "truthy", i.e. containing one or more * values that evaluate to true under javascript language rules. **/ "truthySet": function(set) { for(var i=0; i < set.length; i++) { if(set[i]) return true; } return false; }, /** * SpahQL.DataHelper.coerceKeyForObject(key, obj) -> String, Integer, null * - key (Integer, String): The key to be coerced. * - obj (Object): The value being inspected * * When you want to set a key on an anonymous object, you'll want to coerce the * key to the correct type - numbers for arrays, strings for objects. This function does that. * Returns null if the key can't be coerced for the given object. **/ "coerceKeyForObject": function(key, obj) { var t = this.objectType(obj); if(t == "array") { var k = parseInt(key); return isNaN(k)? null : k; } else if(t == "object") { var k = key.toString(); return (k.match(/^\s*$/))? null : k; } return null; }, /** * SpahQL.DataHelper.deepClone(obj) -> Object * obj (Array, Object): The object to be cloned * * Creates a deep clone of an object or array. All nested objects and arrays * are also deep-cloned. Strings, booleans and numbers are returned as-is, because * all in-place modifications to strings and numbers produce new object assignments * anyway. **/ "deepClone": function(obj) { var objType = this.objectType(obj); if(objType == "array" || objType == "object") { var clone = (objType == "array")? [] : {}; for(var key in obj) { var val = obj[key]; clone[key] = this.deepClone(val); } return clone; } else { return obj; } } });