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

feat: #30 add function "pick", "omit" #31

Merged
merged 4 commits into from
Oct 5, 2023
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 .changeset/chatty-dryers-tickle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"json-origami": minor
---

feat: #30 add function "pick", "omit"
79 changes: 76 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ Whether you're looking to flatten a nested structure or revert a flat map into i
## Key Features

- **Lightweight & Standalone**: Operates without any external dependencies, ensuring quick installations and reduced bundle sizes.
- **fold**: Flatten a multi-layered JSON object into a single-tiered representation.
- **unfold**: Transform a single-layer JSON map back to its original nested structure.
- **twist**: Reshape and reorganize the keys of your JSON structure, mirroring the intricate adjustments made in origami.
- **Simple & Intuitive**: Designed with a simple API that is easy to learn and use.

## Installation

Expand All @@ -25,6 +23,8 @@ npm install json-origami

### fold

Flatten a multi-layered JSON object into a single-tiered representation.

```javascript
import { fold } from 'json-origami'

Expand All @@ -42,6 +42,8 @@ console.log(result)

### unfold

Transform a single-layer JSON map back to its original nested structure.

```javascript
import { unfold } from 'json-origami'

Expand All @@ -59,6 +61,8 @@ console.log(result)

### twist

Reshape and reorganize the keys of your JSON structure, mirroring the intricate adjustments made in origami.

```javascript
import { twist } from 'json-origami'

Expand All @@ -81,6 +85,46 @@ console.log(result)
/// { foo: 1, bar: 2, baz: [3, { e: 4 }] }
```

### pick

Select only the specified keys from a JSON object.

```javascript
import { pick } from 'json-origami'

const obj = {
a: 1,
b: {
c: 2,
d: [3, { e: 4 }]
}
}

const result = pick(obj, ['a', 'b.c'])
console.log(result)
/// { a: 1, b: { c: 2 } }
```

### omit

Remove the specified keys from a JSON object.

```javascript
import { omit } from 'json-origami'

const obj = {
a: 1,
b: {
c: 2,
d: [3, { e: 4 }]
}
}

const result = omit(obj, ['b.c', 'b.d[1]'])
console.log(result)
/// { a: 1, b: { d: [3] } }
```

## Options

### arrayIndex
Expand All @@ -106,6 +150,35 @@ conole.log(result)
/// { a: 1, 'b.c': 2, 'b.d.0': 3, 'b.d.1.e': 4 }
```

### pruneArray

Options for 'unfold', 'twist', 'pick', 'omit'

**Type**: `boolean`
**Default**: `true`

Specifies whether to remove the specified array elements or not.

```javascript
import { twist } from 'json-origami'

const obj = {
a: 1,
b: {
c: 2,
d: [3, 4, 5, 6]
}
}

const result1 = twist(obj, { 'b.d[1]': 'D' }, { pruneArray: true })
console.log(result1)
/// { a: 1, b: { c: 2, d: [3, 5, 6] }, D: 4 }

const result2 = twist(obj, { 'b.d[1]': 'D' }, { pruneArray: false })
console.log(result2)
/// { a: 1, b: { c: 2, d: [3, undefined, 5, 6] }, D: 4 }
```

### keyPrefix

Options for 'fold'
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"access": "public"
},
"scripts": {
"dev": "vitest",
"dev": "vitest --ui",
"build": "tsup",
"test": "run-p test:*",
"test:spec": "vitest --run --silent",
Expand All @@ -50,6 +50,7 @@
"@changesets/cli": "^2.26.2",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"@vitest/ui": "^0.34.6",
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1",
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export type { Folded, Unfolded } from './type'
export { fold } from './fold'
export { unfold } from './unfold'
export { twist } from './twist'
export { omit } from './omit'
export { pick } from './pick'
48 changes: 48 additions & 0 deletions src/omit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { fold } from './fold'
import { unfold } from './unfold'
import { includesKey } from './utils'
import type { Dictionary, DeepKeyOf, OmitOption, Omit, Folded } from './type'

/**
* Returns an object with the specified keys removed from the object.
*
* @example
* ```ts
* const obj = {
* a: 1,
* b: {
* c: 2,
* d: [3, 4]
* }
* }
*
* const omitted = omit(obj, ['a', 'b.c'])
* // omitted is
* // {
* // b: {
* // d: [3, 4]
* // }
* // }
* ```
*
* @param obj
* @param keys
* @param opt
*/
export function omit<D extends Dictionary, K extends DeepKeyOf<D>>(
obj: D,
keys: K[],
opt?: OmitOption
): Omit<D, K> {
const folded = fold(obj)

const targetKeys = new Set(
Object.keys(folded).filter((k) => !keys.some((key) => includesKey(key, k, opt)))
)

const fixedKeyMap = Object.fromEntries(
Object.entries(folded).filter(([k]) => targetKeys.has(k))
) as Folded<Dictionary>

return unfold(fixedKeyMap, opt) as Dictionary
}
Comment on lines +1 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omit関数の実装は、指定されたキーをオブジェクトから削除するという目的に適しています。この関数は、まずオブジェクトをフラットな表現に変換し(fold関数)、次に指定したキーをフィルタリングし、最後に元の構造に戻します(unfold関数)。これらのステップは効率的であり、コードの可読性も高いです。

ただし、一部のパフォーマンス改善が可能です。特に、keys.some((key) => includesKey(key, k, opt))の部分では、各キーに対してincludesKey関数が呼び出されています。これは、大きなキー配列に対しては非効率的になる可能性があります。代わりに、キーをSetに変換し、その存在をチェックすることで、パフォーマンスを向上させることができます。

以下に修正案を示します:

export function omit<D extends Dictionary, K extends DeepKeyOf<D>>(
  obj: D,
  keys: K[],
  opt?: OmitOption
): Omit<D, K> {
  const folded = fold(obj)

+ const keySet = new Set(keys);
- const targetKeys = new Set(
-   Object.keys(folded).filter((k) => !keys.some((key) => includesKey(key, k, opt)))
- )
+ const targetKeys = new Set(
+   Object.keys(folded).filter((k) => !keySet.has(k))
+ )

  const fixedKeyMap = Object.fromEntries(
    Object.entries(folded).filter(([k]) => targetKeys.has(k))
  ) as Folded<Dictionary>

  return unfold(fixedKeyMap, opt) as Dictionary
}

49 changes: 49 additions & 0 deletions src/pick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { fold } from './fold'
import { unfold } from './unfold'
import { includesKey } from './utils'
import type { Dictionary, DeepKeyOf, PickOption, Omit, Folded } from './type'

/**
* Returns an object with the specified keys picked from the object.
*
* @example
* ```ts
* const obj = {
* a: 1,
* b: {
* c: 2,
* d: [3, 4]
* }
* }
*
* const omitted = pick(obj, ['a', 'b.c'])
* // omitted is
* // {
* // a: 1,
* // b: {
* // c: 2
* // }
* // }
* ```
*
* @param obj
* @param keys
* @param opt
*/
export function pick<D extends Dictionary, K extends DeepKeyOf<D>>(
obj: D,
keys: K[],
opt?: PickOption
): Omit<D, K> {
const folded = fold(obj)

const targetKeys = new Set(
Object.keys(folded).filter((k) => keys.some((key) => includesKey(key, k, opt)))
)

const fixedKeyMap = Object.fromEntries(
Object.entries(folded).filter(([k]) => targetKeys.has(k))
) as Folded<Dictionary>

return unfold(fixedKeyMap, opt) as Dictionary
}
Comment on lines +33 to +49
Copy link

@coderabbitai coderabbitai bot Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pick関数の実装は、指定されたキーを持つオブジェクトを正しく生成するように見えます。しかし、パフォーマンスの観点から、行41でkeys.some()を使用している部分が問題となります。このコードは、各キーに対して全てのターゲットキーを走査し、O(n^2)の時間複雑度を持ちます。これを改善するために、keysをSetに変換し、その存在チェックをO(1)の操作にすることを検討してみてください。

-  const targetKeys = new Set(
-    Object.keys(folded).filter((k) => keys.some((key) => includesKey(key, k, opt)))
-  )
+  const keySet = new Set(keys);
+  const targetKeys = new Set(
+    Object.keys(folded).filter((k) => keySet.has(k))
+  )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

includesKey は文字列の一致比較ではないため、 keySet.has とは判定結果が異なります。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mew-ton ご指摘いただきありがとうございます。includesKey関数が単純な文字列の一致比較ではないという点は見落としていました。そのため、提案した修正は適切ではありませんでした。

しかし、パフォーマンスの観点からは依然として改善の余地があります。keys.some()の使用は時間複雑度をO(n^2)にするため、この部分を最適化することを検討してみてください。

具体的には、includesKey関数の結果をキャッシュすることで、同じキーに対する重複した計算を避けることができます。以下に示すように、Mapを使用してキーとそれに対応するincludesKeyの結果を保存し、再利用することができます。

+  const includesKeyCache = new Map();
   const targetKeys = new Set(
-    Object.keys(folded).filter((k) => keys.some((key) => includesKey(key, k, opt)))
+    Object.keys(folded).filter((k) => {
+      if (!includesKeyCache.has(k)) {
+        includesKeyCache.set(k, keys.some((key) => includesKey(key, k, opt)));
+      }
+      return includesKeyCache.get(k);
+    })
   )

この変更により、各キーに対するincludesKeyの計算が最大で1回しか行われないため、パフォーマンスが向上する可能性があります。ただし、キャッシュの使用はメモリ使用量を増加させる可能性があるため、そのトレードオフを考慮に入れてください。

5 changes: 3 additions & 2 deletions src/twist.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { fold, unfold } from '.'
import { fold } from './fold'
import { unfold } from './unfold'
import type { Dictionary, MoveMap, Twist, TwistOption } from './type'

/**
*
* @param obj
* @param moveMap
*/
export function twist<D extends Dictionary, M extends MoveMap>(
export function twist<D extends Dictionary, M extends MoveMap<D>>(
obj: D,
moveMap: M,
option?: TwistOption
Expand Down
79 changes: 71 additions & 8 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export type Dictionary =
| MaybeReadonly<Record<string, unknown>>
| MaybeReadonly<Array<Primitive | Record<string, unknown>>>

/**
* TODO: 深い階層のキーに対応する
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type DeepKeyOf<_D extends Dictionary> = string

/**
*
*/
Expand All @@ -21,7 +27,7 @@ export type Folded<_D extends Dictionary> = Record<string, Primitive>
/**
*
*/
export type MoveMap = Record<string, string>
export type MoveMap<D extends Dictionary> = Record<DeepKeyOf<D>, string>

/**
*
Expand All @@ -33,15 +39,31 @@ export type Unfolded<KV extends Folded<any>> = KV extends Folded<infer D> ? D :
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Twist<_D extends Dictionary, _M extends MoveMap> = object
export type Twist<D extends Dictionary, _M extends MoveMap<D>> = Dictionary

/**
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Omit<D extends Dictionary, _K extends DeepKeyOf<D>> = Dictionary

/**
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type Pick<D extends Dictionary, _K extends DeepKeyOf<D>> = Dictionary
mew-ton marked this conversation as resolved.
Show resolved Hide resolved

/**
*
*/
export type ArrayIndex = 'dot' | 'bracket'

interface CommonOption {
export interface CommonOption {
/**
* Specify the array index style.
* - `dot`: `a.0.b.1.c`
* - `bracket`: `a[0].b[1].c`
*
* @default 'bracet'
*/
arrayIndex?: 'dot' | 'bracket'
Expand All @@ -52,7 +74,8 @@ interface CommonOption {
*/
export interface FoldOption extends CommonOption {
/**
*
* Specify the prefix of the key of the result of fold.
* @default ''
*/
keyPrefix?: string
}
Expand All @@ -66,13 +89,53 @@ export type FixedFoldOption = Readonly<FoldOption & typeof defaultCommonOption>
/**
*
*/
export interface UnfoldOption extends CommonOption {}
export interface UnfoldOption extends CommonOption {
/**
* If true, the array will be pruned if it is empty.
*
* @example
* ```ts
* const kv = {
* 'a[1]': 1,
* 'a[3]': 2,
* 'a[5]': 3
* }
*
* const pruned = unfold(kv, { pruneArray: true })
* // pruned is { a: [1, 2, 3] }
*
* const notPruned = unfold(kv, { pruneArray: false })
* // notPruned is { a: [undefined, 1, undefined, 2, undefined, 3] }
* ```
* @default true
*/
pruneArray?: boolean
}

export const defaultUnfoldOption = {
...defaultCommonOption,
pruneArray: true
} satisfies UnfoldOption

export type FixedUnfoldOption = Readonly<UnfoldOption & typeof defaultUnfoldOption>

/**
*
*/
export interface TwistOption extends UnfoldOption {}

export type FixedTwistOption = Readonly<TwistOption & typeof defaultUnfoldOption>

/**
*
*/
export interface OmitOption extends UnfoldOption {}

export type FixedUnfoldOption = Readonly<UnfoldOption & typeof defaultCommonOption>
export type FixedOmitOption = Readonly<OmitOption & typeof defaultUnfoldOption>

/**
*
*/
export interface TwistOption extends CommonOption {}
export interface PickOption extends UnfoldOption {}

export type FixedTwistOption = Readonly<TwistOption & typeof defaultCommonOption>
export type FixedPickOption = Readonly<PickOption & typeof defaultUnfoldOption>
Loading