diff --git a/lodash.js b/lodash.js index 045daa87c..75d8dab3c 100644 --- a/lodash.js +++ b/lodash.js @@ -2279,6 +2279,37 @@ return !!result; } + /** + * The base implementation of `_.sortedIndex` and `_.sortedLastIndex` without + * support for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} iterator The function called per iteration. + * @param {boolean} [retHighest=false] Specify returning the highest, instead + * of the lowest, index at which a value should be inserted into `array`. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function baseSortedIndex(array, value, iterator, retHighest) { + var low = 0, + high = array ? array.length : low; + + value = iterator(value); + while (low < high) { + var mid = (low + high) >>> 1, + computed = iterator(array[mid]); + + if (retHighest ? computed <= value : computed < value) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + /** * The base implementation of `_.uniq` without support for callback shorthands * and `this` binding. @@ -3362,7 +3393,7 @@ * // => 4 * * // performing a binary search - * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); + * _.indexOf([4, 4, 5, 5, 6, 6], 5, true); * // => 2 */ function indexOf(array, value, fromIndex) { @@ -3490,11 +3521,20 @@ * // using `fromIndex` * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); * // => 1 + * + * // performing a binary search + * _.lastIndexOf([4, 4, 5, 5, 6, 6], 5, true); + * // => 3 */ function lastIndexOf(array, value, fromIndex) { - var index = array ? array.length : 0; + var length = array ? array.length : 0, + index = length; + if (typeof fromIndex == 'number') { index = (fromIndex < 0 ? nativeMax(index + fromIndex, 0) : nativeMin(fromIndex || 0, index - 1)) + 1; + } else if (fromIndex) { + index = sortedLastIndex(array, value) - 1; + return (length && array[index] === value) ? index : -1; } while (index--) { if (array[index] === value) { @@ -3706,38 +3746,53 @@ * into `array`. * @example * - * _.sortedIndex([20, 30, 50], 40); + * _.sortedIndex([30, 50], 40); + * // => 1 + * + * _.sortedIndex([4, 4, 5, 5, 6, 6], 5); * // => 2 * - * var dict = { - * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'forty': 40, 'fifty': 50 } - * }; + * var dict = { 'data': { 'thirty': 30, 'forty': 40, 'fifty': 50 } }; * * // using an iterator function - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'forty', function(word) { - * return this.wordToNumber[word]; + * _.sortedIndex(['thirty', 'fifty'], 'forty', function(word) { + * return this.data[word]; * }, dict); - * // => 2 + * // => 1 * * // using "_.pluck" callback shorthand - * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); - * // => 2 + * _.sortedIndex([{ 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); + * // => 1 */ function sortedIndex(array, value, iterator, thisArg) { - var low = 0, - high = array ? array.length : low; + iterator = iterator == null ? identity : lodash.callback(iterator, thisArg, 1); + return baseSortedIndex(array, value, iterator); + } - // explicitly reference `identity` for better inlining in Firefox - iterator = iterator ? lodash.callback(iterator, thisArg, 1) : identity; - value = iterator(value); - - while (low < high) { - var mid = (low + high) >>> 1; - (iterator(array[mid]) < value) - ? (low = mid + 1) - : (high = mid); - } - return low; + /** + * This method is like `_.sortedIndex` except that it returns the highest + * index at which a value should be inserted into a given sorted array in + * order to maintain the sort order of the array. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to inspect. + * @param {*} value The value to evaluate. + * @param {Function|Object|string} [iterator=identity] The function called + * per iteration. If a property name or object is provided it is used to + * create a "_.pluck" or "_.where" style callback respectively. + * @param {*} [thisArg] The `this` binding of `iterator`. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedLastIndex([4, 4, 5, 5, 6, 6], 5); + * // => 4 + */ + function sortedLastIndex(array, value, iterator, thisArg) { + iterator = iterator == null ? identity : lodash.callback(iterator, thisArg, 1); + return baseSortedIndex(array, value, iterator, true); } /** diff --git a/perf/perf.js b/perf/perf.js index 8a3cd6102..809b632a5 100644 --- a/perf/perf.js +++ b/perf/perf.js @@ -1139,6 +1139,18 @@ }) ); + suites.push( + Benchmark.Suite('`_.indexOf` performing a binary search') + .add(buildName, { + 'fn': 'lodash.indexOf(hundredValues, 99, true)', + 'teardown': 'function multiArrays(){}' + }) + .add(otherName, { + 'fn': '_.indexOf(hundredValues, 99, true)', + 'teardown': 'function multiArrays(){}' + }) + ); + /*--------------------------------------------------------------------------*/ suites.push( @@ -1361,12 +1373,26 @@ suites.push( Benchmark.Suite('`_.lastIndexOf`') - .add(buildName, '\ - lodash.lastIndexOf(numbers, 9)' - ) - .add(otherName, '\ - _.lastIndexOf(numbers, 9)' - ) + .add(buildName, { + 'fn': 'lodash.lastIndexOf(hundredValues, 0)', + 'teardown': 'function multiArrays(){}' + }) + .add(otherName, { + 'fn': '_.lastIndexOf(hundredValues, 0)', + 'teardown': 'function multiArrays(){}' + }) + ); + + suites.push( + Benchmark.Suite('`_.lastIndexOf` performing a binary search') + .add(buildName, { + 'fn': 'lodash.lastIndexOf(hundredValues, 0, true)', + 'teardown': 'function multiArrays(){}' + }) + .add(otherName, { + 'fn': '_.lastIndexOf(hundredValues, 0, true)', + 'teardown': 'function multiArrays(){}' + }) ); /*--------------------------------------------------------------------------*/ diff --git a/test/test.js b/test/test.js index bd22defa7..0b55223c0 100644 --- a/test/test.js +++ b/test/test.js @@ -4248,25 +4248,23 @@ strictEqual(_.indexOf(array, 3), 2); }); - test('should return `-1` for an unmatched value', 4, function() { - strictEqual(_.indexOf(array, 4), -1); - strictEqual(_.indexOf(array, 4, true), -1); - - var empty = []; - strictEqual(_.indexOf(empty, undefined), -1); - strictEqual(_.indexOf(empty, undefined, true), -1); - }); - test('should work with a positive `fromIndex`', 1, function() { strictEqual(_.indexOf(array, 1, 2), 3); }); - test('should work with `fromIndex` >= `array.length`', 12, function() { - _.each([6, 8, Math.pow(2, 32), Infinity], function(fromIndex) { - strictEqual(_.indexOf(array, 1, fromIndex), -1); - strictEqual(_.indexOf(array, undefined, fromIndex), -1); - strictEqual(_.indexOf(array, '', fromIndex), -1); + test('should work with `fromIndex` >= `array.length`', 1, function() { + var values = [6, 8, Math.pow(2, 32), Infinity], + expected = _.map(values, _.constant([-1, -1, -1])); + + var actual = _.map(values, function(fromIndex) { + return [ + _.indexOf(array, undefined, fromIndex), + _.indexOf(array, 1, fromIndex), + _.indexOf(array, '', fromIndex) + ]; }); + + deepEqual(actual, expected); }); test('should treat falsey `fromIndex` values as `0`', 1, function() { @@ -4279,22 +4277,31 @@ deepEqual(actual, expected); }); - test('should treat non-number `fromIndex` values as `0`', 1, function() { - strictEqual(_.indexOf([1, 2, 3], 1, '1'), 0); + 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); }); test('should work with a negative `fromIndex`', 1, function() { strictEqual(_.indexOf(array, 2, -3), 4); }); - test('should work with a negative `fromIndex` <= `-array.length`', 3, function() { - _.each([-6, -8, -Infinity], function(fromIndex) { - strictEqual(_.indexOf(array, 1, fromIndex), 0); - }); - }); + test('should work with a negative `fromIndex` <= `-array.length`', 1, function() { + var values = [-6, -8, -Infinity], + expected = _.map(values, _.constant(0)); - test('should work with `isSorted`', 1, function() { - strictEqual(_.indexOf([1, 2, 3], 1, true), 0); + var actual = _.map(values, function(fromIndex) { + return _.indexOf(array, 1, fromIndex); + }); + + deepEqual(actual, expected); }); }()); @@ -6268,20 +6275,23 @@ strictEqual(_.lastIndexOf(array, 3), 5); }); - test('should return `-1` for an unmatched value', 1, function() { - strictEqual(_.lastIndexOf(array, 4), -1); - }); - test('should work with a positive `fromIndex`', 1, function() { strictEqual(_.lastIndexOf(array, 1, 2), 0); }); - test('should work with `fromIndex` >= `array.length`', 12, function() { - _.each([6, 8, Math.pow(2, 32), Infinity], function(fromIndex) { - strictEqual(_.lastIndexOf(array, undefined, fromIndex), -1); - strictEqual(_.lastIndexOf(array, 1, fromIndex), 3); - strictEqual(_.lastIndexOf(array, '', fromIndex), -1); + test('should work with `fromIndex` >= `array.length`', 1, function() { + var values = [6, 8, Math.pow(2, 32), Infinity], + expected = _.map(values, _.constant([-1, 3, -1])); + + var actual = _.map(values, function(fromIndex) { + return [ + _.lastIndexOf(array, undefined, fromIndex), + _.lastIndexOf(array, 1, fromIndex), + _.lastIndexOf(array, '', fromIndex) + ]; }); + + deepEqual(actual, expected); }); test('should treat falsey `fromIndex` values, except `0` and `NaN`, as `array.length`', 1, function() { @@ -6296,19 +6306,31 @@ deepEqual(actual, expected); }); - test('should treat non-number `fromIndex` values as `array.length`', 2, function() { - strictEqual(_.lastIndexOf(array, 3, '1'), 5); - strictEqual(_.lastIndexOf(array, 3, true), 5); + 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); }); test('should work with a negative `fromIndex`', 1, function() { strictEqual(_.lastIndexOf(array, 2, -3), 1); }); - test('should work with a negative `fromIndex` <= `-array.length`', 3, function() { - _.each([-6, -8, -Infinity], function(fromIndex) { - strictEqual(_.lastIndexOf(array, 1, fromIndex), 0); + test('should work with a negative `fromIndex` <= `-array.length`', 1, function() { + var values = [-6, -8, -Infinity], + expected = _.map(values, _.constant(0)); + + var actual = _.map(values, function(fromIndex) { + return _.lastIndexOf(array, 1, fromIndex); }); + + deepEqual(actual, expected); }); }()); @@ -6316,23 +6338,33 @@ QUnit.module('indexOf methods'); - (function() { - _.each(['indexOf', 'lastIndexOf'], function(methodName) { - var func = _[methodName]; + _.each(['indexOf', 'lastIndexOf'], function(methodName) { + var func = _[methodName]; - test('`_.' + methodName + '` should accept a falsey `array` argument', 1, function() { - var expected = _.map(falsey, _.constant(-1)); + test('`_.' + methodName + '` should accept a falsey `array` argument', 1, function() { + var expected = _.map(falsey, _.constant(-1)); - var actual = _.map(falsey, function(value, index) { - try { - return index ? func(value) : func(); - } catch(e) { } - }); - - deepEqual(actual, expected); + var actual = _.map(falsey, function(value, index) { + try { + return index ? func(value) : func(); + } catch(e) { } }); + + deepEqual(actual, expected); }); - }()); + + + test('`_.' + methodName + '` should return `-1` for an unmatched value', 4, function() { + var array = [1, 2, 3], + empty = []; + + strictEqual(func(array, 4), -1); + strictEqual(func(array, 4, true), -1); + + strictEqual(func(empty, undefined), -1); + strictEqual(func(empty, undefined, true), -1); + }); + }); /*--------------------------------------------------------------------------*/ @@ -8975,38 +9007,94 @@ QUnit.module('lodash.sortedIndex'); (function() { - var array = [20, 30, 50], - objects = [{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }]; + test('should return the correct insert index', 1, function() { + var array = [30, 50], + values = [30, 40, 50], + expected = [0, 1, 1]; - test('should return the insert index of a given value', 2, function() { - strictEqual(_.sortedIndex(array, 40), 2); - strictEqual(_.sortedIndex(array, 30), 1); + var actual = _.map(values, function(value) { + return _.sortedIndex(array, value); + }); + + deepEqual(actual, expected); }); - test('should pass the correct `callback` arguments', 1, function() { + test('should work with an array of strings', 1, function() { + var array = ['a', 'c'], + values = ['a', 'b', 'c'], + expected = [0, 1, 1]; + + var actual = _.map(values, function(value) { + return _.sortedIndex(array, value); + }); + + deepEqual(actual, expected); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.sortedLastIndex'); + + (function() { + test('should return the correct insert index', 1, function() { + var array = [30, 50], + values = [30, 40, 50], + expected = [1, 1, 2]; + + var actual = _.map(values, function(value) { + return _.sortedLastIndex(array, value); + }); + + deepEqual(actual, expected); + }); + + test('should work with an array of strings', 1, function() { + var array = ['a', 'c'], + values = ['a', 'b', 'c'], + expected = [1, 1, 2]; + + var actual = _.map(values, function(value) { + return _.sortedLastIndex(array, value); + }); + + deepEqual(actual, expected); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('sortedIndex methods'); + + _.each(['sortedIndex', 'sortedLastIndex'], function(methodName) { + var array = [30, 50], + func = _[methodName], + objects = [{ 'x': 30 }, { 'x': 50 }]; + + test('`_.' + methodName + '` should pass the correct `callback` arguments', 1, function() { var args; - _.sortedIndex(array, 40, function() { + func(array, 40, function() { args || (args = slice.call(arguments)); }); deepEqual(args, [40]); }); - test('should support the `thisArg` argument', 1, function() { - var actual = _.sortedIndex(array, 40, function(num) { + test('`_.' + methodName + '` should support the `thisArg` argument', 1, function() { + var actual = func(array, 40, function(num) { return this[num]; - }, { '20': 20, '30': 30, '40': 40 }); + }, { '30': 30, '40': 40, '50': 50 }); - strictEqual(actual, 2); + strictEqual(actual, 1); }); - test('should work with a string for `callback`', 1, function() { - var actual = _.sortedIndex(objects, { 'x': 40 }, 'x'); - strictEqual(actual, 2); + test('`_.' + methodName + '` should work with a string for `callback`', 1, function() { + var actual = func(objects, { 'x': 40 }, 'x'); + strictEqual(actual, 1); }); - test('supports arrays with lengths larger than `Math.pow(2, 31) - 1`', 1, function() { + test('`_.' + methodName + '` supports arrays with lengths larger than `Math.pow(2, 31) - 1`', 1, function() { var length = Math.pow(2, 32) - 1, index = length - 1, array = Array(length), @@ -9014,14 +9102,14 @@ if (array.length == length) { array[index] = index; - _.sortedIndex(array, index, function() { steps++; }); + func(array, index, function() { steps++; }); strictEqual(steps, 33); } else { skipTest(); } }); - }()); + }); /*--------------------------------------------------------------------------*/ @@ -11247,7 +11335,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 190, function() { + test('should accept falsey arguments', 191, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._;