UNPKG

can-query-logic

Version:
375 lines (332 loc) 11.4 kB
var set = require("../set"); var assign = require("can-assign"); var arrayUnionIntersectionDifference = require("../array-union-intersection-difference"); var canReflect = require("can-reflect"); var canGet = require("can-key/get/get"); var canSymbol = require("can-symbol"); var canReflect = require("can-reflect"); var keysLogic = require("./types"); // Define the sub-types that BasicQuery will use function KeysAnd(values) { var vals = this.values = {}; canReflect.eachKey(values, function(value, key) { if (canReflect.isPlainObject(value) && !set.isSpecial(value)) { vals[key] = new KeysAnd(value); } else { vals[key] = value; } }); } var isMemberSymbol = canSymbol.for("can.isMember"); KeysAnd.prototype.isMember = function(props, root, rootKey) { var equal = true; var preKey = rootKey ? rootKey + "." : ""; canReflect.eachKey(this.values, function(value, key) { var isMember = value && (value[isMemberSymbol] || value.isMember); if (isMember) { if (!isMember.call(value, canGet(props, key), root || props, preKey + key)) { equal = false; } } else { if (value !== canGet(props, key)) { equal = false; } } }); return equal; }; // ====== DEFINE COMPARISONS ======== // Helpers ---------------------------- function checkIfUniversalAndReturnUniversal(setA) { return set.isEqual(setA, set.UNIVERSAL) ? set.UNIVERSAL : setA; } var MISSING = {}; function eachInUnique(a, acb, b, bcb, defaultReturn) { var bCopy = assign({}, b), res; for (var prop in a) { res = acb(prop, a[prop], (prop in b) ? b[prop] : MISSING, a, b); if (res !== undefined) { return res; } delete bCopy[prop]; } for (prop in bCopy) { res = bcb(prop, MISSING, b[prop], a, b); if (res !== undefined) { return res; } } return defaultReturn; } function keyDiff(valuesA, valuesB) { var keyResults = arrayUnionIntersectionDifference( Object.keys(valuesA), Object.keys(valuesB)); return { aOnlyKeys: keyResults.difference, aAndBKeys: keyResults.intersection, bOnlyKeys: arrayUnionIntersectionDifference( Object.keys(valuesB), Object.keys(valuesA)).difference }; } function notEmpty(value) { return value !== set.EMPTY; } // Difference of two ANDs is used two places function difference(objA, objB) { var valuesA = objA.values, valuesB = objB.values, diff = keyDiff(valuesA, valuesB), aOnlyKeys = diff.aOnlyKeys, aAndBKeys = diff.aAndBKeys, bOnlyKeys = diff.bOnlyKeys; // check if all aAndB are equal // With the shared keys, perform vA \ vB difference. If the DIFFERENCE is: // - EMPTY: vA has nothing outside vB. vA is equal or subset of vB. // - IF sB has keys not in sA, the shared keys will be part of the result; // OTHERWISE, if all empty, sA is subset of sB, EMPTY will be returned // (even if sA has some extra own keys) // - NON-EMPTY: something in sA that is not in sB // Now we need to figure out if it's "product-able" or not. // Product-able -> some part of B is in A. // Perform B ∩ A intersection. INTERSECTION is: // - EMPTY: NOT "product-able". DISJOINT. Must return something. // - non-EMPTY: Use to performa product (in the future.) var sharedKeysAndValues = {}, productAbleKeysAndData = {}, disjointKeysAndValues = {}; aAndBKeys.forEach(function(key) { var difference = set.difference(valuesA[key], valuesB[key]); if (difference === set.EMPTY) { sharedKeysAndValues[key] = valuesA[key]; } else { var intersection = set.intersection(valuesA[key], valuesB[key]); var isProductable = intersection !== set.EMPTY; if (isProductable) { productAbleKeysAndData[key] = { // Products with `difference U intersection` would be subtracted // from produts with `intersection` difference: difference, intersection: intersection }; } else { disjointKeysAndValues[key] = valuesA[key]; } } }); var productAbleKeys = Object.keys(productAbleKeysAndData); var singleProductKeyAndValue; if (productAbleKeys.length === 1) { singleProductKeyAndValue = {}; singleProductKeyAndValue[productAbleKeys[0]] = productAbleKeysAndData[productAbleKeys[0]].difference; } // Now that we've got the shared keys organized // we can make decisions based on this information // and A-only and B-only keys. // if we have any disjoint keys, these sets can not intersect // {age: 21, ...} \ {age: 22, ...} -> {age: 21, ...} if (Object.keys(disjointKeysAndValues).length) { return objA; } // contain all the same keys if ((aOnlyKeys.length === 0) && (bOnlyKeys.length === 0)) { if (productAbleKeys.length > 1) { return set.UNDEFINABLE; } // {color: [RED, GREEN], ...X...} \ {color: [RED], ...X...} -> {color: [GREEN], ...X...} else if (productAbleKeys.length === 1) { assign(sharedKeysAndValues, singleProductKeyAndValue); return new KeysAnd(sharedKeysAndValues); } else { // {...X...} \ {...X...} -> EMPTY return set.EMPTY; } } // sA is likely a subset of sB if (aOnlyKeys.length > 0 && bOnlyKeys.length === 0) { if (productAbleKeys.length > 1) { return set.UNDEFINABLE; } // {age: 35, color: [RED, GREEN], ...X...} \ {color: [RED], ...X...} -> {age: 35, color: [GREEN], ...X...} else if (productAbleKeys.length === 1) { assign(sharedKeysAndValues, singleProductKeyAndValue); aOnlyKeys.forEach(function(key) { sharedKeysAndValues[key] = valuesA[key]; }); return new KeysAnd(sharedKeysAndValues); } else { // sharedKeysAndValues return set.EMPTY; } } // sB is likely subset of sA // {}, {foo: "bar"} -> {foo: NOT("bar")} if (aOnlyKeys.length === 0 && bOnlyKeys.length > 0) { // Lets not figure out productAbleKeys right now. // Example: // {color: [RED, GREEN], ...X...} // \ {age: 35, color: [RED], ...X...} // = OR( {color: [GREEN], ...X...}, {age: NOT(35), color: [RED], ...X...} ) if (productAbleKeys.length > 1) { return set.UNDEFINABLE; } var productAbleOr; if (productAbleKeys.length === 1) { // we add the intersection to the AND // the difference is the or var productableKey = productAbleKeys[0]; productAbleOr = assign({}, sharedKeysAndValues); productAbleOr[productableKey] = productAbleKeysAndData[productableKey].difference; sharedKeysAndValues[productableKey] = productAbleKeysAndData[productableKey].intersection; } var ands = bOnlyKeys.map(function(key) { var shared = assign({}, sharedKeysAndValues); var result = shared[key] = set.difference(set.UNIVERSAL, valuesB[key]); return result === set.EMPTY ? result : new KeysAnd(shared); }).filter(notEmpty); if (productAbleOr) { ands.push(new KeysAnd(productAbleOr)); } // {c: "g"} // \ {c: "g", age: 22, name: "justin"} // = OR[ AND(name: NOT("justin"), c:"g"), AND(age: NOT(22), c: "g") ] if (ands.length > 1) { return new keysLogic.ValuesOr(ands); } else if (ands.length === 1) { // {c: "g"} // \ {c: "g", age: 22} // = AND(age: NOT(22), c: "g") return ands[0]; } else { return set.EMPTY; } } // {name: "Justin"} \\ {age: 35} -> {name: "Justin", age: NOT(35)} if (aOnlyKeys.length > 0 && bOnlyKeys.length > 0) { if (productAbleKeys.length) { throw new Error("Can't handle any productable keys right now"); } // add everything in sA into the result: aOnlyKeys.forEach(function(key) { sharedKeysAndValues[key] = valuesA[key]; }); if (bOnlyKeys.length === 1) { // TODO: de-duplicate below var key = bOnlyKeys[0]; var shared = assign({}, sharedKeysAndValues); shared[key] = set.difference(set.UNIVERSAL, valuesB[key]); return new KeysAnd(shared); } // {foo: "bar"} \\ {name: "Justin", age: 35} -> UNDEFINABLE else { return set.UNDEFINABLE; } } } // KeysAnd comaprisons set.defineComparison(KeysAnd, KeysAnd, { // {name: "Justin"} or {age: 35} -> new OR[{name: "Justin"},{age: 35}] // {age: 2} or {age: 3} -> {age: new OR[2,3]} // {age: 3, name: "Justin"} OR {age: 4} -> {age: 3, name: "Justin"} OR {age: 4} union: function(objA, objB) { // first see if we can union a single property // {age: 21, color: ["R"]} U {age: 21, color: ["B"]} -> {age: 21, color: ["R","B"]} var diff = keyDiff(objA.values, objB.values); // find the different keys var aAndBKeysThatAreNotEqual = [], sameKeys = {}; diff.aAndBKeys.forEach(function(key) { if (!set.isEqual(objA.values[key], objB.values[key])) { aAndBKeysThatAreNotEqual.push(key); } else { sameKeys[key] = objA.values[key]; } }); var aUnequal = {}, bUnequal = {}; aAndBKeysThatAreNotEqual.forEach(function(key){ aUnequal[key] = objA.values[key]; bUnequal[key] = objB.values[key]; }); // if all keys are shared if (!diff.aOnlyKeys.length && !diff.bOnlyKeys.length) { if (aAndBKeysThatAreNotEqual.length === 1) { var keyValue = aAndBKeysThatAreNotEqual[0]; var result = sameKeys[keyValue] = set.union(objA.values[keyValue], objB.values[keyValue]); // if there is only one property, we can just return the universal set return canReflect.size(sameKeys) === 1 && set.isEqual(result, set.UNIVERSAL) ? set.UNIVERSAL : new KeysAnd(sameKeys); } else if (aAndBKeysThatAreNotEqual.length === 0) { // these things are equal return objA; } } // If everything shared is the same if (aAndBKeysThatAreNotEqual.length === 0) { // the set with the extra keys is a subset if (diff.aOnlyKeys.length > 0 && diff.bOnlyKeys.length === 0) { return checkIfUniversalAndReturnUniversal(objB); } else if (diff.aOnlyKeys.length === 0 && diff.bOnlyKeys.length > 0) { return checkIfUniversalAndReturnUniversal(objA); } } // (count > 5 && age > 25 ) || (count > 7 && age > 35 && name > "Justin" ) // // ( age > 25 ) || ( name > "Justin" && age > 35) A U (B & C) => (A U B) & (A U C) // ( age > 25 || name > "Justin" ) && (age > 25) // lets see if one side is different if (diff.aOnlyKeys.length > 0 && diff.bOnlyKeys.length === 0) { // collect shared value if( set.isSubset(new KeysAnd(aUnequal), new KeysAnd(bUnequal) )) { return objB; } } if (diff.bOnlyKeys.length > 0 && diff.aOnlyKeys.length === 0) { // collect shared value if( set.isSubset(new KeysAnd(bUnequal), new KeysAnd(aUnequal) )) { return objA; } } return new keysLogic.ValuesOr([objA, objB]); }, // {foo: zed, abc: d} intersection: function(objA, objB) { // combine all properties ... if the same property, try to take // an intersection ... if an intersection isn't possible ... freak out? var valuesA = objA.values, valuesB = objB.values, foundEmpty = false; var resultValues = {}; eachInUnique(valuesA, function(prop, aVal, bVal) { resultValues[prop] = bVal === MISSING ? aVal : set.intersection(aVal, bVal); if (resultValues[prop] === set.EMPTY) { foundEmpty = true; } }, valuesB, function(prop, aVal, bVal) { resultValues[prop] = bVal; if (resultValues[prop] === set.EMPTY) { foundEmpty = true; } }); if (foundEmpty) { return set.EMPTY; } else { return new KeysAnd(resultValues); } }, // A \ B -> what's in A, but not in B difference: difference }); set.defineComparison(set.UNIVERSAL, KeysAnd, { // A \ B -> what's in A, but not in B difference: function(universe, and) { return difference({ values: {} }, and); } }); module.exports = keysLogic.KeysAnd = KeysAnd;