From e30a20120cf6c78aca2e2ed41d54949b683a37a5 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Wed, 26 Aug 2015 08:27:16 -0700 Subject: [PATCH] Split out `_.sortedIndexOf`, `_.sortedLastIndexOf`, `_.sortedUniq`, and `_.sortedUniqBy`. --- lodash.js | 238 ++++++++++++++++++++++++++++++++------------------- test/test.js | 213 ++++++++++++++++++++++++++------------------- 2 files changed, 278 insertions(+), 173 deletions(-) diff --git a/lodash.js b/lodash.js index 848161a5f..e7cfd0041 100644 --- a/lodash.js +++ b/lodash.js @@ -587,34 +587,6 @@ return result; } - /** - * An implementation of `_.uniq` optimized for sorted arrays without support - * for callback shorthands. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The function invoked per iteration. - * @returns {Array} Returns the new duplicate free array. - */ - function sortedUniq(array, iteratee) { - var seen, - index = -1, - length = array.length, - resIndex = -1, - result = []; - - while (++index < length) { - var value = array[index], - computed = iteratee ? iteratee(value, index, array) : value; - - if (!index || seen !== computed) { - seen = computed; - result[++resIndex] = value; - } - } - return result; - } - /** * Used by `_.trim` and `_.trimLeft` to get the index of the first non-whitespace * character of `string`. @@ -2648,6 +2620,43 @@ }); } + /** + * The base implementation of `_.sortedUniq` and `_.sortedUniqBy` without + * support for callback shorthands. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The function invoked per iteration. + * @returns {Array} Returns the new duplicate free array. + */ + function baseSortedUniq(array, iteratee) { + var index = -1, + indexOf = getIndexOf(), + isCommon = indexOf === baseIndexOf, + length = array.length, + seen = isCommon ? undefined : [], + resIndex = -1, + result = []; + + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value, index, array) : value; + + if (isCommon && value === value) { + if (seen !== computed || !index) { + seen = computed + result[++resIndex] = value; + } + } else { + if (!index || indexOf(seen, computed, 0) < 0) { + seen.push(computed); + result[++resIndex] = value; + } + } + } + return result; + } + /** * The base implementation of `_.uniq` and `_.uniqBy` without support for * callback shorthands. @@ -4662,8 +4671,7 @@ * @category Array * @param {Array} array The array to search. * @param {*} value The value to search for. - * @param {boolean|number} [fromIndex=0] The index to search from or `true` - * to perform a binary search on a sorted array. + * @param {number} [fromIndex=0] The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. * @example * @@ -4673,26 +4681,15 @@ * // using `fromIndex` * _.indexOf([1, 2, 1, 2], 2, 2); * // => 3 - * - * // performing a binary search - * _.indexOf([1, 1, 2, 2], 2, true); - * // => 2 */ function indexOf(array, value, fromIndex) { var length = array ? array.length : 0; if (!length) { return -1; } - if (typeof fromIndex == 'number') { + if (fromIndex) { fromIndex = toInteger(fromIndex); fromIndex = fromIndex < 0 ? nativeMax(length + fromIndex, 0) : fromIndex; - } else if (fromIndex) { - var index = binaryIndex(array, value); - if (index < length && - (value === value ? (value === array[index]) : (array[index] !== array[index]))) { - return index; - } - return -1; } return baseIndexOf(array, value, fromIndex || 0); } @@ -4792,8 +4789,7 @@ * @category Array * @param {Array} array The array to search. * @param {*} value The value to search for. - * @param {boolean|number} [fromIndex=array.length-1] The index to search from - * or `true` to perform a binary search on a sorted array. + * @param {number} [fromIndex=array.length-1] The index to search from. * @returns {number} Returns the index of the matched value, else `-1`. * @example * @@ -4803,10 +4799,6 @@ * // using `fromIndex` * _.lastIndexOf([1, 2, 1, 2], 2, 2); * // => 1 - * - * // performing a binary search - * _.lastIndexOf([1, 1, 2, 2], 2, true); - * // => 3 */ function lastIndexOf(array, value, fromIndex) { var length = array ? array.length : 0; @@ -4814,16 +4806,9 @@ return -1; } var index = length; - if (typeof fromIndex == 'number') { + if (fromIndex !== undefined) { index = toInteger(fromIndex); index = (index < 0 ? nativeMax(length + index, 0) : nativeMin(index, length - 1)) + 1; - } else if (fromIndex) { - index = binaryIndex(array, value, true) - 1; - var other = array[index]; - if (value === value ? (value === other) : (other !== other)) { - return index; - } - return -1; } if (value !== value) { return indexOfNaN(array, index, true); @@ -5050,6 +5035,33 @@ return binaryIndexBy(array, value, getIteratee(iteratee)); } + /** + * This method is like `_.indexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedIndexOf([1, 1, 2, 2], 2); + * // => 2 + */ + function sortedIndexOf(array, value) { + var length = array ? array.length : 0; + if (length) { + var index = binaryIndex(array, value); + if (index < length && + (value === value ? (value === array[index]) : (array[index] !== array[index]))) { + return index; + } + } + return -1; + } + /** * This method is like `_.sortedIndex` except that it returns the highest * index at which `value` should be inserted into `array` in order to @@ -5092,6 +5104,77 @@ return binaryIndexBy(array, value, getIteratee(iteratee), true); } + /** + * This method is like `_.lastIndexOf` except that it performs a binary + * search on a sorted `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.sortedLastIndexOf([1, 1, 2, 2], 2); + * // => 3 + */ + function sortedLastIndexOf(array, value) { + var length = array ? array.length : 0; + if (length) { + var index = binaryIndex(array, value, true) - 1, + other = array[index]; + + if (value === value ? (value === other) : (other !== other)) { + return index; + } + } + return -1 + } + + /** + * This method is like `_.uniq` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to inspect. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniq([1, 1, 2]); + * // => [1, 2] + */ + function sortedUniq(array) { + return (array && array.length) + ? baseSortedUniq(array) + : []; + } + + /** + * This method is like `_.uniqBy` except that it's designed and optimized + * for sorted arrays. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The function invoked per iteration. + * @returns {Array} Returns the new duplicate free array. + * @example + * + * _.sortedUniqBy([1, 1.5, 2, 2.5], function(n) { + * return Math.floor(n); + * }); + * // => [1, 2] + */ + function sortedUniqBy(array, iteratee) { + return (array && array.length) + ? baseSortedUniq(array, getIteratee(iteratee)) + : []; + } + /** * Creates a slice of `array` with `n` elements taken from the beginning. * @@ -5265,31 +5348,22 @@ * Creates a duplicate-free version of an array, using * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) * for equality comparisons, in which only the first occurence of each element - * is kept. Providing `true` for `isSorted` performs a faster search algorithm - * for sorted arrays. + * is kept. * * @static * @memberOf _ * @category Array * @param {Array} array The array to inspect. - * @param {boolean} [isSorted] Specify the array is sorted. * @returns {Array} Returns the new duplicate free array. * @example * * _.uniq([2, 1, 2]); * // => [2, 1] - * - * // using `isSorted` - * _.uniq([1, 1, 2], true); - * // => [1, 2] */ - function uniq(array, isSorted) { - if (!(array && array.length)) { - return []; - } - return (isSorted && typeof isSorted == 'boolean' && getIndexOf() === baseIndexOf) - ? sortedUniq(array) - : baseUniq(array); + function uniq(array) { + return (array && array.length) + ? baseUniq(array) + : []; } /** @@ -5301,7 +5375,6 @@ * @memberOf _ * @category Array * @param {Array} array The array to inspect. - * @param {boolean} [isSorted] Specify the array is sorted. * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. * @returns {Array} Returns the new duplicate free array. * @example @@ -5315,21 +5388,10 @@ * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); * // => [{ 'x': 1 }, { 'x': 2 }] */ - function uniqBy(array, isSorted, iteratee) { - if (!(array && array.length)) { - return []; - } - if (isSorted != null && typeof isSorted != 'boolean') { - iteratee = isSorted; - isSorted = false; - } - var toIteratee = getIteratee(); - if (!(iteratee == null && toIteratee === baseIteratee)) { - iteratee = toIteratee(iteratee); - } - return (isSorted && getIndexOf() === baseIndexOf) - ? sortedUniq(array, iteratee) - : baseUniq(array, iteratee); + function uniqBy(array, iteratee) { + return (array && array.length) + ? baseUniq(array, getIteratee(iteratee)) + : []; } /** @@ -11457,6 +11519,8 @@ lodash.slice = slice; lodash.sortBy = sortBy; lodash.sortByOrder = sortByOrder; + lodash.sortedUniq = sortedUniq; + lodash.sortedUniqBy = sortedUniqBy; lodash.spread = spread; lodash.take = take; lodash.takeRight = takeRight; @@ -11583,8 +11647,10 @@ lodash.some = some; lodash.sortedIndex = sortedIndex; lodash.sortedIndexBy = sortedIndexBy; + lodash.sortedIndexOf = sortedIndexOf; lodash.sortedLastIndex = sortedLastIndex; lodash.sortedLastIndexBy = sortedLastIndexBy; + lodash.sortedLastIndexOf = sortedLastIndexOf; lodash.startCase = startCase; lodash.startsWith = startsWith; lodash.sum = sum; diff --git a/test/test.js b/test/test.js index 15f08d341..cb04a78df 100644 --- a/test/test.js +++ b/test/test.js @@ -5778,18 +5778,6 @@ test('should coerce `fromIndex` to an integer', 1, function() { strictEqual(_.indexOf(array, 2, 1.2), 1); }); - - test('should perform a binary search when `fromIndex` is a non-number truthy value', 1, function() { - var sorted = [4, 4, 5, 5, 6, 6], - values = [true, '1', {}], - expected = _.map(values, _.constant(2)); - - var actual = _.map(values, function(value) { - return _.indexOf(sorted, 5, value); - }); - - deepEqual(actual, expected); - }); }()); /*--------------------------------------------------------------------------*/ @@ -5813,6 +5801,7 @@ } var array = [1, new Foo, 3, new Foo], + sorted = [1, 3, new Foo, new Foo], indexOf = _.indexOf; var largeArray = _.times(LARGE_ARRAY_SIZE, function() { @@ -5855,32 +5844,52 @@ } }); - test('`_.uniq` should work with a custom `_.indexOf` method', 4, function() { - _.each([false, true], function(param) { - if (!isModularize) { - _.indexOf = custom; - deepEqual(_.uniq(array, param), array.slice(0, 3)); - deepEqual(_.uniq(largeArray, param), [largeArray[0]]); - _.indexOf = indexOf; - } - else { - skipTest(2); - } - }); + test('`_.uniq` should work with a custom `_.indexOf` method', 2, function() { + if (!isModularize) { + _.indexOf = custom; + deepEqual(_.uniq(array), array.slice(0, 3)); + deepEqual(_.uniq(largeArray), [largeArray[0]]); + _.indexOf = indexOf; + } + else { + skipTest(2); + } }); - test('`_.uniqBy` should work with a custom `_.indexOf` method', 6, function() { - _.each([[false, _.identity], [true, _.identity], [_.identity]], function(params) { - if (!isModularize) { - _.indexOf = custom; - deepEqual(_.uniqBy.apply(_, [array].concat(params)), array.slice(0, 3)); - deepEqual(_.uniqBy.apply(_, [largeArray].concat(params)), [largeArray[0]]); - _.indexOf = indexOf; - } - else { - skipTest(2); - } - }); + test('`_.uniqBy` should work with a custom `_.indexOf` method', 2, function() { + if (!isModularize) { + _.indexOf = custom; + deepEqual(_.uniqBy(array, _.identity), array.slice(0, 3)); + deepEqual(_.uniqBy(largeArray, _.identity), [largeArray[0]]); + _.indexOf = indexOf; + } + else { + skipTest(2); + } + }); + + test('`_.sortedUniq` should work with a custom `_.indexOf` method', 2, function() { + if (!isModularize) { + _.indexOf = custom; + deepEqual(_.sortedUniq(sorted), sorted.slice(0, 3)); + deepEqual(_.sortedUniq(largeArray), [largeArray[0]]); + _.indexOf = indexOf; + } + else { + skipTest(2); + } + }); + + test('`_.sortedUniqBy` should work with a custom `_.indexOf` method', 2, function() { + if (!isModularize) { + _.indexOf = custom; + deepEqual(_.sortedUniqBy(sorted, _.identity), sorted.slice(0, 3)); + deepEqual(_.sortedUniqBy(largeArray, _.identity), [largeArray[0]]); + _.indexOf = indexOf; + } + else { + skipTest(2); + } }); }()); @@ -9024,9 +9033,9 @@ deepEqual(actual, expected); }); - test('should treat falsey `fromIndex` values, except `0` and `NaN`, as `array.length`', 1, function() { + test('should treat falsey `fromIndex` values correctly', 1, function() { var expected = _.map(falsey, function(value) { - return typeof value == 'number' ? -1 : 5; + return value === undefined ? 5 : -1; }); var actual = _.map(falsey, function(fromIndex) { @@ -9039,27 +9048,16 @@ test('should coerce `fromIndex` to an integer', 1, function() { strictEqual(_.lastIndexOf(array, 2, 4.2), 4); }); - - test('should perform a binary search when `fromIndex` is a non-number truthy value', 1, function() { - var sorted = [4, 4, 5, 5, 6, 6], - values = [true, '1', {}], - expected = _.map(values, _.constant(3)); - - var actual = _.map(values, function(value) { - return _.lastIndexOf(sorted, 5, value); - }); - - deepEqual(actual, expected); - }); }()); /*--------------------------------------------------------------------------*/ QUnit.module('indexOf methods'); - _.each(['indexOf', 'lastIndexOf'], function(methodName) { + _.each(['indexOf', 'lastIndexOf', 'sortedIndexOf', 'sortedLastIndexOf'], function(methodName) { var func = _[methodName], - isIndexOf = methodName == 'indexOf'; + isIndexOf = !/last/i.test(methodName), + isSorted = /^sorted/.test(methodName); test('`_.' + methodName + '` should accept a falsey `array` argument', 1, function() { var expected = _.map(falsey, _.constant(-1)); @@ -9094,11 +9092,20 @@ }); test('`_.' + methodName + '` should match `NaN`', 4, function() { - var array = [1, NaN, 3, NaN, 5, NaN]; - strictEqual(func(array, NaN), isIndexOf ? 1 : 5); - strictEqual(func(array, NaN, 2), isIndexOf ? 3 : 1); - strictEqual(func(array, NaN, -2), isIndexOf ? 5 : 3); - strictEqual(func([1, 2, NaN, NaN], NaN, true), isIndexOf ? 2 : 3); + var array = isSorted + ? [1, 2, NaN, NaN] + : [1, NaN, 3, NaN, 5, NaN]; + + if (isSorted) { + strictEqual(func(array, NaN, true), isIndexOf ? 2 : 3); + skipTest(3); + } + else { + strictEqual(func(array, NaN), isIndexOf ? 1 : 5); + strictEqual(func(array, NaN, 2), isIndexOf ? 3 : 1); + strictEqual(func(array, NaN, -2), isIndexOf ? 5 : 3); + skipTest(); + } }); test('`_.' + methodName + '` should match `-0` as `0`', 2, function() { @@ -14271,6 +14278,53 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('sortedIndexOf methods'); + + _.each(['sortedIndexOf', 'sortedLastIndexOf'], function(methodName) { + var func = _[methodName], + isSortedIndexOf = methodName == 'sortedIndexOf'; + + test('should perform a binary search', 1, function() { + var sorted = [4, 4, 5, 5, 6, 6]; + deepEqual(func(sorted, 5), isSortedIndexOf ? 2 : 3); + }); + }); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.sortedUniq'); + + (function() { + test('should return unique values of a sorted array', 3, function() { + var expected = [1, 2, 3]; + + _.each([[1, 2, 3], [1, 1, 2, 2, 3], [1, 2, 3, 3, 3, 3, 3]], function(array) { + deepEqual(_.sortedUniq(array), expected); + }); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.sortedUniqBy'); + + (function() { + var objects = [{ 'a': 2 }, { 'a': 3 }, { 'a': 1 }, { 'a': 2 }, { 'a': 3 }, { 'a': 1 }]; + + test('should work with an `iteratee` argument', 1, function() { + var array = _.sortBy(objects, 'a'), + expected = [objects[2], objects[0], objects[1]]; + + var actual = _.sortedUniqBy(array, function(object) { + return object.a; + }); + + deepEqual(actual, expected); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.spread'); (function() { @@ -15902,17 +15956,14 @@ (function() { var objects = [{ 'a': 2 }, { 'a': 3 }, { 'a': 1 }, { 'a': 2 }, { 'a': 3 }, { 'a': 1 }]; - test('should work with an `iteratee` argument', 2, function() { - _.each([objects, _.sortBy(objects, 'a')], function(array, index) { - var isSorted = !!index, - expected = isSorted ? [objects[2], objects[0], objects[1]] : objects.slice(0, 3); + test('should work with an `iteratee` argument', 1, function() { + var expected = objects.slice(0, 3); - var actual = _.uniqBy(array, isSorted, function(object) { - return object.a; - }); - - deepEqual(actual, expected); + var actual = _.uniqBy(objects, function(object) { + return object.a; }); + + deepEqual(actual, expected); }); test('should provide the correct `iteratee` arguments', 1, function() { @@ -15925,14 +15976,6 @@ deepEqual(args, [objects[0]]); }); - test('should work with `iteratee` without specifying `isSorted`', 1, function() { - var actual = _.uniqBy(objects, function(object) { - return object.a; - }); - - deepEqual(actual, objects.slice(0, 3)); - }); - test('should work with a "_.property" style `iteratee`', 2, function() { var actual = _.uniqBy(objects, 'a'); @@ -15984,13 +16027,6 @@ deepEqual(func([1, NaN, 3, NaN]), [1, NaN, 3]); }); - test('`_.' + methodName + '` should work with `isSorted`', 3, function() { - var expected = [1, 2, 3]; - deepEqual(func([1, 2, 3], true), expected); - deepEqual(func([1, 1, 2, 2, 3], true), expected); - deepEqual(func([1, 2, 3, 3, 3, 3, 3], true), expected); - }); - test('`_.' + methodName + '` should work with large arrays', 1, function() { var largeArray = [], expected = [0, 'a', {}], @@ -17148,10 +17184,11 @@ QUnit.module('"Arrays" category methods'); (function() { - var args = arguments, + var args = (function() { return arguments; }(1, null, [3], null, 5)), + sortedArgs = (function() { return arguments; }(1, [3], 5, null, null)), array = [1, 2, 3, 4, 5, 6]; - test('should work with `arguments` objects', 28, function() { + test('should work with `arguments` objects', 30, function() { function message(methodName) { return '`_.' + methodName + '` should work with `arguments` objects'; } @@ -17177,8 +17214,10 @@ deepEqual(_.last(args), 5, message('last')); deepEqual(_.lastIndexOf(args, 1), 0, message('lastIndexOf')); deepEqual(_.rest(args, 4), [null, [3], null, 5], message('rest')); - deepEqual(_.sortedIndex(args, 6), 5, message('sortedIndex')); - deepEqual(_.sortedLastIndex(args, 6), 5, message('sortedLastIndex')); + deepEqual(_.sortedIndex(sortedArgs, 6), 3, message('sortedIndex')); + deepEqual(_.sortedIndexOf(sortedArgs, 5), 2, message('sortedIndexOf')); + deepEqual(_.sortedLastIndex(sortedArgs, 5), 3, message('sortedLastIndex')) + deepEqual(_.sortedLastIndexOf(sortedArgs, 1), 0, message('sortedLastIndexOf')); deepEqual(_.take(args, 2), [1, null], message('take')); deepEqual(_.takeRight(args, 1), [5], message('takeRight')); deepEqual(_.takeRightWhile(args, _.identity), [5], message('takeRightWhile')); @@ -17208,7 +17247,7 @@ deepEqual(_.intersection(array, null), [], message('intersection')); deepEqual(_.union(array, null), array, message('union')); }); - }(1, null, [3], null, 5)); + }()); /*--------------------------------------------------------------------------*/ @@ -17319,7 +17358,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 224, function() { + test('should accept falsey arguments', 228, function() { var emptyArrays = _.map(falsey, _.constant([])); _.each(acceptFalsey, function(methodName) {