diff --git a/benchmarks/bundle-size/camelCase.spec.ts b/benchmarks/bundle-size/camelCase.spec.ts index 74f4c1b8d..43b14ae00 100644 --- a/benchmarks/bundle-size/camelCase.spec.ts +++ b/benchmarks/bundle-size/camelCase.spec.ts @@ -14,6 +14,6 @@ describe('camelCase bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'camelCase'); - expect(bundleSize).toMatchInlineSnapshot(`650`); + expect(bundleSize).toMatchInlineSnapshot(`1287`); }); }); diff --git a/benchmarks/bundle-size/chain.spec.ts b/benchmarks/bundle-size/chain.spec.ts new file mode 100644 index 000000000..6bc4f0e02 --- /dev/null +++ b/benchmarks/bundle-size/chain.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { getBundleSize } from './utils/getBundleSize'; + +describe('chain bundle size', () => { + it('es-toolkit', async () => { + const bundleSize = await getBundleSize('es-toolkit/iterator', 'chain'); + expect(bundleSize).toMatchInlineSnapshot(`82`); + }); + + it('itertools', async () => { + const bundleSize = await getBundleSize('itertools', 'chain'); + expect(bundleSize).toMatchInlineSnapshot(`252`); + }); +}); diff --git a/benchmarks/bundle-size/clone.spec.ts b/benchmarks/bundle-size/clone.spec.ts index 93291e10f..81b3e3ad9 100644 --- a/benchmarks/bundle-size/clone.spec.ts +++ b/benchmarks/bundle-size/clone.spec.ts @@ -9,7 +9,7 @@ describe('clone bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'clone'); - expect(bundleSize).toMatchInlineSnapshot(`866`); + expect(bundleSize).toMatchInlineSnapshot(`1001`); }); it('es-toolkit/compat', async () => { diff --git a/benchmarks/bundle-size/cloneDeep.spec.ts b/benchmarks/bundle-size/cloneDeep.spec.ts index 8ff04ed1e..a0a5f0117 100644 --- a/benchmarks/bundle-size/cloneDeep.spec.ts +++ b/benchmarks/bundle-size/cloneDeep.spec.ts @@ -9,11 +9,11 @@ describe('cloneDeep bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'cloneDeep'); - expect(bundleSize).toMatchInlineSnapshot(`3171`); + expect(bundleSize).toMatchInlineSnapshot(`3200`); }); it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'cloneDeep'); - expect(bundleSize).toMatchInlineSnapshot(`3522`); + expect(bundleSize).toMatchInlineSnapshot(`3640`); }); }); diff --git a/benchmarks/bundle-size/enumerate.spec.ts b/benchmarks/bundle-size/enumerate.spec.ts new file mode 100644 index 000000000..2b9b9e84d --- /dev/null +++ b/benchmarks/bundle-size/enumerate.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { getBundleSize } from './utils/getBundleSize'; + +describe('enumerate bundle size', () => { + it('es-toolkit', async () => { + const bundleSize = await getBundleSize('es-toolkit/iterator', 'enumerate'); + expect(bundleSize).toMatchInlineSnapshot(`212`); + }); + + it('@fxts/core', async () => { + const bundleSize = await getBundleSize('@fxts/core', 'zipWithIndex'); + expect(bundleSize).toMatchInlineSnapshot(`2710`); + }); +}); diff --git a/benchmarks/bundle-size/find.spec.ts b/benchmarks/bundle-size/find.spec.ts index 50b099aa2..48017f03b 100644 --- a/benchmarks/bundle-size/find.spec.ts +++ b/benchmarks/bundle-size/find.spec.ts @@ -9,6 +9,6 @@ describe('find bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'find'); - expect(bundleSize).toMatchInlineSnapshot(`7763`); + expect(bundleSize).toMatchInlineSnapshot(`7887`); }); }); diff --git a/benchmarks/bundle-size/findKey.spec.ts b/benchmarks/bundle-size/findKey.spec.ts index 74d84bd54..83cef8665 100644 --- a/benchmarks/bundle-size/findKey.spec.ts +++ b/benchmarks/bundle-size/findKey.spec.ts @@ -14,6 +14,6 @@ describe('findKey bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'findKey'); - expect(bundleSize).toMatchInlineSnapshot(`7688`); + expect(bundleSize).toMatchInlineSnapshot(`7812`); }); }); diff --git a/benchmarks/bundle-size/lastIndexOf.spec.ts b/benchmarks/bundle-size/lastIndexOf.spec.ts index 93453ec40..66bdd9bc9 100644 --- a/benchmarks/bundle-size/lastIndexOf.spec.ts +++ b/benchmarks/bundle-size/lastIndexOf.spec.ts @@ -4,7 +4,7 @@ import { getBundleSize } from './utils/getBundleSize'; describe('lastIndexOf bundle size', () => { it('lodash-es', async () => { const bundleSize = await getBundleSize('lodash-es', 'lastIndexOf'); - expect(bundleSize).toMatchInlineSnapshot(`1586`); + expect(bundleSize).toMatchInlineSnapshot(`2481`); }); it('es-toolkit/compat', async () => { diff --git a/benchmarks/bundle-size/mapKeys.spec.ts b/benchmarks/bundle-size/mapKeys.spec.ts index 84a4ea384..33a3ee032 100644 --- a/benchmarks/bundle-size/mapKeys.spec.ts +++ b/benchmarks/bundle-size/mapKeys.spec.ts @@ -14,6 +14,6 @@ describe('mapKeys bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'mapKeys'); - expect(bundleSize).toMatchInlineSnapshot(`7701`); + expect(bundleSize).toMatchInlineSnapshot(`7825`); }); }); diff --git a/benchmarks/bundle-size/mapValues.spec.ts b/benchmarks/bundle-size/mapValues.spec.ts index 5c16185f9..eb6e3a3d6 100644 --- a/benchmarks/bundle-size/mapValues.spec.ts +++ b/benchmarks/bundle-size/mapValues.spec.ts @@ -14,6 +14,6 @@ describe('mapValues bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'mapValues'); - expect(bundleSize).toMatchInlineSnapshot(`7701`); + expect(bundleSize).toMatchInlineSnapshot(`7825`); }); }); diff --git a/benchmarks/bundle-size/merge.spec.ts b/benchmarks/bundle-size/merge.spec.ts index ff0e094f9..3e254312e 100644 --- a/benchmarks/bundle-size/merge.spec.ts +++ b/benchmarks/bundle-size/merge.spec.ts @@ -9,11 +9,11 @@ describe('merge bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'merge'); - expect(bundleSize).toMatchInlineSnapshot(`521`); + expect(bundleSize).toMatchInlineSnapshot(`542`); }); it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'merge'); - expect(bundleSize).toMatchInlineSnapshot(`5721`); + expect(bundleSize).toMatchInlineSnapshot(`6206`); }); }); diff --git a/benchmarks/bundle-size/mergeWith.spec.ts b/benchmarks/bundle-size/mergeWith.spec.ts index 680a2e50b..46ba68e5e 100644 --- a/benchmarks/bundle-size/mergeWith.spec.ts +++ b/benchmarks/bundle-size/mergeWith.spec.ts @@ -9,11 +9,11 @@ describe('mergeWith bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'mergeWith'); - expect(bundleSize).toMatchInlineSnapshot(`368`); + expect(bundleSize).toMatchInlineSnapshot(`564`); }); it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'mergeWith'); - expect(bundleSize).toMatchInlineSnapshot(`5665`); + expect(bundleSize).toMatchInlineSnapshot(`6150`); }); }); diff --git a/benchmarks/bundle-size/omit.spec.ts b/benchmarks/bundle-size/omit.spec.ts index cfdcbf0f3..a4bfe4767 100644 --- a/benchmarks/bundle-size/omit.spec.ts +++ b/benchmarks/bundle-size/omit.spec.ts @@ -14,6 +14,6 @@ describe('omit bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'omit'); - expect(bundleSize).toMatchInlineSnapshot(`8042`); + expect(bundleSize).toMatchInlineSnapshot(`8227`); }); }); diff --git a/benchmarks/bundle-size/throttle.spec.ts b/benchmarks/bundle-size/throttle.spec.ts index a3eda6f8b..7cdf571a6 100644 --- a/benchmarks/bundle-size/throttle.spec.ts +++ b/benchmarks/bundle-size/throttle.spec.ts @@ -9,7 +9,7 @@ describe('throttle bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'throttle'); - expect(bundleSize).toMatchInlineSnapshot(`764`); + expect(bundleSize).toMatchInlineSnapshot(`855`); }); it('es-toolkit/compat', async () => { diff --git a/benchmarks/bundle-size/utils/getBundleSize.ts b/benchmarks/bundle-size/utils/getBundleSize.ts index df4aa53de..2760f8968 100644 --- a/benchmarks/bundle-size/utils/getBundleSize.ts +++ b/benchmarks/bundle-size/utils/getBundleSize.ts @@ -1,7 +1,9 @@ import esbuild from 'esbuild'; import path from 'path'; -export async function getBundleSize(pkg: 'lodash-es' | 'es-toolkit' | 'es-toolkit/compat', funcName: string) { +type Package = 'lodash-es' | 'es-toolkit' | 'es-toolkit/compat' | 'es-toolkit/iterator' | 'itertools' | '@fxts/core'; + +export async function getBundleSize(pkg: Package, funcName: string) { const script = `import { ${funcName} } from "${pkg}"; console.log(${funcName})`; const bundled = await esbuild.build({ diff --git a/benchmarks/bundle-size/zipIterable.spec.ts b/benchmarks/bundle-size/zipIterable.spec.ts new file mode 100644 index 000000000..f1fb7870e --- /dev/null +++ b/benchmarks/bundle-size/zipIterable.spec.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { getBundleSize } from './utils/getBundleSize'; + +describe('zipIterable bundle size', () => { + it('es-toolkit', async () => { + const bundleSize = await getBundleSize('es-toolkit/iterator', 'zipIterable'); + expect(bundleSize).toMatchInlineSnapshot(`197`); + }); + + it('@fxts/core', async () => { + const bundleSize = await getBundleSize('@fxts/core', 'zip'); + expect(bundleSize).toMatchInlineSnapshot(`6336`); + }); + + it('itertools', async () => { + const bundleSize = await getBundleSize('itertools', 'izip'); + expect(bundleSize).toMatchInlineSnapshot(`337`); + }); +}); diff --git a/benchmarks/bundle-size/zipObjectDeep.spec.ts b/benchmarks/bundle-size/zipObjectDeep.spec.ts index 8269847ac..286ea74de 100644 --- a/benchmarks/bundle-size/zipObjectDeep.spec.ts +++ b/benchmarks/bundle-size/zipObjectDeep.spec.ts @@ -9,6 +9,6 @@ describe('zipObjectDeep bundle size', () => { it('es-toolkit/compat', async () => { const bundleSize = await getBundleSize('es-toolkit/compat', 'zipObjectDeep'); - expect(bundleSize).toMatchInlineSnapshot(`2297`); + expect(bundleSize).toMatchInlineSnapshot(`2935`); }); }); diff --git a/benchmarks/bundle-size/zipWith.spec.ts b/benchmarks/bundle-size/zipWith.spec.ts index e707a4569..ce438c4cd 100644 --- a/benchmarks/bundle-size/zipWith.spec.ts +++ b/benchmarks/bundle-size/zipWith.spec.ts @@ -9,7 +9,7 @@ describe('zipWith bundle size', () => { it('es-toolkit', async () => { const bundleSize = await getBundleSize('es-toolkit', 'zipWith'); - expect(bundleSize).toMatchInlineSnapshot(`198`); + expect(bundleSize).toMatchInlineSnapshot(`200`); }); it('es-toolkit/compat', async () => { diff --git a/benchmarks/package.json b/benchmarks/package.json index 4b5aa5cba..f693bdd78 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -12,8 +12,10 @@ "vitest": "^4.0.17" }, "devDependencies": { + "@fxts/core": "^1.26.0", "@types/lodash": "^4.17.20", "@types/lodash-es": "^4", + "itertools": "^2.6.0", "rfdc": "^1.4.1" } } diff --git a/benchmarks/performance/chain.bench.ts b/benchmarks/performance/chain.bench.ts new file mode 100644 index 000000000..a2a9fe5a5 --- /dev/null +++ b/benchmarks/performance/chain.bench.ts @@ -0,0 +1,58 @@ +import { bench, describe } from 'vitest'; +import { chain as chainEsToolkit_ } from 'es-toolkit/iterator'; +import { chain as chainItertools_ } from 'itertools'; + +const chainEsToolkit = chainEsToolkit_; +const chainItertools = chainItertools_; + +describe('chain', () => { + bench('es-toolkit/chain', () => { + const iter = chainEsToolkit([1, 2, 3], [4, 5, 6], [7, 8, 9]); + for (const _ of iter) { + /* consume */ + } + }); + + bench('itertools/ichain', () => { + const iter = chainItertools([1, 2, 3], [4, 5, 6], [7, 8, 9]); + for (const _ of iter) { + /* consume */ + } + }); + + bench('native spread', () => { + const iter = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ].flat(); + for (const _ of iter) { + /* consume */ + } + }); +}); + +describe('chain/largeArray', () => { + const largeArray = Array.from({ length: 10000 }, (_, i) => i); + + bench('es-toolkit/chain', () => { + const iter = chainEsToolkit(largeArray, largeArray, largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('itertools/ichain', () => { + const iter = chainItertools(largeArray, largeArray, largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('native spread', () => { + const iter = [...largeArray, ...largeArray, ...largeArray]; + for (const _ of iter) { + /* consume */ + } + }); +}); diff --git a/benchmarks/performance/enumerate.bench.ts b/benchmarks/performance/enumerate.bench.ts new file mode 100644 index 000000000..10103a713 --- /dev/null +++ b/benchmarks/performance/enumerate.bench.ts @@ -0,0 +1,66 @@ +import { bench, describe } from 'vitest'; +import { enumerate as enumerateEsToolkit_ } from 'es-toolkit/iterator'; +import { zipWithIndex as zipWithIndexFxts_ } from '@fxts/core'; + +const enumerateEsToolkit = enumerateEsToolkit_; +const zipWithIndexFxts = zipWithIndexFxts_; + +describe('enumerate', () => { + bench('es-toolkit/enumerate', () => { + const iter = enumerateEsToolkit(['a', 'b', 'c']); + for (const _ of iter) { + /* consume */ + } + }); + + bench('@fxts/core/zipWithIndex', () => { + const iter = zipWithIndexFxts(['a', 'b', 'c']); + for (const _ of iter) { + /* consume */ + } + }); + + bench('Array.prototype.entries (native)', () => { + const arr = ['a', 'b', 'c']; + for (const _ of arr.entries()) { + /* consume */ + } + }); + + bench('forEach with index (native)', () => { + const arr = ['a', 'b', 'c']; + arr.forEach((_, __) => { + /* consume */ + }); + }); +}); + +describe('enumerate/largeArray', () => { + const largeArray = Array.from({ length: 10000 }, (_, i) => String(i)); + + bench('es-toolkit/enumerate', () => { + const iter = enumerateEsToolkit(largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('@fxts/core/zipWithIndex', () => { + const iter = zipWithIndexFxts(largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('Array.prototype.entries (native)', () => { + for (const _ of largeArray.entries()) { + /* consume */ + } + }); + + bench('forEach with index (native)', () => { + largeArray.forEach((_, __) => { + /* consume */ + }); + }); +}); diff --git a/benchmarks/performance/zipIterable.bench.ts b/benchmarks/performance/zipIterable.bench.ts new file mode 100644 index 000000000..98b93964c --- /dev/null +++ b/benchmarks/performance/zipIterable.bench.ts @@ -0,0 +1,56 @@ +import { bench, describe } from 'vitest'; +import { zipIterable as zipIterableEsToolkit_ } from 'es-toolkit/iterator'; +import { izip as zipItertools_ } from 'itertools'; +import { zip as zipFxts_ } from '@fxts/core'; + +const zipIterableEsToolkit = zipIterableEsToolkit_; +const zipFxts = zipFxts_; +const zipItertools = zipItertools_; + +describe('zipIterable', () => { + bench('es-toolkit/zipIterable', () => { + const iter = zipIterableEsToolkit([1, 2, 3], ['a', 'b', 'c']); + for (const _ of iter) { + /* consume */ + } + }); + + bench('@fxts/core/zip', () => { + const iter = zipFxts([1, 2, 3], ['a', 'b', 'c']); + for (const _ of iter) { + /* consume */ + } + }); + + bench('itertools/izip', () => { + const iter = zipItertools([1, 2, 3], ['a', 'b', 'c']); + for (const _ of iter) { + /* consume */ + } + }); +}); + +describe('zipIterable/largeArray', () => { + const largeArray = Array.from({ length: 10000 }, (_, i) => i); + + bench('es-toolkit/zipIterable', () => { + const iter = zipIterableEsToolkit(largeArray, largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('@fxts/core/zip', () => { + const iter = zipFxts(largeArray, largeArray); + for (const _ of iter) { + /* consume */ + } + }); + + bench('itertools/izip', () => { + const iter = zipItertools(largeArray, largeArray); + for (const _ of iter) { + /* consume */ + } + }); +}); diff --git a/docs/ja/reference/iterator/chain.md b/docs/ja/reference/iterator/chain.md new file mode 100644 index 000000000..87ad0d749 --- /dev/null +++ b/docs/ja/reference/iterator/chain.md @@ -0,0 +1,42 @@ +# chain + +複数のイテラブルを順番に結合して、1つのイテラブルにします。 + +```typescript +const iterable = chain(iterable1, iterable2, iterable3); +``` + +## 使い方 + +### `chain(...iterables)` + +複数のイテラブルを順番に反復処理したいときに `chain` を使ってください。`[...arr1, ...arr2]` のように新しい配列をメモリに作成せず、実際に反復処理するまで各イテラブルを消費しません。 + +```typescript +import { chain } from 'es-toolkit/iterator'; + +// 複数の配列を順番に結合します。 +const result = chain([1, 2, 3], [4, 5, 6]); +console.log([...result]); // [1, 2, 3, 4, 5, 6] + +// 配列だけでなく、あらゆるイテラブルで動作します。 +const result = chain(new Set([1, 2]), new Map([[3, 'a']]).keys()); +console.log([...result]); // [1, 2, 3] +``` + +空のイテラブルはスキップされます。 + +```typescript +import { chain } from 'es-toolkit/iterator'; + +const result = chain([], [1, 2], []); +console.log([...result]); // [1, 2] +``` + +#### パラメータ + +- `iterables` (`...Iterable[]`): 結合するイテラブルです。 + +#### 返り値 + +(`IterableIterator`): 各イテラブルの要素を順番に yield する遅延評価のイテラブルイテレータです。 \ No newline at end of file diff --git a/docs/ja/reference/iterator/enumerate.md b/docs/ja/reference/iterator/enumerate.md new file mode 100644 index 000000000..4f1269991 --- /dev/null +++ b/docs/ja/reference/iterator/enumerate.md @@ -0,0 +1,47 @@ +# enumerate + +イテラブルの各要素にインデックスを付けてペアにします。 + +```typescript +const iterable = enumerate(iterable, start); +``` + +## 使い方 + +### `enumerate(iterable, start?)` + +反復処理中にインデックスと値を同時に使いたいときに `enumerate` を使ってください。`Array.prototype.entries()` と異なり、配列だけでなくあらゆるイテラブルで動作します。 + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +// 各要素にインデックスを付けます。 +const result = enumerate(['a', 'b', 'c']); +console.log([...result]); // [[0, 'a'], [1, 'b'], [2, 'c']] + +// 配列だけでなく、あらゆるイテラブルで動作します。 +for (const [index, value] of enumerate(new Set(['x', 'y', 'z']))) { + console.log(`${index}: ${value}`); +} +// 0: x +// 1: y +// 2: z +``` + +第2引数で開始インデックスを指定できます。 + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +const result = enumerate(['a', 'b', 'c'], 1); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] +``` + +#### パラメータ + +- `iterable` (`Iterable`): 列挙するイテラブルです。 +- `start` (`number`, オプション): 開始インデックスです。デフォルトは `0` です。 + +#### 返り値 + +(`IterableIterator<[number, T]>`): `[インデックス, 要素]` のタプルを yield する遅延評価のイテラブルイテレータです。 \ No newline at end of file diff --git a/docs/ja/reference/iterator/zipIterable.md b/docs/ja/reference/iterator/zipIterable.md new file mode 100644 index 000000000..e7fa3f256 --- /dev/null +++ b/docs/ja/reference/iterator/zipIterable.md @@ -0,0 +1,53 @@ +# zipIterable + +複数のイテラブルを同じインデックスの要素ごとにまとめて、タプルのイテラブルにします。 + +```typescript +const iterable = zipIterable(iterable1, iterable2); +``` + +## 使い方 + +### `zipIterable(...iterables)` + +複数のイテラブルを同時に反復処理しながら、同じインデックスの要素をまとめたいときに `zipIterable` を使ってください。`es-toolkit/array` の `zip` と異なり、配列だけでなくあらゆるイテラブルを受け取ることができ、実際に反復処理するまで要素を消費しません。 + +最も短いイテラブルが尽きたときに停止します。 + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +// 2つの配列の要素を同じインデックスでまとめます。 +const result = zipIterable([1, 2, 3], ['a', 'b', 'c']); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + +// 配列だけでなく、あらゆるイテラブルで動作します。 +const result = zipIterable(new Set(['alice', 'bob']), [90, 85]); +console.log([...result]); // [['alice', 90], ['bob', 85]] +``` + +長さが異なる場合は、最も短いイテラブルに合わせて停止します。 + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +const result = zipIterable([1, 2, 3], ['a', 'b']); +console.log([...result]); // [[1, 'a'], [2, 'b']] +``` + +### `es-toolkit/array` の `zip` との違い + +| | `array/zip` | `iterator/zipIterable` | +| --- | --- | --- | +| 入力 | `Array` のみ | あらゆる `Iterable` | +| 出力 | `Array` | `IterableIterator`(遅延評価) | +| 長さが異なる場合 | 長い方に合わせ `undefined` で埋める | 短い方で停止 | +| メモリ | 配列全体を即時生成 | 必要な分だけ消費 | + +#### パラメータ + +- `iterables` (`...Iterable[]`): まとめるイテラブルです。 + +#### 返り値 + +(`IterableIterator`): 同じインデックスの要素をタプルとして yield する遅延評価のイテラブルイテレータです。 \ No newline at end of file diff --git a/docs/ko/reference/iterator/chain.md b/docs/ko/reference/iterator/chain.md new file mode 100644 index 000000000..9ee04173f --- /dev/null +++ b/docs/ko/reference/iterator/chain.md @@ -0,0 +1,42 @@ +# chain + +여러 이터러블을 하나의 이터러블로 순서대로 이어붙여요. + +```typescript +const iterable = chain(iterable1, iterable2, iterable3); +``` + +## 사용법 + +### `chain(...iterables)` + +여러 이터러블을 순서대로 순회하고 싶을 때 `chain`을 사용하세요. `[...arr1, ...arr2]`처럼 새로운 배열을 메모리에 만들지 않고, 실제로 순회할 때까지 각 이터러블을 소비하지 않아요. + +```typescript +import { chain } from 'es-toolkit/iterator'; + +// 여러 배열을 순서대로 이어붙여요. +const result = chain([1, 2, 3], [4, 5, 6]); +console.log([...result]); // [1, 2, 3, 4, 5, 6] + +// 배열뿐만 아니라 모든 이터러블에서 동작해요. +const result = chain(new Set([1, 2]), new Map([[3, 'a']]).keys()); +console.log([...result]); // [1, 2, 3] +``` + +빈 이터러블은 건너뛰어요. + +```typescript +import { chain } from 'es-toolkit/iterator'; + +const result = chain([], [1, 2], []); +console.log([...result]); // [1, 2] +``` + +#### 파라미터 + +- `iterables` (`...Iterable[]`): 이어붙일 이터러블들이에요. + +#### 반환 값 + +(`IterableIterator`): 각 이터러블의 요소를 순서대로 yield하는 지연 평가 이터러블 이터레이터예요. \ No newline at end of file diff --git a/docs/ko/reference/iterator/enumerate.md b/docs/ko/reference/iterator/enumerate.md new file mode 100644 index 000000000..274525b43 --- /dev/null +++ b/docs/ko/reference/iterator/enumerate.md @@ -0,0 +1,47 @@ +# enumerate + +이터러블의 각 요소에 인덱스를 함께 묶어줘요. + +```typescript +const iterable = enumerate(iterable, start); +``` + +## 사용법 + +### `enumerate(iterable, start?)` + +순회하면서 인덱스와 값을 함께 사용하고 싶을 때 `enumerate`를 사용하세요. `Array.prototype.entries()`와 달리, 배열뿐만 아니라 모든 이터러블에서 동작해요. + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +// 각 요소에 인덱스를 묶어줘요. +const result = enumerate(['a', 'b', 'c']); +console.log([...result]); // [[0, 'a'], [1, 'b'], [2, 'c']] + +// 배열뿐만 아니라 모든 이터러블에서 동작해요. +for (const [index, value] of enumerate(new Set(['x', 'y', 'z']))) { + console.log(`${index}: ${value}`); +} +// 0: x +// 1: y +// 2: z +``` + +두 번째 인수로 시작 인덱스를 지정할 수 있어요. + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +const result = enumerate(['a', 'b', 'c'], 1); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] +``` + +#### 파라미터 + +- `iterable` (`Iterable`): 열거할 이터러블이에요. +- `start` (`number`, 선택): 시작 인덱스예요. 기본값은 `0`이에요. + +#### 반환 값 + +(`IterableIterator<[number, T]>`): `[인덱스, 요소]` 튜플을 yield하는 지연 평가 이터러블 이터레이터예요. \ No newline at end of file diff --git a/docs/ko/reference/iterator/zipIterable.md b/docs/ko/reference/iterator/zipIterable.md new file mode 100644 index 000000000..155347712 --- /dev/null +++ b/docs/ko/reference/iterator/zipIterable.md @@ -0,0 +1,53 @@ +# zipIterable + +여러 이터러블을 같은 인덱스끼리 묶어 튜플의 이터러블로 만들어줘요. + +```typescript +const iterable = zipIterable(iterable1, iterable2); +``` + +## 사용법 + +### `zipIterable(...iterables)` + +여러 이터러블을 동시에 순회하면서 같은 인덱스의 요소를 묶고 싶을 때 `zipIterable`을 사용하세요. `es-toolkit/array`의 `zip`과 달리, 배열뿐만 아니라 모든 이터러블을 받을 수 있고, 실제로 순회할 때까지 요소를 소비하지 않아요. + +가장 짧은 이터러블이 소진되면 멈춰요. + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +// 두 배열의 요소를 같은 인덱스끼리 묶어요. +const result = zipIterable([1, 2, 3], ['a', 'b', 'c']); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + +// 배열뿐만 아니라 모든 이터러블에서 동작해요. +const result = zipIterable(new Set(['alice', 'bob']), [90, 85]); +console.log([...result]); // [['alice', 90], ['bob', 85]] +``` + +길이가 다른 경우 가장 짧은 이터러블 기준으로 멈춰요. + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +const result = zipIterable([1, 2, 3], ['a', 'b']); +console.log([...result]); // [[1, 'a'], [2, 'b']] +``` + +### `es-toolkit/array`의 `zip`과의 차이점 + +| | `array/zip` | `iterator/zipIterable` | +| --- | --- | --- | +| 입력 | `Array`만 가능 | 모든 `Iterable` 가능 | +| 출력 | `Array` | `IterableIterator` (지연 평가) | +| 길이가 다를 때 | 긴 쪽 기준, `undefined`로 채움 | 짧은 쪽에서 멈춤 | +| 메모리 | 전체 배열 즉시 생성 | 필요한 만큼만 소비 | + +#### 파라미터 + +- `iterables` (`...Iterable[]`): 묶을 이터러블들이에요. + +#### 반환 값 + +(`IterableIterator`): 같은 인덱스의 요소를 튜플로 yield하는 지연 평가 이터러블 이터레이터예요. \ No newline at end of file diff --git a/docs/reference/iterator/chain.md b/docs/reference/iterator/chain.md new file mode 100644 index 000000000..05298a048 --- /dev/null +++ b/docs/reference/iterator/chain.md @@ -0,0 +1,42 @@ +# chain + +Combines multiple iterables into a single iterable, yielding elements from each iterable in order. + +```typescript +const iterable = chain(iterable1, iterable2, iterable3); +``` + +## Usage + +### `chain(...iterables)` + +Use `chain` when you want to iterate over multiple iterables sequentially without creating a new array in memory. Unlike spreading arrays with `[...arr1, ...arr2]`, `chain` is lazy — it does not consume the iterables until iterated. + +```typescript +import { chain } from 'es-toolkit/iterator'; + +// Chain multiple arrays together. +const result = chain([1, 2, 3], [4, 5, 6]); +console.log([...result]); // [1, 2, 3, 4, 5, 6] + +// Works with any iterable, not just arrays. +const result = chain(new Set([1, 2]), new Map([[3, 'a']]).keys()); +console.log([...result]); // [1, 2, 3] +``` + +If an iterable is empty, it is skipped. + +```typescript +import { chain } from 'es-toolkit/iterator'; + +const result = chain([], [1, 2], []); +console.log([...result]); // [1, 2] +``` + +#### Parameters + +- `iterables` (`...Iterable[]`): The iterables to chain together. + +#### Returns + +(`IterableIterator`): A lazy iterable iterator that yields elements from each iterable in order. \ No newline at end of file diff --git a/docs/reference/iterator/enumerate.md b/docs/reference/iterator/enumerate.md new file mode 100644 index 000000000..3dcaeae65 --- /dev/null +++ b/docs/reference/iterator/enumerate.md @@ -0,0 +1,47 @@ +# enumerate + +Pairs each element of an iterable with its index. + +```typescript +const iterable = enumerate(iterable, start); +``` + +## Usage + +### `enumerate(iterable, start?)` + +Use `enumerate` when you need both the index and value while iterating. Unlike `Array.prototype.entries()`, this works with any iterable — not just arrays. + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +// Pair each element with its index. +const result = enumerate(['a', 'b', 'c']); +console.log([...result]); // [[0, 'a'], [1, 'b'], [2, 'c']] + +// Works with any iterable, not just arrays. +for (const [index, value] of enumerate(new Set(['x', 'y', 'z']))) { + console.log(`${index}: ${value}`); +} +// 0: x +// 1: y +// 2: z +``` + +A custom starting index can be provided as the second argument. + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +const result = enumerate(['a', 'b', 'c'], 1); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] +``` + +#### Parameters + +- `iterable` (`Iterable`): The iterable to enumerate. +- `start` (`number`, optional): The starting index. Defaults to `0`. + +#### Returns + +(`IterableIterator<[number, T]>`): A lazy iterable iterator that yields `[index, element]` tuples. \ No newline at end of file diff --git a/docs/reference/iterator/zipIterable.md b/docs/reference/iterator/zipIterable.md new file mode 100644 index 000000000..d8d674619 --- /dev/null +++ b/docs/reference/iterator/zipIterable.md @@ -0,0 +1,53 @@ +# zipIterable + +Combines multiple iterables into a single iterable of tuples, pairing elements at the same index from each iterable. + +```typescript +const iterable = zipIterable(iterable1, iterable2); +``` + +## Usage + +### `zipIterable(...iterables)` + +Use `zipIterable` when you want to iterate over multiple iterables simultaneously, pairing elements at the same index. Unlike `zip` from `es-toolkit/array`, this function accepts any iterable — not just arrays — and is lazy, meaning it does not consume the iterables until iterated. + +It stops when the shortest iterable is exhausted. + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +// Pair elements from two arrays. +const result = zipIterable([1, 2, 3], ['a', 'b', 'c']); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + +// Works with any iterable, not just arrays. +const result = zipIterable(new Set(['alice', 'bob']), [90, 85]); +console.log([...result]); // [['alice', 90], ['bob', 85]] +``` + +If the iterables are of different lengths, the result stops at the shortest one. + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +const result = zipIterable([1, 2, 3], ['a', 'b']); +console.log([...result]); // [[1, 'a'], [2, 'b']] +``` + +### Differences from `zip` in `es-toolkit/array` + +| | `array/zip` | `iterator/zipIterable` | +| --- | --- | --- | +| Input | `Array` only | Any `Iterable` | +| Output | `Array` | `IterableIterator` (lazy) | +| Length mismatch | Pads with `undefined` | Stops at shortest | +| Memory | Allocates full array | Consumes on demand | + +#### Parameters + +- `iterables` (`...Iterable[]`): The iterables to zip together. + +#### Returns + +(`IterableIterator`): A lazy iterable iterator that yields tuples of elements at the same index. \ No newline at end of file diff --git a/docs/zh_hans/reference/iterator/chain.md b/docs/zh_hans/reference/iterator/chain.md new file mode 100644 index 000000000..0da39b56e --- /dev/null +++ b/docs/zh_hans/reference/iterator/chain.md @@ -0,0 +1,42 @@ +# chain + +将多个可迭代对象按顺序合并为一个可迭代对象。 + +```typescript +const iterable = chain(iterable1, iterable2, iterable3); +``` + +## 使用方法 + +### `chain(...iterables)` + +当你想按顺序遍历多个可迭代对象时,请使用 `chain`。与 `[...arr1, ...arr2]` 不同,它不会在内存中创建新数组,在实际遍历之前不会消费各个可迭代对象。 + +```typescript +import { chain } from 'es-toolkit/iterator'; + +// 按顺序合并多个数组。 +const result = chain([1, 2, 3], [4, 5, 6]); +console.log([...result]); // [1, 2, 3, 4, 5, 6] + +// 不仅支持数组,支持所有可迭代对象。 +const result = chain(new Set([1, 2]), new Map([[3, 'a']]).keys()); +console.log([...result]); // [1, 2, 3] +``` + +空的可迭代对象会被跳过。 + +```typescript +import { chain } from 'es-toolkit/iterator'; + +const result = chain([], [1, 2], []); +console.log([...result]); // [1, 2] +``` + +#### 参数 + +- `iterables` (`...Iterable[]`): 要合并的可迭代对象。 + +#### 返回值 + +(`IterableIterator`): 按顺序 yield 各可迭代对象元素的惰性求值迭代器。 \ No newline at end of file diff --git a/docs/zh_hans/reference/iterator/enumerate.md b/docs/zh_hans/reference/iterator/enumerate.md new file mode 100644 index 000000000..fd045a735 --- /dev/null +++ b/docs/zh_hans/reference/iterator/enumerate.md @@ -0,0 +1,47 @@ +# enumerate + +将可迭代对象的每个元素与其索引配对。 + +```typescript +const iterable = enumerate(iterable, start); +``` + +## 使用方法 + +### `enumerate(iterable, start?)` + +当你在遍历时需要同时使用索引和值时,请使用 `enumerate`。与 `Array.prototype.entries()` 不同,它不仅支持数组,还支持所有可迭代对象。 + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +// 将每个元素与其索引配对。 +const result = enumerate(['a', 'b', 'c']); +console.log([...result]); // [[0, 'a'], [1, 'b'], [2, 'c']] + +// 不仅支持数组,支持所有可迭代对象。 +for (const [index, value] of enumerate(new Set(['x', 'y', 'z']))) { + console.log(`${index}: ${value}`); +} +// 0: x +// 1: y +// 2: z +``` + +可以通过第二个参数指定起始索引。 + +```typescript +import { enumerate } from 'es-toolkit/iterator'; + +const result = enumerate(['a', 'b', 'c'], 1); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] +``` + +#### 参数 + +- `iterable` (`Iterable`): 要枚举的可迭代对象。 +- `start` (`number`, 可选): 起始索引。默认为 `0`。 + +#### 返回值 + +(`IterableIterator<[number, T]>`): yield `[索引, 元素]` 元组的惰性求值迭代器。 \ No newline at end of file diff --git a/docs/zh_hans/reference/iterator/zipIterable.md b/docs/zh_hans/reference/iterator/zipIterable.md new file mode 100644 index 000000000..775341f08 --- /dev/null +++ b/docs/zh_hans/reference/iterator/zipIterable.md @@ -0,0 +1,53 @@ +# zipIterable + +将多个可迭代对象按相同索引的元素配对,生成元组的可迭代对象。 + +```typescript +const iterable = zipIterable(iterable1, iterable2); +``` + +## 使用方法 + +### `zipIterable(...iterables)` + +当你想同时遍历多个可迭代对象并将相同索引的元素配对时,请使用 `zipIterable`。与 `es-toolkit/array` 的 `zip` 不同,它不仅支持数组,还支持所有可迭代对象,并且在实际遍历之前不会消费元素。 + +当最短的可迭代对象耗尽时停止。 + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +// 将两个数组的元素按相同索引配对。 +const result = zipIterable([1, 2, 3], ['a', 'b', 'c']); +console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + +// 不仅支持数组,支持所有可迭代对象。 +const result = zipIterable(new Set(['alice', 'bob']), [90, 85]); +console.log([...result]); // [['alice', 90], ['bob', 85]] +``` + +长度不同时,以最短的可迭代对象为准停止。 + +```typescript +import { zipIterable } from 'es-toolkit/iterator'; + +const result = zipIterable([1, 2, 3], ['a', 'b']); +console.log([...result]); // [[1, 'a'], [2, 'b']] +``` + +### 与 `es-toolkit/array` 的 `zip` 的区别 + +| | `array/zip` | `iterator/zipIterable` | +| --- | --- | --- | +| 输入 | 仅支持 `Array` | 支持所有 `Iterable` | +| 输出 | `Array` | `IterableIterator`(惰性求值) | +| 长度不同时 | 以较长的为准,用 `undefined` 填充 | 以较短的为准停止 | +| 内存 | 立即生成完整数组 | 按需消费 | + +#### 参数 + +- `iterables` (`...Iterable[]`): 要配对的可迭代对象。 + +#### 返回值 + +(`IterableIterator`): 将相同索引的元素作为元组 yield 的惰性求值迭代器。 \ No newline at end of file diff --git a/package.json b/package.json index 8ca2b7cfd..dc7215c34 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "./set": "./src/set/index.ts", "./string": "./src/string/index.ts", "./util": "./src/util/index.ts", + "./iterator": "./src/iterator/index.ts", "./package.json": "./package.json" }, "files": [ @@ -242,6 +243,16 @@ "default": "./dist/util/index.js" } }, + "./iterator": { + "import": { + "types": "./dist/iterator/index.d.mts", + "default": "./dist/iterator/index.mjs" + }, + "require": { + "types": "./dist/iterator/index.d.ts", + "default": "./dist/iterator/index.js" + } + }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts" diff --git a/src/index.ts b/src/index.ts index dd1556122..61932d474 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,7 @@ export * from './array/index.ts'; export * from './error/index.ts'; export * from './function/index.ts'; +export * from './iterator/index.ts'; export * from './math/index.ts'; export * from './object/index.ts'; export * from './predicate/index.ts'; diff --git a/src/iterator/chain.spec.ts b/src/iterator/chain.spec.ts new file mode 100644 index 000000000..7e19ef640 --- /dev/null +++ b/src/iterator/chain.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { chain } from './chain'; + +describe('chain', () => { + it('chains multiple arrays in order', () => { + expect([...chain([1, 2, 3], [4, 5, 6])]).toEqual([1, 2, 3, 4, 5, 6]); + }); + + it('works with a single iterable', () => { + expect([...chain([1, 2, 3])]).toEqual([1, 2, 3]); + }); + + it('works with no iterables', () => { + expect([...chain()]).toEqual([]); + }); + + it('works with empty iterables', () => { + expect([...chain([], [1, 2], [])]).toEqual([1, 2]); + }); + + it('works with non-array iterables', () => { + expect([...chain(new Set([1, 2]), new Map([[3, 'a']]).keys())]).toEqual([1, 2, 3]); + }); + + it('works with generator functions', () => { + function* gen() { + yield 4; + yield 5; + } + expect([...chain([1, 2, 3], gen())]).toEqual([1, 2, 3, 4, 5]); + }); + + it('is lazy. does not consume iterables until iterated', () => { + let consumed = false; + function* lazy() { + consumed = true; + yield 1; + } + const result = chain(lazy()); + expect(consumed).toBe(false); + expect([...result]).toStrictEqual([1]); + expect(consumed).toBe(true); + }); +}); diff --git a/src/iterator/chain.ts b/src/iterator/chain.ts new file mode 100644 index 000000000..86cf9f622 --- /dev/null +++ b/src/iterator/chain.ts @@ -0,0 +1,25 @@ +/** + * Combines multiple iterables into a single iterable, yielding elements + * from each iterable in order. + * + * This function is lazy — it does not iterate over the provided iterables + * until the returned iterable is iterated. + * + * @template T - The type of elements in the iterables. + * @param {Iterable[]} iterables - The iterables to chain together. + * @returns {IterableIterator} An iterable iterator that yields elements from each iterable in order. + * + * @example + * const result = chain([1, 2, 3], [4, 5, 6]); + * console.log([...result]); // [1, 2, 3, 4, 5, 6] + * + * @example + * Works with any iterable + * const result = chain(new Set([1, 2]), new Map([[3, 'a']]).keys(), [4]); + * console.log([...result]); // [1, 2, 3, 4] + */ +export function* chain(...iterables: Array>): IterableIterator { + for (let i = 0; i < iterables.length; i++) { + yield* iterables[i]; + } +} diff --git a/src/iterator/enumerate.spec.ts b/src/iterator/enumerate.spec.ts new file mode 100644 index 000000000..eccd1212a --- /dev/null +++ b/src/iterator/enumerate.spec.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { enumerate } from './enumerate'; + +describe('enumerate', () => { + it('pairs each element with its index starting at 0', () => { + expect([...enumerate(['a', 'b', 'c'])]).toEqual([ + [0, 'a'], + [1, 'b'], + [2, 'c'], + ]); + }); + + it('supports a custom start index', () => { + expect([...enumerate(['a', 'b', 'c'], 1)]).toEqual([ + [1, 'a'], + [2, 'b'], + [3, 'c'], + ]); + }); + + it('handles empty iterables', () => { + expect([...enumerate([])]).toEqual([]); + }); + + it('works with non-array iterables', () => { + expect([...enumerate(new Set(['x', 'y', 'z']))]).toEqual([ + [0, 'x'], + [1, 'y'], + [2, 'z'], + ]); + }); + + it('works with generator functions', () => { + function* words() { + yield 'hello'; + yield 'world'; + } + expect([...enumerate(words())]).toEqual([ + [0, 'hello'], + [1, 'world'], + ]); + }); + + it('is lazy. does not consume the iterable until iterated', () => { + let consumed = false; + function* lazy() { + consumed = true; + yield 'a'; + } + const result = enumerate(lazy(), 1); + expect(consumed).toBe(false); + expect([...result]).toEqual([[1, 'a']]); + expect(consumed).toBe(true); + }); +}); diff --git a/src/iterator/enumerate.ts b/src/iterator/enumerate.ts new file mode 100644 index 000000000..d803fd7a7 --- /dev/null +++ b/src/iterator/enumerate.ts @@ -0,0 +1,65 @@ +/** + * Pairs each element of an iterable with its index. + * + * Unlike `Array.prototype.entries()`, this works with any iterable. + * not just arrays. + * This function is lazy. it does not iterate over the provided iterable + * until the returned iterable is iterated. + * + * @template T - The type of elements in the iterable. + * @param {Iterable} iterable - The iterable to enumerate. + * @param {number} [start=0] - The starting index. Defaults to `0`. + * @returns {IterableIterator<[number, T]>} An iterable iterator that yields `[index, element]` tuples. + * + * @example + * const result = enumerate(['a', 'b', 'c']); + * console.log([...result]); // [[0, 'a'], [1, 'b'], [2, 'c']] + * + * @example + * Works with any iterable. not just arrays + * for (const [index, value] of enumerate(new Set(['x', 'y', 'z']))) { + * console.log(`${index}: ${value}`); + * } + * 0: x + * 1: y + * 2: z + * + * @example + * Custom start index + * const result = enumerate(['a', 'b', 'c'], 1); + * console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + * + * @example + * Works with generator functions + * function* words() { + * yield 'hello'; + * yield 'world'; + * } + * console.log([...enumerate(words())]); // [[0, 'hello'], [1, 'world']] + */ +export function enumerate(iterable: Iterable, start?: number): IterableIterator<[number, T]> { + const iterator = iterable[Symbol.iterator](); + let index = start ?? 0; + + const doneResult: IteratorResult<[number, T]> = { + value: undefined, + done: true, + }; + + return { + next(): IteratorResult<[number, T]> { + const result = iterator.next(); + if (result.done) { + return doneResult; + } + + return { + value: [index++, result.value], + done: false, + }; + }, + [Symbol.iterator]() { + return this; + }, + }; +} diff --git a/src/iterator/index.ts b/src/iterator/index.ts new file mode 100644 index 000000000..3b7537f34 --- /dev/null +++ b/src/iterator/index.ts @@ -0,0 +1,3 @@ +export { chain } from './chain'; +export { enumerate } from './enumerate'; +export { zipIterable } from './zipIterable.ts'; diff --git a/src/iterator/zipIterable.spec.ts b/src/iterator/zipIterable.spec.ts new file mode 100644 index 000000000..962cd1b87 --- /dev/null +++ b/src/iterator/zipIterable.spec.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest'; +import { zipIterable } from './zipIterable.ts'; + +describe('zipIterable', () => { + it('zips two arrays of equal length', () => { + expect([...zipIterable([1, 2, 3], ['a', 'b', 'c'])]).toEqual([ + [1, 'a'], + [2, 'b'], + [3, 'c'], + ]); + }); + + it('stops at the shortest iterable', () => { + expect([...zipIterable([1, 2, 3], ['a', 'b'])]).toEqual([ + [1, 'a'], + [2, 'b'], + ]); + }); + + it('handles empty iterables', () => { + expect([...zipIterable([], [1, 2, 3])]).toEqual([]); + }); + + it('works with more than two iterables', () => { + expect([...zipIterable([1, 2], ['a', 'b'], [true, false])]).toEqual([ + [1, 'a', true], + [2, 'b', false], + ]); + }); + + it('works with non-array iterables', () => { + const set = new Set(['x', 'y']); + expect([...zipIterable(set, [1, 2])]).toEqual([ + ['x', 1], + ['y', 2], + ]); + }); + + it('is lazy. does not consume iterables until iterated', () => { + let consumed = false; + function* lazy() { + consumed = true; + yield 1; + } + const result = zipIterable(lazy(), [1]); + expect(consumed).toBe(false); + expect([...result]).toStrictEqual([[1, 1]]); + expect(consumed).toBe(true); + }); +}); diff --git a/src/iterator/zipIterable.ts b/src/iterator/zipIterable.ts new file mode 100644 index 000000000..0ca54241f --- /dev/null +++ b/src/iterator/zipIterable.ts @@ -0,0 +1,93 @@ +/** + * Combines multiple iterables into a single iterable of tuples, + * pairing elements at the same index from each iterable. + * + * Stops when the shortest iterable is exhausted. + * This function is lazy. it does not iterate over the provided iterables + * until the returned iterable is iterated. + * + * @template T - A tuple type representing the element types of each iterable. + * @param {[...{ [K in keyof T]: Iterable }]} iterables - The iterables to zip. + * @returns {IterableIterator} An iterable iterator that yields tuples of elements. + * + * @example + * Basic usage + * const result = zipIterable([1, 2, 3], ['a', 'b', 'c']); + * console.log([...result]); // [[1, 'a'], [2, 'b'], [3, 'c']] + * + * @example + * Stops at the shortest iterable + * const result = zipIterable([1, 2, 3], ['a', 'b']); + * console.log([...result]); // [[1, 'a'], [2, 'b']] + * + * @example + * Works with any iterable. not just arrays + * const names = new Set(['toss', 'tech', 'korea']); + * const scores = [90, 85, 92]; + * for (const [name, score] of zipIterable(names, scores)) { + * console.log(`${name}: ${score}`); + * } + * + * @example + * Merging two parallel API responses by index + * const [users, profiles] = await Promise.all([fetchUsers(), fetchProfiles()]); + * const merged = [...zipIterable(users, profiles)].map(([user, profile]) => ({ + * ...user, + * ...profile, + * })); + * + * @example + * Comparing previous and current values (diff pattern) + * const prev = [100, 200, 300]; + * const curr = [110, 190, 350]; + * const diffs = [...zipIterable(prev, curr)].map(([before, after]) => ({ + * before, + * after, + * delta: after - before, + * percent: ((after - before) / before) * 100, + * })); + * // [{ before: 100, after: 110, delta: 10, percent: 10 }, ...] + * + * @example + * Mapping column definitions to server-returned row arrays (e.g. CSV import) + * const columns = [ + * { key: 'name', label: 'Name' }, + * { key: 'score', label: 'Score' }, + * ]; + * const rows: unknown[][] = [['Toss', 95], ['Tech', 82]]; + * const normalized = rows.map(row => + * Object.fromEntries([...zipIterable(columns, row)].map(([col, value]) => [col.key, value])) + * ); + * // [{ name: 'Toss', score: 95 }, { name: 'Tech', score: 82 }] + * + * @example + * Detecting changed fields between renders (e.g. with usePrevious hook) + * function useChangedFields(current: T, previous: T) { + * return [...zipIterable(Object.entries(current), Object.entries(previous))] + * .filter(([[, curr], [, prev]]) => curr !== prev) + * .map(([[key, curr], [, prev]]) => ({ key, prev, curr })); + * } + * // [{ key: 'score', prev: 82, curr: 95 }] + */ +export function* zipIterable( + ...iterables: { [K in keyof T]: Iterable } +): IterableIterator { + const iterators = iterables.map(iterable => iterable[Symbol.iterator]()); + const len = iterators.length; + + while (true) { + const tuple: unknown[] = new Array(len); + + for (let i = 0; i < len; i++) { + const result = iterators[i].next(); + + if (result.done) { + return; + } + + tuple[i] = result.value; + } + + yield tuple as unknown as T; + } +} diff --git a/yarn.lock b/yarn.lock index ace2173ff..f3894f40c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2121,6 +2121,15 @@ __metadata: languageName: node linkType: hard +"@fxts/core@npm:^1.26.0": + version: 1.26.0 + resolution: "@fxts/core@npm:1.26.0" + dependencies: + tslib: "npm:^2.6.0" + checksum: 10c0/862f595229409edec0fd69efba881b749b2875b4fd079367a0a9cb90fc181a92c94622f2fb01c361e24ea919a179d80b6697b628afafe7adf258492234249469 + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -4557,10 +4566,12 @@ __metadata: version: 0.0.0-use.local resolution: "benchmarks@workspace:benchmarks" dependencies: + "@fxts/core": "npm:^1.26.0" "@types/lodash": "npm:^4.17.20" "@types/lodash-es": "npm:^4" es-toolkit: "workspace:^" esbuild: "npm:0.23.0" + itertools: "npm:^2.6.0" lodash: "npm:^4.17.21" lodash-es: "npm:^4.17.21" rfdc: "npm:^1.4.1" @@ -8250,6 +8261,13 @@ __metadata: languageName: node linkType: hard +"itertools@npm:^2.6.0": + version: 2.6.0 + resolution: "itertools@npm:2.6.0" + checksum: 10c0/2b93bc5596bd62963787f2297a778e105337795ac1d6c4f9104413e54cc7af5f4eefbb7fc8792754710f6e0888e379b7128b2234a290cbcfc2e357ca163c3796 + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -12212,6 +12230,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 + languageName: node + linkType: hard + "tsx@npm:^4.19.0": version: 4.19.0 resolution: "tsx@npm:4.19.0"