diff --git a/benchmarks/performance/flattenObject-map.bench.ts b/benchmarks/performance/flattenObject-map.bench.ts new file mode 100644 index 000000000..f2f06efbf --- /dev/null +++ b/benchmarks/performance/flattenObject-map.bench.ts @@ -0,0 +1,116 @@ +import { bench, describe } from 'vitest'; +import { flattenObject } from '../../src/object/flattenObject.ts'; + +// Create nested objects for testing +function createNestedObject(depth: number, width: number) { + const obj: any = {}; + + for (let i = 0; i < width; i++) { + const key = `key${i}`; + + if (depth > 1) { + obj[key] = createNestedObject(depth - 1, width); + } else { + obj[key] = `value${i}`; + } + } + + return obj; +} + +// Real-world translation-like data structure +const translationLikeData = { + welcome: { + title: 'Welcome to our app', + subtitle: 'Get started today', + buttons: { + login: 'Login', + signup: 'Sign Up', + forgot: 'Forgot Password', + }, + }, + navigation: { + home: 'Home', + about: 'About', + contact: 'Contact', + settings: { + profile: 'Profile', + preferences: 'Preferences', + security: 'Security', + notifications: { + email: 'Email', + push: 'Push', + sms: 'SMS', + }, + }, + }, + errors: { + validation: { + required: 'This field is required', + email: 'Invalid email format', + password: 'Password too weak', + confirm: 'Passwords do not match', + }, + network: ['Connection failed', 'Timeout error', 'Server error'], + auth: { + unauthorized: 'Unauthorized access', + forbidden: 'Access forbidden', + expired: 'Session expired', + }, + }, + features: { + premium: { + analytics: 'Advanced Analytics', + support: 'Priority Support', + storage: 'Unlimited Storage', + }, + free: { + basic: 'Basic Features', + limited: 'Limited Storage', + }, + }, +}; + +describe('flattenObject Map conversion performance', () => { + bench('legacy: flattenObject + Object.entries + new Map', () => { + new Map(Object.entries(flattenObject(translationLikeData))); + }); + + bench('new: flattenObject with Map target', () => { + flattenObject(translationLikeData, new Map()); + }); +}); + +describe('flattenObject Map conversion - large data', () => { + const largeNestedObject = createNestedObject(4, 10); // depth 4, 10 keys per level + + bench('legacy (large): flattenObject + Object.entries + new Map', () => { + new Map(Object.entries(flattenObject(largeNestedObject))); + }); + + bench('new (large): flattenObject with Map target', () => { + flattenObject(largeNestedObject, new Map()); + }); +}); + +describe('flattenObject Map conversion - very large data', () => { + const veryLargeNestedObject = createNestedObject(5, 20); // depth 5, 20 keys per level + + bench('legacy (very large): flattenObject + Object.entries + new Map', () => { + new Map(Object.entries(flattenObject(veryLargeNestedObject))); + }); + + bench('new (very large): flattenObject with Map target', () => { + flattenObject(veryLargeNestedObject, new Map()); + }); +}); + +describe('flattenObject Map with custom delimiter', () => { + bench('legacy: flattenObject with delimiter + Object.entries + new Map', () => { + new Map(Object.entries(flattenObject(translationLikeData, { delimiter: '_' }))); + }); + + bench('new: flattenObject with delimiter and Map target', () => { + flattenObject(translationLikeData, { delimiter: '_', target: new Map() }); + }); +}); diff --git a/docs/ja/reference/object/flattenObject.md b/docs/ja/reference/object/flattenObject.md index a74e4bbf5..878a73d01 100644 --- a/docs/ja/reference/object/flattenObject.md +++ b/docs/ja/reference/object/flattenObject.md @@ -1,6 +1,6 @@ # flattenObject -ネストされたオブジェクトを単純なオブジェクトに平坦化します。 +ネストされたオブジェクトを単純なオブジェクトに平坦化するか、Mapに直接変換します。 - `Array`は平坦化されます。 - `Buffer`や`TypedArray`のような純粋なオブジェクトでないものは平坦化されません。 @@ -8,20 +8,31 @@ ## インターフェース ```typescript -function flattenObject(object: object, { delimiter = '.' }: FlattenObjectOptions = {}): Record; +// Recordを返す +function flattenObject(object: object, options?: { delimiter?: string }): Record; + +// Mapを返す(レガシー構文) +function flattenObject(object: object, target: Map): Map; + +// Mapを返す(区切り文字付き) +function flattenObject(object: object, options: { delimiter?: string; target: Map }): Map; ``` ### パラメータ - `object` (`object`): 平坦化するオブジェクト。 -- `delimiter` (`string`): ネストされたキーの区切り文字。デフォルトは `'.'`。 +- `options` (`object`, オプション): オプションオブジェクト。 + - `delimiter` (`string`): ネストされたキーの区切り文字。デフォルトは `'.'`。 + - `target` (`Map`): 平坦化されたキーと値のペアで満たすMap。 ### 戻り値 -(`T`): 平坦化されたオブジェクト。 +(`Record | Map`): 平坦化されたオブジェクトまたはMap。 ## 例 +### 基本的な使用法(Record) + ```typescript const nestedObject = { a: { @@ -42,6 +53,8 @@ console.log(flattened); // } ``` +### カスタム区切り文字(Record) + ```typescript const flattened = flattenObject(nestedObject, { delimiter: '/' }); console.log(flattened); @@ -52,3 +65,26 @@ console.log(flattened); // 'd/1': 3 // } ``` + +### Mapターゲット(レガシー構文) + +```typescript +const map = flattenObject(nestedObject, new Map()); +console.log(map); +// 出力: Map { 'a.b.c' => 1, 'd.0' => 2, 'd.1' => 3 } + +// 値へのアクセス +console.log(map.get('a.b.c')); // 1 +console.log(map.size); // 3 +``` + +### Map + カスタム区切り文字 + +```typescript +const customMap = flattenObject(nestedObject, { + delimiter: '_', + target: new Map(), +}); +console.log(customMap); +// 出力: Map { 'a_b_c' => 1, 'd_0' => 2, 'd_1' => 3 } +``` diff --git a/docs/ko/reference/object/flattenObject.md b/docs/ko/reference/object/flattenObject.md index d21fbddd1..17402f732 100644 --- a/docs/ko/reference/object/flattenObject.md +++ b/docs/ko/reference/object/flattenObject.md @@ -1,6 +1,6 @@ # flattenObject -중첩된 객체를 단순한 객체로 평탄화해요. +중첩된 객체를 단순한 객체로 평탄화하거나 Map으로 직접 변환해요. - `Array`는 평탄화돼요. - 순수 객체가 아닌 `Buffer`나 `TypedArray`는 평탄화되지 않아요. @@ -8,20 +8,31 @@ ## 인터페이스 ```typescript -function flattenObject(object: object, { delimiter = '.' }: FlattenObjectOptions = {}): Record; +// Record 반환 +function flattenObject(object: object, options?: { delimiter?: string }): Record; + +// Map 반환 (기존 문법) +function flattenObject(object: object, target: Map): Map; + +// Map 반환 (구분자 포함) +function flattenObject(object: object, options: { delimiter?: string; target: Map }): Map; ``` ### 파라미터 - `object` (`object`): 평탄화할 객체. -- `delimiter` (`string`): 중첩된 키의 구분자. 기본값은 `.`. +- `options` (`object`, 선택): 옵션 객체. + - `delimiter` (`string`): 중첩된 키의 구분자. 기본값은 `.`. + - `target` (`Map`): 평탄화된 키-값 쌍으로 채울 Map. ### 반환 값 -(`T`): 평탄화된 객체. +(`Record | Map`): 평탄화된 객체 또는 Map. ## 예시 +### 기본 사용법 (Record) + ```typescript const nestedObject = { a: { @@ -42,6 +53,8 @@ console.log(flattened); // } ``` +### 구분자 지정 (Record) + ```typescript const flattened = flattenObject(nestedObject, { delimiter: '/' }); console.log(flattened); @@ -52,3 +65,26 @@ console.log(flattened); // 'd/1': 3 // } ``` + +### Map 대상 (기존 문법) + +```typescript +const map = flattenObject(nestedObject, new Map()); +console.log(map); +// Output: Map { 'a.b.c' => 1, 'd.0' => 2, 'd.1' => 3 } + +// 값 접근 +console.log(map.get('a.b.c')); // 1 +console.log(map.size); // 3 +``` + +### Map + 구분자 지정 + +```typescript +const customMap = flattenObject(nestedObject, { + delimiter: '_', + target: new Map(), +}); +console.log(customMap); +// Output: Map { 'a_b_c' => 1, 'd_0' => 2, 'd_1' => 3 } +``` diff --git a/docs/reference/object/flattenObject.md b/docs/reference/object/flattenObject.md index a0c44c1b0..2deb82802 100644 --- a/docs/reference/object/flattenObject.md +++ b/docs/reference/object/flattenObject.md @@ -1,6 +1,6 @@ # flattenObject -Flattens a nested object into a single-level object with dot-separated keys. +Flattens a nested object into a single-level object with dot-separated keys or directly into a Map. - `Array`s are flattened. - Non-plain objects, like `Buffer`s or `TypedArray`s, are not flattened. @@ -8,20 +8,31 @@ Flattens a nested object into a single-level object with dot-separated keys. ## Signature ```typescript -function flattenObject(object: object, { delimiter = '.' }: FlattenObjectOptions = {}): Record; +// Return Record +function flattenObject(object: object, options?: { delimiter?: string }): Record; + +// Return Map (legacy syntax) +function flattenObject(object: object, target: Map): Map; + +// Return Map (with custom delimiter) +function flattenObject(object: object, options: { delimiter?: string; target: Map }): Map; ``` ### Parameters - `object` (`object`): The object to flatten. -- `delimiter` (`string`): The delimiter to use between nested keys. Defaults to `'.'`. +- `options` (`object`, optional): The options object. + - `delimiter` (`string`): The delimiter to use between nested keys. Defaults to `'.'`. + - `target` (`Map`): The target Map to populate with flattened key-value pairs. ### Returns -(`T`): The flattened object. +(`Record | Map`): The flattened object or Map. ## Examples +### Basic Usage (Record) + ```typescript const nestedObject = { a: { @@ -42,6 +53,8 @@ console.log(flattened); // } ``` +### Custom Delimiter (Record) + ```typescript const flattened = flattenObject(nestedObject, { delimiter: '/' }); console.log(flattened); @@ -52,3 +65,38 @@ console.log(flattened); // 'd/1': 3 // } ``` + +### Map Target (Legacy Syntax) + +```typescript +const map = flattenObject(nestedObject, new Map()); +console.log(map); +// Output: Map { 'a.b.c' => 1, 'd.0' => 2, 'd.1' => 3 } + +// Access values +console.log(map.get('a.b.c')); // 1 +console.log(map.size); // 3 +``` + +### Map with Custom Delimiter + +```typescript +const customMap = flattenObject(nestedObject, { + delimiter: '_', + target: new Map(), +}); +console.log(customMap); +// Output: Map { 'a_b_c' => 1, 'd_0' => 2, 'd_1' => 3 } +``` + +### Performance Comparison + +Using Map targets provides better performance compared to converting Records to Maps: + +```typescript +// Slower: Object → Array → Map (3 steps) +const slowMap = new Map(Object.entries(flattenObject(largeObject))); + +// Faster: Object → Map (1 step) +const fastMap = flattenObject(largeObject, new Map()); +``` diff --git a/src/object/flattenObject.spec.ts b/src/object/flattenObject.spec.ts index 91728fbf2..d3346d48f 100644 --- a/src/object/flattenObject.spec.ts +++ b/src/object/flattenObject.spec.ts @@ -281,4 +281,65 @@ describe('flattenObject', function () { }); }); }); + + describe('Map functionality', () => { + it('flattens into a provided Map', () => { + const nestedObject = { + a: { + b: { + c: 1, + }, + }, + d: [2, 3], + }; + + const map = new Map(); + const result = flattenObject(nestedObject, map); + + expect(result).toBe(map); + expect(map.get('a.b.c')).toBe(1); + expect(map.get('d.0')).toBe(2); + expect(map.get('d.1')).toBe(3); + expect(map.size).toBe(3); + }); + + it('works with pre-populated Map', () => { + const map = new Map([['existing', 'value']]); + + flattenObject({ a: { b: 1 } }, map); + + expect(map.get('existing')).toBe('value'); + expect(map.get('a.b')).toBe(1); + expect(map.size).toBe(2); + }); + + it('supports custom delimiter with Map using new syntax', () => { + const nestedObject = { + a: { + b: { + c: 1, + }, + }, + d: [2, 3], + }; + + const map = new Map(); + const result = flattenObject(nestedObject, { delimiter: '_', target: map }); + + expect(result).toBe(map); + expect(map.get('a_b_c')).toBe(1); + expect(map.get('d_0')).toBe(2); + expect(map.get('d_1')).toBe(3); + }); + + it('supports custom delimiter with Map using forward slash', () => { + const map = flattenObject( + { user: { profile: { name: 'John' }, settings: { theme: 'dark' } } }, + { delimiter: '/', target: new Map() } + ); + + expect(map.get('user/profile/name')).toBe('John'); + expect(map.get('user/settings/theme')).toBe('dark'); + }); + }); }); diff --git a/src/object/flattenObject.ts b/src/object/flattenObject.ts index 47fda59d0..7b19059c6 100644 --- a/src/object/flattenObject.ts +++ b/src/object/flattenObject.ts @@ -8,12 +8,24 @@ interface FlattenObjectOptions { delimiter?: string; } +interface FlattenObjectMapOptions { + /** + * The delimiter to use between nested keys. + * @default '.' + */ + delimiter?: string; + /** + * The target Map to populate with flattened key-value pairs. + */ + target: Map; +} + /** * Flattens a nested object into a single level object with delimiter-separated keys. * * @param {object} object - The object to flatten. - * @param {string} [options.delimiter='.'] - The delimiter to use between nested keys. - * @returns {Record} - The flattened object. + * @param {FlattenObjectOptions | FlattenObjectMapOptions | Map} [options] - Options for flattening. + * @returns {Record | Map} - The flattened object or Map. * * @example * const nestedObject = { @@ -33,8 +45,36 @@ interface FlattenObjectOptions { * // 'd.0': 2, * // 'd.1': 3 * // } + * + * // Or flatten into a Map + * const map = flattenObject(nestedObject, new Map()); + * console.log(map); + * // Output: Map { 'a.b.c' => 1, 'd.0' => 2, 'd.1' => 3 } + * + * // Or flatten into a Map with custom delimiter + * const customMap = flattenObject(nestedObject, { delimiter: '_', target: new Map() }); + * console.log(customMap); + * // Output: Map { 'a_b_c' => 1, 'd_0' => 2, 'd_1' => 3 } */ -export function flattenObject(object: object, { delimiter = '.' }: FlattenObjectOptions = {}): Record { +export function flattenObject( + object: object, + options?: FlattenObjectOptions | FlattenObjectMapOptions | Map +): Record | Map { + // Legacy support: Map as second parameter + if (options instanceof Map) { + flattenObjectIntoMap(object, options, '', '.'); + return options; + } + + // New syntax: { delimiter, target } + if (options && 'target' in options) { + const { delimiter = '.', target } = options; + flattenObjectIntoMap(object, target, '', delimiter); + return target; + } + + // Original syntax: { delimiter } + const { delimiter = '.' } = options || {}; return flattenObjectImpl(object, '', delimiter); } @@ -63,3 +103,26 @@ function flattenObjectImpl(object: object, prefix = '', delimiter = '.'): Record return result; } + +function flattenObjectIntoMap(object: object, target: Map, prefix = '', delimiter = '.'): void { + const keys = Object.keys(object); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = (object as any)[key]; + + const prefixedKey = prefix ? `${prefix}${delimiter}${key}` : key; + + if (isPlainObject(value) && Object.keys(value).length > 0) { + flattenObjectIntoMap(value, target, prefixedKey, delimiter); + continue; + } + + if (Array.isArray(value)) { + flattenObjectIntoMap(value, target, prefixedKey, delimiter); + continue; + } + + target.set(prefixedKey, value); + } +}