diff --git a/CHANGELOG b/CHANGELOG index a6dcf90..4761a6d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ #1.13.0 +- feat: flow +- feat: invert +- feat: mapValues +- feat: uniqBy - feat: groupBy #1.12.0 diff --git a/README.md b/README.md index effb67f..e5e658a 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ Currently supported utils: - `camelize` - simple camel case - `capitalize` - make first char uppercase - `cloneDeep` - deep clone functionality for objects +- `flow` - generate a composite function that returns the result of provided functions called by the chain; each previous function result passes as the argument of the next function in the chain - `freezeDeep` - deep freezes objects - `get` - get the object members by path - `getArrayHasIntersect` - checks if arrays have at least one common value - `getArrayUniq` - gets unique values form array +- `invert` - Inverts the key-value pairs of an object - `groupBy` - groups the elements of an array by a specified key. - `isEmpty` - checks if value is an empty object, collection, map, or set - `isEqual` - check if passed two values are equal @@ -21,6 +23,7 @@ Currently supported utils: - `isPlainObject` - checks if input is object, not null object and not array object - `isString` - checks if input is a string - `mapKeys` - creates new object with the same values but with keys mapped by the provided function +- `mapValues` - Maps the values of an object or array using the provided iteratee function or property path - `max` - computes the maximum value of array. If array is empty or falsey, undefined is returned - `merge` - deep merge functionality for objects - `min` - computes the minimum value of array. If array is empty or falsey, undefined is returned @@ -31,3 +34,4 @@ Currently supported utils: - `shuffle` - performs pseudo random shuffle on clone of the array - `sum` - calculate sum of array items - `sumBy` - calculate sum of array items using iteratee function or string shortcut +- `uniqBy` - get unique values of array by the iteratee function or property path diff --git a/index.d.ts b/index.d.ts index ae576d0..3b76f4f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,11 +1,13 @@ export function camelize (str: string): string export function capitalize (str: string): string -export function getErrorMessage (obj: obj): string export function cloneDeep (obj: Object): Object +export function flow (funcs: Array): Function export function freezeDeep (obj: Object): Object export function get (obj: Object, path: string | Array, defaultValue: any): any export function getArrayHasIntersect (arr1: Array, arr2: Array): boolean export function getArrayUniq (arr: Array): Array +export function getErrorMessage (obj: obj): string +export function invert (obj: Object): Object export function groupBy (array: Array, key: string | ((item: T) => string)): { [key: string]: Array } export function isEmpty (val: any): boolean export function isEqual (value: any, another: any): boolean @@ -17,6 +19,7 @@ export function isObject (verifiable: any): Boolean export function isPlainObject (val: any): boolean export function isString (val: any): boolean export function mapKeys(obj: Object, mapper: (val: any, key: string) => string): Object +export function mapValues(obj: Object, mapper: (val: any, key: string) => any): Object export function max (array: Array): any export function merge (obj: Object, ...sources: Object[]): Object export function min (array: Array): any @@ -27,3 +30,4 @@ export function pickBy (obj: Object, predicate: (val: any, key: string) => boole export function shuffle (array: Array): Array export function sum (values: Array): number export function sumBy (values: Array, iteratee: Function | string): number +export function uniqBy (array: Array, iteratee: Function | string): Array diff --git a/index.js b/index.js index 7353aee..0d1784f 100644 --- a/index.js +++ b/index.js @@ -3,11 +3,13 @@ const camelize = require('./src/camelize') const capitalize = require('./src/capitalize') const cloneDeep = require('./src/cloneDeep') +const flow = require('./src/flow') const freezeDeep = require('./src/freezeDeep') const get = require('./src/get') const getArrayHasIntersect = require('./src/getArrayHasIntersect') const getArrayUniq = require('./src/getArrayUniq') const getErrorMessage = require('./src/getErrorMessage') +const invert = require('./src/invert') const groupBy = require('./src/groupBy') const isEmpty = require('./src/isEmpty') const isEqual = require('./src/isEqual') @@ -19,6 +21,7 @@ const isObject = require('./src/isObject') const isPlainObject = require('./src/isPlainObject') const isString = require('./src/isString') const mapKeys = require('./src/mapKeys') +const mapValues = require('./src/mapValues') const max = require('./src/max') const merge = require('./src/merge') const min = require('./src/min') @@ -29,6 +32,7 @@ const pickBy = require('./src/pickBy') const shuffle = require('./src/shuffle') const sum = require('./src/sum') const sumBy = require('./src/sumBy') +const uniqBy = require('./src/uniqBy') module.exports = { camelize, @@ -39,6 +43,7 @@ module.exports = { getArrayHasIntersect, getArrayUniq, getErrorMessage, + invert, groupBy, isEmpty, isEqual, @@ -49,7 +54,9 @@ module.exports = { isObject, isPlainObject, isString, + flow, mapKeys, + mapValues, max, merge, min, @@ -59,5 +66,6 @@ module.exports = { pickBy, shuffle, sum, - sumBy + sumBy, + uniqBy } diff --git a/src/flow.js b/src/flow.js new file mode 100644 index 0000000..02b0c1f --- /dev/null +++ b/src/flow.js @@ -0,0 +1,16 @@ +'use strict' + +/** + * Executes a series of functions in a pipeline, passing the result of each function as the argument to the next function. + * + * @param {Array} fns - An array of functions to be executed in order. + * @returns {any} - The final result of the pipeline. + * @throws {Error} - Throws an error if any element in the array is not a function. + */ +const flow = (fns = []) => { + if (!fns.length) return (arg) => arg + if (fns.find(fn => typeof fn !== 'function')) throw new Error('All elements should be functions') + return (...arg) => fns.reduce((acc, fn, i) => i ? fn(acc) : fn(...arg), arg) +} + +module.exports = flow diff --git a/src/invert.js b/src/invert.js new file mode 100644 index 0000000..c2e687c --- /dev/null +++ b/src/invert.js @@ -0,0 +1,17 @@ +'use strict' + +/** + * Inverts the key-value pairs of an object. + * + * @param {object} object - The object to invert. + * @returns {object} - The new object with inverted key-value pairs. + */ +const invert = (object) => { + const newObject = {} + for (const key in object) { + newObject[object[key]] = key + } + return newObject +} + +module.exports = invert diff --git a/src/mapValues.js b/src/mapValues.js new file mode 100644 index 0000000..b1b4d2f --- /dev/null +++ b/src/mapValues.js @@ -0,0 +1,21 @@ +'use strict' + +const get = require('./get') + +/** + * Maps the values of an object or array using the provided iteratee function or property path. + * + * @param {object|Array} src - The source object or array to map. + * @param {Function|string} iteratee - The iteratee function or property path to map the values. + * @returns {object} - The new object with mapped values. + */ +const mapValues = (src, iteratee) => { + const result = {} + const _iteratee = typeof iteratee === 'function' ? iteratee : (value) => get(value, iteratee) + for (const key in src) { + result[key] = _iteratee(src[key]) + } + return result +} + +module.exports = mapValues diff --git a/src/uniqBy.js b/src/uniqBy.js new file mode 100644 index 0000000..3cc5366 --- /dev/null +++ b/src/uniqBy.js @@ -0,0 +1,23 @@ +'use strict' + +const get = require('./get') + +/** + * Get unique values of array by the iteratee function or property path. + * + * @param {Array} arr - The src array. + * @param {Function|string} iteratee - The iteratee function or property path to check the unique values. + * @returns {Array} - The result array unique by iteratee. + */ +const uniqBy = (arr, iteratee) => { + const set = new Set() + const _iteratee = typeof iteratee === 'function' ? iteratee : (value) => get(value, iteratee) + return arr.filter(el => { + const val = iteratee ? _iteratee(el) : el + if (set.has(val)) return false + set.add(val) + return true + }) +} + +module.exports = uniqBy diff --git a/test/flow.js b/test/flow.js new file mode 100644 index 0000000..19c7a54 --- /dev/null +++ b/test/flow.js @@ -0,0 +1,46 @@ +'use strict' + +/* eslint-env mocha */ + +const assert = require('assert') +const flow = require('../src/flow') + +describe('flow', () => { + it('should return the input argument if no functions are provided', () => { + const result = flow()('input') + assert.strictEqual(result, 'input') + }) + + it('should apply the provided functions in sequence', () => { + const addOne = (num) => num + 1 + const double = (num) => num * 2 + const subtractFive = (num) => num - 5 + + const result = flow([addOne, double, subtractFive])(10) + assert.strictEqual(result, 17) + }) + + it('should throw an error if any element in the array is not a function', () => { + const fn1 = () => { } + const fn2 = 123 + const fn3 = () => { } + + assert.throws(() => flow([fn1, fn2, fn3]), Error) + }) + + it('should work with functions that take multiple arguments', () => { + const add = (a, b, c) => a + b + c + const square = (a) => a * a + + const result = flow([add, square])(2, 3, 4) + assert.strictEqual(result, 81) + }) + + it('should work with complex arguments', () => { + const add = (a, b) => [{ a, b }, a + b] + const multiply = ([args, res]) => [args, args.a * args.b * res] + const subtract = ([args, res]) => res - args.a + + assert.strictEqual(flow([add, multiply, subtract])(2, 3), 28) + }) +}) diff --git a/test/getErrorMessage.js b/test/getErrorMessage.js new file mode 100644 index 0000000..2f527ff --- /dev/null +++ b/test/getErrorMessage.js @@ -0,0 +1,32 @@ +'use strict' + +/* eslint-env mocha */ + +const assert = require('assert') +const getErrorMessage = require('../src/getErrorMessage') + +describe('getErrorMessage', () => { + it('should return the error message when it exists', () => { + const error = new Error('Something went wrong') + const errorMessage = getErrorMessage(error) + assert.strictEqual(errorMessage, 'Something went wrong') + }) + + it('should return an empty string when the error message is not provided', () => { + const error = new Error() + const errorMessage = getErrorMessage(error) + assert.strictEqual(errorMessage, '') + }) + + it('should return the base error code when it exists', () => { + const error = new Error('foo bar ERR_INVALID_INPUT') + const errorMessage = getErrorMessage(error) + assert.strictEqual(errorMessage, 'ERR_INVALID_INPUT') + }) + + it('should return the full error message when it does not match the base error code pattern', () => { + const error = new Error('Some other error') + const errorMessage = getErrorMessage(error) + assert.strictEqual(errorMessage, 'Some other error') + }) +}) diff --git a/test/invert.js b/test/invert.js new file mode 100644 index 0000000..a53ad72 --- /dev/null +++ b/test/invert.js @@ -0,0 +1,26 @@ +'use strict' + +/* eslint-env jest */ + +const assert = require('assert') +const invert = require('../src/invert') + +describe('invert', () => { + it('should invert an object', () => { + const object = { a: 1, b: 2 } + const actual = invert(object) + + assert.deepStrictEqual(actual, { 1: 'a', 2: 'b' }) + assert.deepStrictEqual(invert(actual), { a: '1', b: '2' }) + }) + + it('should work with values that shadow keys on `Object.prototype`', () => { + const object = { a: 'hasOwnProperty', b: 'constructor' } + assert.deepStrictEqual(invert(object), { hasOwnProperty: 'a', constructor: 'b' }) + }) + + it('should work with an object that has a `length` property', () => { + const object = { 0: 'a', 1: 'b', length: 2 } + assert.deepStrictEqual(invert(object), { a: '0', b: '1', 2: 'length' }) + }) +}) diff --git a/test/mapValues.js b/test/mapValues.js new file mode 100644 index 0000000..925a2a2 --- /dev/null +++ b/test/mapValues.js @@ -0,0 +1,26 @@ +'use strict' + +/* eslint-env mocha */ + +const assert = require('assert') +const mapValues = require('../src/mapValues') + +describe('mapValues', () => { + const array = [1, 2] + const object = { a: 1, b: 2 } + + it('should map values in `object` to a new object', () => { + const actual = mapValues(object, String) + assert.deepStrictEqual(actual, { a: '1', b: '2' }) + }) + + it('should treat arrays like objects', () => { + const actual = mapValues(array, String) + assert.deepStrictEqual(actual, { 0: '1', 1: '2' }) + }) + + it('should work with `_.property` shorthands', () => { + const actual = mapValues({ a: { b: 2 } }, 'b') + assert.deepStrictEqual(actual, { a: 2 }) + }) +}) diff --git a/test/sumBy.js b/test/sumBy.js index 48cc02b..00303b2 100644 --- a/test/sumBy.js +++ b/test/sumBy.js @@ -18,4 +18,16 @@ describe('sumBy', () => { assert.strictEqual(sumBy([{ foo: 1 }, { foo: 2 }, { foo: 3 }], {}), 0) assert.strictEqual(sumBy([{ foo: 1 }, { foo: 2 }, { foo: 3 }], null), 0) }) + + it('should return 0 if array is empty', () => { + assert.strictEqual(sumBy([], (i) => i.foo), 0) + }) + + it('should return 0 if first argument is not an array', () => { + assert.strictEqual(sumBy({}, (i) => i.foo), 0) + }) + + it('should skip zero or nullable elements', () => { + assert.strictEqual(sumBy([1, 2, undefined, null, 0], (i) => i), 3) + }) }) diff --git a/test/uniqBy.js b/test/uniqBy.js new file mode 100644 index 0000000..244197b --- /dev/null +++ b/test/uniqBy.js @@ -0,0 +1,50 @@ +'use strict' + +/* eslint-env mocha */ + +const uniqBy = require('../src/uniqBy.js') +const assert = require('assert') + +describe('uniqBy', () => { + it('should return an array with unique elements based on the iteratee function', () => { + const arr = [1, 2, 3, 4, 5, 6] + const iteratee = (el) => el % 2 + const result = uniqBy(arr, iteratee) + assert.deepStrictEqual(result, [1, 2]) + }) + + it('should return an array with unique elements based on the property name', () => { + const arr = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'John' }, + { id: 4, name: 'Jane' } + ] + const iteratee = 'name' + const result = uniqBy(arr, iteratee) + assert.deepStrictEqual(result, [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' } + ]) + }) + + it('should return an empty array if the input array is empty', () => { + const arr = [] + const iteratee = (el) => el + const result = uniqBy(arr, iteratee) + assert.deepStrictEqual(result, []) + }) + + it('should return the same array if there are no duplicate elements', () => { + const arr = [1, 2, 3, 4, 5] + const iteratee = (el) => el + const result = uniqBy(arr, iteratee) + assert.deepStrictEqual(result, [1, 2, 3, 4, 5]) + }) + + it('should return array of unique elements if iteratee is not provided', () => { + const arr = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1] + const result = uniqBy(arr) + assert.deepStrictEqual(result, [1, 2, 3, 4, 5]) + }) +})