From f2016e38e2c2379b05c2c40d3cd0c2641b4c865c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Lemaire?= Date: Sun, 4 Jul 2021 23:04:33 +0200 Subject: [PATCH] Initial import from old tscommon project --- .editorconfig | 9 + .gitignore | 3 + README.md | 3 + mod.test.ts | 326 ++++++++++++++++++++++++++++++++++ mod.ts | 480 ++++++++++++++++++++++++++++++++++++++++++++++++++ run | 19 ++ tsconfig.json | 10 ++ 7 files changed, 850 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 mod.test.ts create mode 100644 mod.ts create mode 100755 run create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..83c1115 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{ts,json}] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d69a4c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +deno.d.ts +.vscode +.local diff --git a/README.md b/README.md new file mode 100644 index 0000000..23d9d40 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# typescript/iterators + +[![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/iterators?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=iterators) diff --git a/mod.test.ts b/mod.test.ts new file mode 100644 index 0000000..b359d07 --- /dev/null +++ b/mod.test.ts @@ -0,0 +1,326 @@ +import { + describe, + expect, + it, + mockfn, + patch, +} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts"; +import { + ialternate, + iarray, + iat, + icat, + ichain, + ichainit, + icombine, + IEMPTY, + ifilter, + ifilterclass, + ifiltertype, + ifirst, + ifirstmap, + iforeach, + iloop, + imap, + imaterialize, + imax, + imin, + ipartition, + irange, + irecur, + ireduce, + irepeat, + isingle, + iskip, + istep, + isum, + iunique, + izip, + izipg, +} from "./mod.ts"; + +class A {} +class B {} + +describe("Iterators", () => { + function checkit( + base_iterator: Iterable, + values: T[], + infinite = false, + ) { + function checker() { + let iterator = base_iterator[Symbol.iterator](); + values.forEach((value) => { + let state = iterator.next(); + expect(state.done).toBe(false); + expect(state.value).toEqual(value); + }); + if (!infinite) { + for (let i = 0; i < 3; i++) { + let state = iterator.next(); + expect(state.done).toBe(true); + } + } + } + + // Make two iterations to be sure it unwinds the same + checker(); + checker(); + } + + it("constructs an iterator from a recurrent formula", () => { + checkit(irecur(1, (x) => x + 2), [1, 3, 5], true); + checkit(irecur(4, (x) => x ? x - 1 : null), [4, 3, 2, 1, 0]); + }); + + it("constructs an iterator from an array", () => { + checkit(iarray([]), []); + checkit(iarray([1, 2, 3]), [1, 2, 3]); + }); + + it("constructs an iterator from a single value", () => { + checkit(isingle(1), [1]); + checkit(isingle("a"), ["a"]); + }); + + it("repeats a value", () => { + checkit(irepeat("a"), ["a", "a", "a", "a"], true); + checkit(irepeat("a", 3), ["a", "a", "a"]); + }); + + it("calls a function for each yielded value", () => { + let iterator = iarray([1, 2, 3]); + let result: number[] = []; + iforeach(iterator, (val) => result.push(val)); + expect(result).toEqual([1, 2, 3]); + + result = []; + iforeach(iterator, (i) => { + result.push(i); + if (i == 2) { + return null; + } else { + return undefined; + } + }); + expect(result).toEqual([1, 2]); + + result = []; + iforeach(iterator, (i) => { + result.push(i); + return i; + }, 2); + expect(result).toEqual([1, 2]); + + result = []; + for (let i of iterator) { + result.push(i); + } + expect(result).toEqual([1, 2, 3]); + }); + + it("finds the first item passing a predicate", () => { + expect(ifirst(iarray( []), (i) => i % 2 == 0)).toBeNull(); + expect(ifirst(iarray([1, 2, 3]), (i) => i % 2 == 0)).toBe(2); + expect(ifirst(iarray([1, 3, 5]), (i) => i % 2 == 0)).toBeNull(); + }); + + it("finds the first item mapping to a value", () => { + let predicate = (i: number) => i % 2 == 0 ? (i * 4).toString() : null; + expect(ifirstmap(iarray([]), predicate)).toBeNull(); + expect(ifirstmap(iarray([1, 2, 3]), predicate)).toBe("8"); + expect(ifirstmap(iarray([1, 3, 5]), predicate)).toBeNull(); + }); + + it("materializes an array from an iterator", () => { + expect(imaterialize(iarray([1, 2, 3]))).toEqual([1, 2, 3]); + + expect(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2)).toThrow( + "Length limit on iterator materialize", + ); + }); + + it("creates an iterator in a range of integers", () => { + checkit(irange(4), [0, 1, 2, 3]); + checkit(irange(4, 1), [1, 2, 3, 4]); + checkit(irange(5, 3, 2), [3, 5, 7, 9, 11]); + checkit(irange(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); + }); + + it("uses a step iterator to scan numbers", () => { + checkit(istep(), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], true); + checkit(istep(3), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], true); + checkit(istep(3, irepeat(1, 4)), [3, 4, 5, 6, 7]); + checkit(istep(8, IEMPTY), [8]); + checkit(istep(1, irange()), [1, 1, 2, 4, 7, 11, 16], true); + }); + + it("skips a number of values", () => { + checkit(iskip(irange(7), 3), [3, 4, 5, 6]); + checkit(iskip(irange(7), 12), []); + checkit(iskip(IEMPTY, 3), []); + }); + + it("gets a value at an iterator position", () => { + expect(iat(irange(), -1)).toBeNull(); + expect(iat(irange(), 0)).toBe(0); + expect(iat(irange(), 8)).toBe(8); + expect(iat(irange(5), 8)).toBeNull(); + expect(iat(IEMPTY, 0)).toBeNull(); + }); + + it("chains iterator of iterators", () => { + checkit(ichainit(IEMPTY), []); + checkit( + ichainit(iarray([iarray([1, 2, 3]), iarray([]), iarray([4, 5])])), + [1, 2, 3, 4, 5], + ); + }); + + it("chains iterators", () => { + checkit(ichain(), []); + checkit(ichain(irange(3)), [0, 1, 2]); + checkit(ichain(iarray([1, 2]), iarray([]), iarray([3, 4, 5])), [ + 1, + 2, + 3, + 4, + 5, + ]); + }); + + it("loops an iterator", () => { + checkit(iloop(irange(3), 2), [0, 1, 2, 0, 1, 2]); + checkit( + iloop(irange(1)), + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + true, + ); + + let onloop = mockfn(); + let iterator = iloop(irange(2), 3, onloop)[Symbol.iterator](); + + expect(iterator.next().value).toBe(0); + expect(onloop).toHaveBeenCalledTimes(0); + + expect(iterator.next().value).toBe(1); + expect(onloop).toHaveBeenCalledTimes(0); + + expect(iterator.next().value).toBe(0); + expect(onloop).toHaveBeenCalledTimes(1); + + expect(iterator.next().value).toBe(1); + expect(onloop).toHaveBeenCalledTimes(1); + + expect(iterator.next().value).toBe(0); + expect(onloop).toHaveBeenCalledTimes(2); + + expect(iterator.next().value).toBe(1); + expect(onloop).toHaveBeenCalledTimes(2); + + expect(iterator.next().value).toBeUndefined(); + expect(onloop).toHaveBeenCalledTimes(2); + }); + + it("maps an iterator", () => { + checkit(imap(IEMPTY, (i) => i * 2), []); + checkit(imap(irange(3), (i) => i * 2), [0, 2, 4]); + }); + + it("reduces an iterator", () => { + expect(ireduce(IEMPTY, (a, b) => a + b, 2)).toBe(2); + expect(ireduce([9], (a, b) => a + b, 2)).toBe(11); + expect(ireduce([9, 1], (a, b) => a + b, 2)).toBe(12); + }); + + it("filters an iterator with a predicate", () => { + checkit(imap(IEMPTY, (i) => i % 3 == 0), []); + checkit(ifilter(irange(12), (i) => i % 3 == 0), [0, 3, 6, 9]); + }); + + it("filters an iterator with a type guard", () => { + let result = ifiltertype( + <(number | string)[]> [1, "a", 2, "b"], + (x): x is number => typeof x == "number", + ); + checkit(result, [1, 2]); + }); + + it("filters an iterator with a class type", () => { + let o1 = new A(); + let o2 = new A(); + let o3 = new B(); + let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], A); + checkit(result, [o1, o2]); + }); + + it("combines iterators", () => { + let iterator = icombine(iarray([1, 2, 3]), iarray(["a", "b"])); + checkit(iterator, [ + [1, "a"], + [1, "b"], + [2, "a"], + [2, "b"], + [3, "a"], + [3, "b"], + ]); + }); + + it("zips iterators", () => { + checkit(izip(IEMPTY, IEMPTY), []); + checkit(izip(iarray([1, 2, 3]), iarray(["a", "b"])), [[1, "a"], [ + 2, + "b", + ]]); + + checkit(izipg(IEMPTY, IEMPTY), []); + checkit( + izipg(iarray([1, 2, 3]), iarray(["a", "b"])), + <[number | undefined, string | undefined][]> [[1, "a"], [2, "b"], [ + 3, + undefined, + ]], + ); + }); + + it("partitions iterators", () => { + let [it1, it2] = ipartition(IEMPTY, () => true); + checkit(it1, []); + checkit(it2, []); + + [it1, it2] = ipartition(irange(5), (i) => i % 2 == 0); + checkit(it1, [0, 2, 4]); + checkit(it2, [1, 3]); + }); + + it("alternatively pick from several iterables", () => { + checkit(ialternate([]), []); + checkit( + ialternate([[1, 2, 3, 4], [], iarray([5, 6]), IEMPTY, iarray([7, 8, 9])]), + [1, 5, 7, 2, 6, 8, 3, 9, 4], + ); + }); + + it("returns unique items", () => { + checkit(iunique(IEMPTY), []); + checkit(iunique(iarray([5, 3, 2, 3, 4, 5])), [5, 3, 2, 4]); + checkit(iunique(iarray([5, 3, 2, 3, 4, 5]), 4), [5, 3, 2, 4]); + expect(() => imaterialize(iunique(iarray([5, 3, 2, 3, 4, 5]), 3))).toThrow( + "Unique count limit on iterator", + ); + }); + + it("uses ireduce for some common functions", () => { + expect(isum(IEMPTY)).toEqual(0); + expect(isum(irange(4))).toEqual(6); + + expect(icat(IEMPTY)).toEqual(""); + expect(icat(iarray(["a", "bc", "d"]))).toEqual("abcd"); + + expect(imin(IEMPTY)).toEqual(Infinity); + expect(imin(iarray([3, 8, 2, 4]))).toEqual(2); + + expect(imax(IEMPTY)).toEqual(-Infinity); + expect(imax(iarray([3, 8, 2, 4]))).toEqual(8); + }); +}); diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..d7f75b4 --- /dev/null +++ b/mod.ts @@ -0,0 +1,480 @@ +/** + * Empty iterator + */ +export const IATEND: Iterator = { + next: function () { + return { done: true, value: undefined }; + }, +}; + +/** + * Empty iterable + */ +export const IEMPTY: Iterable = { + [Symbol.iterator]: () => IATEND, +}; + +/** + * Iterable constructor, from an initial value, and a step value + */ +export function irecur(start: T, step: (a: T) => T | null): Iterable { + return { + [Symbol.iterator]: function* () { + let val: T | null = start; + do { + yield val; + val = step(val); + } while (val !== null); + }, + }; +} + +/** + * Iterable constructor, from an array + * + * The iterator will yield the next value each time it is called, then undefined when the array's end is reached. + */ +export function iarray(array: T[], offset = 0): Iterable { + return { + [Symbol.iterator]: function () { + return array.slice(offset)[Symbol.iterator](); + }, + }; +} + +/** + * Iterable constructor, from a single value + * + * The value will be yielded only once, not repeated over. + */ +export function isingle(value: T): Iterable { + return iarray([value]); +} + +/** + * Iterable that repeats the same value. + */ +export function irepeat(value: T, count = -1): Iterable { + return { + [Symbol.iterator]: function* () { + let n = count; + while (n != 0) { + yield value; + n--; + } + }, + }; +} + +/** + * Equivalent of Array.forEach for all iterables. + * + * If the callback returns *stopper*, the iteration is stopped. + */ +export function iforeach( + iterable: Iterable, + callback: (_: T) => any, + stopper: any = null, +): void { + for (let value of iterable) { + if (callback(value) === stopper) { + break; + } + } +} + +/** + * Returns the first item passing a predicate + */ +export function ifirst( + iterable: Iterable, + predicate: (item: T) => boolean, +): T | null { + for (let value of iterable) { + if (predicate(value)) { + return value; + } + } + return null; +} + +/** + * Returns the first non-null result of a value-yielding predicate, applied to each iterator element + */ +export function ifirstmap( + iterable: Iterable, + predicate: (item: T1) => T2 | null, +): T2 | null { + for (let value of iterable) { + let res = predicate(value); + if (res !== null) { + return res; + } + } + return null; +} + +/** + * Materialize an array from consuming an iterable + * + * To avoid materializing infinite iterators (and bursting memory), the item count is limited to 1 million, and an + * exception is thrown when this limit is reached. + */ +export function imaterialize(iterable: Iterable, limit = 1000000): T[] { + let result: T[] = []; + + for (let value of iterable) { + result.push(value); + if (result.length >= limit) { + throw new Error("Length limit on iterator materialize"); + } + } + + return result; +} + +/** + * Iterate over natural integers + * + * If *count* is not specified, the iterator is infinite + */ +export function irange( + count: number = -1, + start = 0, + step = 1, +): Iterable { + return { + [Symbol.iterator]: function* () { + let i = start; + let n = count; + while (n != 0) { + yield i; + i += step; + n--; + } + }, + }; +} + +/** + * Iterate over numbers, by applying a step taken from an other iterator + * + * This iterator stops when the "step iterator" stops + * + * With no argument, istep() == irange() + */ +export function istep(start = 0, step_iterable = irepeat(1)): Iterable { + return { + [Symbol.iterator]: function* () { + let i = start; + yield i; + for (let step of step_iterable) { + i += step; + yield i; + } + }, + }; +} + +/** + * Skip a given number of values from an iterator, discarding them. + */ +export function iskip(iterable: Iterable, count = 1): Iterable { + return { + [Symbol.iterator]: function () { + let iterator = iterable[Symbol.iterator](); + let n = count; + while (n-- > 0) { + iterator.next(); + } + return iterator; + }, + }; +} + +/** + * Return the value at a given position in the iterator + */ +export function iat(iterable: Iterable, position: number): T | null { + if (position < 0) { + return null; + } else { + if (position > 0) { + iterable = iskip(iterable, position); + } + let iterator = iterable[Symbol.iterator](); + let state = iterator.next(); + return state.done ? null : state.value; + } +} + +/** + * Chain an iterable of iterables. + * + * This will yield values from the first yielded iterator, then the second one, and so on... + */ +export function ichainit(iterables: Iterable>): Iterable { + return { + [Symbol.iterator]: function* () { + for (let iterable of iterables) { + for (let value of iterable) { + yield value; + } + } + }, + }; +} + +/** + * Chain iterables. + * + * This will yield values from the first iterator, then the second one, and so on... + */ +export function ichain(...iterables: Iterable[]): Iterable { + if (iterables.length == 0) { + return IEMPTY; + } else { + return ichainit(iterables); + } +} + +/** + * Loop an iterator for a number of times. + * + * If count is negative, if will loop forever (infinite iterator). + * + * onloop may be used to know when the iterator resets. + */ +export function iloop( + base: Iterable, + count = -1, + onloop?: Function, +): Iterable { + return { + [Symbol.iterator]: function* () { + let n = count; + let start = false; + while (n-- != 0) { + for (let value of base) { + if (start) { + if (onloop) { + onloop(); + } + start = false; + } + yield value; + } + start = true; + } + }, + }; +} + +/** + * Iterator version of "map". + */ +export function imap( + iterable: Iterable, + mapfunc: (_: T1) => T2, +): Iterable { + return { + [Symbol.iterator]: function* () { + for (let value of iterable) { + yield mapfunc(value); + } + }, + }; +} + +/** + * Iterator version of "reduce". + */ +export function ireduce( + iterable: Iterable, + reduce: (item1: T, item2: T) => T, + init: T, +): T { + let result = init; + for (let value of iterable) { + result = reduce(result, value); + } + return result; +} + +/** + * Iterator version of "filter". + */ +export function ifilter( + iterable: Iterable, + filterfunc: (_: T) => boolean, +): Iterable { + return { + [Symbol.iterator]: function* () { + for (let value of iterable) { + if (filterfunc(value)) { + yield value; + } + } + }, + }; +} + +/** + * Type filter, to return a list of instances of a given type + */ +export function ifiltertype( + iterable: Iterable, + filter: (item: any) => item is T, +): Iterable { + return ifilter(iterable, filter); +} + +/** + * Class filter, to return a list of instances of a given type + */ +export function ifilterclass( + iterable: Iterable, + classref: { new (...args: any[]): T }, +): Iterable { + return ifilter(iterable, (item): item is T => item instanceof classref); +} + +/** + * Combine two iterables. + * + * This iterates through the second one several times, so if one iterator may be infinite, + * it should be the first one. + */ +export function icombine( + it1: Iterable, + it2: Iterable, +): Iterable<[T1, T2]> { + return ichainit(imap(it1, (v1) => imap(it2, (v2): [T1, T2] => [v1, v2]))); +} + +/** + * Advance through two iterables at the same time, yielding item pairs + * + * Iteration will stop at the first of the two iterators that stops. + */ +export function izip( + it1: Iterable, + it2: Iterable, +): Iterable<[T1, T2]> { + return { + [Symbol.iterator]: function* () { + let iterator1 = it1[Symbol.iterator](); + let iterator2 = it2[Symbol.iterator](); + let state1 = iterator1.next(); + let state2 = iterator2.next(); + while (!state1.done && !state2.done) { + yield [state1.value, state2.value]; + state1 = iterator1.next(); + state2 = iterator2.next(); + } + }, + }; +} + +/** + * Advance two iterables at the same time, yielding item pairs (greedy version) + * + * Iteration will stop when both iterators are consumed, returning partial couples (undefined in the peer) if needed. + */ +export function izipg( + it1: Iterable, + it2: Iterable, +): Iterable<[T1 | undefined, T2 | undefined]> { + return { + [Symbol.iterator]: function* () { + let iterator1 = it1[Symbol.iterator](); + let iterator2 = it2[Symbol.iterator](); + let state1 = iterator1.next(); + let state2 = iterator2.next(); + while (!state1.done || !state2.done) { + yield [state1.value, state2.value]; + state1 = iterator1.next(); + state2 = iterator2.next(); + } + }, + }; +} + +/** + * Partition in two iterables, one with values that pass the predicate, the other with values that don't + */ +export function ipartition( + iterable: Iterable, + predicate: (item: T) => boolean, +): [Iterable, Iterable] { + return [ + ifilter(iterable, predicate), + ifilter(iterable, (x) => !predicate(x)), + ]; +} + +/** + * Alternate between several iterables (pick one from the first one, then one from the second...) + */ +export function ialternate(iterables: Iterable[]): Iterable { + return { + [Symbol.iterator]: function* () { + let iterators = iterables.map((iterable) => iterable[Symbol.iterator]()); + let done: boolean; + do { + done = false; + // TODO Remove "dried-out" iterators + for (let iterator of iterators) { + let state = iterator.next(); + if (!state.done) { + done = true; + yield state.value; + } + } + } while (done); + }, + }; +} + +/** + * Yield items from an iterator only once. + * + * Beware that even if this function is not materializing, it keeps track of yielded item, and may choke on + * infinite or very long streams. Thus, no more than *limit* items will be yielded (an error is thrown + * when this limit is reached). + * + * This function is O(n²) + */ +export function iunique( + iterable: Iterable, + limit = 1000000, +): Iterable { + return { + [Symbol.iterator]: function* () { + let done: T[] = []; + let n = limit; + for (let value of iterable) { + if (done.indexOf(value) < 0) { + if (n-- > 0) { + done.push(value); + yield value; + } else { + throw new Error("Unique count limit on iterator"); + } + } + } + }, + }; +} + +/** + * Common reduce shortcuts + */ +export const isum = (iterable: Iterable) => + ireduce(iterable, (a, b) => a + b, 0); +export const icat = (iterable: Iterable) => + ireduce(iterable, (a, b) => a + b, ""); +export const imin = (iterable: Iterable) => + ireduce(iterable, Math.min, Infinity); +export const imax = (iterable: Iterable) => + ireduce(iterable, Math.max, -Infinity); diff --git a/run b/run new file mode 100755 index 0000000..74d1c6d --- /dev/null +++ b/run @@ -0,0 +1,19 @@ +#!/bin/sh +# Simplified run tool for deno commands + +if test $# -eq 0 +then + echo "Usage: $0 [file or command]" + exit 1 +elif echo $1 | grep -q '.*.ts' +then + denocmd=run + denoargs=$1 + shift +else + denocmd=$1 + shift +fi + +denoargs="$(cat config/$denocmd.flags 2> /dev/null) $denoargs $@" +exec deno $denocmd $denoargs diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e28737f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "ESNext", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "preserveConstEnums": true + } +}