From 3c6999f3a4c04bf8fbe944a9b72b2ae1ded2bdd2 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 7 Jun 2012 12:42:24 -0400 Subject: [PATCH] Ensure all "Arrays" category methods allow a falsey `array` argument. [closes #23, #24] Former-commit-id: 66d09d3c8f3c045daf310c46581afa085daa57de --- lodash.js | 242 ++++++++++++++++++++++++++++++++------------------- test/test.js | 76 ++++++++++++++++ 2 files changed, 229 insertions(+), 89 deletions(-) diff --git a/lodash.js b/lodash.js index 348459f55..d24f6113e 100644 --- a/lodash.js +++ b/lodash.js @@ -890,9 +890,12 @@ * // => [1, 2, 3] */ function compact(array) { + var result = []; + if (!array) { + return result; + } var index = -1, - length = array.length, - result = []; + length = array.length; while (++index < length) { if (array[index]) { @@ -919,9 +922,12 @@ * // => [1, 3, 4] */ function difference(array) { + var result = []; + if (!array) { + return result; + } var index = -1, length = array.length, - result = [], flattened = concat.apply(result, slice.call(arguments, 1)); while (++index < length) { @@ -945,14 +951,16 @@ * @param {Object} [guard] Internally used to allow this method to work with * others like `_.map` without using their callback `index` argument for `n`. * @returns {Mixed} Returns the first value or an array of the first `n` values - * of the `array`. + * of `array`. * @example * * _.first([5, 4, 3, 2, 1]); * // => 5 */ function first(array, n, guard) { - return (n == undefined || guard) ? array[0] : slice.call(array, 0, n); + if (array) { + return (n == undefined || guard) ? array[0] : slice.call(array, 0, n); + } } /** @@ -974,10 +982,13 @@ * // => [1, 2, 3, [[4]]]; */ function flatten(array, shallow) { + var result = []; + if (!array) { + return result; + } var value, index = -1, - length = array.length, - result = []; + length = array.length; while (++index < length) { value = array[index]; @@ -1016,12 +1027,15 @@ * // => { '3': ['one', 'two'], '5': ['three'] } */ function groupBy(array, callback, thisArg) { + var result = {}; + if (!array) { + return result; + } var prop, value, index = -1, isFunc = typeof callback == 'function', - length = array.length, - result = {}; + length = array.length; if (isFunc && thisArg) { callback = iteratorBind(callback, thisArg); @@ -1031,58 +1045,7 @@ prop = isFunc ? callback(value, index, array) : value[callback]; (hasOwnProperty.call(result, prop) ? result[prop] : result[prop] = []).push(value); } - return result - } - - /** - * Produces a new sorted array, ranked in ascending order by the results of - * running each element of `array` through `callback`. The `callback` is - * bound to `thisArg` and invoked with 3 arguments; (value, index, array). The - * `callback` argument may also be the name of a property to sort by (e.g. 'length'). - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to iterate over. - * @param {Function|String} callback The function called per iteration or - * property name to sort by. - * @param {Mixed} [thisArg] The `this` binding for the callback. - * @returns {Array} Returns a new array of sorted values. - * @example - * - * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); - * // => [3, 1, 2] - * - * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); - * // => [3, 1, 2] - * - * _.sortBy(['larry', 'brendan', 'moe'], 'length'); - * // => ['moe', 'larry', 'brendan'] - */ - function sortBy(array, callback, thisArg) { - if (typeof callback == 'string') { - var prop = callback; - callback = function(array) { return array[prop]; }; - } else if (thisArg) { - callback = iteratorBind(callback, thisArg); - } - return pluck(map(array, function(value, index) { - return { - 'criteria': callback(value, index, array), - 'value': value - }; - }).sort(function(left, right) { - var a = left.criteria, - b = right.criteria; - - if (a === undefined) { - return 1; - } - if (b === undefined) { - return -1; - } - return a < b ? -1 : a > b ? 1 : 0; - }), 'value'); + return result; } /** @@ -1110,6 +1073,9 @@ * // => 2 */ function indexOf(array, value, fromIndex) { + if (!array) { + return -1; + } var index = -1, length = array.length; @@ -1130,7 +1096,7 @@ } /** - * Gets all but the last value of the `array`. Pass `n` to exclude the last `n` + * Gets all but the last value of `array`. Pass `n` to exclude the last `n` * values from the result. * * @static @@ -1140,13 +1106,16 @@ * @param {Number} [n] The number of elements to return. * @param {Object} [guard] Internally used to allow this method to work with * others like `_.map` without using their callback `index` argument for `n`. - * @returns {Array} Returns all but the last value or `n` values of the `array`. + * @returns {Array} Returns all but the last value or `n` values of `array`. * @example * * _.initial([3, 2, 1]); * // => [3, 2] */ function initial(array, n, guard) { + if (!array) { + return []; + } return slice.call(array, 0, -((n == undefined || guard) ? 1 : n)); } @@ -1165,11 +1134,14 @@ * // => [1, 2] */ function intersection(array) { + var result = []; + if (!array) { + return result; + } var value, index = -1, length = array.length, - others = slice.call(arguments, 1), - result = []; + others = slice.call(arguments, 1); while (++index < length) { value = array[index]; @@ -1201,11 +1173,14 @@ * // => [[1, 5, 7], [1, 2, 3]] */ function invoke(array, methodName) { + var result = []; + if (!array) { + return result; + } var args = slice.call(arguments, 2), index = -1, length = array.length, - isFunc = typeof methodName == 'function', - result = []; + isFunc = typeof methodName == 'function'; while (++index < length) { result[index] = (isFunc ? methodName : array[index][methodName]).apply(array[index], args); @@ -1224,15 +1199,18 @@ * @param {Number} [n] The number of elements to return. * @param {Object} [guard] Internally used to allow this method to work with * others like `_.map` without using their callback `index` argument for `n`. - * @returns {Array} Returns all but the last value or `n` values of the `array`. + * @returns {Mixed} Returns the last value or an array of the last `n` values + * of `array`. * @example * * _.last([3, 2, 1]); * // => 1 */ function last(array, n, guard) { - var length = array.length; - return (n == undefined || guard) ? array[length - 1] : slice.call(array, -n || length); + if (array) { + var length = array.length; + return (n == undefined || guard) ? array[length - 1] : slice.call(array, -n || length); + } } /** @@ -1255,6 +1233,9 @@ * // => 1 */ function lastIndexOf(array, value, fromIndex) { + if (!array) { + return -1; + } var index = array.length; if (fromIndex && typeof fromIndex == 'number') { index = (fromIndex < 0 ? Math.max(0, index + fromIndex) : Math.min(fromIndex, index - 1)) + 1; @@ -1292,12 +1273,16 @@ * // => { 'name': 'curly', 'age': 60 }; */ function max(array, callback, thisArg) { - var current, - computed = -Infinity, - index = -1, - length = array.length, + var computed = -Infinity, result = computed; + if (!array) { + return result; + } + var current, + index = -1, + length = array.length; + if (!callback) { while (++index < length) { if (array[index] > result) { @@ -1338,12 +1323,16 @@ * // => 2 */ function min(array, callback, thisArg) { - var current, - computed = Infinity, - index = -1, - length = array.length, + var computed = Infinity, result = computed; + if (!array) { + return result; + } + var current, + index = -1, + length = array.length; + if (!callback) { while (++index < length) { if (array[index] < result) { @@ -1386,6 +1375,9 @@ * // => ['moe', 'larry', 'curly'] */ function pluck(array, property) { + if (!array) { + return []; + } var index = -1, length = array.length, result = Array(length); @@ -1445,7 +1437,7 @@ /** * The opposite of `_.initial`, this method gets all but the first value of - * the `array`. Pass `n` to exclude the first `n` values from the result. + * `array`. Pass `n` to exclude the first `n` values from the result. * * @static * @memberOf _ @@ -1455,13 +1447,16 @@ * @param {Number} [n] The number of elements to return. * @param {Object} [guard] Internally used to allow this method to work with * others like `_.map` without using their callback `index` argument for `n`. - * @returns {Array} Returns all but the first value or `n` values of the `array`. + * @returns {Array} Returns all but the first value or `n` values of `array`. * @example * * _.rest([3, 2, 1]); * // => [2, 1] */ function rest(array, n, guard) { + if (!array) { + return []; + } return slice.call(array, (n == undefined || guard) ? 1 : n); } @@ -1480,6 +1475,9 @@ * // => [4, 1, 6, 3, 5, 2] */ function shuffle(array) { + if (!array) { + return []; + } var rand, index = -1, length = array.length, @@ -1493,12 +1491,66 @@ return result; } + /** + * Produces a new sorted array, ranked in ascending order by the results of + * running each element of `array` through `callback`. The `callback` is + * bound to `thisArg` and invoked with 3 arguments; (value, index, array). The + * `callback` argument may also be the name of a property to sort by (e.g. 'length'). + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to iterate over. + * @param {Function|String} callback The function called per iteration or + * property name to sort by. + * @param {Mixed} [thisArg] The `this` binding for the callback. + * @returns {Array} Returns a new array of sorted values. + * @example + * + * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); + * // => [3, 1, 2] + * + * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); + * // => [3, 1, 2] + * + * _.sortBy(['larry', 'brendan', 'moe'], 'length'); + * // => ['moe', 'larry', 'brendan'] + */ + function sortBy(array, callback, thisArg) { + if (!array) { + return []; + } + if (typeof callback == 'string') { + var prop = callback; + callback = function(array) { return array[prop]; }; + } else if (thisArg) { + callback = iteratorBind(callback, thisArg); + } + return pluck(map(array, function(value, index) { + return { + 'criteria': callback(value, index, array), + 'value': value + }; + }).sort(function(left, right) { + var a = left.criteria, + b = right.criteria; + + if (a === undefined) { + return 1; + } + if (b === undefined) { + return -1; + } + return a < b ? -1 : a > b ? 1 : 0; + }), 'value'); + } + /** * Uses a binary search to determine the smallest index at which the `value` - * should be inserted into the `array` in order to maintain the sort order - * of the sorted `array`. If `callback` is passed, it will be executed for - * `value` and each element in the `array` to compute their sort ranking. - * The `callback` is bound to `thisArg` and invoked with 1 argument; (value). + * should be inserted into `array` in order to maintain the sort order of the + * sorted `array`. If `callback` is passed, it will be executed for `value` and + * each element in `array` to compute their sort ranking. The `callback` is + * bound to `thisArg` and invoked with 1 argument; (value). * * @static * @memberOf _ @@ -1508,7 +1560,7 @@ * @param {Function} [callback=identity] The function called per iteration. * @param {Mixed} [thisArg] The `this` binding for the callback. * @returns {Number} Returns the index at which the value should be inserted - * into the array. + * into `array`. * @example * * _.sortedIndex([20, 30, 40], 35); @@ -1529,6 +1581,9 @@ * // => 2 */ function sortedIndex(array, value, callback, thisArg) { + if (!array) { + return 0; + } var mid, low = 0, high = array.length; @@ -1608,10 +1663,13 @@ * // => [1, 2, 3] */ function uniq(array, isSorted, callback, thisArg) { + var result = []; + if (!array) { + return result; + } var computed, index = -1, length = array.length, - result = [], seen = []; // juggle arguments @@ -1654,10 +1712,13 @@ * // => [2, 3, 4] */ function without(array) { + var result = []; + if (!array) { + return result; + } var excluded = slice.call(arguments, 1), index = -1, - length = array.length, - result = []; + length = array.length; while (++index < length) { if (indexOf(excluded, array[index]) < 0) { @@ -1683,7 +1744,10 @@ * _.zip(['moe', 'larry', 'curly'], [30, 40, 50], [true, false, false]); * // => [['moe', 30, true], ['larry', 40, false], ['curly', 50, false]] */ - function zip() { + function zip(array) { + if (!array) { + return []; + } var index = -1, length = max(pluck(arguments, 'length')), result = Array(length); diff --git a/test/test.js b/test/test.js index b62b26227..1c27d23b1 100644 --- a/test/test.js +++ b/test/test.js @@ -708,6 +708,82 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash "Arrays" methods'); + + (function() { + test('should allow a falsey `array` argument', function() { + _.each([ + 'compact', + 'difference', + 'first', + 'flatten', + 'groupBy', + 'indexOf', + 'initial', + 'intersection', + 'invoke', + 'last', + 'lastIndexOf', + 'max', + 'min', + 'pluck', + 'range', + 'rest', + 'shuffle', + 'sortBy', + 'sortedIndex', + 'union', + 'uniq', + 'without', + 'zip' + ], function(methodName) { + var pass = true; + try { + _[methodName](); + } catch(e) { + pass = false; + } + ok(pass, methodName + ' allows a falsey `array` argument'); + }); + }); + }()); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash "Collections" methods'); + + (function() { + test('should allow a falsey `collection` argument', function() { + _.each([ + 'contains', + 'every', + 'filter', + 'find', + 'forEach', + 'map', + 'reduce', + 'reduceRight', + 'reject', + 'some', + 'toArray' + ], function(methodName) { + var pass = true; + try { + if (/^(?:contains|toArray)$/.test(methodName)) { + _[methodName](null); + } else { + _[methodName](null, _.identity); + } + } catch(e) { + pass = false; + } + ok(pass, methodName + ' allows a falsey `collection` argument'); + }); + }); + }()); + + /*--------------------------------------------------------------------------*/ + // explicitly call `QUnit.start()` for Narwhal, Rhino, and RingoJS QUnit.start();