From b9307163b974991885b50e3ac09b54bd00a06153 Mon Sep 17 00:00:00 2001
From: Alfredo Mesen
Date: Tue, 3 May 2011 21:09:54 -0600
Subject: [PATCH 01/30] 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 @@ _.without([1, 2, 1, 0, 3, 1, 4], 0, 1);
- _.uniq(array, [isSorted])
+ _.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/30] 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/30] `_.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/30] `_.isEqual`: Move the internal `eq` method into the
main closure. Remove strict type checking.
---
underscore.js | 113 +++++++++++++++++++++++++-------------------------
1 file changed, 56 insertions(+), 57 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;
- }
- }
- // Remove the object from the stack of traversed objects.
- stack.pop();
- return result;
+ // 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;
}
- // Expose the recursive `isEqual` method.
- return function(a, b) {
- return eq(a, b, []);
- };
- })();
+ // 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;
+ }
+
+ // 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/30] `_.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/30] `_.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/30] `_.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/30] `_.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 c7c57ca6ff0a7d175913e4c040450f5a15a9bdaa Mon Sep 17 00:00:00 2001
From: Michael Ficarra
Date: Fri, 15 Jul 2011 18:15:10 -0400
Subject: [PATCH 09/30] _.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 10/30] * 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 11/30] 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 @@ _([1, 2, 3]).value();
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 12/30] 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 @@ _([1, 2, 3]).value();
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 13/30] 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 @@ _([1, 2, 3]).value();
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 14/30] 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 @@
any, include,
invoke, pluck,
max, min,
- sortBy, groupBy, sortedIndex,
+ sortBy, groupBy,
+ sortedIndex, shuffle,
toArray, size
@@ -464,6 +466,25 @@ _.sortedIndex([10, 20, 30, 40, 50], 35);
=> 3
+
+ _.shuffle(list)
+
+ Returns a shuffled list.
+
+
+_.shuffle([1, 2, 3, 4, 5, 6]);
+=>
+(reshuffle)
+
+
+
_.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 15/30] Remove dupe inclusion of underscore-min.js. Get rid of
'reshuffle' in index.html.
---
index.html | 14 ++------------
1 file changed, 2 insertions(+), 12 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 @@ _.sortedIndex([10, 20, 30, 40, 50], 35);
_.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(list)
From a8f0445192aeac118b3f347a3abfc7bf5af51fd3 Mon Sep 17 00:00:00 2001
From: Malcolm Locke
Date: Wed, 31 Aug 2011 22:39:05 +1200
Subject: [PATCH 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] Add a comprehensive test suite for `isEqual`.
---
test/objects.js | 290 ++++++++++++++++++++++++++++++++++++------------
1 file changed, 222 insertions(+), 68 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;
+ function First() {
+ this.value = 1;
+ }
+ First.prototype.value = 1;
+ function Second() {
+ this.value = 1;
+ }
+ Second.prototype.value = 2;
- var moe = {name : 'moe', lucky : [13, 27, 34]};
- var clone = {name : 'moe', lucky : [13, 27, 34]};
+ // 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 21/30] `_.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 22/30] `_.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 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 28/30] 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 29/30] 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: "