Skip to content

Commit

Permalink
Merge pull request #15 from Baek2back/itertools/basic
Browse files Browse the repository at this point in the history
Itertools/basic
  • Loading branch information
Baek2back authored Jul 4, 2024
2 parents 451561a + a43158d commit ccae063
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/eight-humans-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@iter-x/itertools": patch
---

implement basic operation of itertools
19 changes: 19 additions & 0 deletions packages/itertools/src/accumulate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { accumulate } from "./accumulate";
import { take } from "./take";

describe("accumulate", () => {
it("should accumulate values from iterator", () => {
const iterable = accumulate([1, 2, 3], (x, y) => x + y);

expect(take(Number.POSITIVE_INFINITY, iterable)).toEqual([1, 3, 6]);
});

it("should accumulate values from iterator with initial value", () => {
const iterable = accumulate([1, 2, 3, 4, 5], (a, b) => a + b, 100);

expect(take(Number.POSITIVE_INFINITY, iterable)).toEqual([
100, 101, 103, 106, 110, 115,
]);
});
});
36 changes: 36 additions & 0 deletions packages/itertools/src/accumulate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
type Reducer<T, U> = (acc: U, item: T) => U;

/**
*
* @example
* ```typescript
* accumulate([1,2,3,4,5], (a, b) => a + b, 100) // => 100 101 103 106 110 115
* accumulate([1,2,3,4,5], (a, b) => a * b) // => 1 2 6 24 120
* ```
*/

export function* accumulate<T, U = T>(
iterable: Iterable<T>,
func: Reducer<T, U>,
initial?: U,
): IterableIterator<U> {
const iterator = iterable[Symbol.iterator]();
let accumulator: U;

if (initial === undefined) {
const first = iterator.next();
if (first.done) return;
accumulator = first.value as unknown as U;
} else {
accumulator = initial;
}

yield accumulator;

while (true) {
const item = iterator.next();
if (item.done) break;
accumulator = func(accumulator, item.value);
yield accumulator;
}
}
32 changes: 32 additions & 0 deletions packages/itertools/src/batched.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { batched } from "./batched";
import { take } from "./take";

describe("batched", () => {
it("returns remainder in final batch", () => {
const result = take(
Number.POSITIVE_INFINITY,
batched([1, 1, 1, 1, 1, 1, 2, 2], 3),
);
expect(result).toEqual([
[1, 1, 1],
[1, 1, 1],
[2, 2],
]);
});

it("throw error when batch length does not matched n with strict option", () => {
expect(() => {
take(
Number.POSITIVE_INFINITY,
batched([1, 1, 1, 1, 1, 1, 2, 2], 3, { strict: true }),
);
}).toThrow("batched(): incomplete batch");
});

it("throw error when n is less than 1", () => {
expect(() => {
take(Number.POSITIVE_INFINITY, batched([1, 1, 1, 1, 1, 1, 2, 2], 0));
}).toThrow("n must be at least one");
});
});
38 changes: 38 additions & 0 deletions packages/itertools/src/batched.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @example
* ```typescript
* batched('ABCDEFG', 3) // => ABC DEF G
* ```
*/
export function* batched<T>(
iterable: Iterable<T>,
n: number,
options: { strict?: boolean } = {},
): IterableIterator<T[]> {
const { strict = false } = options;

if (n < 1) {
throw new Error("n must be at least one");
}

const iterator = iterable[Symbol.iterator]();

while (true) {
const batch: T[] = [];
for (let i = 0; i < n; i++) {
const { value, done } = iterator.next();
if (done) {
if (batch.length === 0) {
return;
}
if (strict && batch.length !== n) {
throw new Error("batched(): incomplete batch");
}
yield batch;
return;
}
batch.push(value);
}
yield batch;
}
}
11 changes: 11 additions & 0 deletions packages/itertools/src/chain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from "vitest";
import { chain } from "./chain";
import { take } from "./take";

describe("concat", () => {
it("concat two iterables", () => {
const result = take(Number.POSITIVE_INFINITY, chain("ABC", "DEF"));

expect(result).toEqual(["A", "B", "C", "D", "E", "F"]);
});
});
12 changes: 12 additions & 0 deletions packages/itertools/src/chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { flatten } from "./flatten";

/**
* Make an iterator that returns elements from the first iterable until it is exhausted, then proceeds to the next iterable,
* until all of the iterables are exhausted. Used for treating consecutive sequences as a single sequence.
*
* @example
* chain("ABC", "DEF") // => A B C D E F
*/
export function chain<T>(...iterables: Iterable<T>[]): IterableIterator<T> {
return flatten(iterables);
}
22 changes: 22 additions & 0 deletions packages/itertools/src/compress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { compress } from "./compress";
import { take } from "./take";

describe("compress", () => {
it("compress on empty list", () => {
expect(take(Number.POSITIVE_INFINITY, compress([], []))).toEqual([]);
});

it("compress removes selected items", () => {
expect(
take(Number.POSITIVE_INFINITY, compress("ABCDEF", [1, 0, 1, 0, 1, 1])),
).toEqual(["A", "C", "E", "F"]);

expect(
take(
Number.POSITIVE_INFINITY,
compress("ABCDEF", [true, false, true, false, true, true]),
),
).toEqual(["A", "C", "E", "F"]);
});
});
17 changes: 17 additions & 0 deletions packages/itertools/src/compress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { zip } from "./zip";

/**
* Compress an iterable by filter out data that is not selected.
* @example
* compress('ABCDEF', [1,0,1,0,1,1]) // => A C E F
*/
export function* compress<T>(
data: Iterable<T>,
selectors: Iterable<number | boolean>,
): IterableIterator<T> {
for (const [d, selector] of zip(data, selectors)) {
if (selector) {
yield d;
}
}
}
29 changes: 29 additions & 0 deletions packages/itertools/src/flatten.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { flatten } from "./flatten";
import { take } from "./take";

describe("flatten", () => {
it("return flatten iterables in iterables of iterables", () => {
const result = take(
Number.POSITIVE_INFINITY,
flatten([
[1, 2],
[3, 4],
]),
);

expect(result).toEqual([1, 2, 3, 4]);
});

it("only one level flatten", () => {
const result = take(
Number.POSITIVE_INFINITY,
flatten([[[1, 2]], [[3, 4]]]),
);

expect(result).toEqual([
[1, 2],
[3, 4],
]);
});
});
15 changes: 15 additions & 0 deletions packages/itertools/src/flatten.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Return an flatten iterator only one level of nesting in iterables of iterables
*
* @example
* flatten([[0, 1], [2, 3]]) // => 0 1 2 3
*/
export function* flatten<T>(
nestedIterables: Iterable<Iterable<T>>,
): IterableIterator<T> {
for (const iterable of nestedIterables) {
for (const it of iterable) {
yield it;
}
}
}
18 changes: 18 additions & 0 deletions packages/itertools/src/starmap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { starmap } from "./starmap";
import { take } from "./take";

describe("starmap", () => {
it("starmap with pre-zipped tuple", () => {
const result = take(
Number.POSITIVE_INFINITY,
starmap(Math.pow, [
[2, 5],
[3, 2],
[10, 3],
]),
);

expect(result).toEqual([32, 9, 1000]);
});
});
14 changes: 14 additions & 0 deletions packages/itertools/src/starmap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @example
* ```typescript
* starmap(pow, [[2,5], [3,2], [10,3]]) // => 32 9 1000
* ```
*/
export function* starmap<T extends unknown[], R>(
func: (...args: T) => R,
iterable: Iterable<T>,
): IterableIterator<R> {
for (const args of iterable) {
yield func(...args);
}
}
28 changes: 28 additions & 0 deletions packages/itertools/src/zip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { take } from "./take";
import { zip } from "./zip";

describe("zip", () => {
it("zips an iterable correctly", () => {
const result = take(Number.POSITIVE_INFINITY, zip([1, 2], [3, 4]));
expect(result).toEqual([
[1, 3],
[2, 4],
]);
});

it("return an empty iterables when pass empty iterables", () => {
const result = take(Number.POSITIVE_INFINITY, zip([], []));
expect(result).toEqual([]);
});

it("throw Error pass different length iterables when strict option is true", () => {
expect(() => {
take(Number.POSITIVE_INFINITY, zip([1, 2], [1, 2, 3], { strict: true }));
}).toThrow("zip() argument 2 is longer than argument 1");

expect(() => {
take(Number.POSITIVE_INFINITY, zip([1, 2, 3], [1, 2], { strict: true }));
}).toThrow("zip() argument 1 is longer than argument 2");
});
});
31 changes: 31 additions & 0 deletions packages/itertools/src/zip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
*
* @example
* [...zip([1, 2], [1, 2])] // => [[1, 1], [2, 2]]
* [...zip([1, 2], [1, 2, 3], { strict: true })] // => Error("zip() argument 2 is longer than argument 1")
*/
export function* zip<T1, T2>(
xs: Iterable<T1>,
ys: Iterable<T2>,
options: { strict?: boolean } = {},
): IterableIterator<[T1, T2]> {
const { strict = false } = options;

const [ixs, iys] = [xs[Symbol.iterator](), ys[Symbol.iterator]()];

while (true) {
const [x, y] = [ixs.next(), iys.next()];
if (!x.done && !y.done) {
yield [x.value, y.value];
} else {
if (strict) {
throw new Error(
`zip() argument ${x.done ? 2 : 1} is longer than argument ${
x.done ? 1 : 2
}`,
);
}
return;
}
}
}

0 comments on commit ccae063

Please sign in to comment.