From e031af03ed0f34afa7d53ca6aeb5e35025fb9a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=ED=95=B4=EC=A4=80?= <99087502+haejunejung@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:05:33 +0900 Subject: [PATCH] feat(groupBy): Add groupBy (#411) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(isNumericString): Add isNumericString * Update packages/utils/src/validator/isNumericString/index.ts * Update docs/docs/utils/validator/isNumericString.md * Create little-roses-itch.md * Update packages/utils/src/validator/isNumericString/index.ts * feat(groupBy): Add groupBy * Update packages/utils/src/object/groupBy/index.ts * Update docs/docs/utils/object/groupBy.md * Create blue-rules-rule.md --------- Co-authored-by: Gromit (전민재) <64779472+ssi02014@users.noreply.github.com> --- .changeset/blue-rules-rule.md | 5 ++ docs/docs/utils/object/groupBy.md | 52 +++++++++++ .../utils/src/object/groupBy/groupBy.bench.ts | 17 ++++ .../utils/src/object/groupBy/groupBy.spec.ts | 87 +++++++++++++++++++ packages/utils/src/object/groupBy/index.ts | 51 +++++++++++ packages/utils/src/object/index.ts | 1 + 6 files changed, 213 insertions(+) create mode 100644 .changeset/blue-rules-rule.md create mode 100644 docs/docs/utils/object/groupBy.md create mode 100644 packages/utils/src/object/groupBy/groupBy.bench.ts create mode 100644 packages/utils/src/object/groupBy/groupBy.spec.ts create mode 100644 packages/utils/src/object/groupBy/index.ts diff --git a/.changeset/blue-rules-rule.md b/.changeset/blue-rules-rule.md new file mode 100644 index 000000000..5a8efa1e9 --- /dev/null +++ b/.changeset/blue-rules-rule.md @@ -0,0 +1,5 @@ +--- +"@modern-kit/utils": minor +--- + +feat(groupBy): Add groupBy - @haejunejung diff --git a/docs/docs/utils/object/groupBy.md b/docs/docs/utils/object/groupBy.md new file mode 100644 index 000000000..bc24a3210 --- /dev/null +++ b/docs/docs/utils/object/groupBy.md @@ -0,0 +1,52 @@ +# groupBy + +배열의 요소들을 제공된 콜백 함수 `callbackFn`에 따라 그룹화하여, 각 키에 해당하는 항목들의 배열을 포함하는 객체를 반환합니다. + +## Code + +[🔗 실제 구현 코드 확인](https://github.com/modern-agile-team/modern-kit/blob/main/packages/utils/src/object/groupBy/index.ts) + +## Benchmark +- `hz`: 초당 작업 수 +- `mean`: 평균 응답 시간(ms) + +|이름|hz|mean|성능| +|------|---|---|---| +|modern-kit/groupBy|20,066.27|0.0498|`fastest`| +|lodash/groupBy|7,716.57|0.1296|`slowest`| + +- **modern-kit/groupBy** + - `2.60x` faster than lodash/groupBy + +## Interface +```ts title="typescript" +function groupBy( + items: T[] | readonly T[], + callbackFn: (item: T) => K +): Record +``` + +## Usage + +```ts title="typescript" +import { groupBy } from '@modern-kit/utils'; +const items = [ + { category: 'fruit', name: 'apple' }, + { category: 'fruit', name: 'banana' }, + { category: 'vegetable', name: 'carrot' }, + { category: 'fruit', name: 'pear' }, + { category: 'vegetable', name: 'broccoli' }, +]; +const group = groupBy(items, (item) => item.category); +// { +// fruit: [ +// { category: 'fruit', name: 'apple' }, +// { category: 'fruit', name: 'banana' }, +// { category: 'fruit', name: 'pear' }, +// ], +// vegetable: [ +// { category: 'vegetable', name: 'carrot' }, +// { category: 'vegetable', name: 'broccoli' }, +// ], +// }; +``` \ No newline at end of file diff --git a/packages/utils/src/object/groupBy/groupBy.bench.ts b/packages/utils/src/object/groupBy/groupBy.bench.ts new file mode 100644 index 000000000..82be5cb02 --- /dev/null +++ b/packages/utils/src/object/groupBy/groupBy.bench.ts @@ -0,0 +1,17 @@ +import { bench, describe } from 'vitest'; +import { groupBy } from '.'; +import { groupBy as groupByLodash } from 'lodash-es'; + +describe('groupBy', () => { + const obj = Array.from({ length: 10000 }, () => { + return { category: 'fruit', name: 'apple' }; + }); + + bench('modern-kit/groupBy', () => { + groupBy(obj, (item) => item.category); + }); + + bench('lodash/groupBy', () => { + groupByLodash(obj, (item) => item.category); + }); +}); diff --git a/packages/utils/src/object/groupBy/groupBy.spec.ts b/packages/utils/src/object/groupBy/groupBy.spec.ts new file mode 100644 index 000000000..cbbf77ccb --- /dev/null +++ b/packages/utils/src/object/groupBy/groupBy.spec.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest'; +import { groupBy } from '.'; + +describe('groupBy', () => { + it('should group elements by a given key', () => { + const items = [ + { category: 'fruit', name: 'apple' }, + { category: 'fruit', name: 'banana' }, + { category: 'vegetable', name: 'carrot' }, + { category: 'fruit', name: 'pear' }, + { category: 'vegetable', name: 'broccoli' }, + ]; + + const actual = groupBy(items, (item) => item.category); + const expected = { + fruit: [ + { category: 'fruit', name: 'apple' }, + { category: 'fruit', name: 'banana' }, + { category: 'fruit', name: 'pear' }, + ], + vegetable: [ + { category: 'vegetable', name: 'carrot' }, + { category: 'vegetable', name: 'broccoli' }, + ], + }; + + expect(actual).toEqual(expected); + }); + + it('should handle an empty array', () => { + const items: Array> = []; + + const actual = groupBy(items, (item) => item.category); + const expected = {}; + + expect(actual).toEqual(expected); + }); + + it('should group handle numeric key', () => { + const items = [ + { category: 1, name: 'apple' }, + { category: 2, name: 'banana' }, + { category: 1, name: 'carrot' }, + { category: 2, name: 'pear' }, + ]; + + const actual = groupBy(items, (item) => item.category); + const expected = { + '1': [ + { category: 1, name: 'apple' }, + { category: 1, name: 'carrot' }, + ], + '2': [ + { category: 2, name: 'banana' }, + { category: 2, name: 'pear' }, + ], + }; + + expect(actual).toEqual(expected); + }); + + it('should group handle symbol key', () => { + const symbolA = Symbol('A'); + const symbolB = Symbol('B'); + + const items = [ + { category: symbolA, name: 'apple' }, + { category: symbolB, name: 'banana' }, + { category: symbolA, name: 'carrot' }, + { category: symbolB, name: 'pear' }, + ]; + + const actual = groupBy(items, (item) => item.category); + const expected = { + [symbolA]: [ + { category: symbolA, name: 'apple' }, + { category: symbolA, name: 'carrot' }, + ], + [symbolB]: [ + { category: symbolB, name: 'banana' }, + { category: symbolB, name: 'pear' }, + ], + }; + + expect(actual).toEqual(expected); + }); +}); diff --git a/packages/utils/src/object/groupBy/index.ts b/packages/utils/src/object/groupBy/index.ts new file mode 100644 index 000000000..eea4b07a6 --- /dev/null +++ b/packages/utils/src/object/groupBy/index.ts @@ -0,0 +1,51 @@ +/** + * @description + * 배열의 요소들을 제공된 콜백 함수 `callbackFn`에 따라 그룹화하여, 각 키에 해당하는 항목들의 배열을 포함하는 객체를 반환합니다. + * + * @template T - 배열 항목의 타입을 나타냅니다. + * @template K - 그룹화 키의 타입을 나타냅니다. `PropertyKey`로 제한되어 있어, 문자열, 숫자, 심볼 등 가능한 모든 키 타입을 사용할 수 있습니다. + * @param {T[] | readonly T[]} items - 그룹화를 진행할 항목들의 배열입니다. + * @param {(item: T) => K} callbackFn - 각 항목에 대해 그룹화 키를 반환하는 함수입니다. + * @returns {Record} - 각 키가 콜백 함수 `callbackFn`의 결과이고, 값이 해당 키에 속하는 항목들의 배열인 객체를 반환합니다. + * + * @example + * const array = [ + * { category: 'fruit', name: 'apple' }, + * { category: 'fruit', name: 'banana' }, + * { category: 'vegetable', name: 'carrot' }, + * { category: 'fruit', name: 'pear' }, + * { category: 'vegetable', name: 'broccoli' }, + * ]; + * + * const group = groupBy(array, (item) => item.category); + * // { + * // fruit: [ + * // { category: 'fruit', name: 'apple' }, + * // { category: 'fruit', name: 'banana' }, + * // { category: 'fruit', name: 'pear' }, + * // ], + * // vegetable: [ + * // { category: 'vegetable', name: 'carrot' }, + * // { category: 'vegetable', name: 'broccoli' }, + * // ], + * // } + */ + +export function groupBy( + items: T[] | readonly T[], + callbackFn: (item: T) => K +): Record { + const group = {} as Record; + + for (const item of items) { + const key = callbackFn(item); + + if (!(key in group)) { + group[key] = []; + } + + group[key].push(item); + } + + return group; +} diff --git a/packages/utils/src/object/index.ts b/packages/utils/src/object/index.ts index 88ffa28d3..599f5b9be 100644 --- a/packages/utils/src/object/index.ts +++ b/packages/utils/src/object/index.ts @@ -1,4 +1,5 @@ export * from './deleteFalsyProperties'; +export * from './groupBy'; export * from './invert'; export * from './mapKeys'; export * from './mapValues';