Skip to content

Commit

Permalink
feat(groupBy): Add groupBy (#411)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
haejunejung and ssi02014 authored Aug 12, 2024
1 parent 1d0d473 commit e031af0
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-rules-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@modern-kit/utils": minor
---

feat(groupBy): Add groupBy - @haejunejung
52 changes: 52 additions & 0 deletions docs/docs/utils/object/groupBy.md
Original file line number Diff line number Diff line change
@@ -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<T, K extends PropertyKey>(
items: T[] | readonly T[],
callbackFn: (item: T) => K
): Record<K, T[]>
```

## 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' },
// ],
// };
```
17 changes: 17 additions & 0 deletions packages/utils/src/object/groupBy/groupBy.bench.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
87 changes: 87 additions & 0 deletions packages/utils/src/object/groupBy/groupBy.spec.ts
Original file line number Diff line number Diff line change
@@ -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<Record<string, string>> = [];

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);
});
});
51 changes: 51 additions & 0 deletions packages/utils/src/object/groupBy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @description
* 배열의 요소들을 제공된 콜백 함수 `callbackFn`에 따라 그룹화하여, 각 키에 해당하는 항목들의 배열을 포함하는 객체를 반환합니다.
*
* @template T - 배열 항목의 타입을 나타냅니다.
* @template K - 그룹화 키의 타입을 나타냅니다. `PropertyKey`로 제한되어 있어, 문자열, 숫자, 심볼 등 가능한 모든 키 타입을 사용할 수 있습니다.
* @param {T[] | readonly T[]} items - 그룹화를 진행할 항목들의 배열입니다.
* @param {(item: T) => K} callbackFn - 각 항목에 대해 그룹화 키를 반환하는 함수입니다.
* @returns {Record<K, T[]>} - 각 키가 콜백 함수 `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<T, K extends PropertyKey>(
items: T[] | readonly T[],
callbackFn: (item: T) => K
): Record<K, T[]> {
const group = {} as Record<K, T[]>;

for (const item of items) {
const key = callbackFn(item);

if (!(key in group)) {
group[key] = [];
}

group[key].push(item);
}

return group;
}
1 change: 1 addition & 0 deletions packages/utils/src/object/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './deleteFalsyProperties';
export * from './groupBy';
export * from './invert';
export * from './mapKeys';
export * from './mapValues';
Expand Down

0 comments on commit e031af0

Please sign in to comment.