Created
August 30, 2012 12:43
-
-
Save mattparker/3527740 to your computer and use it in GitHub Desktop.
YUI Y.Array set methods (intersect, diff, union)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
YUI.add("arraySet", function (Y) { | |
"use strict"; | |
var A = Y.Array, | |
L = Y.Lang, | |
iO = A.indexOf; | |
/** | |
* Intersection of two or more arrays. | |
* | |
* @method intersect | |
* @param {Array} First array (or array-like thing - is passed through Y.Array() first. | |
* @param {Array} Second array (or array-like thing - is passed through Y.Array() first. | |
* @param {Function} Comparison function (optional). If not passed, | |
* will use indexOf tests (which should do strict === comparison). If | |
* a function is passed, it will be used to compare the items in the | |
* arrays. This function will receive two arguments: the first | |
* will be an item from the first array, the second will be an item | |
* from the second array. This might be useful for arrays of objects | |
* for example. Note that the return value may contain duplicate values - you | |
* will need to use Y.Array.unique(returnedArray, comparisonFunction). This | |
* is because there's no prior reason that the comparison function used | |
* to determine the intersection should also be used as the equality function | |
* when determining uniqueness of values (although you're into the edge cases | |
* where they're not). | |
* | |
* @return {Array} Array of items that appear in both original arrays. | |
*/ | |
A.intersect = function (arr1, arr2, comparison) { | |
var i = 0, | |
args, | |
a1, | |
a2, | |
len1 = 0, | |
ret = [], | |
fn; | |
if (comparison && L.isFunction(comparison)) { | |
fn = function (item) { | |
var i; | |
for (i = 0; i < a2.length; i = i + 1) { | |
if (comparison(item, a2[i])) { | |
ret.push(item, a2[i]); | |
} | |
} | |
}; | |
} else { | |
fn = function (item) { | |
if (iO(a2, item) !== -1) { | |
ret.push(item); | |
} | |
}; | |
} | |
a1 = new A(arr1); | |
a2 = new A(arr2); | |
len1 = a1.length; | |
args = new A(arguments); | |
// we've got more than two arrays: | |
if ((args.length === 3 && !L.isFunction(comparison)) || | |
(args.length > 3)) { | |
// intersection of the first two: | |
if (L.isFunction(args[args.length - 1])) { | |
ret = A.intersect(a1, a2, args[args.length - 1]); | |
} else { | |
ret = A.intersect(a1, a2); | |
} | |
// replace the first two with their intersection | |
args.splice(0, 2, ret); | |
// rinse and repeat | |
return A.intersect.apply(this, args); | |
} | |
for (i = 0; i < len1; i = i + 1) { | |
fn(a1[i]); | |
} | |
return ret; | |
}; | |
/** | |
* Returns union of two or more arrays, with duplicates removed | |
* | |
* @method union | |
* @param {Array} | |
* @param {Array} | |
* ... (you can pass as many arrays as you like. Each argument | |
* will be passed through Y.Array so conversions will happen | |
* if possible; if not will be skipped. | |
* @param {Function} Comparison function to be used in call to unique (optional) | |
* @return {Array} | |
*/ | |
A.union = function (arr1, arr2, comparison) { | |
var args = new A(arguments), | |
a1 = new A(arr1), | |
a2 = new A(arr2), | |
a3; | |
// more than 2: do the first two and then recurse. | |
if (args.length > 2 && !L.isFunction(comparison)) { | |
a3 = A.union(arr1, arr2); | |
args.splice(0, 2, a3); | |
return A.union.apply(this, args); | |
} | |
// set up arguments for call to apply: | |
a2.unshift(a1.length, 0); | |
a1.splice.apply(a1, a2); | |
return A.unique(a1, comparison); | |
}; | |
/** | |
* Calculates the difference between two arrays: that is, | |
* the values in arr1 that are not contained in arr2. | |
* | |
* @method diff | |
* @param {Array} arr1 Or array-like something - passed through Y.Array first | |
* @param {Array} arr2 Or array-like something - passed through Y.Array first | |
* @param {Function} comparison To tell if two items are equal. | |
* The function will receive two arguments: the first is the value from | |
* the first array, the second is a value from the second array to compare to. | |
* The comparison function is optional. If the function returns true | |
* they are the same. | |
* @return {Array} | |
*/ | |
A.diff = function (arr1, arr2, comparison) { | |
var i = 0, | |
ret = [], | |
a1, | |
a2, | |
cb, | |
fn = (comparison && L.isFunction(comparison)) ? | |
comparison : | |
false; | |
a1 = new A(arr1); | |
a2 = new A(arr2); | |
if (fn) { | |
cb = function (a) { return fn(a1[i], a); }; | |
for (i = 0; i < a1.length; i = i + 1) { | |
if (A.find(a2, cb) === null) { | |
ret.push(a1[i]); | |
} | |
} | |
} else { | |
for (i = 0; i < a1.length; i = i + 1) { | |
if (iO(a2, a1[i]) === -1) { | |
ret.push(a1[i]); | |
} | |
} | |
} | |
return ret; | |
}; | |
}, "0.1", {requires: ["array"]}); |
Here are the tests for the above. It also passes jslint and has had some thought about performance given to it (though not claiming perfection!).
YUI.add("arraySet-tests", function (Y) {
var ASSERT = Y.Assert,
ARRAYASSERT = Y.ArrayAssert;
var testSimpleIntersects = new Y.Test.Case({
name: "Test of simple arrays of numbers, strings",
testNumbers: function () {
var a1 = [1,2,3,4,5],
a2 = [3,4,5,6],
answer = Y.Array.intersect(a1, a2);
ARRAYASSERT.itemsAreSame([3,4,5], answer, "Intersection of arrays of numbers.");
},
testStrings: function () {
var a1 = ["a", "b", "c", "d"],
a2 = ["a", "e", "c"],
answer = Y.Array.intersect(a1, a2);
ARRAYASSERT.itemsAreSame(["a", "c"], answer, "Intersection of arrays of strings.");
},
testMixed: function () {
var a1 = [1, "a", {}, "b", 2, "c", "d"],
a2 = [2, "d", {}],
answer = Y.Array.intersect(a1, a2);
ARRAYASSERT.itemsAreSame([2, "d"], answer, "Intersection of arrays of strings.");
},
testNoIntersection: function () {
var a1 = ["a", 1, 2, 3, "b", "c", "d"],
a2 = [4, 5, "e", "f"],
answer = Y.Array.intersect(a1, a2);
ARRAYASSERT.itemsAreSame([], answer, "Intersection of arrays of strings.");
},
testError: function () {
var a1 = "bob",
a2 = function() {};
answer = Y.Array.intersect(a1, a2);
ASSERT.areEqual(false, answer, "Return false if not arrays passed.");
}
});
var testMoreThanTwoArraysIntersects = new Y.Test.Case({
name: "Test of more than two arrays of numbers, strings",
testNumbers: function () {
var a1 = [1,2,3,4,5],
a2 = [3,4,5,6],
a3 = [3, 5];
answer = Y.Array.intersect(a1, a2, a3);
ARRAYASSERT.itemsAreSame([3,5], answer, "Intersection of 3 arrays of numbers.");
},
testStrings: function () {
var a1 = ["a", "b", "c"],
a2 = ["d", "c"],
a3 = ["b", "c", "d"],
answer = Y.Array.intersect(a1, a2, a3);
ARRAYASSERT.itemsAreSame(["c"], answer, "Intersections of 3 arrays of strings");
},
testNoIntersection: function () {
var a1 = ["a", "b", "c"],
a2 = [1, 2],
a3 = [5, "d"],
answer = Y.Array.intersect(a1, a2, a3);
ARRAYASSERT.itemsAreSame([], answer, "Intersections of 3 arrays of strings");
},
testIntersectionOfEmptyArray: function () {
var a1 = ["a", "b", "c"],
a2 = [],
a3 = ["b", "c", "d"],
answer = Y.Array.intersect(a1, a2, a3);
ARRAYASSERT.itemsAreSame([], answer, "Intersections of an empty arrays is empty");
}
});
var testIntersectsWithCustomComparison = new Y.Test.Case({
name: "Test of two arrays with custom comparisons",
testNumbersNear: function () {
var a1 = [1, 7, 13],
a2 = [2, 3, 13, 14],
com = function (a,b) { return ((a-b) < 2 && (a-b) > -2)},
answer = Y.Array.intersect(a1, a2, com);
// manually uniqueness using a === b:
answer = Y.Array.unique(answer);
ARRAYASSERT.itemsAreSame([1, 2, 13, 14], answer, "Intersection of 2 arrays of numbers that are close.");
},
testDates: function () {
var a1 = [new Date("01/04/2012"), new Date("25/08/2012")],
a2 = [new Date("01/04/2012"), new Date("02/04/2012")],//"2012-04-02")],
com = function (a,b) {
return (a.getMonth() == b.getMonth() && a.getYear() == b.getYear() && a.getDay() == b.getDay());
},
answer = Y.Array.intersect(a1, a2, com),
ansStr,
expected = new Date("01/04/2012");
answer = Y.Array.unique(answer, com);
ansStr = answer[0];
ARRAYASSERT.itemsAreSame(expected.toString(), ansStr.toString(), "Intersection of 2 arrays of date objects.");
ASSERT.areEqual(1, answer.length, "One value in the answer");
},
testObjectPropertyWithMoreThanTwoArrays: function () {
var a1 = [{p: 2}, {p: 3}, {p: 5}],
a2 = [{p: 3}, {p: 10}, {p: 14}],
a3 = [{p: 10}, {p: 3}, {p: 5}, {p: 14}],
com = function (a, b) { return a.p === b.p;},
answer = Y.Array.intersect(a1, a2, a3, com);
// manually unique-ify the response
answer = Y.Array.unique(answer, com);
ARRAYASSERT.itemsAreSame(3, answer[0].p, "Intersection of 3 arrays of objects with custom comparison");
ASSERT.areEqual(1, answer.length, "One value in the answer");
},
testObjectPropertyWithMoreThanTwoArraysOrderDoesntMatter: function () {
var a3 = [{p: 2}, {p: 3}, {p: 5}],
a1 = [{p: 3}, {p: 10}, {p: 14}],
a2 = [{p: 10}, {p: 3}, {p: 5}, {p: 14}],
com = function (a, b) { return a.p === b.p;},
answer = Y.Array.intersect(a1, a2, a3, com);
answer = Y.Array.unique(answer, com);
ARRAYASSERT.itemsAreSame(3, answer[0].p, "Intersection of 3 arrays of objects with custom comparison - order of args doesnt matter");
ASSERT.areEqual(1, answer.length, "One value in the answer");
}
});
var suite = new Y.Test.Suite("ArraySet.intersect");
suite.add(testSimpleIntersects);
suite.add(testMoreThanTwoArraysIntersects);
suite.add(testIntersectsWithCustomComparison);
Y.Test.Runner.add(suite);
var testUnions = new Y.Test.Case({
name : "Test unions of arrays",
"union of 2 numerical arrays with no duplicates should return one array" : function () {
var a1 = [1,2,3],
a2 = [4,5,6],
answer = Y.Array.union(a1, a2);
ARRAYASSERT.itemsAreSame([1,2,3,4,5,6], answer, "Straightforward union of distinct numbers");
},
"union of 2 numerical arrays should remove duplicates" : function () {
var a1 = [1,4,2,3],
a2 = [2,4,5,6],
answer = Y.Array.union(a1, a2);
ARRAYASSERT.itemsAreSame([1,4,2,3,5,6], answer, "Straightforward union of distinct numbers with duplicates removed");
},
"union of 2 arrays should remove duplicates with strict equality": function () {
var a1 = [1,2,3, undefined, false],
a2 = ["1", "2", "3", null, true, 0],
answer = Y.Array.union(a1, a2);
ARRAYASSERT.itemsAreSame([1,2,3, undefined, false, "1", "2", "3", null, true, 0], answer, "Test strict equality in removing duplicates");
},
"simple union of 3 arrays should return one array": function () {
var a1 = [1,2,3],
a2 = [4,5,6],
a3 = [7,8,9],
answer = Y.Array.union(a1, a2, a3);
ARRAYASSERT.itemsAreSame([1,2,3, 4,5,6,7,8,9], answer);
},
"simple union of 3 arrays should remove duplicates": function () {
var a1 = [1,2,3],
a2 = [2,5,6],
a3 = [7,1,9],
answer = Y.Array.union(a1, a2, a3);
ARRAYASSERT.itemsAreSame([1,2,3,5,6,7,9], answer);
},
"simple union of objects should not remove duplicates": function () {
var a1 = [{a:2}],
a2 = [{a:2}],
a3 = [{a:3}],
answer = Y.Array.union(a1, a2, a3);
ASSERT.areEqual(2, answer[0].a);
ASSERT.areEqual(2, answer[1].a);
ASSERT.areEqual(3, answer[2].a);
ASSERT.areEqual(3, answer.length);
},
"union of objects with comparison function should remove duplicates": function () {
var a1 = [{a:2}],
a2 = [{a:2}],
a3 = [{a:3}],
f = function (x, y) { return (x.a === y.a);},
answer = Y.Array.union(a1, a2, a3, f);
ASSERT.areEqual(2, answer[0].a);
ASSERT.areEqual(3, answer[1].a);
ASSERT.areEqual(2, answer.length);
}
});
var suite2 = new Y.Test.Suite("ArraySet.union");
suite2.add(testUnions);
Y.Test.Runner.add(suite2);
var testDiff = new Y.Test.Case({
name : "Test diff of arrays",
"diff of two different numerical arrays should not remove any" : function () {
var a1 = [1,2,3],
a2 = [4,5,6],
answer = Y.Array.diff(a1, a2);
ARRAYASSERT.itemsAreSame([1,2,3], answer);
},
"diff of two identical numerical arrays should return empty array" : function () {
var a1 = [1,2,3],
a2 = [1,2,3],
answer = Y.Array.diff(a1, a2);
ARRAYASSERT.itemsAreSame([], answer);
},
"diff of arrays of objects should not remove any": function () {
var a1 = [{a:1}],
a2 = [{a:1}],
answer = Y.Array.diff(a1, a2);
ASSERT.areEqual(1, answer[0].a, answer);
ASSERT.areEqual(1, answer.length, answer);
},
"diff of arrays of objects with comparison function should remove some" : function () {
var a1 = [{a:1}],
a2 = [{a:1}],
comp = function (x,y) { return (x.a === y.a);},
answer = Y.Array.diff(a1, a2, comp);
ASSERT.areEqual(0, answer.length);
},
"diff of mixed values with crazy comparison function should remove some" : function () {
var a1 = [1, "abc", {a:5}, function () {return 2}, true, undefined],
a2 = [{a:2}, "ab", 1],
comp = function (x,y) {
var c,d, L = Y.Lang;
if (L.isFunction(x)) {
c = x();
} else if (L.isObject(x)) {
c = x.a;
} else {
c = x;
}
if (L.isFunction(y)) {
d = y();
} else if (L.isObject(y)) {
d = y.a;
} else {
d = y;
}
// note weak comparison
ret = (c == d);
return ret;
},
answer = Y.Array.diff(a1, a2, comp);
// should be:
// ["abc", {a:5}, undefined]
ASSERT.areEqual(3, answer.length, "3 items left");
ASSERT.areEqual("abc", answer[0]);
ASSERT.areEqual(5, answer[1].a);
ASSERT.areEqual(undefined, answer[2]);
}
});
var suite3 = new Y.Test.Suite("ArraySet.diff");
suite3.add(testDiff);
Y.Test.Runner.add(suite3);
Y.Test.Runner.run();
});
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's some test cases, that all pass. The code above also passes jslint and I've done some work on performance on it too (though not claiming perfection!)