Added _.sortedLastIndex and allow _.lastIndexOf to work with sorted arrays and _.sortedLastIndex.

This commit is contained in:
John-David Dalton
2014-06-29 20:57:28 -07:00
parent 7eb3754807
commit 7400064cd5
3 changed files with 268 additions and 99 deletions

103
lodash.js
View File

@@ -2279,6 +2279,37 @@
return !!result; 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 * The base implementation of `_.uniq` without support for callback shorthands
* and `this` binding. * and `this` binding.
@@ -3362,7 +3393,7 @@
* // => 4 * // => 4
* *
* // performing a binary search * // performing a binary search
* _.indexOf([1, 1, 2, 2, 3, 3], 2, true); * _.indexOf([4, 4, 5, 5, 6, 6], 5, true);
* // => 2 * // => 2
*/ */
function indexOf(array, value, fromIndex) { function indexOf(array, value, fromIndex) {
@@ -3490,11 +3521,20 @@
* // using `fromIndex` * // using `fromIndex`
* _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3);
* // => 1 * // => 1
*
* // performing a binary search
* _.lastIndexOf([4, 4, 5, 5, 6, 6], 5, true);
* // => 3
*/ */
function lastIndexOf(array, value, fromIndex) { function lastIndexOf(array, value, fromIndex) {
var index = array ? array.length : 0; var length = array ? array.length : 0,
index = length;
if (typeof fromIndex == 'number') { if (typeof fromIndex == 'number') {
index = (fromIndex < 0 ? nativeMax(index + fromIndex, 0) : nativeMin(fromIndex || 0, index - 1)) + 1; 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--) { while (index--) {
if (array[index] === value) { if (array[index] === value) {
@@ -3706,38 +3746,53 @@
* into `array`. * into `array`.
* @example * @example
* *
* _.sortedIndex([20, 30, 50], 40); * _.sortedIndex([30, 50], 40);
* // => 1
*
* _.sortedIndex([4, 4, 5, 5, 6, 6], 5);
* // => 2 * // => 2
* *
* var dict = { * var dict = { 'data': { 'thirty': 30, 'forty': 40, 'fifty': 50 } };
* 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'forty': 40, 'fifty': 50 }
* };
* *
* // using an iterator function * // using an iterator function
* _.sortedIndex(['twenty', 'thirty', 'fifty'], 'forty', function(word) { * _.sortedIndex(['thirty', 'fifty'], 'forty', function(word) {
* return this.wordToNumber[word]; * return this.data[word];
* }, dict); * }, dict);
* // => 2 * // => 1
* *
* // using "_.pluck" callback shorthand * // using "_.pluck" callback shorthand
* _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); * _.sortedIndex([{ 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x');
* // => 2 * // => 1
*/ */
function sortedIndex(array, value, iterator, thisArg) { function sortedIndex(array, value, iterator, thisArg) {
var low = 0, iterator = iterator == null ? identity : lodash.callback(iterator, thisArg, 1);
high = array ? array.length : low; return baseSortedIndex(array, value, iterator);
}
// explicitly reference `identity` for better inlining in Firefox /**
iterator = iterator ? lodash.callback(iterator, thisArg, 1) : identity; * This method is like `_.sortedIndex` except that it returns the highest
value = iterator(value); * index at which a value should be inserted into a given sorted array in
* order to maintain the sort order of the array.
while (low < high) { *
var mid = (low + high) >>> 1; * @static
(iterator(array[mid]) < value) * @memberOf _
? (low = mid + 1) * @category Array
: (high = mid); * @param {Array} array The array to inspect.
} * @param {*} value The value to evaluate.
return low; * @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);
} }
/** /**

View File

@@ -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( suites.push(
@@ -1361,12 +1373,26 @@
suites.push( suites.push(
Benchmark.Suite('`_.lastIndexOf`') Benchmark.Suite('`_.lastIndexOf`')
.add(buildName, '\ .add(buildName, {
lodash.lastIndexOf(numbers, 9)' 'fn': 'lodash.lastIndexOf(hundredValues, 0)',
) 'teardown': 'function multiArrays(){}'
.add(otherName, '\ })
_.lastIndexOf(numbers, 9)' .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(){}'
})
); );
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/

View File

@@ -4248,25 +4248,23 @@
strictEqual(_.indexOf(array, 3), 2); 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() { test('should work with a positive `fromIndex`', 1, function() {
strictEqual(_.indexOf(array, 1, 2), 3); strictEqual(_.indexOf(array, 1, 2), 3);
}); });
test('should work with `fromIndex` >= `array.length`', 12, function() { test('should work with `fromIndex` >= `array.length`', 1, function() {
_.each([6, 8, Math.pow(2, 32), Infinity], function(fromIndex) { var values = [6, 8, Math.pow(2, 32), Infinity],
strictEqual(_.indexOf(array, 1, fromIndex), -1); expected = _.map(values, _.constant([-1, -1, -1]));
strictEqual(_.indexOf(array, undefined, fromIndex), -1);
strictEqual(_.indexOf(array, '', fromIndex), -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() { test('should treat falsey `fromIndex` values as `0`', 1, function() {
@@ -4279,22 +4277,31 @@
deepEqual(actual, expected); deepEqual(actual, expected);
}); });
test('should treat non-number `fromIndex` values as `0`', 1, function() { test('should perform a binary search when `fromIndex` is a non-number truthy value', 1, function() {
strictEqual(_.indexOf([1, 2, 3], 1, '1'), 0); 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() { test('should work with a negative `fromIndex`', 1, function() {
strictEqual(_.indexOf(array, 2, -3), 4); strictEqual(_.indexOf(array, 2, -3), 4);
}); });
test('should work with a negative `fromIndex` <= `-array.length`', 3, function() { test('should work with a negative `fromIndex` <= `-array.length`', 1, function() {
_.each([-6, -8, -Infinity], function(fromIndex) { var values = [-6, -8, -Infinity],
strictEqual(_.indexOf(array, 1, fromIndex), 0); expected = _.map(values, _.constant(0));
});
});
test('should work with `isSorted`', 1, function() { var actual = _.map(values, function(fromIndex) {
strictEqual(_.indexOf([1, 2, 3], 1, true), 0); return _.indexOf(array, 1, fromIndex);
});
deepEqual(actual, expected);
}); });
}()); }());
@@ -6268,20 +6275,23 @@
strictEqual(_.lastIndexOf(array, 3), 5); 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() { test('should work with a positive `fromIndex`', 1, function() {
strictEqual(_.lastIndexOf(array, 1, 2), 0); strictEqual(_.lastIndexOf(array, 1, 2), 0);
}); });
test('should work with `fromIndex` >= `array.length`', 12, function() { test('should work with `fromIndex` >= `array.length`', 1, function() {
_.each([6, 8, Math.pow(2, 32), Infinity], function(fromIndex) { var values = [6, 8, Math.pow(2, 32), Infinity],
strictEqual(_.lastIndexOf(array, undefined, fromIndex), -1); expected = _.map(values, _.constant([-1, 3, -1]));
strictEqual(_.lastIndexOf(array, 1, fromIndex), 3);
strictEqual(_.lastIndexOf(array, '', fromIndex), -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() { test('should treat falsey `fromIndex` values, except `0` and `NaN`, as `array.length`', 1, function() {
@@ -6296,19 +6306,31 @@
deepEqual(actual, expected); deepEqual(actual, expected);
}); });
test('should treat non-number `fromIndex` values as `array.length`', 2, function() { test('should perform a binary search when `fromIndex` is a non-number truthy value', 1, function() {
strictEqual(_.lastIndexOf(array, 3, '1'), 5); var sorted = [4, 4, 5, 5, 6, 6],
strictEqual(_.lastIndexOf(array, 3, true), 5); 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() { test('should work with a negative `fromIndex`', 1, function() {
strictEqual(_.lastIndexOf(array, 2, -3), 1); strictEqual(_.lastIndexOf(array, 2, -3), 1);
}); });
test('should work with a negative `fromIndex` <= `-array.length`', 3, function() { test('should work with a negative `fromIndex` <= `-array.length`', 1, function() {
_.each([-6, -8, -Infinity], function(fromIndex) { var values = [-6, -8, -Infinity],
strictEqual(_.lastIndexOf(array, 1, fromIndex), 0); 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'); QUnit.module('indexOf methods');
(function() { _.each(['indexOf', 'lastIndexOf'], function(methodName) {
_.each(['indexOf', 'lastIndexOf'], function(methodName) { var func = _[methodName];
var func = _[methodName];
test('`_.' + methodName + '` should accept a falsey `array` argument', 1, function() { test('`_.' + methodName + '` should accept a falsey `array` argument', 1, function() {
var expected = _.map(falsey, _.constant(-1)); var expected = _.map(falsey, _.constant(-1));
var actual = _.map(falsey, function(value, index) { var actual = _.map(falsey, function(value, index) {
try { try {
return index ? func(value) : func(); return index ? func(value) : func();
} catch(e) { } } catch(e) { }
});
deepEqual(actual, expected);
}); });
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'); QUnit.module('lodash.sortedIndex');
(function() { (function() {
var array = [20, 30, 50], test('should return the correct insert index', 1, function() {
objects = [{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }]; var array = [30, 50],
values = [30, 40, 50],
expected = [0, 1, 1];
test('should return the insert index of a given value', 2, function() { var actual = _.map(values, function(value) {
strictEqual(_.sortedIndex(array, 40), 2); return _.sortedIndex(array, value);
strictEqual(_.sortedIndex(array, 30), 1); });
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; var args;
_.sortedIndex(array, 40, function() { func(array, 40, function() {
args || (args = slice.call(arguments)); args || (args = slice.call(arguments));
}); });
deepEqual(args, [40]); deepEqual(args, [40]);
}); });
test('should support the `thisArg` argument', 1, function() { test('`_.' + methodName + '` should support the `thisArg` argument', 1, function() {
var actual = _.sortedIndex(array, 40, function(num) { var actual = func(array, 40, function(num) {
return this[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() { test('`_.' + methodName + '` should work with a string for `callback`', 1, function() {
var actual = _.sortedIndex(objects, { 'x': 40 }, 'x'); var actual = func(objects, { 'x': 40 }, 'x');
strictEqual(actual, 2); 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, var length = Math.pow(2, 32) - 1,
index = length - 1, index = length - 1,
array = Array(length), array = Array(length),
@@ -9014,14 +9102,14 @@
if (array.length == length) { if (array.length == length) {
array[index] = index; array[index] = index;
_.sortedIndex(array, index, function() { steps++; }); func(array, index, function() { steps++; });
strictEqual(steps, 33); strictEqual(steps, 33);
} }
else { else {
skipTest(); skipTest();
} }
}); });
}()); });
/*--------------------------------------------------------------------------*/ /*--------------------------------------------------------------------------*/
@@ -11247,7 +11335,7 @@
var acceptFalsey = _.difference(allMethods, rejectFalsey); var acceptFalsey = _.difference(allMethods, rejectFalsey);
test('should accept falsey arguments', 190, function() { test('should accept falsey arguments', 191, function() {
var emptyArrays = _.map(falsey, _.constant([])), var emptyArrays = _.map(falsey, _.constant([])),
isExposed = '_' in root, isExposed = '_' in root,
oldDash = root._; oldDash = root._;