From 4c09879aabbe8fe5a10cf90161d75361bb9984ae Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sun, 12 Jul 2015 23:31:46 -0700 Subject: [PATCH] Add `_.assignWith`, `_.cloneDeepWith`, `_.cloneWith`, `_.extendWith`, `_.isEqualWith`, `_.isMatchWith`, and `_.mergeWith`. --- lodash.src.js | 390 ++++++++++++++++++++++++++++++++------------------ test/test.js | 313 ++++++++++++++++++++++------------------ 2 files changed, 428 insertions(+), 275 deletions(-) diff --git a/lodash.src.js b/lodash.src.js index fa1c4984e..a05f0da71 100644 --- a/lodash.src.js +++ b/lodash.src.js @@ -2342,6 +2342,48 @@ }; } + /** + * The base implementation of `_.merge` without support multiple sources. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {Function} [customizer] The function to customize merged values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + * @returns {Object} Returns `object`. + */ + function baseMerge(object, source, customizer, stackA, stackB) { + var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), + props = isSrcArr ? undefined : keysIn(source); + + arrayEach(props || source, function(srcValue, key) { + if (props) { + key = srcValue; + srcValue = source[key]; + } + if (isObjectLike(srcValue)) { + stackA || (stackA = []); + stackB || (stackB = []); + baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB); + } + else { + var value = object[key], + result = customizer ? customizer(value, srcValue, key, object, source) : undefined, + isCommon = result === undefined; + + if (isCommon) { + result = srcValue; + } + if ((result !== undefined || (isSrcArr && !(key in object))) && + (isCommon || (result === result ? (result !== value) : (value === value)))) { + object[key] = result; + } + } + }); + return object; + } + /** * A specialized version of `baseMerge` for arrays and objects which performs * deep merges and tracks traversed objects enabling objects with circular @@ -2959,7 +3001,7 @@ * @private * @param {Object} source The object to copy properties from. * @param {Array} props The property names to copy. - * @param {Function} customizer The function to customize copied values. + * @param {Function} [customizer] The function to customize copied values. * @param {Object} [object={}] The object to copy properties to. * @returns {Object} Returns `object`. */ @@ -2972,9 +3014,10 @@ while (++index < length) { var key = props[index], value = object[key], - result = customizer(value, source[key], key, object, source); + result = customizer ? customizer(value, source[key], key, object, source) : source[key]; - if ((result === result ? (result !== value) : (value === value)) || + if (!customizer || + (result === result ? (result !== value) : (value === value)) || (value === undefined && !(key in object))) { object[key] = result; } @@ -4067,7 +4110,7 @@ return sourceValue; } return isObject(objectValue) - ? merge(objectValue, sourceValue, mergeDefaults) + ? mergeWith(objectValue, sourceValue, mergeDefaults) : objectValue; } @@ -7713,10 +7756,7 @@ /*------------------------------------------------------------------------*/ /** - * Creates a shallow clone of `value`. If `customizer` is provided it's invoked - * to produce the cloned value. If `customizer` returns `undefined` cloning - * is handled by the method instead. The `customizer` is invoked with up to - * three arguments; (value [, index|key, object]). + * Creates a shallow clone of `value`. * * **Note:** This method is loosely based on the * [structured clone algorithm](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm). @@ -7729,7 +7769,6 @@ * @memberOf _ * @category Lang * @param {*} value The value to clone. - * @param {Function} [customizer] The function to customize cloning. * @returns {*} Returns the cloned value. * @example * @@ -7741,8 +7780,25 @@ * var shallow = _.clone(users); * shallow[0] === users[0]; * // => true + */ + function clone(value) { + return baseClone(value); + } + + /** + * This method is like `_.clone` except that it accepts `customizer` which + * is invoked to produce the cloned value. If `customizer` returns `undefined` + * cloning is handled by the method instead. The `customizer` is invoked with + * up to three arguments; (value [, index|key, object]). + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the cloned value. + * @example * - * // using a customizer callback * var el = _.clone(document.body, function(value) { * if (_.isElement(value)) { * return value.cloneNode(false); @@ -7756,10 +7812,8 @@ * el.childNodes.length; * // => 0 */ - function clone(value, customizer) { - return typeof customizer == 'function' - ? baseClone(value, false, customizer) - : baseClone(value); + function cloneWith(value, customizer) { + return baseClone(value, false, customizer); } /** @@ -7769,7 +7823,6 @@ * @memberOf _ * @category Lang * @param {*} value The value to recursively clone. - * @param {Function} [customizer] The function to customize cloning. * @returns {*} Returns the deep cloned value. * @example * @@ -7781,8 +7834,22 @@ * var deep = _.cloneDeep(users); * deep[0] === users[0]; * // => false + */ + function cloneDeep(value) { + return baseClone(value, true); + } + + /** + * This method is like `_.cloneWith` except that it recursively clones `value`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to recursively clone. + * @param {Function} [customizer] The function to customize cloning. + * @returns {*} Returns the deep cloned value. + * @example * - * // using a customizer callback * var el = _.cloneDeep(document.body, function(value) { * if (_.isElement(value)) { * return value.cloneNode(true); @@ -7796,10 +7863,8 @@ * el.childNodes.length; * // => 20 */ - function cloneDeep(value, customizer) { - return typeof customizer == 'function' - ? baseClone(value, true, customizer) - : baseClone(value, true); + function cloneDeepWith(value, customizer) { + return baseClone(value, true, customizer); } /** @@ -7991,16 +8056,12 @@ /** * Performs a deep comparison between two values to determine if they are - * equivalent. If `customizer` is provided it's invoked to compare values. - * If `customizer` returns `undefined` comparisons are handled by the method - * instead. The `customizer` is invoked with up to three arguments: - * (value, other [, index|key]). + * equivalent. * * **Note:** This method supports comparing arrays, booleans, `Date` objects, * numbers, `Object` objects, regexes, and strings. Objects are compared by * their own, not inherited, enumerable properties. Functions and DOM nodes - * are **not** supported. Provide a customizer function to extend support - * for comparing other values. + * are **not** supported. * * @static * @memberOf _ @@ -8008,7 +8069,6 @@ * @category Lang * @param {*} value The value to compare. * @param {*} other The other value to compare. - * @param {Function} [customizer] The function to customize comparisons. * @returns {boolean} Returns `true` if the values are equivalent, else `false`. * @example * @@ -8020,19 +8080,38 @@ * * _.isEqual(object, other); * // => true + */ + function isEqual(value, other) { + return baseIsEqual(value, other); + } + + /** + * This method is like `_.isEqual` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined` comparisons + * are handled by the method instead. The `customizer` is invoked with up to + * three arguments: (value, other [, index|key]). + * + * @static + * @memberOf _ + * @alias eq + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example * - * // using a customizer callback * var array = ['hello', 'goodbye']; * var other = ['hi', 'goodbye']; * - * _.isEqual(array, other, function(value, other) { + * _.isEqualWith(array, other, function(value, other) { * if (_.every([value, other], RegExp.prototype.test, /^h(?:i|ello)$/)) { * return true; * } * }); * // => true */ - function isEqual(value, other, customizer) { + function isEqualWith(value, other, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; var result = customizer ? customizer(value, other) : undefined; return result === undefined ? baseIsEqual(value, other, customizer) : !!result; @@ -8142,10 +8221,7 @@ /** * Performs a deep comparison between `object` and `source` to determine if - * `object` contains equivalent property values. If `customizer` is provided - * it's invoked to compare values. If `customizer` returns `undefined` - * comparisons are handled by the method instead. The `customizer` is invoked - * with three arguments: (value, other, index|key). + * `object` contains equivalent property values. * * **Note:** This method supports comparing properties of arrays, booleans, * `Date` objects, numbers, `Object` objects, regexes, and strings. Functions @@ -8157,7 +8233,6 @@ * @category Lang * @param {Object} object The object to inspect. * @param {Object} source The object of property values to match. - * @param {Function} [customizer] The function to customize comparisons. * @returns {boolean} Returns `true` if `object` is a match, else `false`. * @example * @@ -8168,8 +8243,26 @@ * * _.isMatch(object, { 'age': 36 }); * // => false + */ + function isMatch(object, source) { + return baseIsMatch(object, getMatchData(source)); + } + + /** + * This method is like `_.isMatch` except that it accepts `customizer` which + * is invoked to compare values. If `customizer` returns `undefined` comparisons + * are handled by the method instead. The `customizer` is invoked with three + * arguments: (value, other, index|key). + * + * @static + * @memberOf _ + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Function} [customizer] The function to customize comparisons. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example * - * // using a customizer callback * var object = { 'greeting': 'hello' }; * var source = { 'greeting': 'hi' }; * @@ -8178,7 +8271,7 @@ * }); * // => true */ - function isMatch(object, source, customizer) { + function isMatchWith(object, source, customizer) { customizer = typeof customizer == 'function' ? customizer : undefined; return baseIsMatch(object, getMatchData(source), customizer); } @@ -8528,89 +8621,9 @@ /*------------------------------------------------------------------------*/ - /** - * Recursively merges own enumerable properties of the source object(s), that - * don't resolve to `undefined` into the destination object. Subsequent sources - * overwrite property assignments of previous sources. If `customizer` is - * provided it's invoked to produce the merged values of the destination and - * source properties. If `customizer` returns `undefined` merging is handled - * by the method instead. The `customizer` is invoked with five arguments: - * (objectValue, sourceValue, key, object, source). - * - * @static - * @memberOf _ - * @category Object - * @param {Object} object The destination object. - * @param {...Object} [sources] The source objects. - * @param {Function} [customizer] The function to customize assigned values. - * @returns {Object} Returns `object`. - * @example - * - * var users = { - * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] - * }; - * - * var ages = { - * 'data': [{ 'age': 36 }, { 'age': 40 }] - * }; - * - * _.merge(users, ages); - * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } - * - * // using a customizer callback - * var object = { - * 'fruits': ['apple'], - * 'vegetables': ['beet'] - * }; - * - * var other = { - * 'fruits': ['banana'], - * 'vegetables': ['carrot'] - * }; - * - * _.merge(object, other, function(a, b) { - * if (_.isArray(a)) { - * return a.concat(b); - * } - * }); - * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } - */ - var merge = createAssigner(function baseMerge(object, source, customizer, stackA, stackB) { - var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), - props = isSrcArr ? undefined : keysIn(source); - - arrayEach(props || source, function(srcValue, key) { - if (props) { - key = srcValue; - srcValue = source[key]; - } - if (isObjectLike(srcValue)) { - stackA || (stackA = []); - stackB || (stackB = []); - baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB); - } - else { - var value = object[key], - result = customizer ? customizer(value, srcValue, key, object, source) : undefined, - isCommon = result === undefined; - - if (isCommon) { - result = srcValue; - } - if ((result !== undefined || (isSrcArr && !(key in object))) && - (isCommon || (result === result ? (result !== value) : (value === value)))) { - object[key] = result; - } - } - }); - return object; - }); - /** * Assigns own enumerable properties of source object(s) to the destination * object. Subsequent sources overwrite property assignments of previous sources. - * If `customizer` is provided it's invoked to produce the assigned values. - * The `customizer` is invoked with five arguments: (objectValue, sourceValue, key, object, source). * * **Note:** This method mutates `object` and is based on * [`Object.assign`](http://ecma-international.org/ecma-262/6.0/#sec-object.assign). @@ -8620,28 +8633,41 @@ * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. - * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @example * * _.assign({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' }); * // => { 'user': 'fred', 'age': 40 } + */ + var assign = createAssigner(function(object, source) { + copyObject(source, keys(source), object); + }); + + /** + * This method is like `_.assign` except that it accepts `customizer` which + * is invoked to produce the assigned values. The `customizer` is invoked + * with five arguments: (objectValue, sourceValue, key, object, source). * - * // using a customizer callback - * var defaults = _.partialRight(_.assign, function(value, other) { + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var defaults = _.partialRight(_.assignWith, function(value, other) { * return _.isUndefined(value) ? other : value; * }); * * defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); * // => { 'user': 'barney', 'age': 36 } */ - var assign = createAssigner(function(object, source, customizer) { - var props = keys(source); - if (customizer) { - copyObjectWith(source, props, customizer, object); - } else { - copyObject(source, props, object); - } + var assignWith = createAssigner(function(object, source, customizer) { + copyObjectWith(source, keys(source), customizer, object); }); /** @@ -8706,7 +8732,7 @@ */ var defaults = restParam(function(args) { args.push(undefined, extendDefaults); - return extend.apply(undefined, args); + return extendWith.apply(undefined, args); }); /** @@ -8729,7 +8755,7 @@ */ var defaultsDeep = restParam(function(args) { args.push(undefined, mergeDefaults); - return merge.apply(undefined, args); + return mergeWith.apply(undefined, args); }); /** @@ -8741,20 +8767,38 @@ * @category Object * @param {Object} object The destination object. * @param {...Object} [sources] The source objects. - * @param {Function} [customizer] The function to customize assigned values. * @returns {Object} Returns `object`. * @example * * _.extend({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' }); * // => { 'user': 'fred', 'age': 40 } */ - var extend = createAssigner(function(object, source, customizer) { - var props = keysIn(source); - if (customizer) { - copyObjectWith(source, props, customizer, object); - } else { - copyObject(source, props, object); - } + var extend = createAssigner(function(object, source) { + copyObject(source, keysIn(source), object); + }); + + /** + * This method is like `_.assignWith` except that it iterates over own and + * inherited source properties. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var defaults = _.partialRight(_.extendWith, function(value, other) { + * return _.isUndefined(value) ? other : value; + * }); + * + * defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); + * // => { 'user': 'barney', 'age': 36 } + */ + var extendWith = createAssigner(function(object, source, customizer) { + copyObjectWith(source, keysIn(source), customizer, object); }); /** @@ -9265,6 +9309,71 @@ return result; } + /** + * Recursively merges own enumerable properties of the source object(s), that + * don't resolve to `undefined` into the destination object. Subsequent sources + * overwrite property assignments of previous sources. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * var users = { + * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] + * }; + * + * var ages = { + * 'data': [{ 'age': 36 }, { 'age': 40 }] + * }; + * + * _.merge(users, ages); + * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } + */ + var merge = createAssigner(function(object, source) { + baseMerge(object, source); + }); + + /** + * This method is like `_.merge` except that it accepts `customizer` which + * is invoked to produce the merged values of the destination and source + * properties. If `customizer` returns `undefined` merging is handled by the + * method instead. The `customizer` is invoked with five arguments: + * (objectValue, sourceValue, key, object, source). + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} sources The source objects. + * @param {Function} customizer The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var other = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.mergeWith(object, other, function(a, b) { + * if (_.isArray(a)) { + * return a.concat(b); + * } + * }); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } + */ + var mergeWith = createAssigner(function(object, source, customizer) { + baseMerge(object, source, customizer); + }); + /** * The opposite of `_.pick`; this method creates an object composed of the * own and inherited enumerable properties of `object` that are not omitted. @@ -11335,6 +11444,7 @@ lodash.after = after; lodash.ary = ary; lodash.assign = assign; + lodash.assignWith = assignWith; lodash.at = at; lodash.before = before; lodash.bind = bind; @@ -11359,6 +11469,7 @@ lodash.dropRightWhile = dropRightWhile; lodash.dropWhile = dropWhile; lodash.extend = extend; + lodash.extendWith = extendWith; lodash.fill = fill; lodash.filter = filter; lodash.flatten = flatten; @@ -11382,6 +11493,7 @@ lodash.matchesProperty = matchesProperty; lodash.memoize = memoize; lodash.merge = merge; + lodash.mergeWith = mergeWith; lodash.method = method; lodash.methodOf = methodOf; lodash.mixin = mixin; @@ -11454,6 +11566,8 @@ lodash.ceil = ceil; lodash.clone = clone; lodash.cloneDeep = cloneDeep; + lodash.cloneDeepWith = cloneDeepWith; + lodash.cloneWith = cloneWith; lodash.deburr = deburr; lodash.endsWith = endsWith; lodash.escape = escape; @@ -11488,10 +11602,12 @@ lodash.isElement = isElement; lodash.isEmpty = isEmpty; lodash.isEqual = isEqual; + lodash.isEqualWith = isEqualWith; lodash.isError = isError; lodash.isFinite = isFinite; lodash.isFunction = isFunction; lodash.isMatch = isMatch; + lodash.isMatchWith = isMatchWith; lodash.isNaN = isNaN; lodash.isNative = isNative; lodash.isNull = isNull; diff --git a/test/test.js b/test/test.js index 05645729f..4dded1f69 100644 --- a/test/test.js +++ b/test/test.js @@ -1039,27 +1039,35 @@ var func = _[methodName]; test('`_.' + methodName + '` should assign properties of a source object to the destination object', 1, function() { - deepEqual(_.assign({ 'a': 1 }, { 'b': 2 }), { 'a': 1, 'b': 2 }); + deepEqual(func({ 'a': 1 }, { 'b': 2 }), { 'a': 1, 'b': 2 }); }); test('`_.' + methodName + '` should accept multiple source objects', 2, function() { var expected = { 'a': 1, 'b': 2, 'c': 3 }; - deepEqual(_.assign({ 'a': 1 }, { 'b': 2 }, { 'c': 3 }), expected); - deepEqual(_.assign({ 'a': 1 }, { 'b': 2, 'c': 2 }, { 'c': 3 }), expected); + deepEqual(func({ 'a': 1 }, { 'b': 2 }, { 'c': 3 }), expected); + deepEqual(func({ 'a': 1 }, { 'b': 2, 'c': 2 }, { 'c': 3 }), expected); }); test('`_.' + methodName + '` should overwrite destination properties', 1, function() { var expected = { 'a': 3, 'b': 2, 'c': 1 }; - deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected); + deepEqual(func({ 'a': 1, 'b': 2 }, expected), expected); }); test('`_.' + methodName + '` should assign source properties with nullish values', 1, function() { var expected = { 'a': null, 'b': undefined, 'c': null }; - deepEqual(_.assign({ 'a': 1, 'b': 2 }, expected), expected); + deepEqual(func({ 'a': 1, 'b': 2 }, expected), expected); }); + }); + + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.assignWith and lodash.extendWith'); + + _.each(['assignWith', 'extendWith'], function(methodName) { + var func = _[methodName]; test('`_.' + methodName + '` should work with a `customizer` callback', 1, function() { - var actual = _.assign({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) { + var actual = func({ 'a': 1, 'b': 2 }, { 'a': 3, 'c': 3 }, function(a, b) { return typeof a == 'undefined' ? b : a; }); @@ -1068,7 +1076,7 @@ test('`_.' + methodName + '` should work with a `customizer` that returns `undefined`', 1, function() { var expected = { 'a': undefined }; - deepEqual(_.assign({}, expected, _.identity), expected); + deepEqual(func({}, expected, _.identity), expected); }); }); @@ -1963,23 +1971,6 @@ var expected = typeof value == 'function' ? { 'c': Foo.c } : (value && {}); deepEqual(func(value), expected); }); - - test('`_.' + methodName + '` should work with a `customizer` callback and ' + key, 4, function() { - var customizer = function(value) { - return _.isPlainObject(value) ? undefined : value; - }; - - var actual = func(value, customizer); - - deepEqual(actual, value); - strictEqual(actual, value); - - var object = { 'a': value, 'b': { 'c': value } }; - actual = func(object, customizer); - - deepEqual(actual, object); - notStrictEqual(actual, object); - }); }); test('`_.' + methodName + '` should clone array buffers', 2, function() { @@ -2024,22 +2015,6 @@ notStrictEqual(actual, shadowObject); }); - test('`_.' + methodName + '` should provide the correct `customizer` arguments', 1, function() { - var argsList = [], - foo = new Foo; - - func(foo, function() { - argsList.push(slice.call(arguments)); - }); - - deepEqual(argsList, isDeep ? [[foo], [1, 'a', foo]] : [[foo]]); - }); - - test('`_.' + methodName + '` should handle cloning if `customizer` returns `undefined`', 1, function() { - var actual = func({ 'a': { 'b': 'c' } }, _.noop); - deepEqual(actual, { 'a': { 'b': 'c' } }); - }); - test('`_.' + methodName + '` should clone `index` and `input` array properties', 2, function() { var array = /x/.exec('vwxyz'), actual = func(array); @@ -2122,6 +2097,46 @@ } }); }); + + _.each(['cloneWith', 'cloneDeepWith'], function(methodName) { + var func = _[methodName], + isDeepWith = methodName == 'cloneDeepWith'; + + test('`_.' + methodName + '` should provide the correct `customizer` arguments', 1, function() { + var argsList = [], + foo = new Foo; + + func(foo, function() { + argsList.push(slice.call(arguments)); + }); + + deepEqual(argsList, isDeepWith ? [[foo], [1, 'a', foo]] : [[foo]]); + }); + + test('`_.' + methodName + '` should handle cloning if `customizer` returns `undefined`', 1, function() { + var actual = func({ 'a': { 'b': 'c' } }, _.noop); + deepEqual(actual, { 'a': { 'b': 'c' } }); + }); + + _.forOwn(uncloneable, function(value, key) { + test('`_.' + methodName + '` should work with a `customizer` callback and ' + key, 4, function() { + var customizer = function(value) { + return _.isPlainObject(value) ? undefined : value; + }; + + var actual = func(value, customizer); + + deepEqual(actual, value); + strictEqual(actual, value); + + var object = { 'a': value, 'b': { 'c': value } }; + actual = func(object, customizer); + + deepEqual(actual, object); + notStrictEqual(actual, object); + }); + }); + }); }(1, 2, 3)); /*--------------------------------------------------------------------------*/ @@ -5156,8 +5171,20 @@ }); _.each(['assign', 'extend', 'merge'], function(methodName) { + var func = _[methodName]; + + test('`_.' + methodName + '` should not treat `object` as `source`', 1, function() { + function Foo() {} + Foo.prototype.a = 1; + + var actual = func(new Foo, { 'b': 2 }); + ok(!_.has(actual, 'a')); + }); + }); + + _.each(['assignWith', 'extendWith', 'mergeWith'], function(methodName) { var func = _[methodName], - isMerge = methodName == 'merge'; + isMergeWith = methodName == 'mergeWith'; test('`_.' + methodName + '` should provide the correct `customizer` arguments', 3, function() { var args, @@ -5192,20 +5219,12 @@ }); var expected = [[objectValue, sourceValue, 'a', object, source]]; - if (isMerge) { + if (isMergeWith) { expected.push([undefined, 2, 'b', sourceValue, sourceValue]); } deepEqual(argsList, expected, 'object property values'); }); - test('`_.' + methodName + '` should not treat `object` as `source`', 1, function() { - function Foo() {} - Foo.prototype.a = 1; - - var actual = func(new Foo, { 'b': 2 }); - ok(!_.has(actual, 'a')); - }); - test('`_.' + methodName + '` should not treat the second argument as a `customizer` callback', 2, function() { function callback() {} callback.b = 2; @@ -6968,82 +6987,6 @@ deepEqual(actual, expected); }); - test('should provide the correct `customizer` arguments', 1, function() { - var argsList = [], - object1 = { 'a': [1, 2], 'b': null }, - object2 = { 'a': [1, 2], 'b': null }; - - object1.b = object2; - object2.b = object1; - - var expected = [ - [object1, object2], - [object1.a, object2.a, 'a'], - [object1.a[0], object2.a[0], 0], - [object1.a[1], object2.a[1], 1], - [object1.b, object2.b, 'b'], - [object1.b.a, object2.b.a, 'a'], - [object1.b.a[0], object2.b.a[0], 0], - [object1.b.a[1], object2.b.a[1], 1], - [object1.b.b, object2.b.b, 'b'] - ]; - - _.isEqual(object1, object2, function() { - argsList.push(slice.call(arguments)); - }); - - deepEqual(argsList, expected); - }); - - test('should handle comparisons if `customizer` returns `undefined`', 3, function() { - strictEqual(_.isEqual('a', 'a', _.noop), true); - strictEqual(_.isEqual(['a'], ['a'], _.noop), true); - strictEqual(_.isEqual({ '0': 'a' }, { '0': 'a' }, _.noop), true); - }); - - test('should not handle comparisons if `customizer` returns `true`', 3, function() { - var customizer = function(value) { - return _.isString(value) || undefined; - }; - - strictEqual(_.isEqual('a', 'b', customizer), true); - strictEqual(_.isEqual(['a'], ['b'], customizer), true); - strictEqual(_.isEqual({ '0': 'a' }, { '0': 'b' }, customizer), true); - }); - - test('should not handle comparisons if `customizer` returns `false`', 3, function() { - var customizer = function(value) { - return _.isString(value) ? false : undefined; - }; - - strictEqual(_.isEqual('a', 'a', customizer), false); - strictEqual(_.isEqual(['a'], ['a'], customizer), false); - strictEqual(_.isEqual({ '0': 'a' }, { '0': 'a' }, customizer), false); - }); - - test('should return a boolean value even if `customizer` does not', 2, function() { - var actual = _.isEqual('a', 'b', _.constant('c')); - strictEqual(actual, true); - - var values = _.without(falsey, undefined), - expected = _.map(values, _.constant(false)); - - actual = []; - _.each(values, function(value) { - actual.push(_.isEqual('a', 'a', _.constant(value))); - }); - - deepEqual(actual, expected); - }); - - test('should ensure `customizer` is a function', 1, function() { - var array = [1, 2, 3], - eq = _.partial(_.isEqual, array), - actual = _.map([array, [1, 0, 3]], eq); - - deepEqual(actual, [true, false]); - }); - test('should work as an iteratee for `_.every`', 1, function() { var actual = _.every([1, 1, 1], _.partial(_.isEqual, 1)); ok(actual); @@ -7175,6 +7118,88 @@ /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.isEqualWith'); + + (function() { + test('should provide the correct `customizer` arguments', 1, function() { + var argsList = [], + object1 = { 'a': [1, 2], 'b': null }, + object2 = { 'a': [1, 2], 'b': null }; + + object1.b = object2; + object2.b = object1; + + var expected = [ + [object1, object2], + [object1.a, object2.a, 'a'], + [object1.a[0], object2.a[0], 0], + [object1.a[1], object2.a[1], 1], + [object1.b, object2.b, 'b'], + [object1.b.a, object2.b.a, 'a'], + [object1.b.a[0], object2.b.a[0], 0], + [object1.b.a[1], object2.b.a[1], 1], + [object1.b.b, object2.b.b, 'b'] + ]; + + _.isEqualWith(object1, object2, function() { + argsList.push(slice.call(arguments)); + }); + + deepEqual(argsList, expected); + }); + + test('should handle comparisons if `customizer` returns `undefined`', 3, function() { + strictEqual(_.isEqualWith('a', 'a', _.noop), true); + strictEqual(_.isEqualWith(['a'], ['a'], _.noop), true); + strictEqual(_.isEqualWith({ '0': 'a' }, { '0': 'a' }, _.noop), true); + }); + + test('should not handle comparisons if `customizer` returns `true`', 3, function() { + var customizer = function(value) { + return _.isString(value) || undefined; + }; + + strictEqual(_.isEqualWith('a', 'b', customizer), true); + strictEqual(_.isEqualWith(['a'], ['b'], customizer), true); + strictEqual(_.isEqualWith({ '0': 'a' }, { '0': 'b' }, customizer), true); + }); + + test('should not handle comparisons if `customizer` returns `false`', 3, function() { + var customizer = function(value) { + return _.isString(value) ? false : undefined; + }; + + strictEqual(_.isEqualWith('a', 'a', customizer), false); + strictEqual(_.isEqualWith(['a'], ['a'], customizer), false); + strictEqual(_.isEqualWith({ '0': 'a' }, { '0': 'a' }, customizer), false); + }); + + test('should return a boolean value even if `customizer` does not', 2, function() { + var actual = _.isEqualWith('a', 'b', _.constant('c')); + strictEqual(actual, true); + + var values = _.without(falsey, undefined), + expected = _.map(values, _.constant(false)); + + actual = []; + _.each(values, function(value) { + actual.push(_.isEqualWith('a', 'a', _.constant(value))); + }); + + deepEqual(actual, expected); + }); + + test('should ensure `customizer` is a function', 1, function() { + var array = [1, 2, 3], + eq = _.partial(_.isEqualWith, array), + actual = _.map([array, [1, 0, 3]], eq); + + deepEqual(actual, [true, false]); + }); + }()); + + /*--------------------------------------------------------------------------*/ + QUnit.module('lodash.isError'); (function() { @@ -7567,7 +7592,13 @@ deepEqual(actual, [false, true]); }); + }()); + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.isMatchWith'); + + (function() { test('should provide the correct `customizer` arguments', 1, function() { var argsList = [], object1 = { 'a': [1, 2], 'b': null }, @@ -7591,7 +7622,7 @@ [object1.b.b.b, object2.b.b.b, 'b'] ]; - _.isMatch(object1, object2, function() { + _.isMatchWith(object1, object2, function() { argsList.push(slice.call(arguments)); }); @@ -7599,12 +7630,12 @@ }); test('should handle comparisons if `customizer` returns `undefined`', 1, function() { - strictEqual(_.isMatch({ 'a': 1 }, { 'a': 1 }, _.noop), true); + strictEqual(_.isMatchWith({ 'a': 1 }, { 'a': 1 }, _.noop), true); }); test('should return a boolean value even if `customizer` does not', 2, function() { var object = { 'a': 1 }, - actual = _.isMatch(object, { 'a': 1 }, _.constant('a')); + actual = _.isMatchWith(object, { 'a': 1 }, _.constant('a')); strictEqual(actual, true); @@ -7612,7 +7643,7 @@ actual = []; _.each(falsey, function(value) { - actual.push(_.isMatch(object, { 'a': 2 }, _.constant(value))); + actual.push(_.isMatchWith(object, { 'a': 2 }, _.constant(value))); }); deepEqual(actual, expected); @@ -7620,7 +7651,7 @@ test('should ensure `customizer` is a function', 1, function() { var object = { 'a': 1 }, - matches = _.partial(_.isMatch, object), + matches = _.partial(_.isMatchWith, object), actual = _.map([object, { 'a': 2 }], matches); deepEqual(actual, [true, false]); @@ -10450,23 +10481,29 @@ deepEqual(actual, values); }); + }(1, 2, 3)); + /*--------------------------------------------------------------------------*/ + + QUnit.module('lodash.mergeWith'); + + (function() { test('should handle merging if `customizer` returns `undefined`', 2, function() { - var actual = _.merge({ 'a': { 'b': [1, 1] } }, { 'a': { 'b': [0] } }, _.noop); + var actual = _.mergeWith({ 'a': { 'b': [1, 1] } }, { 'a': { 'b': [0] } }, _.noop); deepEqual(actual, { 'a': { 'b': [0, 1] } }); - actual = _.merge([], [undefined], _.identity); + actual = _.mergeWith([], [undefined], _.identity); deepEqual(actual, [undefined]); }); test('should defer to `customizer` when it returns a value other than `undefined`', 1, function() { - var actual = _.merge({ 'a': { 'b': [0, 1] } }, { 'a': { 'b': [2] } }, function(a, b) { + var actual = _.mergeWith({ 'a': { 'b': [0, 1] } }, { 'a': { 'b': [2] } }, function(a, b) { return _.isArray(a) ? a.concat(b) : undefined; }); deepEqual(actual, { 'a': { 'b': [0, 1, 2] } }); }); - }(1, 2, 3)); + }()); /*--------------------------------------------------------------------------*/ @@ -11678,8 +11715,8 @@ source = { 'a': { 'b': 2, 'c': 3 } }, expected = { 'a': { 'b': 1, 'c': 3 } }; - var defaultsDeep = _.partialRight(_.merge, function deep(value, other) { - return _.isObject(value) ? _.merge(value, other, deep) : value; + var defaultsDeep = _.partialRight(_.mergeWith, function deep(value, other) { + return _.isObject(value) ? _.mergeWith(value, other, deep) : value; }); deepEqual(defaultsDeep(object, source), expected); @@ -17453,7 +17490,7 @@ var acceptFalsey = _.difference(allMethods, rejectFalsey); - test('should accept falsey arguments', 214, function() { + test('should accept falsey arguments', 221, function() { var emptyArrays = _.map(falsey, _.constant([])); _.each(acceptFalsey, function(methodName) {