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;
}
/**
* 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);
}
/**

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(
@@ -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(){}'
})
);
/*--------------------------------------------------------------------------*/

View File

@@ -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._;