From 7c5e305fa99c37377b9f9b753fddecb9a1ebf606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Jos=C3=A9=20Ag=C3=A1mez=20Licha?= Date: Thu, 21 Apr 2022 19:54:15 +0200 Subject: [PATCH] feat(object): add getValue, setValue, fix merge --- docs/array.md | 11 ---- docs/object.md | 91 ++++++++++++++++++++++++++++- package.json | 2 +- src/array.ts | 4 -- src/misc.ts | 14 +++++ src/object.ts | 138 ++++++++++++++++++++++++++++++++++++++++---- src/validator.ts | 2 +- test/array.spec.ts | 5 -- test/misc.spec.ts | 25 +++++--- test/object.spec.ts | 101 +++++++++++++++++++++++++++++--- 10 files changed, 341 insertions(+), 52 deletions(-) diff --git a/docs/array.md b/docs/array.md index 074215e..46a2769 100644 --- a/docs/array.md +++ b/docs/array.md @@ -290,17 +290,6 @@ utils.maxBy(people, 'age') // { name: 'Foo', age: 42 } ``` --- -## merge(a: T[], b: T[]): T[] - -Merge two arrays. - -```ts -utils.merge([1, 2, 3, 4], [4, 5, 6]) // [1, 2, 3, 4, 5, 6] -utils.merge([1, 2, 3], [4, 5, 6]) // [1, 2, 3, 4, 5, 6] -``` - ---- - ## minBy, K extends keyof T>(arr: T[], key: K): T Find the minimum item of an array by given key. diff --git a/docs/object.md b/docs/object.md index 7ab0cd3..da598d3 100644 --- a/docs/object.md +++ b/docs/object.md @@ -1,6 +1,49 @@ # Object Functions -## const createObject(): object +## clone(target: T): T + +Recursively clones an object. An empty object is returned for uncloneable values such as error WeakMaps. + +```ts + const map = new Map(); + map.set('key', 'value'); + + const set = new Set(); + set.add('value1'); + set.add('value2'); + + const target = { + field1: 1, + field2: undefined, + field3: { + child: 'child' + }, + field4: [2, 3, 4, { foo: { a: 'a', b: { c: 'c' } } }], + field5: true, + empty: null, + map, + set, + bool: new Boolean(false), + num: new Number(5), + str: new String('str'), + symbol: Object(Symbol(6)), + date: new Date('2022-04-18T00:00:00.000Z'), + reg: /\d+/, + error: new Error('Clone function error'), + greet: (): string => { + return 'hello friend!' + }, + sum: function (a: number, b: number): number { + return a + b + } + } + + const clonedObject = utils.clone(target) +``` + +--- + +## createObject(): object Create an empty map that does not have properties. @@ -37,12 +80,18 @@ const obj = { b: { c: 42 } - } + }, + foo: [ + 2, + { bar: 'baz' } + ] } utils.getValue(obj, 'a.b.c') // 42 utils.getValue(obj, 'a.b.c.d') // undefined utils.getValue(obj, 'a.b.c.d', 'default') // 'default' +utils.getValue(obj, 'foo[1]["bar"]') // 'baz' +utils.getValue(obj, 'foo[0]') // 2 ``` --- @@ -63,6 +112,18 @@ utils.invert(obj) // { bar: 'foo', fuzz: 'baz', lorem: 'ergo' } --- +## merge(a: T[], b: T[]): T[] + +Recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. + +```ts +utils.merge([1, 2, 3, 4], [4, 5, 6]) // [4, 5, 6, 4]) +utils.merge([1, 2, 3], [4, 5, 6]) // [4, 5, 6]) +utils.merge({ a: [{ b: 2 }, { d: 4 }] }, { a: [{ c: 3 }, { e: 5 }] }) // { a: [{ b: 2, c: 3 }, { d: 4, e: 5 }] }) +``` + +--- + ## omit(object: T, keys: K[]): Omit Omit a subset of properties from an object @@ -144,6 +205,32 @@ utils.renameKeys(obj, { foo: 'bar', baz: 'fuz' }) // { bar: 'bar', fuz: 42 } --- +## setValue(target: T, path: string, value: unknown): unknown + +Sets the value to the path of the object. If a part of the route doesn't exist, it is created. Arrays are created for missing index properties, while objects are created for all other missing properties. This function does not modify the original object, returning a new object with the changes applied. + +```ts +const target = { + a: { + b: { + c: 42 + } + }, + foo: [ + 2, + { bar: 'baz' } + ] +} + +utils.setValue(target, 'a.b.c', 'foo') // { a: { b: { c: 'foo' } }, foo: [2, { bar: 'baz' }] }) +utils.setValue(target, 'a.b.c.d', 'bar') // { a: { b: { c: { d: 'bar' } } }, foo: [2, { bar: 'baz' }] }) +utils.setValue(target, 'foo[1]["bar"]', 'bazbar') // { a: { b: { c: 42 } }, foo: [2, { bar: 'bazbar' }] }) +utils.setValue(target, 'foo[0]', 'bar') // { a: { b: { c: 42 } }, foo: ['bar', { bar: 'baz' }] }) +utils.setValue(target, 'foo[]', 'bar') // { a: { b: { c: 42 } }, foo: [2, { bar: 'baz' }, 'bar'] }) +``` + +--- + ## sortKeys(object: T): object Sort an object by its properties, diff --git a/package.json b/package.json index 85d423b..5efda57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devnetic/utils", - "version": "2.0.0", + "version": "2.1.0", "description": "Common utils for every single day tasks", "main": "lib/index.js", "types": "lib/types/index.d.ts", diff --git a/src/array.ts b/src/array.ts index f6e9df5..a1433b6 100644 --- a/src/array.ts +++ b/src/array.ts @@ -134,10 +134,6 @@ export const maxBy = , K extends keyof T>(arr: }) } -export const merge = (a: T[], b: T[]): T[] => { - return [...new Set([...a, ...b])] -} - export const minBy = , K extends keyof T>(arr: T[], key: K): T => { return arr.reduce((prev, curr) => { return curr[key] < prev[key] ? curr : prev diff --git a/src/misc.ts b/src/misc.ts index 7c57652..bd8d51f 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -48,6 +48,20 @@ export const getQueryStringValue = (url: string, key: string): string | undefine } export const getType = (value: unknown = undefined): string => { + const type = typeof value + + if (type !== 'object') { + if (type === 'function') { + return 'Function' + } + + if (type === 'bigint') { + return 'BigInt' + } + + return type + } + const typeRegex = /\[object (.*)\]/ return (Object.prototype.toString.call(value).match(typeRegex) as string[])[1] diff --git a/src/object.ts b/src/object.ts index db38a32..8057160 100644 --- a/src/object.ts +++ b/src/object.ts @@ -1,6 +1,76 @@ +import { getType } from './misc' +import { isNil, isObject } from './validator' + export type IsNullable = T extends null | undefined ? K : never export type NullableKeys = { [K in keyof T]-?: IsNullable }[keyof T] +export const clone = (target: T): T => { + if (isNil(target) || !isObject(target)) { + return target + } + + const cloned: any = {} + + for (const attr in target) { + const value: any = target[attr] + const type: string = getType(value) + + switch (type) { + case 'Array': + cloned[attr] = Object.values(value).map(clone) + break + + case 'Function': + cloned[attr] = value.valueOf() + break + + case 'Error': + cloned[attr] = Object.getOwnPropertyNames(value).reduce((acc: any, prop) => { + acc[prop] = value[prop] + + return acc + }, new Error()) + + break + + case 'Object': + cloned[attr] = Object.keys(value).reduce((cloned: any, key: string) => { + cloned[key] = clone(value[key]) + + return cloned + }, {}) + + break + + case 'Symbol': + cloned[attr] = Object(value.valueOf()) + + break + + case 'WeakMap': + cloned[attr] = {} + + break + + case 'Date': + case 'Boolean': + case 'Map': + case 'Number': + case 'RegExp': + case 'Set': + case 'String': + cloned[attr] = new value.constructor(value.valueOf()) + + break + + default: + cloned[attr] = value + } + } + + return cloned +} + export const createObject = (): object => { return Object.create(null) } @@ -19,17 +89,13 @@ export const fromEntries = (entries: Iterable | ArrayLike): object => { }, {}) } -export const getValue = (object: object, path: string, defaultValue?: unknown): unknown => { - const keys = path.split('.') - let result: any = object - - for (let index = 0; index < keys.length; index++) { - const key = keys[index] - - result = result[key] - } - - return result ?? defaultValue +export const getValue = (target: object, path: string, defaultValue?: unknown): unknown => { + return path.replace(/(?:\["?(\w+)"?])/g, '.$1') + .replace(/^\./, '') + .split('.') + .reduce((result: any, property: string) => { + return result[property] ?? defaultValue + }, target) } export const invert = (object: object): object => { @@ -38,6 +104,23 @@ export const invert = (object: object): object => { }, {}) } +export const merge = (...sources: T[]): T => { + return sources.reduce((prev: any, obj: any) => { + (Object.keys(obj) as Array).forEach((key) => { + const pVal = prev[key] + const oVal = obj[key] + + if ((Array.isArray(pVal) && Array.isArray(oVal)) || (isObject(pVal) && isObject(oVal))) { + prev[key] = merge(pVal, oVal) + } else { + prev[key] = oVal + } + }) + + return prev + }, Array.isArray(sources[0]) ? [] : {}) +} + export const omit = (object: T, keys: K[]): Omit => { return Object.entries(object).reduce((result: object, [key, value]: [any, any]) => { if (keys.includes(key)) { @@ -80,6 +163,39 @@ export const renameKeys = (object: T, map: }, {}) } +export const setValue = (target: T, path: string, value: unknown): unknown => { + const segments: string[] = path.replace(/(?:\[["']?(\w*)["']?])/g, '.$1') + .replace(/^\./, '') + .replace(/\.$/, '') + .split('.') + + const limit = segments.length - 1 + let clonedTarget: any = clone(target) + + // Store the reference to the result object + const resultReference = clonedTarget + + for (let i = 0; i < limit; ++i) { + const key = segments[i] + + if (typeof clonedTarget[key] !== 'object') { + clonedTarget[key] = {} + } + + clonedTarget = clonedTarget[key] + } + + const key = segments[limit] + + if (Array.isArray(clonedTarget[key])) { + clonedTarget[key].push(value) + } else { + clonedTarget[key] = value + } + + return resultReference +} + export const sortKeys = (object: T): object => { return Object.entries(object).sort(([keyA, valueA], [keyB, valueB]): number => { return keyA.localeCompare(keyB) diff --git a/src/validator.ts b/src/validator.ts index 1d63f10..dae973f 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -245,7 +245,7 @@ export const isRegExp = (value: T): boolean => { } export const isString = (value: T): boolean => { - return getType(value) === 'String' + return typeof value === 'string' || value instanceof String } export const isSubsetOf = (set: T[], subset: T[]): boolean => { diff --git a/test/array.spec.ts b/test/array.spec.ts index 2157b87..92144cd 100644 --- a/test/array.spec.ts +++ b/test/array.spec.ts @@ -185,11 +185,6 @@ test('should returns the the minimun item of and array by key', t => { t.is(utils.minBy(people, 'age'), people[1]) }) -test('should merge two arrays', t => { - t.deepEqual(utils.merge([1, 2, 3, 4], [4, 5, 6]), [1, 2, 3, 4, 5, 6]) - t.deepEqual(utils.merge([1, 2, 3], [4, 5, 6]), [1, 2, 3, 4, 5, 6]) -}) - test('should returns the partion of an array based on a predicate', t => { const people = [ { name: 'Bar', age: 24 }, diff --git a/test/misc.spec.ts b/test/misc.spec.ts index 3d61dab..daacf4b 100644 --- a/test/misc.spec.ts +++ b/test/misc.spec.ts @@ -24,18 +24,25 @@ test('should returns the correct type', t => { t.is(utils.getType([]), 'Array') t.is(utils.getType(new Array()), 'Array') t.is(utils.getType(new Date()), 'Date') - t.is(utils.getType(String()), 'String') - t.is(utils.getType(''), 'String') - t.is(utils.getType('123'), 'String') - t.is(utils.getType(123), 'Number') - t.is(utils.getType(123.4), 'Number') - t.is(utils.getType(true), 'Boolean') - t.is(utils.getType(false), 'Boolean') + t.is(utils.getType(String()), 'string') + t.is(utils.getType(''), 'string') + t.is(utils.getType('123'), 'string') + t.is(utils.getType(new String('123')), 'String') + t.is(utils.getType(123), 'number') + t.is(utils.getType(123.4), 'number') + t.is(utils.getType(Number(123)), 'number') + t.is(utils.getType(Number(123.4)), 'number') + t.is(utils.getType(new Number(123)), 'Number') + t.is(utils.getType(new Number(123.4)), 'Number') + t.is(utils.getType(true), 'boolean') + t.is(utils.getType(false), 'boolean') + t.is(utils.getType(new Boolean(true)), 'Boolean') + t.is(utils.getType(new Boolean(false)), 'Boolean') t.is(utils.getType(BigInt(1)), 'BigInt') t.is(utils.getType(() => { }), 'Function') t.is(utils.getType(BigInt), 'Function') - t.is(utils.getType(undefined), 'Undefined') - t.is(utils.getType(), 'Undefined') + t.is(utils.getType(undefined), 'undefined') + t.is(utils.getType(), 'undefined') t.is(utils.getType(null), 'Null') t.is(utils.getType(/\d/), 'RegExp') }) diff --git a/test/object.spec.ts b/test/object.spec.ts index 575fc8c..004a469 100644 --- a/test/object.spec.ts +++ b/test/object.spec.ts @@ -2,6 +2,58 @@ import test from 'ava' import * as utils from './../src' +test('should deep clone an object', t => { + const map = new Map() + map.set('key', 'value') + + const weakMap = new WeakMap() + weakMap.set({ key: 'value' }, 'WeakMap value') + + const set = new Set() + set.add('value1') + set.add('value2') + + const target = { + field1: 1, + field2: undefined, + field3: { + child: 'child' + }, + field4: [2, 3, 4, { foo: { a: 'a', b: { c: 'c' } } }], + field5: true, + empty: null, + map, + weakMap, + set, + bool: new Boolean(false), + num: new Number(5), + str: new String('str'), + symbol: Object(Symbol(6)), + date: new Date('2022-04-18T00:00:00.000Z'), + reg: /\d+/, + error: new Error('Clone function error'), + greet: (): string => { + return 'hello friend!' + }, + sum: function (a: number, b: number): number { + return a + b + } + } + + const clonedObject = utils.clone(target) + Reflect.set(target, 'weakMap', {}) + + t.deepEqual(target, clonedObject) + t.is(target.greet(), clonedObject.greet()) + t.is(target.sum(1, 2), clonedObject.sum(1, 2)) + t.not(target, clonedObject) + + clonedObject.greet = () => 'hello world!' + + t.is(target.greet(), 'hello friend!') + t.is(clonedObject.greet(), 'hello world!') +}) + test('should create a empty object', t => { const object = utils.createObject() @@ -27,12 +79,19 @@ test('should get the value at given path of an object', t => { b: { c: 42 } - } + }, + foo: [ + 2, + { bar: 'baz' } + ] } + t.is(utils.getValue(obj, ''), undefined) t.is(utils.getValue(obj, 'a.b.c'), 42) t.is(utils.getValue(obj, 'a.b.c.d'), undefined) t.is(utils.getValue(obj, 'a.b.c.d', 'default'), 'default') + t.is(utils.getValue(obj, 'foo[1]["bar"]'), 'baz') + t.is(utils.getValue(obj, 'foo[0]'), 2) }) test('should inverted the keys and values of an object', t => { @@ -45,6 +104,12 @@ test('should inverted the keys and values of an object', t => { t.deepEqual(utils.invert(obj), { bar: 'foo', fuzz: 'baz', lorem: 'ergo' }) }) +test('should merge a set of objects', t => { + t.deepEqual(utils.merge([1, 2, 3, 4], [4, 5, 6]), [4, 5, 6, 4]) + t.deepEqual(utils.merge([1, 2, 3], [4, 5, 6]), [4, 5, 6]) + t.deepEqual(utils.merge({ a: [{ b: 2 }, { d: 4 }] }, { a: [{ c: 3 }, { e: 5 }] }), { a: [{ b: 2, c: 3 }, { d: 4, e: 5 }] }) +}) + test('should omit a subset of properties from an object', t => { const obj = { foo: 'bar', @@ -68,13 +133,13 @@ test('should pick a subset of properties from an object', t => { test('should extract values of a property from an array of object', t => { t.deepEqual(utils.pluck([{ a: 1 }, { a: 2 }, { a: 3 }], 'a'), [1, 2, 3]) t.deepEqual(utils.pluck( - [ - { name: 'John', age: 20 }, - { name: 'Smith', age: 25 }, - { name: 'Peter', age: 30 }, - ], - 'name' - ), + [ + { name: 'John', age: 20 }, + { name: 'Smith', age: 25 }, + { name: 'Peter', age: 30 }, + ], + 'name' + ), ['John', 'Smith', 'Peter'] ) }) @@ -98,6 +163,26 @@ test('should rename keys of an object', t => { t.deepEqual(utils.renameKeys(obj, { foo: 'bar', baz: 'fuz' }), { bar: 'bar', fuz: 42 }) }) +test('should set the value at given path of an object', t => { + const target = { + a: { + b: { + c: 42 + } + }, + foo: [ + 2, + { bar: 'baz' } + ] + } + + t.deepEqual(utils.setValue(target, 'a.b.c', 'foo'), { a: { b: { c: 'foo' } }, foo: [2, { bar: 'baz' }] }) + t.deepEqual(utils.setValue(target, 'a.b.c.d', 'bar'), { a: { b: { c: { d: 'bar' } } }, foo: [2, { bar: 'baz' }] }) + t.deepEqual(utils.setValue(target, 'foo[1]["bar"]', 'bazbar'), { a: { b: { c: 42 } }, foo: [2, { bar: 'bazbar' }] }) + t.deepEqual(utils.setValue(target, 'foo[0]', 'bar'), { a: { b: { c: 42 } }, foo: ['bar', { bar: 'baz' }] }) + t.deepEqual(utils.setValue(target, 'foo[]', 'bar'), { a: { b: { c: 42 } }, foo: [2, { bar: 'baz' }, 'bar'] }) +}) + test('should sort keys of an object', t => { const obj = { foo: 'bar',