From b9432e1cfd6479fbcdbdc07ba23188186ba71c6e Mon Sep 17 00:00:00 2001 From: novo <63547292+de-novo@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:00:59 +0900 Subject: [PATCH] feat(set): add `set` function (#223) * feat: set * test: set * chore: add doc and benchmark * chore: fix benchmark name --- benchmarks/set.bench.ts | 19 +++++++ docs/.vitepress/en.mts | 1 + docs/.vitepress/ko.mts | 1 + docs/ko/reference/object/set.md | 46 +++++++++++++++++ docs/reference/object/set.md | 46 +++++++++++++++++ src/object/index.ts | 1 + src/object/set.spec.ts | 88 +++++++++++++++++++++++++++++++++ src/object/set.ts | 57 +++++++++++++++++++++ 8 files changed, 259 insertions(+) create mode 100644 benchmarks/set.bench.ts create mode 100644 docs/ko/reference/object/set.md create mode 100644 docs/reference/object/set.md create mode 100644 src/object/set.spec.ts create mode 100644 src/object/set.ts diff --git a/benchmarks/set.bench.ts b/benchmarks/set.bench.ts new file mode 100644 index 00000000..c5d8659d --- /dev/null +++ b/benchmarks/set.bench.ts @@ -0,0 +1,19 @@ +import { describe, bench } from 'vitest'; +import { set } from 'es-toolkit'; +import { set as lodashSet } from 'lodash'; + +describe('set', () => { + bench('es-toolkit/set-1', () => { + set({}, 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z', 1); + }); + bench('es-toolkit/set-2', () => { + set({}, 'a[0][1][2][3][4][5][6]', 1); + }); + + bench('lodash/set-1', () => { + lodashSet({}, 'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z', 1); + }); + bench('lodash/set-2', () => { + lodashSet({}, 'a[0][1][2][3][4][5][6]', 1); + }); +}); diff --git a/docs/.vitepress/en.mts b/docs/.vitepress/en.mts index 9caef24a..873f0712 100644 --- a/docs/.vitepress/en.mts +++ b/docs/.vitepress/en.mts @@ -138,6 +138,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'pick', link: '/reference/object/pick' }, { text: 'pickBy', link: '/reference/object/pickBy' }, { text: 'invert', link: '/reference/object/invert' }, + { text: 'set', link: '/reference/object/set' }, ], }, { diff --git a/docs/.vitepress/ko.mts b/docs/.vitepress/ko.mts index a1b55413..4e7ff589 100644 --- a/docs/.vitepress/ko.mts +++ b/docs/.vitepress/ko.mts @@ -149,6 +149,7 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'pick', link: '/ko/reference/object/pick' }, { text: 'pickBy', link: '/ko/reference/object/pickBy' }, { text: 'invert', link: '/ko/reference/object/invert' }, + { text: 'set', link: '/ko/reference/object/set' }, ], }, { diff --git a/docs/ko/reference/object/set.md b/docs/ko/reference/object/set.md new file mode 100644 index 00000000..a10eeaab --- /dev/null +++ b/docs/ko/reference/object/set.md @@ -0,0 +1,46 @@ +# set + +지정된 경로에 주어진 값을 설정해요. 경로의 일부가 존재하지 않으면 생성됩니다. + +## Signature + +```typescript +function set(obj: Settable, path: Path, value: any): T; +``` + +### Parameters + +- `obj` (Settable): 값을 설정할 객체예요. +- `path` (Path): 값을 설정할 속성의 경로예요. +- `value` (any): 설정할 값이에요. + +### Returns + +(`T`): 수정된 객체를 반환해요. T를 지정하지 않으면 unknown이에요. + +## Examples + +```typescript +// 중첩된 객체에 값 설정 +const obj = { a: { b: { c: 3 } } }; +set(obj, 'a.b.c', 4); +console.log(obj.a.b.c); // 4 + +// 배열에 값 설정 +const arr = [1, 2, 3]; +set(arr, 1, 4); +console.log(arr[1]); // 4 + +// 존재하지 않는 경로 생성 및 값 설정 +const obj2 = {}; +set(obj2, 'a.b.c', 4); +console.log(obj2); // { a: { b: { c: 4 } } } + +// 인터페이스 사용 +interface O { + a: number; +} +const obj3 = {}; +const result = set(obj3, 'a', 1); // result 타입 = { a: number } +console.log(result); // { a: 1 } +``` diff --git a/docs/reference/object/set.md b/docs/reference/object/set.md new file mode 100644 index 00000000..e50d0d3b --- /dev/null +++ b/docs/reference/object/set.md @@ -0,0 +1,46 @@ +# set + +Sets the given value at the specified path of the object. If any part of the path does not exist, it will be created. + +## Signature + +```typescript +function set(obj: Settable, path: Path, value: any): T; +``` + +### Parameters + +- `obj` (Settable): The object to modify. +- `path` (Path): The path of the property to set. +- `value` (any): The value to set. + +### Returns + +(`T`): Returns the modified object. If T is not specified, it defaults to unknown. + +## Examples + +```typescript +// Set a value in a nested object +const obj = { a: { b: { c: 3 } } }; +set(obj, 'a.b.c', 4); +console.log(obj.a.b.c); // 4 + +// Set a value in an array +const arr = [1, 2, 3]; +set(arr, 1, 4); +console.log(arr[1]); // 4 + +// Create non-existent path and set value +const obj2 = {}; +set(obj2, 'a.b.c', 4); +console.log(obj2); // { a: { b: { c: 4 } } } + +// Use with interface +interface O { + a: number; +} +const obj3 = {}; +const result = set(obj3, 'a', 1); // typeof result = { a: number } +console.log(result); // { a: 1 } +``` diff --git a/src/object/index.ts b/src/object/index.ts index e7c0a7f2..ae2b628e 100644 --- a/src/object/index.ts +++ b/src/object/index.ts @@ -4,3 +4,4 @@ export { pick } from './pick.ts'; export { pickBy } from './pickBy.ts'; export { invert } from './invert.ts'; export { clone } from './clone.ts'; +export { set } from './set.ts'; diff --git a/src/object/set.spec.ts b/src/object/set.spec.ts new file mode 100644 index 00000000..e649d76c --- /dev/null +++ b/src/object/set.spec.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest'; +import { set } from './set'; + +describe('set', () => { + //-------------------------------------------------------------------------------- + // object + //-------------------------------------------------------------------------------- + it('should set a value on an object', () => { + interface Test { + a: number; + } + const result = set({}, 'a', 1); + result.a; + expect(result).toEqual({ a: 1 }); + }); + + it('should set a value on an object with nested path', () => { + const result = set<{ a: { b: number } }>({}, 'a.b', 1); + expect(result).toEqual({ a: { b: 1 } }); + }); + + it('should set a value on an object with nested path', () => { + const result = set<{ a: { b: { c: { d: number } } } }>({}, 'a.b.c.d', 1); + expect(result).toEqual({ a: { b: { c: { d: 1 } } } }); + }); + + //-------------------------------------------------------------------------------- + // array + //-------------------------------------------------------------------------------- + it('should set a value on an array', () => { + const result = set([], 0, 1); + expect(result).toEqual([1]); + expect(result[0]).toEqual(1); + }); + + it('should set a value on an array with nested path', () => { + const result = set([], '0.0', 1); + expect(result).toEqual([[1]]); + expect(result[0][0]).toEqual(1); + }); + + it('should set a value on an array with nested path', () => { + const result = set([], '0.0.0', 1); + expect(result).toEqual([[[1]]]); + expect(result[0][0][0]).toEqual(1); + }); + it('should set a value on an array with nested path', () => { + const arr = [1, 2, 3]; + set(arr, 1, 4); + expect(arr).toEqual([1, 4, 3]); + expect(arr[1]).toEqual(4); + }); + + //-------------------------------------------------------------------------------- + // object and array + //-------------------------------------------------------------------------------- + it('should set a value on an object and array', () => { + const result = set>([], '0.a', 1); + expect(result).toEqual([{ a: 1 }]); + expect(result[0].a).toEqual(1); + }); + + it('should set a value on an object and array', () => { + const result = set<{ a: number[] }>({}, 'a.0', 1); + expect(result).toEqual({ a: [1] }); + expect(result.a[0]).toEqual(1); + }); + + it('should set a value on an object and array', () => { + const result = set<{ a: number[][] }>({}, 'a.0.0', 1); + expect(result).toEqual({ a: [[1]] }); + expect(result.a[0][0]).toEqual(1); + }); + + it('should set a value on an object and array', () => { + const result = set<{ a: number[][][] }>({}, 'a[0][0][0]', 1); + expect(result).toEqual({ a: [[[1]]] }); + expect(result.a[0][0][0]).toEqual(1); + }); + + //-------------------------------------------------------------------------------- + // not support map and set + //-------------------------------------------------------------------------------- + it('not support map and set', () => { + expect(() => set>(new Map(), 'a', 1)).toThrow(TypeError); + expect(() => set>(new Set(), 1, 2)).toThrow(TypeError); + }); +}); diff --git a/src/object/set.ts b/src/object/set.ts new file mode 100644 index 00000000..cd0bc023 --- /dev/null +++ b/src/object/set.ts @@ -0,0 +1,57 @@ +/** + * Sets the value at the specified path of the given object. If any part of the path does not exist, it will be created. + * + * @template T - The type of the object. + * @param {Settable} obj - The object to modify. + * @param {Path} path - The path of the property to set. + * @param {any} value - The value to set. + * @returns {T} - The modified object. + * + * @example + * // Set a value in a nested object + * const obj = { a: { b: { c: 3 } } }; + * set(obj, 'a.b.c', 4); + * console.log(obj.a.b.c); // 4 + * + * @example + * // Set a value in an array + * const arr = [1, 2, 3]; + * set(arr, 1, 4); + * console.log(arr[1]); // 4 + * + * @example + * // Create non-existent path and set value + * const obj = {}; + * set(obj, 'a.b.c', 4); + * console.log(obj); // { a: { b: { c: 4 } } } + */ +export function set(obj: Settable, path: Path, value: any): T { + if (obj instanceof Map || obj instanceof Set) { + throw new TypeError('Set or Map is not supported'); + } + //TODO: memoize + const keys = Array.isArray(path) + ? path + : String(path as string) + .replace(/\[|\]/g, match => { + return match === '[' ? '.' : ''; + }) + .split('.'); + + let pointer: any = obj; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + const nextKey = keys[i + 1]; + if (pointer[key] == null || typeof pointer[key] !== 'object') { + pointer[key] = /^\d+$/.test(nextKey as string) ? [] : {}; + } + pointer = pointer[key]; + } + + pointer[keys[keys.length - 1]] = value; + return obj as T; +} + +type Settable = object | any[]; +type Path = string | number | Array;