Permalink
Browse files

merging in #351, @kitcambridge's continued quest to implement _.isEqual

  • Loading branch information...
2 parents 135f93e + efd29e1 commit 2a697e4bf528e2cc7f2634c0f879d795f00561f8 @jashkenas jashkenas committed Nov 14, 2011
Showing with 68 additions and 56 deletions.
  1. +10 −8 test/objects.js
  2. +58 −48 underscore.js
View
@@ -94,19 +94,21 @@ $(document).ready(function() {
// 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(new String("Curly"), "Curly"), "String primitives and their corresponding object wrappers are equal");
+ ok(_.isEqual("Curly", new String("Curly")), "Commutative equality is implemented for string objects and primitives");
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 equal");
+ ok(_.isEqual(new Number(75), 75), "Commutative equality is implemented for number objects and primitives");
+ ok(!_.isEqual(new Number(0), -0), "`new Number(0)` and `-0` are not equal");
+ ok(!_.isEqual(0, new Number(-0)), "Commutative equality is implemented for `new Number(0)` and `-0`");
- 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");
@@ -119,8 +121,8 @@ $(document).ready(function() {
// 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(true, new Boolean(true)), "Boolean primitives and their corresponding object wrappers are 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.
@@ -178,7 +180,7 @@ $(document).ready(function() {
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 not equal");
+ 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");
@@ -240,7 +242,7 @@ $(document).ready(function() {
// 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 not equal");
- ok(!_.isEqual({value: 1}, new First), "Object instances and objects sharing equivalent properties are not identical");
+ ok(!_.isEqual({value: 1}, new First), "Object instances and objects sharing equivalent properties are not equal");
ok(!_.isEqual({value: 2}, new Second), "The prototype chain of objects should not be examined");
// Circular Arrays.
View
@@ -672,48 +672,42 @@
// 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) || (b == null)) return a === b;
+ if (a == null || b == null) return a === b;
// 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 (_.isFunction(a.isEqual)) return a.isEqual(b);
if (_.isFunction(b.isEqual)) return b.isEqual(a);
- // Compare object types.
- var typeA = typeof a;
- 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.
- 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 = _.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);
- 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) {
- // Ensure commutative equality for RegExps.
- return isRegExpA && isRegExpB &&
- a.source == b.source &&
- a.global == b.global &&
- a.multiline == b.multiline &&
- a.ignoreCase == b.ignoreCase;
+ // Compare `[[Class]]` names.
+ var className = toString.call(a);
+ if (className != toString.call(b)) return false;
+ switch (className) {
+ // Strings, numbers, dates, and booleans are compared by value.
+ case '[object String]':
+ // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+ // equivalent to `new String("5")`.
+ return String(a) == String(b);
+ case '[object Number]':
+ a = +a;
+ b = +b;
+ // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+ // other numeric values.
+ return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b);
+ case '[object Date]':
+ case '[object Boolean]':
+ // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+ // millisecond representations. Note that invalid dates with millisecond representations
+ // of `NaN` are not equivalent.
+ return +a == +b;
+ // RegExps are compared by their source patterns and flags.
+ case '[object RegExp]':
+ return 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;
- // Arrays or Arraylikes with different lengths are not equal.
- if (a.length !== b.length) return false;
- // Objects with different constructors are not equal.
- if (a.constructor !== b.constructor) return false;
+ if (typeof a != 'object' || typeof b != 'object') 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;
@@ -725,21 +719,37 @@
// Add the first object to the stack of traversed objects.
stack.push(a);
var size = 0, result = true;
- // 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;
+ // Recursively compare objects and arrays.
+ if (className == '[object Array]') {
+ // Compare array lengths to determine if a deep comparison is necessary.
+ size = a.length;
+ result = size == b.length;
+ if (result) {
+ // Deep compare the 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;
+ }
}
- }
- // Ensure that both objects contain the same number of properties.
- if (result) {
- for (key in b) {
- if (hasOwnProperty.call(b, key) && !(size--)) break;
+ } else {
+ // Objects with different constructors are not equivalent.
+ if ("constructor" in a != "constructor" in b || a.constructor != b.constructor) return false;
+ // 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;
}
// Remove the first object from the stack of traversed objects.
stack.pop();

0 comments on commit 2a697e4

Please sign in to comment.