From b9307163b974991885b50e3ac09b54bd00a06153 Mon Sep 17 00:00:00 2001 From: Alfredo Mesen Date: Tue, 3 May 2011 21:09:54 -0600 Subject: [PATCH 01/31] Add optional iterator to _.uniq --- index.html | 6 +++++- test/arrays.js | 8 ++++++++ underscore.js | 12 +++++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 685706fee..db4de3758 100644 --- a/index.html +++ b/index.html @@ -547,16 +547,20 @@

Array Functions

- uniq_.uniq(array, [isSorted]) + uniq_.uniq(array, [isSorted], [iterator]) Alias: unique
Produces a duplicate-free version of the array, using === to test object equality. If you know in advance that the array is sorted, passing true for isSorted will run a much faster algorithm. + Can receive an iterator to determine which part of the element gets tested.

 _.uniq([1, 2, 1, 3, 1, 4]);
 => [1, 2, 3, 4]
+
+_.uniq([{name:'moe'}, {name:'curly'}, {name:'larry'}, {name:'curly'}], false, function (value) { return value.name; })
+=>[{name:'moe'}, {name:'curly'}, {name:'larry'}]
 

diff --git a/test/arrays.js b/test/arrays.js index e031afe9b..985f4d956 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -61,6 +61,14 @@ $(document).ready(function() { var list = [1, 1, 1, 2, 2, 3]; equals(_.uniq(list, true).join(', '), '1, 2, 3', 'can find the unique values of a sorted array faster'); + var list = [{name:'moe'}, {name:'curly'}, {name:'larry'}, {name:'curly'}]; + var iterator = function(value) { return value.name; }; + equals(_.map(_.uniq(list, false, iterator), iterator).join(', '), 'moe, curly, larry', 'can find the unique values of an array using a custom iterator'); + + var iterator = function(value) { return value +1; }; + var list = [1, 2, 2, 3, 4, 4]; + equals(_.uniq(list, true, iterator).join(', '), '1, 2, 3, 4', 'iterator works with sorted array'); + var result = (function(){ return _.uniq(arguments); })(1, 2, 1, 3, 1, 4); equals(result.join(', '), '1, 2, 3, 4', 'works on an arguments object'); }); diff --git a/underscore.js b/underscore.js index eaba008c4..3d83511fc 100644 --- a/underscore.js +++ b/underscore.js @@ -323,11 +323,17 @@ // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. - _.uniq = _.unique = function(array, isSorted) { - return _.reduce(array, function(memo, el, i) { - if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el; + _.uniq = _.unique = function(array, isSorted, iterator) { + var initial = iterator ? _.map(array, iterator) : array; + var result = []; + _.reduce(initial, function(memo, el, i) { + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) { + memo[memo.length] = el; + result[result.length] = array[i]; + } return memo; }, []); + return result; }; // Produce an array that contains every item shared between all the From 5c2c3ce464474cd0db58796c908ebc16198ab9cb Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Tue, 12 Jul 2011 19:42:36 -0600 Subject: [PATCH 02/31] Rewrite `_.isEqual` and add support for comparing cyclic structures. --- test/objects.js | 40 ++++++++++++++++++++ underscore.js | 97 +++++++++++++++++++++++++++++-------------------- 2 files changed, 97 insertions(+), 40 deletions(-) diff --git a/test/objects.js b/test/objects.js index 48971c079..f1cc7e1f6 100644 --- a/test/objects.js +++ b/test/objects.js @@ -82,6 +82,46 @@ $(document).ready(function() { ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'wrapped objects are not equal'); equals(_({x: 1, y: 2}).chain().isEqual(_({x: 1, y: 2}).chain()).value(), true, 'wrapped objects are equal'); + + // Objects with circular references. + var circularA = {'abc': null}, circularB = {'abc': null}; + circularA.abc = circularA; + circularB.abc = circularB; + ok(_.isEqual(circularA, circularB), 'objects with a circular reference'); + circularA.def = 1; + circularB.def = 1; + ok(_.isEqual(circularA, circularB), 'objects with identical properties and a circular reference'); + circularA.def = 1; + circularB.def = 0; + ok(!_.isEqual(circularA, circularB), 'objects with different properties and a circular reference'); + + // Arrays with circular references. + circularA = []; + circularB = []; + circularA.push(circularA); + circularB.push(circularB); + ok(_.isEqual(circularA, circularB), 'arrays with a circular reference'); + circularA.push('abc'); + circularB.push('abc'); + ok(_.isEqual(circularA, circularB), 'arrays with identical indices and a circular reference'); + circularA.push('hello'); + circularB.push('goodbye'); + ok(!_.isEqual(circularA, circularB), 'arrays with different properties and a circular reference'); + + // Hybrid cyclic structures. + circularA = [{'abc': null}]; + circularB = [{'abc': null}]; + circularA[0].abc = circularA; + circularB[0].abc = circularB; + circularA.push(circularA); + circularB.push(circularB); + ok(_.isEqual(circularA, circularB), 'cyclic structure'); + circularA[0].def = 1; + circularB[0].def = 1; + ok(_.isEqual(circularA, circularB), 'cyclic structure with identical properties'); + circularA[0].def = 1; + circularB[0].def = 0; + ok(!_.isEqual(circularA, circularB), 'cyclic structure with different properties'); }); test("objects: isEmpty", function() { diff --git a/underscore.js b/underscore.js index 5757609fa..6bc1ed9e4 100644 --- a/underscore.js +++ b/underscore.js @@ -391,7 +391,6 @@ return -1; }; - // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { if (array == null) return -1; @@ -538,7 +537,6 @@ }; }; - // Object Functions // ---------------- @@ -596,44 +594,63 @@ }; // Perform a deep comparison to check if two objects are equal. - _.isEqual = function(a, b) { - // Check object identity. - if (a === b) return true; - // Different types? - var atype = typeof(a), btype = typeof(b); - if (atype != btype) return false; - // Basic equality test (watch out for coercions). - if (a == b) return true; - // One is falsy and the other truthy. - if ((!a && b) || (a && !b)) return false; - // Unwrap any wrapped objects. - if (a._chain) a = a._wrapped; - if (b._chain) b = b._wrapped; - // One of them implements an isEqual()? - if (a.isEqual) return a.isEqual(b); - if (b.isEqual) return b.isEqual(a); - // Check dates' integer values. - if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); - // Both are NaN? - if (_.isNaN(a) && _.isNaN(b)) return false; - // Compare regular expressions. - if (_.isRegExp(a) && _.isRegExp(b)) - return a.source === b.source && - a.global === b.global && - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline; - // If a is not an object by this point, we can't handle it. - if (atype !== 'object') return false; - // Check for different array lengths before comparing contents. - if (a.length && (a.length !== b.length)) return false; - // Nothing else worked, deep compare the contents. - var aKeys = _.keys(a), bKeys = _.keys(b); - // Different object sizes? - if (aKeys.length != bKeys.length) return false; - // Recursive comparison of contents. - for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; - return true; - }; + _.isEqual = (function() { + function eq(a, b, stack) { + // Identical objects are equal. + if (a === b) return true; + // A strict comparison is necessary because `null == undefined`. + if (a == null) return a === b; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + // Compare functions by reference. + if (_.isFunction(a)) return _.isFunction(b) && a == b; + // Compare strings, numbers, dates, and booleans by value. + if (_.isString(a)) return _.isString(b) && String(a) == String(b); + if (_.isNumber(a)) return _.isNumber(b) && +a == +b; + if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); + if (_.isBoolean(a)) return _.isBoolean(b) && +a == +b; + // Compare RegExps by their source patterns and flags. + if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + // Recursively compare objects and arrays. + if (typeof a != 'object') return false; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual) return a.isEqual(b); + if (b.isEqual) return b.isEqual(a); + // Compare array lengths to determine if a deep comparison is necessary. + if (a.length && (a.length !== b.length)) return false; + // Assume equality for cyclic structures. + var length = stack.length; + while (length--) { + if (stack[length] == a) return true; + } + // Add the object to the stack of traversed objects. + stack.push(a); + var result = true; + // Deep comparse the contents. + var aKeys = _.keys(a), bKeys = _.keys(b); + // Ensure that both objects contain the same number of properties. + if (result = aKeys.length == bKeys.length) { + // Recursively compare properties. + for (var key in a) { + if (!(result = key in b && eq(a[key], b[key], stack))) break; + } + } + // Remove the object from the stack of traversed objects. + stack.pop(); + return result; + } + // Expose the recursive `isEqual` method. + return function(a, b) { + return eq(a, b, []); + }; + })(); // Is a given array or object empty? _.isEmpty = function(obj) { From e21b346cbf265278145c253d4f5c784858ccfc3e Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Tue, 12 Jul 2011 19:54:56 -0600 Subject: [PATCH 03/31] `_.isEqual`: Compare object types rather than `[[Class]]` names. --- underscore.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/underscore.js b/underscore.js index 6bc1ed9e4..0611f8e1d 100644 --- a/underscore.js +++ b/underscore.js @@ -600,9 +600,9 @@ if (a === b) return true; // A strict comparison is necessary because `null == undefined`. if (a == null) return a === b; - // Compare `[[Class]]` names. - var className = toString.call(a); - if (className != toString.call(b)) return false; + // Compare object types. + var typeA = typeof a; + if (typeA != typeof b) return false; // Compare functions by reference. if (_.isFunction(a)) return _.isFunction(b) && a == b; // Compare strings, numbers, dates, and booleans by value. @@ -616,7 +616,7 @@ a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; // Recursively compare objects and arrays. - if (typeof a != 'object') return false; + if (typeA != 'object') return false; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; From 9d0b43221acd394d201d44a74d005e1d471b6bce Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Tue, 12 Jul 2011 20:22:05 -0600 Subject: [PATCH 04/31] `_.isEqual`: Move the internal `eq` method into the main closure. Remove strict type checking. --- underscore.js | 111 +++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/underscore.js b/underscore.js index 0611f8e1d..f636d0444 100644 --- a/underscore.js +++ b/underscore.js @@ -593,64 +593,63 @@ return obj; }; - // Perform a deep comparison to check if two objects are equal. - _.isEqual = (function() { - function eq(a, b, stack) { - // Identical objects are equal. - if (a === b) return true; - // A strict comparison is necessary because `null == undefined`. - if (a == null) return a === b; - // Compare object types. - var typeA = typeof a; - if (typeA != typeof b) return false; - // Compare functions by reference. - if (_.isFunction(a)) return _.isFunction(b) && a == b; - // Compare strings, numbers, dates, and booleans by value. - if (_.isString(a)) return _.isString(b) && String(a) == String(b); - if (_.isNumber(a)) return _.isNumber(b) && +a == +b; - if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); - if (_.isBoolean(a)) return _.isBoolean(b) && +a == +b; - // Compare RegExps by their source patterns and flags. - if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - // Recursively compare objects and arrays. - if (typeA != 'object') return false; - // Unwrap any wrapped objects. - if (a._chain) a = a._wrapped; - if (b._chain) b = b._wrapped; - // Invoke a custom `isEqual` method if one is provided. - if (a.isEqual) return a.isEqual(b); - if (b.isEqual) return b.isEqual(a); - // Compare array lengths to determine if a deep comparison is necessary. - if (a.length && (a.length !== b.length)) return false; - // Assume equality for cyclic structures. - var length = stack.length; - while (length--) { - if (stack[length] == a) return true; - } - // Add the object to the stack of traversed objects. - stack.push(a); - var result = true; - // Deep comparse the contents. - var aKeys = _.keys(a), bKeys = _.keys(b); - // Ensure that both objects contain the same number of properties. - if (result = aKeys.length == bKeys.length) { - // Recursively compare properties. - for (var key in a) { - if (!(result = key in b && eq(a[key], b[key], stack))) break; - } + // Internal recursive comparison function. + function eq(a, b, stack) { + // Identical objects are equal. + if (a === b) return true; + // A strict comparison is necessary because `null == undefined`. + if (a == null) return a === b; + // Compare object types. + var typeA = typeof a; + if (typeA != typeof b) return false; + // The type comparison above prevents unwanted type coercion. + if (a == b) return true; + // Ensure that both values are truthy or falsy. + if ((!a && b) || (a && !b)) return false; + // `NaN` values are toxic. + if (_.isNaN(a) || _.isNaN(b)) return false; + if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); + // Compare RegExps by their source patterns and flags. + if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + // Recursively compare objects and arrays. + if (typeA != 'object') return false; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // Invoke a custom `isEqual` method if one is provided. + if (a.isEqual) return a.isEqual(b); + if (b.isEqual) return b.isEqual(a); + // Compare array lengths to determine if a deep comparison is necessary. + if (a.length && (a.length !== b.length)) return false; + // Assume equality for cyclic structures. + var length = stack.length; + while (length--) { + if (stack[length] == a) return true; + } + // Add the object to the stack of traversed objects. + stack.push(a); + var result = true; + // Deep comparse the contents. + var aKeys = _.keys(a), bKeys = _.keys(b); + // Ensure that both objects contain the same number of properties. + if (result = aKeys.length == bKeys.length) { + // Recursively compare properties. + for (var key in a) { + if (!(result = key in b && eq(a[key], b[key], stack))) break; } - // Remove the object from the stack of traversed objects. - stack.pop(); - return result; } - // Expose the recursive `isEqual` method. - return function(a, b) { - return eq(a, b, []); - }; - })(); + // Remove the object from the stack of traversed objects. + stack.pop(); + return result; + } + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, []); + }; // Is a given array or object empty? _.isEmpty = function(obj) { From cf812e77bc271dab9f6987466dc0e84ff88b5198 Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Tue, 12 Jul 2011 22:16:12 -0600 Subject: [PATCH 05/31] `_.isEqual`: Ensure that `0` and `-0` are not equivalent. `NaN` values should be equal. --- test/objects.js | 2 +- underscore.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/objects.js b/test/objects.js index f1cc7e1f6..c32a13ad7 100644 --- a/test/objects.js +++ b/test/objects.js @@ -73,7 +73,7 @@ $(document).ready(function() { ok(!_.isEqual(5, NaN), '5 is not equal to NaN'); ok(NaN != NaN, 'NaN is not equal to NaN (native equality)'); ok(NaN !== NaN, 'NaN is not equal to NaN (native identity)'); - ok(!_.isEqual(NaN, NaN), 'NaN is not equal to NaN'); + ok(_.isEqual(NaN, NaN), 'NaN is equal to NaN'); ok(_.isEqual(new Date(100), new Date(100)), 'identical dates are equal'); ok(_.isEqual((/hello/ig), (/hello/ig)), 'identical regexes are equal'); ok(!_.isEqual(null, [1]), 'a falsy is never equal to a truthy'); diff --git a/underscore.js b/underscore.js index f636d0444..ca9133f04 100644 --- a/underscore.js +++ b/underscore.js @@ -596,7 +596,7 @@ // Internal recursive comparison function. function eq(a, b, stack) { // Identical objects are equal. - if (a === b) return true; + if (a === b) return a != 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null) return a === b; // Compare object types. @@ -606,8 +606,8 @@ if (a == b) return true; // Ensure that both values are truthy or falsy. if ((!a && b) || (a && !b)) return false; - // `NaN` values are toxic. - if (_.isNaN(a) || _.isNaN(b)) return false; + // `NaN` values are equal. + if (_.isNaN(a)) return _.isNaN(b); if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && @@ -631,11 +631,11 @@ } // Add the object to the stack of traversed objects. stack.push(a); - var result = true; - // Deep comparse the contents. + // Deep compare the contents. var aKeys = _.keys(a), bKeys = _.keys(b); // Ensure that both objects contain the same number of properties. - if (result = aKeys.length == bKeys.length) { + var result = aKeys.length == bKeys.length; + if (result) { // Recursively compare properties. for (var key in a) { if (!(result = key in b && eq(a[key], b[key], stack))) break; From b6a02fa6bb08545bd21c24f823f4c541371689ed Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Tue, 12 Jul 2011 22:37:09 -0600 Subject: [PATCH 06/31] `_.isEqual`: Use a strict comparison to avoid an unnecessary division for `false` values. --- underscore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/underscore.js b/underscore.js index ca9133f04..1a68a9784 100644 --- a/underscore.js +++ b/underscore.js @@ -596,7 +596,7 @@ // Internal recursive comparison function. function eq(a, b, stack) { // Identical objects are equal. - if (a === b) return a != 0 || 1 / a == 1 / b; + if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null) return a === b; // Compare object types. From 365eea6aa7570b738850a1652fdd51b13208fef9 Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Wed, 13 Jul 2011 10:48:16 -0600 Subject: [PATCH 07/31] `_.isEqual`: Streamline the deep comparison algorithm and remove the dependency on `_.keys`. --- test/objects.js | 6 ++++++ underscore.js | 15 ++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/test/objects.js b/test/objects.js index c32a13ad7..08206b913 100644 --- a/test/objects.js +++ b/test/objects.js @@ -65,6 +65,11 @@ $(document).ready(function() { }); test("objects: isEqual", function() { + function Foo(){ this.own = 'a'; } + function Bar(){ this.own = 'a'; } + Foo.prototype.inherited = 1; + Bar.prototype.inherited = 2; + var moe = {name : 'moe', lucky : [13, 27, 34]}; var clone = {name : 'moe', lucky : [13, 27, 34]}; ok(moe != clone, 'basic equality between objects is false'); @@ -80,6 +85,7 @@ $(document).ready(function() { ok(_.isEqual({isEqual: function () { return true; }}, {}), 'first object implements `isEqual`'); ok(_.isEqual({}, {isEqual: function () { return true; }}), 'second object implements `isEqual`'); ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); + ok(!_.isEqual(new Foo, new Bar), 'objects with different inherited properties are not equal'); ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'wrapped objects are not equal'); equals(_({x: 1, y: 2}).chain().isEqual(_({x: 1, y: 2}).chain()).value(), true, 'wrapped objects are equal'); diff --git a/underscore.js b/underscore.js index 1a68a9784..14d5c01ed 100644 --- a/underscore.js +++ b/underscore.js @@ -632,14 +632,19 @@ // Add the object to the stack of traversed objects. stack.push(a); // Deep compare the contents. - var aKeys = _.keys(a), bKeys = _.keys(b); + var size = 0, sizeRight = 0, result = true, key; + for (key in a) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = key in b && eq(a[key], b[key], stack))) break; + } // Ensure that both objects contain the same number of properties. - var result = aKeys.length == bKeys.length; if (result) { - // Recursively compare properties. - for (var key in a) { - if (!(result = key in b && eq(a[key], b[key], stack))) break; + for (key in b) { + if (++sizeRight > size) break; } + result = size == sizeRight; } // Remove the object from the stack of traversed objects. stack.pop(); From a12d0035cb33e01c6a4eb9f8507c6f4f6cffb777 Mon Sep 17 00:00:00 2001 From: Kit Goncharov Date: Wed, 13 Jul 2011 14:24:28 -0600 Subject: [PATCH 08/31] `_.isEqual`: Ensure commutative equality for dates and RegExps. --- test/objects.js | 4 ++++ underscore.js | 27 ++++++++++++++++----------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/test/objects.js b/test/objects.js index 08206b913..0b1685a88 100644 --- a/test/objects.js +++ b/test/objects.js @@ -79,9 +79,13 @@ $(document).ready(function() { ok(NaN != NaN, 'NaN is not equal to NaN (native equality)'); ok(NaN !== NaN, 'NaN is not equal to NaN (native identity)'); ok(_.isEqual(NaN, NaN), 'NaN is equal to NaN'); + ok(!_.isEqual(5, NaN), '`5` is not equal to `NaN`'); + ok(!_.isEqual(false, NaN), '`false` is not equal to `NaN`'); ok(_.isEqual(new Date(100), new Date(100)), 'identical dates are equal'); ok(_.isEqual((/hello/ig), (/hello/ig)), 'identical regexes are equal'); + ok(!_.isEqual({source: '(?:)', global: true, multiline: true, ignoreCase: true}, /(?:)/gim), 'RegExp-like objects and RegExps are not equal'); ok(!_.isEqual(null, [1]), 'a falsy is never equal to a truthy'); + ok(!_.isEqual(undefined, null), '`undefined` is not equal to `null`'); ok(_.isEqual({isEqual: function () { return true; }}, {}), 'first object implements `isEqual`'); ok(_.isEqual({}, {isEqual: function () { return true; }}), 'second object implements `isEqual`'); ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); diff --git a/underscore.js b/underscore.js index 14d5c01ed..9d95ebfbc 100644 --- a/underscore.js +++ b/underscore.js @@ -595,7 +595,8 @@ // Internal recursive comparison function. function eq(a, b, stack) { - // Identical objects are equal. + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null) return a === b; @@ -604,17 +605,21 @@ if (typeA != typeof b) return false; // The type comparison above prevents unwanted type coercion. if (a == b) return true; - // Ensure that both values are truthy or falsy. + // Optimization; ensure that both values are truthy or falsy. if ((!a && b) || (a && !b)) return false; // `NaN` values are equal. if (_.isNaN(a)) return _.isNaN(b); - if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); + // Compare dates by their millisecond values. + var isDateA = _.isDate(a), isDateB = _.isDate(b); + if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. - if (_.isRegExp(a)) return _.isRegExp(b) && a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; - // Recursively compare objects and arrays. + var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); + if (isRegExpA || isRegExpB) return isRegExpA && isRegExpB && + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; @@ -629,9 +634,9 @@ while (length--) { if (stack[length] == a) return true; } - // Add the object to the stack of traversed objects. + // Add the first object to the stack of traversed objects. stack.push(a); - // Deep compare the contents. + // Deep compare the two objects. var size = 0, sizeRight = 0, result = true, key; for (key in a) { // Count the expected number of properties. @@ -646,7 +651,7 @@ } result = size == sizeRight; } - // Remove the object from the stack of traversed objects. + // Remove the first object from the stack of traversed objects. stack.pop(); return result; } From 9e8fc8304046bdaa46d597e4260133464ebab19f Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 13 Jul 2011 17:05:20 -0400 Subject: [PATCH 09/31] fixing silly typo. --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 0641e9059..de196cdea 100644 --- a/index.html +++ b/index.html @@ -1283,7 +1283,7 @@

Change Log

1.1.7July 13, 2011
Added _.groupBy, which aggregates a collection into groups of like items. - Added _.untion and _.difference, to complement the + Added _.union and _.difference, to complement the (re-named) _.intersection. Various improvements for support of sparse arrays. _.toArray now returns a clone, if directly passed an array. From c7c57ca6ff0a7d175913e4c040450f5a15a9bdaa Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Fri, 15 Jul 2011 18:15:10 -0400 Subject: [PATCH 10/31] _.isEqual improvements --- test/objects.js | 9 +++++++-- underscore.js | 22 ++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/test/objects.js b/test/objects.js index 0b1685a88..4234f0fad 100644 --- a/test/objects.js +++ b/test/objects.js @@ -72,6 +72,8 @@ $(document).ready(function() { var moe = {name : 'moe', lucky : [13, 27, 34]}; var clone = {name : 'moe', lucky : [13, 27, 34]}; + var isEqualObj = {isEqual: function (o) { return o.isEqual == this.isEqual; }, unique: {}}; + var isEqualObjClone = {isEqual: isEqualObj.isEqual, unique: {}}; ok(moe != clone, 'basic equality between objects is false'); ok(_.isEqual(moe, clone), 'deep equality is true'); ok(_(moe).isEqual(clone), 'OO-style deep equality works'); @@ -86,8 +88,11 @@ $(document).ready(function() { ok(!_.isEqual({source: '(?:)', global: true, multiline: true, ignoreCase: true}, /(?:)/gim), 'RegExp-like objects and RegExps are not equal'); ok(!_.isEqual(null, [1]), 'a falsy is never equal to a truthy'); ok(!_.isEqual(undefined, null), '`undefined` is not equal to `null`'); - ok(_.isEqual({isEqual: function () { return true; }}, {}), 'first object implements `isEqual`'); - ok(_.isEqual({}, {isEqual: function () { return true; }}), 'second object implements `isEqual`'); + ok(_.isEqual(isEqualObj, isEqualObj), 'both objects implement `isEqual`, same objects'); + ok(_.isEqual(isEqualObj, isEqualObjClone), 'both objects implement `isEqual`, different objects'); + ok(_.isEqual(isEqualObjClone, isEqualObj), 'both objects implement `isEqual`, different objects, swapped'); + ok(!_.isEqual(isEqualObj, {}), 'first object implements `isEqual`'); + ok(!_.isEqual({}, isEqualObj), 'second object implements `isEqual`'); ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); ok(!_.isEqual(new Foo, new Bar), 'objects with different inherited properties are not equal'); ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'wrapped objects are not equal'); diff --git a/underscore.js b/underscore.js index 9d95ebfbc..3ad38311b 100644 --- a/underscore.js +++ b/underscore.js @@ -606,29 +606,27 @@ // The type comparison above prevents unwanted type coercion. if (a == b) return true; // Optimization; ensure that both values are truthy or falsy. - if ((!a && b) || (a && !b)) return false; + if (!a != !b) return false; // `NaN` values are equal. if (_.isNaN(a)) return _.isNaN(b); // Compare dates by their millisecond values. - var isDateA = _.isDate(a), isDateB = _.isDate(b); - if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); + if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. - var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); - if (isRegExpA || isRegExpB) return isRegExpA && isRegExpB && - a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; + if (_.isRegExp(a)) + return _.isRegExp(b) && + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. - if (a.isEqual) return a.isEqual(b); - if (b.isEqual) return b.isEqual(a); + if (typeof a.isEqual == 'function') return a.isEqual(b); // Compare array lengths to determine if a deep comparison is necessary. - if (a.length && (a.length !== b.length)) return false; + if ('length' in a && (a.length !== b.length)) return false; // Assume equality for cyclic structures. var length = stack.length; while (length--) { From 34f10467b3f6c476da14bf293359f58089b2067e Mon Sep 17 00:00:00 2001 From: Nadav Date: Wed, 20 Jul 2011 03:41:27 -0700 Subject: [PATCH 11/31] * Added _.escape() for escaping special HTML chars * Added support for auto-escaping of values using ```<%== ... %>``` --- underscore.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/underscore.js b/underscore.js index 4f5601325..1cf51a7dc 100644 --- a/underscore.js +++ b/underscore.js @@ -733,6 +733,11 @@ for (var i = 0; i < n; i++) iterator.call(context, i); }; + // Escape string for HTML + _.escape = function(string) { + return (''+string).replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); + }; + // Add your own custom functions to the Underscore object, ensuring that // they're correctly added to the OOP wrapper as well. _.mixin = function(obj) { @@ -753,7 +758,8 @@ // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g + interpolate : /<%=([\s\S]+?)%>/g, + encode : /<%==([\s\S]+?)%>/g }; // JavaScript micro-templating, similar to John Resig's implementation. @@ -765,6 +771,9 @@ 'with(obj||{}){__p.push(\'' + str.replace(/\\/g, '\\\\') .replace(/'/g, "\\'") + .replace(c.encode, function(match, code) { + return "',_.escape(" + code.replace(/\\'/g, "'") + "),'"; + }) .replace(c.interpolate, function(match, code) { return "'," + code.replace(/\\'/g, "'") + ",'"; }) From 75b21957c750fef21e2e24ad6bf902b06137a619 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 3 Aug 2011 11:16:57 -0400 Subject: [PATCH 12/31] Issue #252 #154 #148 documenting numeric length key caveat --- index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.html b/index.html index de196cdea..c2d37cd0a 100644 --- a/index.html +++ b/index.html @@ -1235,6 +1235,13 @@

Duck Typing

another type, if you're setting properties with names like "concat" and "charCodeAt". So be aware.

+ +

+ In a similar fashion, _.each and all of the other functions + based on it are designed to be able to iterate over any Array-like + JavaScript object, including arguments, NodeLists, and more. + Passing hash-like objects with a numeric length key won't work. +

Links & Suggested Reading

From 6cf647505fba5b0dc182d41cba1342cefe3acd35 Mon Sep 17 00:00:00 2001 From: Brian Haveri Date: Thu, 4 Aug 2011 12:42:29 -0600 Subject: [PATCH 13/31] Added link to Underscore.php in the docs --- index.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/index.html b/index.html index 87dee9701..3289b5d36 100644 --- a/index.html +++ b/index.html @@ -1256,6 +1256,14 @@

Links & Suggested Reading

available on GitHub.

+

+ Underscore.php, + a PHP port of the functions that are applicable in both languages. + Includes OOP-wrapping and chaining. + The source is + available on GitHub. +

+

Underscore.string, an Underscore extension that adds functions for string-manipulation: From 610b3471742db0ee117e5d6117875277cc396aba Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Thu, 11 Aug 2011 10:53:29 -0400 Subject: [PATCH 14/31] Adding Underscore-perl --- index.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.html b/index.html index 3289b5d36..f04572ff9 100644 --- a/index.html +++ b/index.html @@ -1264,6 +1264,13 @@

Links & Suggested Reading

available on GitHub.

+

+ Underscore-perl, + a Perl port of many of the Underscore.js functions, + aimed at on Perl hashes and arrays, also + available on GitHub. +

+

Underscore.string, an Underscore extension that adds functions for string-manipulation: From f4cba513b96a12367cbf6f5e704b6bfbcfdb492e Mon Sep 17 00:00:00 2001 From: Ryan W Tenney Date: Thu, 25 Aug 2011 21:44:29 +0000 Subject: [PATCH 15/31] Added function shuffle, with test case. --- index.html | 23 ++++++++++++++++++++++- test/collections.js | 7 +++++++ underscore.js | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index f04572ff9..3254edcfc 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,7 @@ margin: 0px 0 30px; } + @@ -141,7 +142,8 @@

Table of Contents

any, include, invoke, pluck, max, min, - sortBy, groupBy, sortedIndex, + sortBy, groupBy, + sortedIndex, shuffle, toArray, size

@@ -464,6 +466,25 @@

Collection Functions (Arrays or Objects)

=> 3 +

+ shuffle_.shuffle(list) +
+ Returns a shuffled list. +

+
+_.shuffle([1, 2, 3, 4, 5, 6]);
+=> 
+(reshuffle)
+
+ +

toArray_.toArray(list)
diff --git a/test/collections.js b/test/collections.js index 005ee169e..9afe95849 100644 --- a/test/collections.js +++ b/test/collections.js @@ -204,6 +204,13 @@ $(document).ready(function() { equals(index, 3, '35 should be inserted at index 3'); }); + test('collections: shuffle', function() { + var numbers = _.range(10); + var shuffled = _.shuffle(numbers).sort(); + notStrictEqual(numbers, shuffled, 'original object is unmodified'); + equals(shuffled.join(','), numbers.join(','), 'contains the same members before and after shuffle'); + }); + test('collections: toArray', function() { ok(!_.isArray(arguments), 'arguments object is not an array'); ok(_.isArray(_.toArray(arguments)), 'arguments object converted into array'); diff --git a/underscore.js b/underscore.js index 0d587b41c..a19f3186e 100644 --- a/underscore.js +++ b/underscore.js @@ -239,6 +239,21 @@ return result.value; }; + // Shuffle an array. + _.shuffle = function(obj) { + var shuffled = [], rand; + each(obj, function(value, index, list) { + if (index == 0) { + shuffled[0] = value; + } else { + rand = Math.floor(Math.random() * (index + 1)); + shuffled[index] = shuffled[rand]; + shuffled[rand] = value; + } + }); + return shuffled; + }; + // Sort the object's values by a criterion produced by an iterator. _.sortBy = function(obj, iterator, context) { return _.pluck(_.map(obj, function(value, index, list) { From 9996ecae5c69eb4ba153edb271ca44a039c9f159 Mon Sep 17 00:00:00 2001 From: Ryan W Tenney Date: Thu, 25 Aug 2011 22:13:11 +0000 Subject: [PATCH 16/31] Remove dupe inclusion of underscore-min.js. Get rid of 'reshuffle' in index.html. --- index.html | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index 3254edcfc..c33cc95d5 100644 --- a/index.html +++ b/index.html @@ -65,7 +65,6 @@ margin: 0px 0 30px; } - @@ -469,21 +468,12 @@

Collection Functions (Arrays or Objects)

shuffle_.shuffle(list)
- Returns a shuffled list. + Returns a shuffled copy of list.

 _.shuffle([1, 2, 3, 4, 5, 6]);
-=> 
-(reshuffle)
-
- +=> [4, 1, 6, 3, 5, 2] +

toArray_.toArray(list) From a8f0445192aeac118b3f347a3abfc7bf5af51fd3 Mon Sep 17 00:00:00 2001 From: Malcolm Locke Date: Wed, 31 Aug 2011 22:39:05 +1200 Subject: [PATCH 17/31] Add an optional index argument to _.last() This makes _.last() behave the same as _.first(). Passing an optional second argument n will return the last n elements of the array. --- test/arrays.js | 2 ++ underscore.js | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/test/arrays.js b/test/arrays.js index 78cf098a0..bbf74e6da 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -26,6 +26,8 @@ $(document).ready(function() { test("arrays: last", function() { equals(_.last([1,2,3]), 3, 'can pull out the last element of an array'); + equals(_.last([1,2,3], 0).join(', '), "", 'can pass an index to last'); + equals(_.last([1,2,3], 2).join(', '), '2, 3', 'can pass an index to last'); var result = (function(){ return _(arguments).last(); })(1, 2, 3, 4); equals(result, 4, 'works on an arguments object'); }); diff --git a/underscore.js b/underscore.js index 0d587b41c..7963394cb 100644 --- a/underscore.js +++ b/underscore.js @@ -306,9 +306,10 @@ return slice.call(array, (index == null) || guard ? 1 : index); }; - // Get the last element of an array. - _.last = function(array) { - return array[array.length - 1]; + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n) { + return (n != null) ? slice.call(array, array.length - n) : array[array.length - 1]; }; // Trim out all falsy values from an array. From e449b00a26d0bfc29c0be42f1ea98c7d8f5493b9 Mon Sep 17 00:00:00 2001 From: Malcolm Locke Date: Thu, 1 Sep 2011 01:10:10 +1200 Subject: [PATCH 18/31] Add guard check to _.last() Allows _.last() to work as expected with _.map(). --- test/arrays.js | 2 ++ underscore.js | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/arrays.js b/test/arrays.js index bbf74e6da..950e8417f 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -30,6 +30,8 @@ $(document).ready(function() { equals(_.last([1,2,3], 2).join(', '), '2, 3', 'can pass an index to last'); var result = (function(){ return _(arguments).last(); })(1, 2, 3, 4); equals(result, 4, 'works on an arguments object'); + result = _.map([[1,2,3],[1,2,3]], _.last); + equals(result.join(','), '3,3', 'works well with _.map'); }); test("arrays: compact", function() { diff --git a/underscore.js b/underscore.js index 7963394cb..239d7dd34 100644 --- a/underscore.js +++ b/underscore.js @@ -307,9 +307,9 @@ }; // Get the last element of an array. Passing **n** will return the last N - // values in the array. - _.last = function(array, n) { - return (n != null) ? slice.call(array, array.length - n) : array[array.length - 1]; + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, array.length - n) : array[array.length - 1]; }; // Trim out all falsy values from an array. From bf3aa97c3614f032983afe0740813021e70a06dd Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Sun, 4 Sep 2011 19:34:19 -0400 Subject: [PATCH 19/31] reverting some changes to isEqual that were a little too aggressive --- test/objects.js | 4 ++-- underscore.js | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/test/objects.js b/test/objects.js index 4234f0fad..bb0f98553 100644 --- a/test/objects.js +++ b/test/objects.js @@ -72,8 +72,8 @@ $(document).ready(function() { var moe = {name : 'moe', lucky : [13, 27, 34]}; var clone = {name : 'moe', lucky : [13, 27, 34]}; - var isEqualObj = {isEqual: function (o) { return o.isEqual == this.isEqual; }, unique: {}}; - var isEqualObjClone = {isEqual: isEqualObj.isEqual, unique: {}}; + var isEqualObj = {isEqual: function (o) { return o.isEqual == this.isEqual; }, unique: {}}; + var isEqualObjClone = {isEqual: isEqualObj.isEqual, unique: {}}; ok(moe != clone, 'basic equality between objects is false'); ok(_.isEqual(moe, clone), 'deep equality is true'); ok(_(moe).isEqual(clone), 'OO-style deep equality works'); diff --git a/underscore.js b/underscore.js index 3ad38311b..7615b5d5e 100644 --- a/underscore.js +++ b/underscore.js @@ -610,14 +610,16 @@ // `NaN` values are equal. if (_.isNaN(a)) return _.isNaN(b); // Compare dates by their millisecond values. - if (_.isDate(a)) return _.isDate(b) && a.getTime() == b.getTime(); + var isDateA = _.isDate(a), isDateB = _.isDate(b); + if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. - if (_.isRegExp(a)) - return _.isRegExp(b) && - a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; + var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); + if (isRegExpA || isRegExpB) + return isRegExpA && isRegExpB && + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. @@ -625,6 +627,7 @@ if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. if (typeof a.isEqual == 'function') return a.isEqual(b); + if (typeof b.isEqual == 'function') return b.isEqual(a); // Compare array lengths to determine if a deep comparison is necessary. if ('length' in a && (a.length !== b.length)) return false; // Assume equality for cyclic structures. From 6f62f258cb4c1c45248c30c56cfd0ceea7051086 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Mon, 5 Sep 2011 12:25:59 -0600 Subject: [PATCH 20/31] Add support for comparing string, number, and boolean object wrappers. Ignore inherited properties when deep comparing objects. Use a more efficient `while` loop for comparing arrays and array-like objects. --- underscore.js | 75 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/underscore.js b/underscore.js index 7615b5d5e..825e9405d 100644 --- a/underscore.js +++ b/underscore.js @@ -603,23 +603,29 @@ // Compare object types. var typeA = typeof a; if (typeA != typeof b) return false; - // The type comparison above prevents unwanted type coercion. - if (a == b) return true; // Optimization; ensure that both values are truthy or falsy. if (!a != !b) return false; - // `NaN` values are equal. - if (_.isNaN(a)) return _.isNaN(b); + // Compare string objects by value. + var isStringA = _.isString(a), isStringB = _.isString(b); + if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b); + // Compare number objects by value. `NaN` values are equal. + var isNumberA = toString.call(a) == '[object Number]', isNumberB = toString.call(b) == '[object Number]'; + if (isNumberA || isNumberB) return isNumberA && isNumberB && (_.isNaN(a) ? _.isNaN(b) : +a == +b); + // Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0. + var isBooleanA = toString.call(a) == '[object Boolean]', isBooleanB = toString.call(b) == '[object Boolean]'; + if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b; // Compare dates by their millisecond values. var isDateA = _.isDate(a), isDateB = _.isDate(b); if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); if (isRegExpA || isRegExpB) - return isRegExpA && isRegExpB && - a.source == b.source && - a.global == b.global && - a.multiline == b.multiline && - a.ignoreCase == b.ignoreCase; + // Ensure commutative equality for RegExps. + return isRegExpA && isRegExpB && + a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. @@ -627,30 +633,47 @@ if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. if (typeof a.isEqual == 'function') return a.isEqual(b); - if (typeof b.isEqual == 'function') return b.isEqual(a); - // Compare array lengths to determine if a deep comparison is necessary. - if ('length' in a && (a.length !== b.length)) return false; - // Assume equality for cyclic structures. + // If only `b` provides an `isEqual` method, `a` and `b` are not equal. + if (typeof b.isEqual == 'function') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic structures is + // adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = stack.length; while (length--) { + // Linear search. Performance is inversely proportional to the number of unique nested + // structures. if (stack[length] == a) return true; } // Add the first object to the stack of traversed objects. stack.push(a); - // Deep compare the two objects. - var size = 0, sizeRight = 0, result = true, key; - for (key in a) { - // Count the expected number of properties. - size++; - // Deep compare each member. - if (!(result = key in b && eq(a[key], b[key], stack))) break; - } - // Ensure that both objects contain the same number of properties. - if (result) { - for (key in b) { - if (++sizeRight > size) break; + var size = 0, result = true; + if (a.length === +a.length || b.length === +b.length) { + // Compare object lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare array-like object contents, ignoring non-numeric properties. + while (size--) { + // Ensure commutative equality for sparse arrays. + if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (hasOwnProperty.call(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (hasOwnProperty.call(b, key) && !size--) break; + } + result = !size; } - result = size == sizeRight; } // Remove the first object from the stack of traversed objects. stack.pop(); From e9faa401082034b5ac6ce6dc3855db714451b747 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Mon, 5 Sep 2011 12:27:03 -0600 Subject: [PATCH 21/31] Add a comprehensive test suite for `isEqual`. --- test/objects.js | 294 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 224 insertions(+), 70 deletions(-) diff --git a/test/objects.js b/test/objects.js index bb0f98553..6fc60f1e4 100644 --- a/test/objects.js +++ b/test/objects.js @@ -65,78 +65,232 @@ $(document).ready(function() { }); test("objects: isEqual", function() { - function Foo(){ this.own = 'a'; } - function Bar(){ this.own = 'a'; } - Foo.prototype.inherited = 1; - Bar.prototype.inherited = 2; - - var moe = {name : 'moe', lucky : [13, 27, 34]}; - var clone = {name : 'moe', lucky : [13, 27, 34]}; + function First() { + this.value = 1; + } + First.prototype.value = 1; + function Second() { + this.value = 1; + } + Second.prototype.value = 2; + + // Basic equality and identity comparisons. + ok(_.isEqual(null, null), "`null` is equal to `null`"); + ok(_.isEqual(), "`undefined` is equal to `undefined`"); + + ok(!_.isEqual(0, -0), "`0` is not equal to `-0`"); + ok(!_.isEqual(-0, 0), "Commutative equality is implemented for `0` and `-0`"); + ok(!_.isEqual(null, undefined), "`null` is not equal to `undefined`"); + ok(!_.isEqual(undefined, null), "Commutative equality is implemented for `null` and `undefined`"); + + // String object and primitive comparisons. + ok(_.isEqual("Curly", "Curly"), "Identical string primitives are equal"); + ok(_.isEqual(new String("Curly"), new String("Curly")), "String objects with identical primitive values are equal"); + + ok(!_.isEqual("Curly", "Larry"), "String primitives with different values are not equal"); + ok(!_.isEqual(new String("Curly"), "Curly"), "String primitives and their corresponding object wrappers are not equal"); + ok(!_.isEqual("Curly", new String("Curly")), "Commutative equality is implemented for string objects and primitives"); + ok(!_.isEqual(new String("Curly"), new String("Larry")), "String objects with different primitive values are not equal"); + ok(!_.isEqual(new String("Curly"), {toString: function(){ return "Curly"; }}), "String objects and objects with a custom `toString` method are not equal"); + + // Number object and primitive comparisons. + ok(_.isEqual(75, 75), "Identical number primitives are equal"); + ok(_.isEqual(new Number(75), new Number(75)), "Number objects with identical primitive values are equal"); + + ok(!_.isEqual(75, new Number(75)), "Number primitives and their corresponding object wrappers are not equal"); + ok(!_.isEqual(new Number(75), 75), "Commutative equality is implemented for number objects and primitives"); + ok(!_.isEqual(new Number(75), new Number(63)), "Number objects with different primitive values are not equal"); + ok(!_.isEqual(new Number(63), {valueOf: function(){ return 63; }}), "Number objects and objects with a `valueOf` method are not equal"); + + // Comparisons involving `NaN`. + ok(_.isEqual(NaN, NaN), "`NaN` is equal to `NaN`"); + ok(!_.isEqual(61, NaN), "A number primitive is not equal to `NaN`"); + ok(!_.isEqual(new Number(79), NaN), "A number object is not equal to `NaN`"); + ok(!_.isEqual(Infinity, NaN), "`Infinity` is not equal to `NaN`"); + + // Boolean object and primitive comparisons. + ok(_.isEqual(true, true), "Identical boolean primitives are equal"); + ok(_.isEqual(new Boolean, new Boolean), "Boolean objects with identical primitive values are equal"); + ok(!_.isEqual(true, new Boolean(true)), "Boolean primitives and their corresponding object wrappers are not equal"); + ok(!_.isEqual(new Boolean(true), true), "Commutative equality is implemented for booleans"); + ok(!_.isEqual(new Boolean(true), new Boolean), "Boolean objects with different primitive values are not equal"); + + // Common type coercions. + ok(!_.isEqual(true, new Boolean(false)), "Boolean objects are not equal to the boolean primitive `true`"); + ok(!_.isEqual("75", 75), "String and number primitives with like values are not equal"); + ok(!_.isEqual(new Number(63), new String(63)), "String and number objects with like values are not equal"); + ok(!_.isEqual(75, "75"), "Commutative equality is implemented for like string and number values"); + ok(!_.isEqual(0, ""), "Number and string primitives with like values are not equal"); + ok(!_.isEqual(1, true), "Number and boolean primitives with like values are not equal"); + ok(!_.isEqual(new Boolean(false), new Number(0)), "Boolean and number objects with like values are not equal"); + ok(!_.isEqual(false, new String("")), "Boolean primitives and string objects with like values are not equal"); + ok(!_.isEqual(12564504e5, new Date(2009, 9, 25)), "Dates and their corresponding numeric primitive values are not equal"); + + // Dates. + ok(_.isEqual(new Date(2009, 9, 25), new Date(2009, 9, 25)), "Date objects referencing identical times are equal"); + ok(!_.isEqual(new Date(2009, 9, 25), new Date(2009, 11, 13)), "Date objects referencing different times are not equal"); + ok(!_.isEqual(new Date(2009, 11, 13), { + getTime: function(){ + return 12606876e5; + } + }), "Date objects and objects with a `getTime` method are not equal"); + ok(!_.isEqual(new Date("Curly"), new Date("Curly")), "Invalid dates are not equal"); + + // Functions. + ok(!_.isEqual(First, Second), "Different functions with identical bodies and source code representations are not equal"); + + // RegExps. + ok(_.isEqual(/(?:)/gim, /(?:)/gim), "RegExps with equivalent patterns and flags are equal"); + ok(!_.isEqual(/(?:)/g, /(?:)/gi), "RegExps with equivalent patterns and different flags are not equal"); + ok(!_.isEqual(/Moe/gim, /Curly/gim), "RegExps with different patterns and equivalent flags are not equal"); + ok(!_.isEqual(/(?:)/gi, /(?:)/g), "Commutative equality is implemented for RegExps"); + ok(!_.isEqual(/Curly/g, {source: "Larry", global: true, ignoreCase: false, multiline: false}), "RegExps and RegExp-like objects are not equal"); + + // Empty arrays, array-like objects, and object literals. + ok(_.isEqual({}, {}), "Empty object literals are equal"); + ok(_.isEqual([], []), "Empty array literals are equal"); + ok(_.isEqual([{}], [{}]), "Empty nested arrays and objects are equal"); + ok(_.isEqual({length: 0}, []), "Array-like objects and arrays are equal"); + ok(_.isEqual([], {length: 0}), "Commutative equality is implemented for array-like objects"); + + ok(!_.isEqual({}, []), "Object literals and array literals are not equal"); + ok(!_.isEqual([], {}), "Commutative equality is implemented for objects and arrays"); + + // Arrays with primitive and object values. + ok(_.isEqual([1, "Larry", true], [1, "Larry", true]), "Arrays containing identical primitives are equal"); + ok(_.isEqual([/Moe/g, new Date(2009, 9, 25)], [/Moe/g, new Date(2009, 9, 25)]), "Arrays containing equivalent elements are equal"); + + // Multi-dimensional arrays. + var a = [new Number(47), false, "Larry", /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]; + var b = [new Number(47), false, "Larry", /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]; + ok(_.isEqual(a, b), "Arrays containing nested arrays and objects are recursively compared"); + + // Overwrite the methods defined in ES 5.1 section 15.4.4. + a.forEach = a.map = a.filter = a.every = a.indexOf = a.lastIndexOf = a.some = a.reduce = a.reduceRight = null; + b.join = b.pop = b.reverse = b.shift = b.slice = b.splice = b.concat = b.sort = b.unshift = null; + + // Array elements and properties. + ok(_.isEqual(a, b), "Arrays containing equivalent elements and different non-numeric properties are equal"); + a.push("White Rocks"); + ok(!_.isEqual(a, b), "Arrays of different lengths are not equal"); + a.push("East Boulder"); + b.push("Gunbarrel Ranch", "Teller Farm"); + ok(!_.isEqual(a, b), "Arrays of identical lengths containing different elements are not equal"); + + // Sparse arrays. + ok(_.isEqual(Array(3), Array(3)), "Sparse arrays of identical lengths are equal"); + ok(!_.isEqual(Array(3), Array(6)), "Sparse arrays of different lengths are not equal"); + + // According to the Microsoft deviations spec, section 2.1.26, JScript 5.x treats `undefined` + // elements in arrays as elisions. Thus, sparse arrays and dense arrays containing `undefined` + // values are equivalent. + if (0 in [undefined]) { + ok(!_.isEqual(Array(3), [undefined, undefined, undefined]), "Sparse and dense arrays are not equal"); + ok(!_.isEqual([undefined, undefined, undefined], Array(3)), "Commutative equality is implemented for sparse and dense arrays"); + } + + // Simple objects. + ok(_.isEqual({a: "Curly", b: 1, c: true}, {a: "Curly", b: 1, c: true}), "Objects containing identical primitives are equal"); + ok(_.isEqual({a: /Curly/g, b: new Date(2009, 11, 13)}, {a: /Curly/g, b: new Date(2009, 11, 13)}), "Objects containing equivalent members are equal"); + ok(!_.isEqual({a: 63, b: 75}, {a: 61, b: 55}), "Objects of identical sizes with different values are not equal"); + ok(!_.isEqual({a: 63, b: 75}, {a: 61, c: 55}), "Objects of identical sizes with different property names are not equal"); + ok(!_.isEqual({a: 1, b: 2}, {a: 1}), "Objects of different sizes are not equal"); + ok(!_.isEqual({a: 1}, {a: 1, b: 2}), "Commutative equality is implemented for objects"); + ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), "Objects with identical keys and different values are not equivalent"); + + // `A` contains nested objects and arrays. + a = { + name: new String("Moe Howard"), + age: new Number(77), + stooge: true, + hobbies: ["acting"], + film: { + name: "Sing a Song of Six Pants", + release: new Date(1947, 9, 30), + stars: [new String("Larry Fine"), "Shemp Howard"], + minutes: new Number(16), + seconds: 54 + } + }; + + // `B` contains equivalent nested objects and arrays. + b = { + name: new String("Moe Howard"), + age: new Number(77), + stooge: true, + hobbies: ["acting"], + film: { + name: "Sing a Song of Six Pants", + release: new Date(1947, 9, 30), + stars: [new String("Larry Fine"), "Shemp Howard"], + minutes: new Number(16), + seconds: 54 + } + }; + ok(_.isEqual(a, b), "Objects with nested equivalent members are recursively compared"); + + // Instances. + ok(_.isEqual(new First, new First), "Object instances are equal"); + ok(_.isEqual(new First, new Second), "Objects with different constructors and identical own properties are equal"); + ok(_.isEqual({value: 1}, new First), "Object instances and objects sharing equivalent properties are identical"); + ok(!_.isEqual({value: 2}, new Second), "The prototype chain of objects should not be examined"); + + // Circular Arrays. + (a = []).push(a); + (b = []).push(b); + ok(_.isEqual(a, b), "Arrays containing circular references are equal"); + a.push(new String("Larry")); + b.push(new String("Larry")); + ok(_.isEqual(a, b), "Arrays containing circular references and equivalent properties are equal"); + a.push("Shemp"); + b.push("Curly"); + ok(!_.isEqual(a, b), "Arrays containing circular references and different properties are not equal"); + + // Circular Objects. + a = {abc: null}; + b = {abc: null}; + a.abc = a; + b.abc = b; + ok(_.isEqual(a, b), "Objects containing circular references are equal"); + a.def = 75; + b.def = 75; + ok(_.isEqual(a, b), "Objects containing circular references and equivalent properties are equal"); + a.def = new Number(75); + b.def = new Number(63); + ok(!_.isEqual(a, b), "Objects containing circular references and different properties are not equal"); + + // Cyclic Structures. + a = [{abc: null}]; + b = [{abc: null}]; + (a[0].abc = a).push(a); + (b[0].abc = b).push(b); + ok(_.isEqual(a, b), "Cyclic structures are equal"); + a[0].def = "Larry"; + b[0].def = "Larry"; + ok(_.isEqual(a, b), "Cyclic structures containing equivalent properties are equal"); + a[0].def = new String("Larry"); + b[0].def = new String("Curly"); + ok(!_.isEqual(a, b), "Cyclic structures containing different properties are not equal"); + + // Complex Circular References. + a = {foo: {b: {foo: {c: {foo: null}}}}}; + b = {foo: {b: {foo: {c: {foo: null}}}}}; + a.foo.b.foo.c.foo = a; + b.foo.b.foo.c.foo = b; + ok(_.isEqual(a, b), "Cyclic structures with nested and identically-named properties are equal"); + + // Chaining. + ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'Chained objects containing different values are not equal'); + equals(_({x: 1, y: 2}).chain().isEqual(_({x: 1, y: 2}).chain()).value(), true, '`isEqual` can be chained'); + + // Custom `isEqual` methods. var isEqualObj = {isEqual: function (o) { return o.isEqual == this.isEqual; }, unique: {}}; var isEqualObjClone = {isEqual: isEqualObj.isEqual, unique: {}}; - ok(moe != clone, 'basic equality between objects is false'); - ok(_.isEqual(moe, clone), 'deep equality is true'); - ok(_(moe).isEqual(clone), 'OO-style deep equality works'); - ok(!_.isEqual(5, NaN), '5 is not equal to NaN'); - ok(NaN != NaN, 'NaN is not equal to NaN (native equality)'); - ok(NaN !== NaN, 'NaN is not equal to NaN (native identity)'); - ok(_.isEqual(NaN, NaN), 'NaN is equal to NaN'); - ok(!_.isEqual(5, NaN), '`5` is not equal to `NaN`'); - ok(!_.isEqual(false, NaN), '`false` is not equal to `NaN`'); - ok(_.isEqual(new Date(100), new Date(100)), 'identical dates are equal'); - ok(_.isEqual((/hello/ig), (/hello/ig)), 'identical regexes are equal'); - ok(!_.isEqual({source: '(?:)', global: true, multiline: true, ignoreCase: true}, /(?:)/gim), 'RegExp-like objects and RegExps are not equal'); - ok(!_.isEqual(null, [1]), 'a falsy is never equal to a truthy'); - ok(!_.isEqual(undefined, null), '`undefined` is not equal to `null`'); - ok(_.isEqual(isEqualObj, isEqualObj), 'both objects implement `isEqual`, same objects'); - ok(_.isEqual(isEqualObj, isEqualObjClone), 'both objects implement `isEqual`, different objects'); - ok(_.isEqual(isEqualObjClone, isEqualObj), 'both objects implement `isEqual`, different objects, swapped'); - ok(!_.isEqual(isEqualObj, {}), 'first object implements `isEqual`'); - ok(!_.isEqual({}, isEqualObj), 'second object implements `isEqual`'); - ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'objects with the same number of undefined keys are not equal'); - ok(!_.isEqual(new Foo, new Bar), 'objects with different inherited properties are not equal'); - ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'wrapped objects are not equal'); - equals(_({x: 1, y: 2}).chain().isEqual(_({x: 1, y: 2}).chain()).value(), true, 'wrapped objects are equal'); - - // Objects with circular references. - var circularA = {'abc': null}, circularB = {'abc': null}; - circularA.abc = circularA; - circularB.abc = circularB; - ok(_.isEqual(circularA, circularB), 'objects with a circular reference'); - circularA.def = 1; - circularB.def = 1; - ok(_.isEqual(circularA, circularB), 'objects with identical properties and a circular reference'); - circularA.def = 1; - circularB.def = 0; - ok(!_.isEqual(circularA, circularB), 'objects with different properties and a circular reference'); - - // Arrays with circular references. - circularA = []; - circularB = []; - circularA.push(circularA); - circularB.push(circularB); - ok(_.isEqual(circularA, circularB), 'arrays with a circular reference'); - circularA.push('abc'); - circularB.push('abc'); - ok(_.isEqual(circularA, circularB), 'arrays with identical indices and a circular reference'); - circularA.push('hello'); - circularB.push('goodbye'); - ok(!_.isEqual(circularA, circularB), 'arrays with different properties and a circular reference'); - - // Hybrid cyclic structures. - circularA = [{'abc': null}]; - circularB = [{'abc': null}]; - circularA[0].abc = circularA; - circularB[0].abc = circularB; - circularA.push(circularA); - circularB.push(circularB); - ok(_.isEqual(circularA, circularB), 'cyclic structure'); - circularA[0].def = 1; - circularB[0].def = 1; - ok(_.isEqual(circularA, circularB), 'cyclic structure with identical properties'); - circularA[0].def = 1; - circularB[0].def = 0; - ok(!_.isEqual(circularA, circularB), 'cyclic structure with different properties'); + + ok(_.isEqual(isEqualObj, isEqualObjClone), 'Both objects implement identical `isEqual` methods'); + ok(_.isEqual(isEqualObjClone, isEqualObj), 'Commutative equality is implemented for objects with custom `isEqual` methods'); + ok(!_.isEqual(isEqualObj, {}), 'Objects that do not implement equivalent `isEqual` methods are not equal'); + ok(!_.isEqual({}, isEqualObj), 'Commutative equality is implemented for objects with different `isEqual` methods'); }); test("objects: isEmpty", function() { From 54245bc679d448427bebd71a8608283476b2d752 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Mon, 5 Sep 2011 12:34:09 -0600 Subject: [PATCH 22/31] `_.isEqual`: Add an early comparison for `NaN` values. --- underscore.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/underscore.js b/underscore.js index 825e9405d..46f0ef685 100644 --- a/underscore.js +++ b/underscore.js @@ -605,12 +605,14 @@ if (typeA != typeof b) return false; // Optimization; ensure that both values are truthy or falsy. if (!a != !b) return false; + // `NaN` values are equal. + if (_.isNaN(a)) return _.isNaN(b); // Compare string objects by value. var isStringA = _.isString(a), isStringB = _.isString(b); if (isStringA || isStringB) return isStringA && isStringB && String(a) == String(b); - // Compare number objects by value. `NaN` values are equal. - var isNumberA = toString.call(a) == '[object Number]', isNumberB = toString.call(b) == '[object Number]'; - if (isNumberA || isNumberB) return isNumberA && isNumberB && (_.isNaN(a) ? _.isNaN(b) : +a == +b); + // Compare number objects by value. + var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b); + if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b; // Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0. var isBooleanA = toString.call(a) == '[object Boolean]', isBooleanB = toString.call(b) == '[object Boolean]'; if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b; From 4fa97eb2fa7c387913bead871767912ab22d5ee2 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Mon, 5 Sep 2011 15:51:09 -0600 Subject: [PATCH 23/31] `_.isBoolean` should return `true` for boolean object wrappers. --- test/objects.js | 6 +++--- underscore.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/objects.js b/test/objects.js index 6fc60f1e4..95e5ab829 100644 --- a/test/objects.js +++ b/test/objects.js @@ -318,14 +318,14 @@ $(document).ready(function() { parent.iElement = document.createElement('div');\ parent.iArguments = (function(){ return arguments; })(1, 2, 3);\ parent.iArray = [1, 2, 3];\ - parent.iString = 'hello';\ - parent.iNumber = 100;\ + parent.iString = new String('hello');\ + parent.iNumber = new Number(100);\ parent.iFunction = (function(){});\ parent.iDate = new Date();\ parent.iRegExp = /hi/;\ parent.iNaN = NaN;\ parent.iNull = null;\ - parent.iBoolean = false;\ + parent.iBoolean = new Boolean(false);\ parent.iUndefined = undefined;\ " ); diff --git a/underscore.js b/underscore.js index 46f0ef685..e2a2fb260 100644 --- a/underscore.js +++ b/underscore.js @@ -614,7 +614,7 @@ var isNumberA = _.isNumber(a), isNumberB = _.isNumber(b); if (isNumberA || isNumberB) return isNumberA && isNumberB && +a == +b; // Compare boolean objects by value. The value of `true` is 1; the value of `false` is 0. - var isBooleanA = toString.call(a) == '[object Boolean]', isBooleanB = toString.call(b) == '[object Boolean]'; + var isBooleanA = _.isBoolean(a), isBooleanB = _.isBoolean(b); if (isBooleanA || isBooleanB) return isBooleanA && isBooleanB && +a == +b; // Compare dates by their millisecond values. var isDateA = _.isDate(a), isDateB = _.isDate(b); @@ -738,7 +738,7 @@ // Is a given value a boolean? _.isBoolean = function(obj) { - return obj === true || obj === false; + return obj === true || obj === false || typeof obj == 'object' && toString.call(obj) == '[object Boolean]'; }; // Is a given value a date? From 1facc0e4fee98df915d0ecd8f20ce56482ba6875 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 4 Oct 2011 15:56:26 -0400 Subject: [PATCH 24/31] merging in Tim Smart's gorgeous deep equality patch for _.isEqual --- underscore.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/underscore.js b/underscore.js index fbd8e423b..fcd3d09c9 100644 --- a/underscore.js +++ b/underscore.js @@ -631,22 +631,21 @@ if (isDateA || isDateB) return isDateA && isDateB && a.getTime() == b.getTime(); // Compare RegExps by their source patterns and flags. var isRegExpA = _.isRegExp(a), isRegExpB = _.isRegExp(b); - if (isRegExpA || isRegExpB) + if (isRegExpA || isRegExpB) { // Ensure commutative equality for RegExps. return isRegExpA && isRegExpB && a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; + } // Ensure that both values are objects. if (typeA != 'object') return false; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. - if (typeof a.isEqual == 'function') return a.isEqual(b); - // If only `b` provides an `isEqual` method, `a` and `b` are not equal. - if (typeof b.isEqual == 'function') return false; + if (_.isFunction(a.isEqual)) return a.isEqual(b); // Assume equality for cyclic structures. The algorithm for detecting cyclic structures is // adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = stack.length; @@ -748,7 +747,7 @@ // Is a given value a boolean? _.isBoolean = function(obj) { - return obj === true || obj === false || typeof obj == 'object' && toString.call(obj) == '[object Boolean]'; + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; }; // Is a given value a date? From 7d0e4169a9453da3c664f82327840a861244c574 Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 4 Oct 2011 15:56:32 -0400 Subject: [PATCH 25/31] shortening module names. --- test/arrays.js | 2 +- test/chaining.js | 2 +- test/collections.js | 2 +- test/functions.js | 2 +- test/objects.js | 2 +- test/utility.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/arrays.js b/test/arrays.js index 78cf098a0..2d69235c4 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Array-only functions (last, compact, uniq, and so on...)"); + module("Arrays"); test("arrays: first", function() { equals(_.first([1,2,3]), 1, 'can pull out the first element of an array'); diff --git a/test/chaining.js b/test/chaining.js index e633ba5ad..64b0500ef 100644 --- a/test/chaining.js +++ b/test/chaining.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Underscore chaining."); + module("Chaining"); test("chaining: map/flatten/reduce", function() { var lyrics = [ diff --git a/test/collections.js b/test/collections.js index 005ee169e..95e111591 100644 --- a/test/collections.js +++ b/test/collections.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Collection functions (each, any, select, and so on...)"); + module("Collections"); test("collections: each", function() { _.each([1, 2, 3], function(num, i) { diff --git a/test/functions.js b/test/functions.js index 2a1bd51bd..af35e5eff 100644 --- a/test/functions.js +++ b/test/functions.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Function functions (bind, bindAll, and so on...)"); + module("Functions"); test("functions: bind", function() { var context = {name : 'moe'}; diff --git a/test/objects.js b/test/objects.js index ded65c317..e05c0ddc5 100644 --- a/test/objects.js +++ b/test/objects.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Object functions (values, extend, isEqual, and so on...)"); + module("Objects"); test("objects: keys", function() { var exception = /object/; diff --git a/test/utility.js b/test/utility.js index 94252a654..58368a13e 100644 --- a/test/utility.js +++ b/test/utility.js @@ -1,6 +1,6 @@ $(document).ready(function() { - module("Utility functions (uniqueId, template)"); + module("Utility"); test("utility: noConflict", function() { var underscore = _.noConflict(); From 348c93515cf56263828c683ebab055e6c800b63b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Tue, 4 Oct 2011 17:23:55 -0400 Subject: [PATCH 26/31] Issue #272 ... min and max of empty objects. --- test/collections.js | 6 ++++++ underscore.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/test/collections.js b/test/collections.js index 90cdd3d87..cf1c815d6 100644 --- a/test/collections.js +++ b/test/collections.js @@ -177,6 +177,9 @@ $(document).ready(function() { var neg = _.max([1, 2, 3], function(num){ return -num; }); equals(neg, 1, 'can perform a computation-based max'); + + equals(-Infinity, _.max({}), 'Maximum value of an empty object'); + equals(-Infinity, _.max([]), 'Maximum value of an empty array'); }); test('collections: min', function() { @@ -184,6 +187,9 @@ $(document).ready(function() { var neg = _.min([1, 2, 3], function(num){ return -num; }); equals(neg, 3, 'can perform a computation-based min'); + + equals(Infinity, _.min({}), 'Minimum value of an empty object'); + equals(Infinity, _.min([]), 'Minimum value of an empty array'); }); test('collections: sortBy', function() { diff --git a/underscore.js b/underscore.js index f59d040c7..0db66d0b6 100644 --- a/underscore.js +++ b/underscore.js @@ -220,6 +220,7 @@ // Return the maximum element or (element-based computation). _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return -Infinity; var result = {computed : -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; @@ -231,6 +232,7 @@ // Return the minimum element (or element-based computation). _.min = function(obj, iterator, context) { if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + if (!iterator && _.isEmpty(obj)) return Infinity; var result = {computed : Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; From 6d9d071b2f3c0d375517163005ff27a04383c4d6 Mon Sep 17 00:00:00 2001 From: Pier Paolo Ramon Date: Wed, 5 Oct 2011 14:06:18 +0200 Subject: [PATCH 27/31] Implemented _.init as per #319 --- underscore.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/underscore.js b/underscore.js index 0db66d0b6..dfc875d3a 100644 --- a/underscore.js +++ b/underscore.js @@ -323,6 +323,14 @@ return slice.call(array, (index == null) || guard ? 1 : index); }; + // Returns everything but the last entry of the array. Especcialy useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.init = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { From dcda142655619b92ba2f3cc8b904d473ad426e29 Mon Sep 17 00:00:00 2001 From: Pier Paolo Ramon Date: Wed, 5 Oct 2011 14:14:51 +0200 Subject: [PATCH 28/31] Tests for _.init --- test/arrays.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/arrays.js b/test/arrays.js index 02e282b8a..3d5cc3327 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -24,6 +24,15 @@ $(document).ready(function() { equals(_.flatten(result).join(','), '2,3,2,3', 'works well with _.map'); }); + test("arrays: init", function() { + equals(_.init([1,2,3,4,5]).join(", "), "1, 2, 3, 4", 'working init()'); + equals(_.init([1,2,3,4],2).join(", "), "1, 2", 'init can take an index'); + var result = (function(){ return _(arguments).init(); })(1, 2, 3, 4); + equals(result.join(", "), "1, 2, 3", 'init works on arguments object'); + result = _.map([[1,2,3],[1,2,3]], _.init); + equals(_.flatten(result).join(','), '1,2,1,2', 'init works with _.map'); + }); + test("arrays: last", function() { equals(_.last([1,2,3]), 3, 'can pull out the last element of an array'); equals(_.last([1,2,3], 0).join(', '), "", 'can pass an index to last'); From ac191a28a54302613d706af1f04236af8f195e5b Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 5 Oct 2011 15:32:34 -0400 Subject: [PATCH 29/31] merging in #324 as _.initial --- test/arrays.js | 14 +++++++------- underscore.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/arrays.js b/test/arrays.js index 3d5cc3327..80da71f6f 100644 --- a/test/arrays.js +++ b/test/arrays.js @@ -24,13 +24,13 @@ $(document).ready(function() { equals(_.flatten(result).join(','), '2,3,2,3', 'works well with _.map'); }); - test("arrays: init", function() { - equals(_.init([1,2,3,4,5]).join(", "), "1, 2, 3, 4", 'working init()'); - equals(_.init([1,2,3,4],2).join(", "), "1, 2", 'init can take an index'); - var result = (function(){ return _(arguments).init(); })(1, 2, 3, 4); - equals(result.join(", "), "1, 2, 3", 'init works on arguments object'); - result = _.map([[1,2,3],[1,2,3]], _.init); - equals(_.flatten(result).join(','), '1,2,1,2', 'init works with _.map'); + test("arrays: initial", function() { + equals(_.initial([1,2,3,4,5]).join(", "), "1, 2, 3, 4", 'working initial()'); + equals(_.initial([1,2,3,4],2).join(", "), "1, 2", 'initial can take an index'); + var result = (function(){ return _(arguments).initial(); })(1, 2, 3, 4); + equals(result.join(", "), "1, 2, 3", 'initial works on arguments object'); + result = _.map([[1,2,3],[1,2,3]], _.initial); + equals(_.flatten(result).join(','), '1,2,1,2', 'initial works with _.map'); }); test("arrays: last", function() { diff --git a/underscore.js b/underscore.js index dfc875d3a..e183d070a 100644 --- a/underscore.js +++ b/underscore.js @@ -327,7 +327,7 @@ // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. The **guard** check allows it to work with // `_.map`. - _.init = function(array, n, guard) { + _.initial = function(array, n, guard) { return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); }; From cc6a9d494d77ff910064d17719848445c05642ee Mon Sep 17 00:00:00 2001 From: Jeremy Ashkenas Date: Wed, 5 Oct 2011 16:19:00 -0400 Subject: [PATCH 30/31] Merging in escaping for Underscore templates, using <%- syntax. Sorry Eco. --- test/utility.js | 4 ++++ underscore.js | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/test/utility.js b/test/utility.js index 58368a13e..976b3b996 100644 --- a/test/utility.js +++ b/test/utility.js @@ -81,6 +81,10 @@ $(document).ready(function() { var withNewlinesAndTabs = _.template('This\n\t\tis: <%= x %>.\n\tok.\nend.'); equals(withNewlinesAndTabs({x: 'that'}), 'This\n\t\tis: that.\n\tok.\nend.'); + var template = _.template("<%- value %>"); + var result = template({value: "