Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS utils group by #377

Merged
merged 8 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/app/universal-ts-utils/src/internal/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type RecordKeyType = string | number | symbol

export type KeysMatching<T extends object, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]
2 changes: 2 additions & 0 deletions packages/app/universal-ts-utils/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@ export * from './public/array/sortByField.js'

// object
export * from './public/object/areDeepEqual.js'
export * from './public/object/groupBy.js'
export * from './public/object/groupByUnique.js'
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { compare } from '../../internal/compare'

type KeysMatching<T extends object, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T]
import type { KeysMatching } from '../../internal/types'

/**
* Sorts an array of objects based on a specified string field and order.
Expand Down
179 changes: 179 additions & 0 deletions packages/app/universal-ts-utils/src/public/object/groupBy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { describe, expect, it } from 'vitest'
import { groupBy } from './groupBy'

describe('groupBy', () => {
it('Empty array', () => {
const array: { id: string }[] = []
const result = groupBy(array, 'id')
expect(Object.keys(result)).length(0)
})

type TestType = {
id?: number | null
name: string
bool: boolean
nested?: {
code: number
}
}

it('Correctly groups by string values', () => {
const input: TestType[] = [
{
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},
{
id: 2,
name: 'c',
bool: true,
nested: { code: 200 },
},
{
id: 3,
name: 'b',
bool: true,
nested: { code: 300 },
},
{
id: 4,
name: 'a',
bool: true,
nested: { code: 400 },
},
]

const result: Record<string, TestType[]> = groupBy(input, 'name')
expect(result).toStrictEqual({
a: [
{
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},
{
id: 4,
name: 'a',
bool: true,
nested: { code: 400 },
},
],
b: [
{
id: 3,
name: 'b',
bool: true,
nested: { code: 300 },
},
],
c: [
{
id: 2,
name: 'c',
bool: true,
nested: { code: 200 },
},
],
})
})

it('Correctly groups by number values', () => {
const input: TestType[] = [
{
id: 1,
name: 'a',
bool: true,
},
{
id: 1,
name: 'b',
bool: false,
},
{
id: 2,
name: 'c',
bool: false,
},
{
id: 3,
name: 'd',
bool: false,
},
]

const result: Record<number, TestType[]> = groupBy(input, 'id')

expect(result).toStrictEqual({
1: [
{
id: 1,
name: 'a',
bool: true,
},
{
id: 1,
name: 'b',
bool: false,
},
],
2: [
{
id: 2,
name: 'c',
bool: false,
},
],
3: [
{
id: 3,
name: 'd',
bool: false,
},
],
})
})

it('Correctly handles undefined and null', () => {
const input: TestType[] = [
{
id: 1,
name: 'a',
bool: true,
},
{
name: 'c',
bool: true,
},
{
id: null,
name: 'd',
bool: true,
},
{
id: 1,
name: 'b',
bool: true,
},
]

const result = groupBy(input, 'id')

expect(result).toStrictEqual({
1: [
{
id: 1,
name: 'a',
bool: true,
},
{
id: 1,
name: 'b',
bool: true,
},
],
})
})
})
29 changes: 29 additions & 0 deletions packages/app/universal-ts-utils/src/public/object/groupBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { KeysMatching, RecordKeyType } from '../../internal/types'

/**
* @param array The array of objects to be grouped.
* @param selector The key used for grouping the objects.
* @returns An object where the keys are unique values from the given selector and the values are the corresponding objects from the array.
*/
export const groupBy = <
T extends object,
K extends KeysMatching<T, RecordKeyType | null | undefined>,
>(
array: T[],
selector: K,
): Record<RecordKeyType, T[]> => {
return array.reduce(
(acc, item) => {
const key = item[selector] as RecordKeyType | null | undefined
if (key === undefined || key === null) {
return acc
}
if (!acc[key]) {
acc[key] = []
}
acc[key].push(item)
return acc
},
{} as Record<RecordKeyType, T[]>,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, expect, it } from 'vitest'
import { groupByUnique } from './groupByUnique'

describe('groupByUnique', () => {
it('Empty array', () => {
const array: { id: string }[] = []
const result = groupByUnique(array, 'id')
expect(Object.keys(result)).length(0)
})

type TestType = {
id?: number | null
name: string
bool: boolean
nested: {
code: number
}
}

it('Correctly groups by string values', () => {
const input: TestType[] = [
{
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},
{
id: 2,
name: 'b',
bool: true,
nested: { code: 200 },
},
]

const result: Record<string, TestType> = groupByUnique(input, 'name')
expect(result).toStrictEqual({
a: {
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},

b: {
id: 2,
name: 'b',
bool: true,
nested: { code: 200 },
},
})
})

it('Correctly groups by number values', () => {
const input: TestType[] = [
{
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},
{
id: 2,
name: 'b',
bool: true,
nested: { code: 200 },
},
]

const result: Record<number, TestType> = groupByUnique(input, 'id')

expect(result).toStrictEqual({
1: {
id: 1,
name: 'a',
bool: true,
nested: { code: 100 },
},
2: {
id: 2,
name: 'b',
bool: true,
nested: { code: 200 },
},
})
})

it('Correctly handles undefined', () => {
const input: TestType[] = [
{
id: 1,
name: 'name',
bool: true,
nested: { code: 100 },
},
{
name: 'invalid',
bool: true,
nested: { code: 100 },
},
{
id: 3,
name: 'name 2',
bool: true,
nested: { code: 100 },
},
]

const result = groupByUnique(input, 'id')

expect(result).toStrictEqual({
1: {
id: 1,
name: 'name',
bool: true,
nested: { code: 100 },
},
3: {
id: 3,
name: 'name 2',
bool: true,
nested: { code: 100 },
},
})
})

it('throws on duplicated value', () => {
const input: { name: string }[] = [
{
id: 1,
name: 'test',
},
{
id: 2,
name: 'work',
},
{
id: 3,
name: 'test',
},
] as never[]

expect(() => groupByUnique(input, 'name')).toThrowError(
'Duplicated item for selector name with value test',
)
})
})
Loading
Loading