From 7a94f472c7f24f469b6d9b0d2eef65eb0571f5df Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 8 Sep 2014 22:05:59 -0700 Subject: [PATCH] Add `_.deburr`, `_.thru`, & `_.words`. --- lodash.js | 637 +++++++++++++++++++++++++++------------------------ test/test.js | 10 +- 2 files changed, 347 insertions(+), 300 deletions(-) diff --git a/lodash.js b/lodash.js index b161c32e9..4f577f934 100644 --- a/lodash.js +++ b/lodash.js @@ -204,7 +204,7 @@ }; /** - * Used to convert characters to HTML entities. + * Used to map characters to HTML entities. * * **Note:** Though the ">" character is escaped for symmetry, characters like * ">" and "/" don't require escaping in HTML and have no special meaning @@ -226,7 +226,7 @@ '`': '`' }; - /** Used to convert HTML entities to characters */ + /** Used to map HTML entities to characters */ var htmlUnescapes = { '&': '&', '<': '<', @@ -236,11 +236,7 @@ '`': '`' }; - /** - * Used to convert latin-1 supplement letters to basic latin (ASCII) letters. - * See [Wikipedia](http://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) - * for more details. - */ + /** Used to map latin-1 supplementary letters to basic latin letters */ var deburredLetters = { '\xC0': 'A', '\xC1': 'A', '\xC2': 'A', '\xC3': 'A', '\xC4': 'A', '\xC5': 'A', '\xE0': 'a', '\xE1': 'a', '\xE2': 'a', '\xE3': 'a', '\xE4': 'a', '\xE5': 'a', @@ -297,6 +293,184 @@ /*--------------------------------------------------------------------------*/ + /** + * A specialized version of `_.forEach` for arrays without support for + * callback shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array.length; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.forEachRight` for arrays without support for + * callback shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEachRight(array, iteratee) { + var length = array.length; + + while (length--) { + if (iteratee(array[length], length, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.every` for arrays without support for callback + * shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns `true` if all elements passed the predicate check, + * else `false` + */ + function arrayEvery(array, predicate) { + var index = -1, + length = array.length; + + while (++index < length) { + if (!predicate(array[index], index, array)) { + return false; + } + } + return true; + } + + /** + * A specialized version of `_.map` for arrays without support for callback + * shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * A specialized version of `_.filter` for arrays without support for callback + * shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function arrayFilter(array, predicate) { + var index = -1, + length = array.length, + resIndex = -1, + result = []; + + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result[++resIndex] = value; + } + } + return result; + } + + /** + * A specialized version of `_.reduce` for arrays without support for callback + * shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initFromArray=false] Specify using the first element of + * `array` as the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initFromArray) { + var index = -1, + length = array.length; + + if (initFromArray && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * A specialized version of `_.reduceRight` for arrays without support for + * callback shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initFromArray=false] Specify using the last element of + * `array` as the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduceRight(array, iteratee, accumulator, initFromArray) { + var length = array.length; + + if (initFromArray && length) { + accumulator = array[--length]; + } + while (length--) { + accumulator = iteratee(accumulator, array[length], length, array); + } + return accumulator; + } + + /** + * A specialized version of `_.some` for arrays without support for callback + * shorthands or `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passed the predicate check, + * else `false`. + */ + function arraySome(array, predicate) { + var index = -1, + length = array.length; + + while (++index < length) { + if (predicate(array[index], index, array)) { + return true; + } + } + return false; + } + /** * The base implementation of `_.at` without support for strings and individual * key arguments. @@ -485,30 +659,7 @@ } /** - * Creates a function that produces compound words out of the words in a - * given string. - * - * @private - * @param {Function} callback The function invoked to combine each word. - * @returns {Function} Returns the new compounder function. - */ - function createCompounder(callback) { - return function(string) { - var index = -1, - words = string != null && String(string).replace(reLatin1, deburrLetter).match(reWords), - length = words ? words.length : 0, - result = ''; - - while (++index < length) { - result = callback(result, words[index], index, words); - } - return result; - }; - } - - /** - * Used by `createCompounder` to convert latin-1 supplement letters to basic - * latin (ASCII) letters. + * Used by `deburr` to convert latin-1 to basic latin letters. * * @private * @param {string} letter The matched letter to deburr. @@ -574,6 +725,58 @@ (charCode >= 8192 && (charCode <= 8202 || charCode == 8232 || charCode == 8233 || charCode == 8239 || charCode == 8287 || charCode == 12288 || charCode == 65279))); } + /** + * Replaces all `placeholder` elements in `array` with an internal placeholder + * and returns an array of their indexes. + * + * @private + * @param {Array} array The array to modify. + * @param {*} placeholder The placeholder to replace. + * @returns {Array} Returns the new array of placeholder indexes. + */ + function replaceHolders(array, placeholder) { + var index = -1, + length = array.length, + resIndex = -1, + result = []; + + while (++index < length) { + if (array[index] === placeholder) { + array[index] = PLACEHOLDER; + result[++resIndex] = index; + } + } + return result; + } + + /** + * An implementation of `_.uniq` optimized for sorted arrays without support + * for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The function invoked per iteration. + * @returns {Array} Returns the new duplicate-value-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`. @@ -790,15 +993,15 @@ * `pairs`, `partial`, `partialRight`, `partition`, `pick`, `pluck`, `property`, * `pull`, `pullAt`, `push`, `range`, `reject`, `remove`, `rest`, `reverse`, * `shuffle`, `slice`, `sort`, `sortBy`, `splice`, `take`, `takeRight`, - * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `times`, `toArray`, - * `transform`, `union`, `uniq`, `unshift`, `unzip`, `values`, `valuesIn`, - * `where`, `without`, `wrap`, `xor`, `zip`, and `zipObject` + * `takeRightWhile`, `takeWhile`, `tap`, `throttle`, `thru`, `times`, + * `toArray`, `transform`, `union`, `uniq`, `unshift`, `unzip`, `values`, + * `valuesIn`, `where`, `without`, `wrap`, `xor`, `zip`, and `zipObject` * * The non-chainable wrapper functions are: * `attempt`, `camelCase`, `capitalize`, `clone`, `cloneDeep`, `contains`, - * `endsWith`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, `findKey`, - * `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, `has`, - * `identity`, `indexOf`, `isArguments`, `isArray`, `isBoolean`, isDate`, + * `deburr`, endsWith`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, + * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, + * `has`, `identity`, `indexOf`, `isArguments`, `isArray`, `isBoolean`, isDate`, * `isElement`, `isEmpty`, `isEqual`, `isError`, `isFinite`, `isFunction`, * `isNative`, `isNaN`, `isNull`, `isNumber`, `isObject`, `isPlainObject`, * `isRegExp`, `isString`, `isUndefined`, `join`, `kebabCase`, `last`, @@ -806,7 +1009,7 @@ * `padRight`, `parseInt`, `pop`, `random`, `reduce`, `reduceRight`, `repeat`, * `result`, `runInContext`, `shift`, `size`, `snakeCase`, `some`, `sortedIndex`, * `sortedLastIndex`, `startsWith`, `template`, `trim`, `trimLeft`, `trimRight`, - * `trunc`, `unescape`, `uniqueId`, and `value` + * `trunc`, `unescape`, `uniqueId`, `value`, and `words` * * The wrapper function `sample` will return a wrapped value when `n` is * provided, otherwise it will return an unwrapped value. @@ -1088,184 +1291,6 @@ /*------------------------------------------------------------------------*/ - /** - * A specialized version of `_.forEach` for arrays without support for - * callback shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEach(array, iteratee) { - var index = -1, - length = array.length; - - while (++index < length) { - if (iteratee(array[index], index, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.forEachRight` for arrays without support for - * callback shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns `array`. - */ - function arrayEachRight(array, iteratee) { - var length = array.length; - - while (length--) { - if (iteratee(array[length], length, array) === false) { - break; - } - } - return array; - } - - /** - * A specialized version of `_.every` for arrays without support for callback - * shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns `true` if all elements passed the predicate check, - * else `false` - */ - function arrayEvery(array, predicate) { - var index = -1, - length = array.length; - - while (++index < length) { - if (!predicate(array[index], index, array)) { - return false; - } - } - return true; - } - - /** - * A specialized version of `_.map` for arrays without support for callback - * shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @returns {Array} Returns the new mapped array. - */ - function arrayMap(array, iteratee) { - var index = -1, - length = array.length, - result = Array(length); - - while (++index < length) { - result[index] = iteratee(array[index], index, array); - } - return result; - } - - /** - * A specialized version of `_.filter` for arrays without support for callback - * shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the new filtered array. - */ - function arrayFilter(array, predicate) { - var index = -1, - length = array.length, - resIndex = -1, - result = []; - - while (++index < length) { - var value = array[index]; - if (predicate(value, index, array)) { - result[++resIndex] = value; - } - } - return result; - } - - /** - * A specialized version of `_.reduce` for arrays without support for callback - * shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initFromArray=false] Specify using the first element of - * `array` as the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduce(array, iteratee, accumulator, initFromArray) { - var index = -1, - length = array.length; - - if (initFromArray && length) { - accumulator = array[++index]; - } - while (++index < length) { - accumulator = iteratee(accumulator, array[index], index, array); - } - return accumulator; - } - - /** - * A specialized version of `_.reduceRight` for arrays without support for - * callback shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} iteratee The function invoked per iteration. - * @param {*} [accumulator] The initial value. - * @param {boolean} [initFromArray=false] Specify using the last element of - * `array` as the initial value. - * @returns {*} Returns the accumulated value. - */ - function arrayReduceRight(array, iteratee, accumulator, initFromArray) { - var length = array.length; - - if (initFromArray && length) { - accumulator = array[--length]; - } - while (length--) { - accumulator = iteratee(accumulator, array[length], length, array); - } - return accumulator; - } - - /** - * A specialized version of `_.some` for arrays without support for callback - * shorthands or `this` binding. - * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {boolean} Returns `true` if any element passed the predicate check, - * else `false`. - */ - function arraySome(array, predicate) { - var index = -1, - length = array.length; - - while (++index < length) { - if (predicate(array[index], index, array)) { - return true; - } - } - return false; - } - /** * Used by `_.defaults` to customize its `_.assign` use. * @@ -2551,6 +2576,28 @@ return cache; }; + /** + * Creates a function that produces compound words out of the words in a + * given string. + * + * @private + * @param {Function} callback The function invoked to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + var index = -1, + array = words(deburr(string)), + length = array.length, + result = ''; + + while (++index < length) { + result = callback(result, array[index], index, words); + } + return result; + }; + } + /** * Creates a function that produces an instance of `Ctor` regardless of * whether it was invoked as part of a `new` expression or by `call` or `apply`. @@ -2981,30 +3028,6 @@ return result; } - /** - * Replaces all `placeholder` elements in `array` with an internal placeholder - * and returns an array of their indexes. - * - * @private - * @param {Array} array The array to modify. - * @param {*} placeholder The placeholder to replace. - * @returns {Array} Returns the new array of placeholder indexes. - */ - function replaceHolders(array, placeholder) { - var index = -1, - length = array.length, - resIndex = -1, - result = []; - - while (++index < length) { - if (array[index] === placeholder) { - array[index] = PLACEHOLDER; - result[++resIndex] = index; - } - } - return result; - } - /** * Sets metadata for `func`. * @@ -3108,34 +3131,6 @@ return result; } - /** - * An implementation of `_.uniq` optimized for sorted arrays without support - * for callback shorthands and `this` binding. - * - * @private - * @param {Array} array The array to inspect. - * @param {Function} [iteratee] The function invoked per iteration. - * @returns {Array} Returns the new duplicate-value-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; - } - /** * Converts `value` to an array-like object if it is not one. * @@ -4501,6 +4496,22 @@ return value; } + /** + * This method is like `_.tap` except that it returns the result of `interceptor`. + * + * @static + * @memberOf _ + * @category Chain + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @param {*} [thisArg] The `this` binding of `interceptor`. + * @returns {*} Returns the result of `interceptor`. + * @example + */ + function thru(value, interceptor, thisArg) { + return interceptor.call(thisArg, value); + } + /** * Enables explicit method chaining on the wrapper object. * @@ -8030,11 +8041,28 @@ * // => 'Fred' */ function capitalize(string) { - if (string == null) { - return ''; - } - string = String(string); - return string.charAt(0).toUpperCase() + string.slice(1); + string = string == null ? '' : String(string); + return string ? (string.charAt(0).toUpperCase() + string.slice(1)) : string; + } + + /** + * Deburrs `string` by converting latin-1 supplementary letters to basic latin letters. + * See [Wikipedia](http://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * for more details. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the beburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = string == null ? '' : String(string); + return string ? string.replace(reLatin1, deburrLetter) : string; } /** @@ -8092,7 +8120,7 @@ function escape(string) { // reset `lastIndex` because in IE < 9 `String#replace` does not string = string == null ? '' : String(string); - return (reUnescapedHtml.lastIndex = 0, reUnescapedHtml.test(string)) + return string && (reUnescapedHtml.lastIndex = 0, reUnescapedHtml.test(string)) ? string.replace(reUnescapedHtml, escapeHtmlChar) : string; } @@ -8113,7 +8141,7 @@ */ function escapeRegExp(string) { string = string == null ? '' : String(string); - return (reRegExpChars.lastIndex = 0, reRegExpChars.test(string)) + return string && (reRegExpChars.lastIndex = 0, reRegExpChars.test(string)) ? string.replace(reRegExpChars, '\\$&') : string; } @@ -8207,7 +8235,7 @@ */ function padLeft(string, length, chars) { string = string == null ? '' : String(string); - return createPad(string, length, chars) + string; + return string ? (createPad(string, length, chars) + string) : string; } /** @@ -8235,7 +8263,7 @@ */ function padRight(string, length, chars) { string = string == null ? '' : String(string); - return string + createPad(string, length, chars); + return string ? (string + createPad(string, length, chars)) : string; } /** @@ -8716,11 +8744,29 @@ */ function unescape(string) { string = string == null ? '' : String(string); - return (reEscapedHtml.lastIndex = 0, reEscapedHtml.test(string)) + return string && (reEscapedHtml.lastIndex = 0, reEscapedHtml.test(string)) ? string.replace(reEscapedHtml, unescapeHtmlChar) : string; } + /** + * Splits `string` into an array of its words. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to inspect. + * @returns {Array} Returns the words of `string`. + * @example + * + * _.words('hello world'); + * // => ['hello', 'world'] + */ + function words(string) { + string = string == null ? '' : String(string); + return (string && string.match(reWords)) || []; + } + /*------------------------------------------------------------------------*/ /** @@ -8963,12 +9009,9 @@ object.prototype[methodName] = (function(methodName) { return function() { if (chain || this.__chain__) { - var queue = baseSlice(this.__queue__), - result = object(this.__wrapped__); - + var result = object(this.__wrapped__); result.__chain__ = this.__chain__; - result.__queue__ = queue; - queue.push([methodName, object, arguments]); + (result.__queue__ = baseSlice(this.__queue__)).push([methodName, object, arguments]); return result; } var args = [this.value()]; @@ -9401,6 +9444,7 @@ lodash.takeWhile = takeWhile; lodash.tap = tap; lodash.throttle = throttle; + lodash.thru = thru; lodash.times = times; lodash.toArray = toArray; lodash.transform = transform; @@ -9442,6 +9486,7 @@ lodash.clone = clone; lodash.cloneDeep = cloneDeep; lodash.contains = contains; + lodash.deburr = deburr; lodash.endsWith = endsWith; lodash.escape = escape; lodash.escapeRegExp = escapeRegExp; @@ -9507,6 +9552,7 @@ lodash.trunc = trunc; lodash.unescape = unescape; lodash.uniqueId = uniqueId; + lodash.words = words; // add aliases lodash.all = every; @@ -9569,10 +9615,11 @@ arrayEach(['join', 'pop', 'shift'], function(methodName) { var func = arrayProto[methodName]; lodash.prototype[methodName] = function() { - if (!this.__chain__) { - return func.apply(this.value(), arguments) + var chainAll = this.__chain__; + if (!chainAll) { + return func.apply(this.value(), arguments); } - var result = new lodashWrapper(value.__wrapped__, value.__chain__, baseSlice(value.__queue__)) + var result = new lodashWrapper(this.__wrapped__, chainAll, baseSlice(this.__queue__)); result.__queue__.push([func, null, arguments]); return result; }; @@ -9605,16 +9652,14 @@ isSplice = methodName == 'splice'; lodash.prototype[methodName] = function() { - var chainAll = this.__chain__, - value = this.value(), - result = func.apply(value, arguments); - - if (value.length === 0) { - delete value[0]; - } - return (chainAll || isSplice) - ? new lodashWrapper(result, chainAll) - : result; + var args = arguments; + return this[isSplice ? 'thru' : 'tap'](function(value) { + var result = func.apply(value, args); + if (value.length === 0) { + delete value[0]; + } + return result; + }); }; }); } diff --git a/test/test.js b/test/test.js index 1d8c1695c..f5e7644f8 100644 --- a/test/test.js +++ b/test/test.js @@ -12254,12 +12254,13 @@ 'partialRight', 'tap', 'throttle', + 'thru', 'wrap' ]; var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 195, function() { + test('should accept falsey arguments', 197, function() { var emptyArrays = _.map(falsey, _.constant([])), isExposed = '_' in root, oldDash = root._; @@ -12294,7 +12295,7 @@ }); // skip tests for missing methods of modularized builds - _.each(['noConflict', 'runInContext', 'tap'], function(methodName) { + _.each(['noConflict', 'runInContext', 'tap', 'thru'], function(methodName) { if (!_[methodName]) { skipTest(); } @@ -12325,7 +12326,7 @@ }); }); - test('should throw a TypeError for falsey arguments', 20, function() { + test('should throw a TypeError for falsey arguments', 21, function() { _.each(rejectFalsey, function(methodName) { var expected = _.map(falsey, _.constant(true)), func = _[methodName]; @@ -12344,7 +12345,7 @@ }); }); - test('should handle `null` `thisArg` arguments', 43, function() { + test('should handle `null` `thisArg` arguments', 44, function() { var expected = (function() { return this; }).call(null); var funcs = [ @@ -12390,6 +12391,7 @@ 'tap', 'times', 'transform', + 'thru', 'uniq' ];