From 057633ba9fd6e02a0b4652ab2773209d19eb48c9 Mon Sep 17 00:00:00 2001 From: andrew brown Date: Wed, 20 Apr 2016 13:55:51 -0700 Subject: [PATCH 1/4] Implemented _.isEq() method. _.isEq(a, b) performs a SameValueZero comparison on values a and b. http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero --- underscore.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/underscore.js b/underscore.js index 1630b7286..a5b5b4e2d 100644 --- a/underscore.js +++ b/underscore.js @@ -1264,6 +1264,13 @@ return eq(a, b); }; + // Performs a SameValueZero comparison (http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + // to check for equality between two values. + // Differs from _.isEqual() in that 0 and -0 are considered equal by _.eq(). + _.isEq = function(a, b) { + return a === b || (a !== a && b !== b); + }; + // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { From 2e5386264a45b1d0c1a621ceef4adec4ed9229be Mon Sep 17 00:00:00 2001 From: andrew brown Date: Thu, 21 Apr 2016 15:08:05 -0700 Subject: [PATCH 2/4] Modified _.isEqual() to use SameValueZero logic when comparing sets and maps. Implemented an internal isEq function that is called by deepEq when testing equality of key values for maps or sets. 0 and -0 should be considered equal for set and keys of maps (but not values) which is why this change was needed. --- underscore.js | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/underscore.js b/underscore.js index a5b5b4e2d..bccdc01db 100644 --- a/underscore.js +++ b/underscore.js @@ -1154,7 +1154,7 @@ // Internal recursive comparison function for `isEqual`. - var eq, deepEq; + var eq, isEq, deepEq; eq = function(a, b, aStack, bStack) { // 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). @@ -1169,6 +1169,20 @@ return deepEq(a, b, aStack, bStack); }; + // Internal recursive comparison function for `isEqual`. + isEq = function(a, b, aStack, bStack) { + // Performs a SameValueZero comparison to check for equality between two values. + // http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero + // Differs from eq in that 0 and -0 will be considered equal by isEq. + // Used in deepEq for testing Map and Set equality. + if (a === b) return a === b || (a !== a && b !== b); + if (a == null || b == null) return a === b; + if (a !== a) return b !== b; + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + }; + // Internal recursive comparison function for `isEqual`. deepEq = function(a, b, aStack, bStack) { // Unwrap any wrapped objects. @@ -1205,8 +1219,8 @@ if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; - // Objects with different constructors are not equivalent, but `Object`s or `Array`s - // from different frames are. + // Objects with different constructors are not equivalent, but `Object`s, `Array`s, + // `Map`s, or `Set`s from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) @@ -1218,7 +1232,7 @@ // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. - // It's done here since we only need them for objects and arrays comparison. + // It's done here since we only need them for objects, arrays, maps, and sets comparison. aStack = aStack || []; bStack = bStack || []; var length = aStack.length; @@ -1232,7 +1246,7 @@ aStack.push(a); bStack.push(b); - // Recursively compare objects and arrays. + // Recursively compare objects, arrays, maps and sets. if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; @@ -1241,7 +1255,7 @@ while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } - } else { + } else if (className === '[object Object]') { // Deep compare objects. var keys = _.keys(a), key; length = keys.length; @@ -1252,7 +1266,21 @@ key = keys[length]; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } + } else { + // Deep compare sets and maps. + var size = a.size; + // Ensure that both objects are of the same size before comparing deep equality. + if (b.size !== size) return false; + while (size--) { + // Deep compare the keys of each member, using SameValueZero (isEq) for the keys + if (!(isEq(a.keys().next().value, b.keys().next().value, aStack, bStack))) return false; + // If the objects are maps deep compare the values. Value equality does not use SameValueZero. + if (className === '[object Map]') { + if (!(eq(a.values().next().value, b.values().next().value, aStack, bStack))) return false; + } + } } + // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); @@ -1264,13 +1292,6 @@ return eq(a, b); }; - // Performs a SameValueZero comparison (http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) - // to check for equality between two values. - // Differs from _.isEqual() in that 0 and -0 are considered equal by _.eq(). - _.isEq = function(a, b) { - return a === b || (a !== a && b !== b); - }; - // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { From c05eefecc3d0ef71608733f61ec071533e643889 Mon Sep 17 00:00:00 2001 From: andrew brown Date: Thu, 21 Apr 2016 15:40:48 -0700 Subject: [PATCH 3/4] Added test cases for sets and maps with keys of 0 and -0. Added strings with test case purpose to a couple of tests that were missing them. --- test/objects.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/test/objects.js b/test/objects.js index 91c49a4cc..1e3bc2edf 100644 --- a/test/objects.js +++ b/test/objects.js @@ -559,12 +559,20 @@ var other = {a: 1}; assert.strictEqual(_.isEqual(new Foo, other), false, 'Objects from different constructors are not equal'); - // Tricky object cases val comparisions - assert.equal(_.isEqual([0], [-0]), false); - assert.equal(_.isEqual({a: 0}, {a: -0}), false); - assert.equal(_.isEqual([NaN], [NaN]), true); - assert.equal(_.isEqual({a: NaN}, {a: NaN}), true); + assert.equal(_.isEqual([0], [-0]), false, '0 and -0 are not equal as array values'); + assert.equal(_.isEqual({a: 0}, {a: -0}), false, '0 and -0 are not equal as object values'); + assert.equal(_.isEqual([NaN], [NaN]), true, 'NaN and NaN are equal as array values'); + assert.equal(_.isEqual({a: NaN}, {a: NaN}), true, 'NaN and NaN are equal as object values'); + + // SameValueZero tests for isEq function used for sets and maps + var set0 = new Set().add(0); + var setNeg0 = new Set().add(-0); + var map0 = new Map().set(0, 0); + var mapNeg0 = new Map().set(-0, 0); + assert.equal(_.isEqual(set0, setNeg0), true, 'In sets 0 and -0 are equal'); + assert.equal(_.isEqual(map0, mapNeg0), true, 'In maps keys of 0 and -0 are equal'); + if (typeof Symbol !== 'undefined') { var symbol = Symbol('x'); From f4c98592a2e9625d742cc91cb2a931a6832d5a30 Mon Sep 17 00:00:00 2001 From: andrew brown Date: Tue, 3 May 2016 09:03:23 -0700 Subject: [PATCH 4/4] Modified test.js to guard against lack of support for Set and Map in the module.js used by Travis CI --- test/objects.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/test/objects.js b/test/objects.js index 1e3bc2edf..8505dbbc2 100644 --- a/test/objects.js +++ b/test/objects.js @@ -566,12 +566,16 @@ assert.equal(_.isEqual({a: NaN}, {a: NaN}), true, 'NaN and NaN are equal as object values'); // SameValueZero tests for isEq function used for sets and maps - var set0 = new Set().add(0); - var setNeg0 = new Set().add(-0); - var map0 = new Map().set(0, 0); - var mapNeg0 = new Map().set(-0, 0); - assert.equal(_.isEqual(set0, setNeg0), true, 'In sets 0 and -0 are equal'); - assert.equal(_.isEqual(map0, mapNeg0), true, 'In maps keys of 0 and -0 are equal'); + if (typeof Set !== 'undefined') { + var set0 = new Set().add(0); + var setNeg0 = new Set().add(-0); + assert.equal(_.isEqual(set0, setNeg0), true, 'In sets 0 and -0 are equal'); + } + if (typeof Map !== 'undefined') { + var map0 = new Map().set(0, 0); + var mapNeg0 = new Map().set(-0, 0); + assert.equal(_.isEqual(map0, mapNeg0), true, 'In maps keys of 0 and -0 are equal'); + } if (typeof Symbol !== 'undefined') {