Skip to content

Commit

Permalink
TS utils group by (#377)
Browse files Browse the repository at this point in the history
* Creating external folder

* Extracting compare to a new internal file and reusing it

* Name change

* Group by method

* groupByUnique

* Exporting group by
  • Loading branch information
CarlosGamero authored Nov 6, 2024
1 parent 36c3ee7 commit 5d3aa72
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 4 deletions.
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

0 comments on commit 5d3aa72

Please sign in to comment.