Skip to content

Commit

Permalink
Merge pull request #940 from devoxa/flatten-unflatten
Browse files Browse the repository at this point in the history
Add flatten & unflatten functions
  • Loading branch information
kodiakhq[bot] authored Oct 31, 2024
2 parents 29259eb + 4279dc6 commit 2dd069d
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 1 deletion.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,17 @@ flocky.escapeRegExp('Hey. (1 + 1 = 2)')

<sup>[Source](./src/escapeRegExp/escapeRegExp.ts) • Minify: 93 B • Minify & GZIP: 90 B<sup>

### flatten(object)

Flattens a nested object into an object with dot notation keys.

```js
flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } })
// -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }
```

<sup>[Source](./src/flatten/flatten.ts) • Minify: 172 B • Minify & GZIP: 136 B<sup>

### get(object, path, defaultValue?)

Get the value at a `path` of an `object` (with an optional `defaultValue`)
Expand Down Expand Up @@ -518,6 +529,17 @@ flocky.toMap(

<sup>[Source](./src/toMap/toMap.ts) • Minify: 95 B • Minify & GZIP: 95 B<sup>

### unflatten(object)

Unflattens an object with dot notation keys into a nested object.

```js
flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 })
// -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } }
```

<sup>[Source](./src/unflatten/unflatten.ts) • Minify: 199 B • Minify & GZIP: 145 B<sup>

### unique(array, identity?)

Create a duplicate-free version of an array, in which only the first occurrence of each element is kept.
Expand Down
25 changes: 25 additions & 0 deletions src/flatten/flatten.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { flatten } from './flatten'

describe('flatten', () => {
test('flattens the object', () => {
expect(flatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })

expect(flatten({ a: { b: 1 } })).toEqual({ 'a.b': 1 })
expect(flatten({ a: { b: 1, c: 2 } })).toEqual({ 'a.b': 1, 'a.c': 2 })
expect(flatten({ a: { b: 1, c: 2 }, d: { e: 3 } })).toEqual({ 'a.b': 1, 'a.c': 2, 'd.e': 3 })

expect(flatten({ a: { b: { c: 1 } } })).toEqual({ 'a.b.c': 1 })
expect(flatten({ a: { b: { c: 1, d: 2 } } })).toEqual({ 'a.b.c': 1, 'a.b.d': 2 })
expect(flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } })).toEqual({
'a.b.c': 1,
'a.b.d': 2,
'e.f.g': 3,
})

// Check that the types are correct
const result = flatten({ a: { b: { c: 1, d: 2 } }, e: { f: { g: 3 } } })
expect(result['a.b.c'] + 1).toEqual(2)
expect(result['a.b.d'] + 1).toEqual(3)
expect(result['e.f.g'] + 1).toEqual(4)
})
})
62 changes: 62 additions & 0 deletions src/flatten/flatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* ### flatten(object)
*
* Flattens a nested object into an object with dot notation keys.
*
* ```js
* flocky.flatten({ a: { b: 1, c: 2 }, d: { e: { f: 3 } } })
* // -> { 'a.b': 1, 'a.c': 2, 'd.e.f': 3 }
* ```
*/

type Entry = { key: string; value: unknown; optional: boolean }

type Explode<T> = _Explode<T extends readonly unknown[] ? { '0': T[number] } : T>

type _Explode<T> = T extends object
? {
[K in keyof T]-?: K extends string
? Explode<T[K]> extends infer E
? E extends Entry
? {
key: `${K}${E['key'] extends '' ? '' : '.'}${E['key']}`
value: E['value']
optional: E['key'] extends ''
? object extends Pick<T, K>
? true
: false
: E['optional']
}
: never
: never
: never
}[keyof T]
: { key: ''; value: T; optional: false }

type Collapse<T extends Entry> = {
[E in Extract<T, { optional: false }> as E['key']]: E['value']
} & Partial<{ [E in Extract<T, { optional: true }> as E['key']]: E['value'] }> extends infer O
? { [K in keyof O]: O[K] }
: never

type FlattenObject<T> = Collapse<Explode<T>>

export function flatten<TObject extends Record<string, unknown>>(object: TObject) {
const result: Record<string, unknown> = {}

function recurse(current: Record<string, unknown>, prefix = '') {
for (const key in current) {
const value = current[key]
const nextKey = prefix ? `${prefix}.${key}` : key

if (typeof value === 'object' && value !== null) {
recurse(value as Record<string, unknown>, nextKey)
} else {
result[nextKey] = value
}
}
}

recurse(object)
return result as FlattenObject<TObject>
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { compact } from './compact/compact'
export { debounce } from './debounce/debounce'
export { duplicates } from './duplicates/duplicates'
export { escapeRegExp } from './escapeRegExp/escapeRegExp'
export { flatten } from './flatten/flatten'
export { get } from './get/get'
export { hash } from './hash/hash'
export { identifier } from './identifier/identifier'
Expand All @@ -28,4 +29,5 @@ export { slugify } from './slugify/slugify'
export { sum } from './sum/sum'
export { throttle } from './throttle/throttle'
export { toMap } from './toMap/toMap'
export { unflatten } from './unflatten/unflatten'
export { unique } from './unique/unique'
17 changes: 16 additions & 1 deletion src/typeHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export type JSONValue = string | number | boolean | null | undefined | JSONObject | JSONArray
type JSONObject = { [key: string]: JSONValue }
type JSONArray = Array<JSONValue>

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TAnyFunction<TReturn> = (...args: any[]) => TReturn

export type Simplify<T> = T extends unknown ? { [K in keyof T]: Simplify<T[K]> } : never

export type RecursiveUnionToIntersection<TObjectWithUnions> = {
[TKey in keyof TObjectWithUnions]: TObjectWithUnions[TKey] extends object
? RecursiveUnionToIntersection<UnionToIntersection<TObjectWithUnions[TKey]>>
: TObjectWithUnions[TKey]
}

export type UnionToIntersection<TUnion> = (
TUnion extends any ? (k: TUnion) => void : never
) extends (k: infer TIntersection) => void
? TIntersection
: never
25 changes: 25 additions & 0 deletions src/unflatten/unflatten.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { unflatten } from './unflatten'

describe('unflatten', () => {
test('unflattens the object', () => {
expect(unflatten({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 })

expect(unflatten({ 'a.b': 1 })).toEqual({ a: { b: 1 } })
expect(unflatten({ 'a.b': 1, 'a.c': 2 })).toEqual({ a: { b: 1, c: 2 } })
expect(unflatten({ 'a.b': 1, 'a.c': 2, 'd.e': 3 })).toEqual({ a: { b: 1, c: 2 }, d: { e: 3 } })

expect(unflatten({ 'a.b.c': 1 })).toEqual({ a: { b: { c: 1 } } })
expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2 })).toEqual({ a: { b: { c: 1, d: 2 } } })
expect(unflatten({ 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 })).toEqual({
a: { b: { c: 1, d: 2 } },
e: { f: { g: 3 } },
})

// Check that the types are correct
const result = unflatten({ 'a.a.a': 1, 'a.b.c': 1, 'a.b.d': 2, 'e.f.g': 3 })
expect(result.a.a.a + 1).toEqual(2)
expect(result.a.b.c + 1).toEqual(2)
expect(result.a.b.d + 1).toEqual(3)
expect(result.e.f.g + 1).toEqual(4)
})
})
45 changes: 45 additions & 0 deletions src/unflatten/unflatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { RecursiveUnionToIntersection, Simplify } from '../typeHelpers'

/**
* ### unflatten(object)
*
* Unflattens an object with dot notation keys into a nested object.
*
* ```js
* flocky.unflatten({ 'a.b': 1, 'a.c': 2, 'd.e.f': 3 })
* // -> { a: { b: 1, c: 2 }, d: { e: { f: 3 } } }
* ```
*/

type UnflattenObject<TObject extends Record<string, unknown>> = {
[TKey in keyof TObject as TKey extends `${infer TKeyPrefix}.${string}`
? TKeyPrefix
: TKey]: TKey extends `${string}.${infer TKeySuffix}`
? UnflattenObject<{ [key in TKeySuffix]: TObject[TKey] }>
: TObject[TKey]
} extends infer TResult
? { [TResultKey in keyof TResult]: TResult[TResultKey] }
: never

export function unflatten<TObject extends Record<string, unknown>>(object: TObject) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = {}

for (const key in object) {
const keys = key.split('.')
let current = result

for (let i = 0; i < keys.length; i++) {
const part = keys[i]

if (i === keys.length - 1) {
current[part] = object[key]
} else {
if (!current[part]) current[part] = {}
current = current[part]
}
}
}

return result as Simplify<RecursiveUnionToIntersection<UnflattenObject<TObject>>>
}

0 comments on commit 2dd069d

Please sign in to comment.