diff --git a/README.md b/README.md index 23d9d40..b81898e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,82 @@ # typescript/iterators [![Build Status](https://thunderk.visualstudio.com/typescript/_apis/build/status/iterators?branchName=master)](https://dev.azure.com/thunderk/typescript/_build?pipelineNameFilter=iterators) + +## About + +Lazy iterators to work on dynamic data sets without materializing them. + +They allow to work on infinite streams of values, with limited memory +consumption. + +Functions in this library that do not return an Iterable are "materializing", +meaning that they may consume iterators up to the end, and will not work well on +infinite iterators (a limit is often provided to break out). + +These iterators are guaranteed to be repeatable, meaning that calling +Symbol.iterator on them will start over. + +## Import + +In deno: + +```typescript +import { iarray, isum } from "https://js.thunderk.net/iterators/mod.ts"; +``` + +In browser: + +```html + +``` + +## Use + +Examples (edge cases can be found in each function's documentation): + +```typescript +// Create iterables +const i1 = iarray([1, 2, 3]); // 1 2 3 +const i2 = isingle(4); // 4 +const i3 = irecur(0, (x) => x - 1); // 0 -1 -2 -3 -4 ... +const i4 = irange(5); // 0 1 2 3 4 +const i5 = irange(5, 1, 2); // 1 3 5 7 9 +const i6 = istep(4, iarray([1, 10, 1, -1])); // 4 5 15 16 15 +const i7 = irepeat(2); // 2 2 2 2 2 ... + +// Consume iterables +iforeach(i1, console.log); +ifirst(i6); // 4 +ifirstmap(i1, (x) => x > 1 ? -x : null); // -2 +imaterialize(i1); // [1, 2, 3] +ilength(i1); // 3 +iat(i5, 3); // 7 +ireduce(i1, (a, b) => a + b, 0); // 6 +isum(i1); // 6 +icat(iarray(["a", "b", "c"])); // abc +imin(i1); // 1 +imax(i1); // 3 + +// Transform iterables +iloop(i1); // 1 2 3 1 2 3 ... +iskip(i3, 2); // -2 -3 -4 -5 -6 ... +imap(i1, (x) => x * 2); // 2 4 6 +ifilter(i3, (x) => x % 4 == 0); // 0 -4 -8 -12 ... +ipartition(i3, (x) => x % 4 == 0); // [0 -4 -8 ..., -1 -2 -3 -5 ...] +iunique(iarray([1, 4, 2, 4, 3, 1, 5])); // 1 4 2 3 5 + +// Combine iterables +ichain(i1, i2); // 1 2 3 4 +ichainit(iarray([i1, i2])); // 1 2 3 4 +icombine(iarray([0, 1]), iarray(["a", "b"])); // [0, "a"] [0, "b"] [1, "a"] [2, "b"] +izip(iarray([0, 1]), iarray(["a", "b"])); // [0, "a"] [1, "b"] +ialternate(iarray([0, 1]), iarray(["a", "b"])); // 0 "a" 1 "b" + +// Type filter (result is a typed iterable) +ifiltertype(iarray([1, "a", 2, "b"]), (x): x is number => typeof x == "number"); // 1 2 +class A {} +class B {} +ifilterclass(iarray([new A(), new B(), new A()]), A); // A A +``` diff --git a/deps.testing.ts b/deps.testing.ts new file mode 100644 index 0000000..7abf437 --- /dev/null +++ b/deps.testing.ts @@ -0,0 +1,6 @@ +export { + describe, + expect, + it, + mockfn, +} from "https://js.thunderk.net/devtools@1.3.0/testing.ts"; diff --git a/doc/about.md b/doc/about.md new file mode 100644 index 0000000..15813e4 --- /dev/null +++ b/doc/about.md @@ -0,0 +1,13 @@ +## About + +Lazy iterators to work on dynamic data sets without materializing them. + +They allow to work on infinite streams of values, with limited memory +consumption. + +Functions in this library that do not return an Iterable are "materializing", +meaning that they may consume iterators up to the end, and will not work well on +infinite iterators (a limit is often provided to break out). + +These iterators are guaranteed to be repeatable, meaning that calling +Symbol.iterator on them will start over. diff --git a/doc/import.md b/doc/import.md new file mode 100644 index 0000000..0c9c9b5 --- /dev/null +++ b/doc/import.md @@ -0,0 +1,15 @@ +## Import + +In deno: + +```typescript +import { iarray, isum } from "https://js.thunderk.net/iterators/mod.ts"; +``` + +In browser: + +```html + +``` diff --git a/doc/index b/doc/index new file mode 100644 index 0000000..b1dffcc --- /dev/null +++ b/doc/index @@ -0,0 +1,3 @@ +about +import +use diff --git a/doc/use.md b/doc/use.md new file mode 100644 index 0000000..757a9b6 --- /dev/null +++ b/doc/use.md @@ -0,0 +1,48 @@ +## Use + +Examples (edge cases can be found in each function's documentation): + +```typescript +// Create iterables +const i1 = iarray([1, 2, 3]); // 1 2 3 +const i2 = isingle(4); // 4 +const i3 = irecur(0, (x) => x - 1); // 0 -1 -2 -3 -4 ... +const i4 = irange(5); // 0 1 2 3 4 +const i5 = irange(5, 1, 2); // 1 3 5 7 9 +const i6 = istep(4, iarray([1, 10, 1, -1])); // 4 5 15 16 15 +const i7 = irepeat(2); // 2 2 2 2 2 ... + +// Consume iterables +iforeach(i1, console.log); +ifirst(i6); // 4 +ifirstmap(i1, (x) => x > 1 ? -x : null); // -2 +imaterialize(i1); // [1, 2, 3] +ilength(i1); // 3 +iat(i5, 3); // 7 +ireduce(i1, (a, b) => a + b, 0); // 6 +isum(i1); // 6 +icat(iarray(["a", "b", "c"])); // abc +imin(i1); // 1 +imax(i1); // 3 + +// Transform iterables +iloop(i1); // 1 2 3 1 2 3 ... +iskip(i3, 2); // -2 -3 -4 -5 -6 ... +imap(i1, (x) => x * 2); // 2 4 6 +ifilter(i3, (x) => x % 4 == 0); // 0 -4 -8 -12 ... +ipartition(i3, (x) => x % 4 == 0); // [0 -4 -8 ..., -1 -2 -3 -5 ...] +iunique(iarray([1, 4, 2, 4, 3, 1, 5])); // 1 4 2 3 5 + +// Combine iterables +ichain(i1, i2); // 1 2 3 4 +ichainit(iarray([i1, i2])); // 1 2 3 4 +icombine(iarray([0, 1]), iarray(["a", "b"])); // [0, "a"] [0, "b"] [1, "a"] [2, "b"] +izip(iarray([0, 1]), iarray(["a", "b"])); // [0, "a"] [1, "b"] +ialternate(iarray([0, 1]), iarray(["a", "b"])); // 0 "a" 1 "b" + +// Type filter (result is a typed iterable) +ifiltertype(iarray([1, "a", 2, "b"]), (x): x is number => typeof x == "number"); // 1 2 +class A {} +class B {} +ifilterclass(iarray([new A(), new B(), new A()]), A); // A A +``` diff --git a/mod.test.ts b/mod.test.ts index b359d07..dcd0903 100644 --- a/mod.test.ts +++ b/mod.test.ts @@ -1,10 +1,4 @@ -import { - describe, - expect, - it, - mockfn, - patch, -} from "https://code.thunderk.net/typescript/devtools/raw/1.3.0/testing.ts"; +import { describe, expect, it, mockfn } from "./deps.testing.ts"; import { ialternate, iarray, @@ -20,6 +14,7 @@ import { ifirst, ifirstmap, iforeach, + ilength, iloop, imap, imaterialize, @@ -133,13 +128,23 @@ describe("Iterators", () => { }); it("materializes an array from an iterator", () => { + expect(imaterialize(IEMPTY)).toEqual([]); expect(imaterialize(iarray([1, 2, 3]))).toEqual([1, 2, 3]); - + expect(() => imaterialize(irepeat(null))).toThrow( + "Length limit on iterator materialize", + ); expect(() => imaterialize(iarray([1, 2, 3, 4, 5]), 2)).toThrow( "Length limit on iterator materialize", ); }); + it("counts items in an iterator", () => { + expect(ilength(IEMPTY)).toEqual(0); + expect(ilength(iarray([1, 2, 3]))).toEqual(3); + expect(ilength(irange())).toEqual(Infinity); + expect(ilength(iarray([1, 2, 3, 4, 5]), 3)).toEqual(Infinity); + }); + it("creates an iterator in a range of integers", () => { checkit(irange(4), [0, 1, 2, 3]); checkit(irange(4, 1), [1, 2, 3, 4]); @@ -225,6 +230,8 @@ describe("Iterators", () => { it("maps an iterator", () => { checkit(imap(IEMPTY, (i) => i * 2), []); checkit(imap(irange(3), (i) => i * 2), [0, 2, 4]); + const r: Iterable = imap(irange(3), (i) => i.toString()); + checkit(r, ["0", "1", "2"]); }); it("reduces an iterator", () => { @@ -239,7 +246,7 @@ describe("Iterators", () => { }); it("filters an iterator with a type guard", () => { - let result = ifiltertype( + let result: Iterable = ifiltertype( <(number | string)[]> [1, "a", 2, "b"], (x): x is number => typeof x == "number", ); @@ -250,7 +257,7 @@ describe("Iterators", () => { let o1 = new A(); let o2 = new A(); let o3 = new B(); - let result = ifilterclass([1, "a", o1, 2, o2, o3, "b"], A); + let result: Iterable = ifilterclass([1, "a", o1, 2, o2, o3, "b"], A); checkit(result, [o1, o2]); }); diff --git a/mod.ts b/mod.ts index d7f75b4..cfc5494 100644 --- a/mod.ts +++ b/mod.ts @@ -133,6 +133,25 @@ export function imaterialize(iterable: Iterable, limit = 1000000): T[] { return result; } +/** + * Count items in an iterator + * + * To avoid counting infinite iterators (and bursting memory), the item count is limited by + * the *limit* parameter, and Infinity will be returned after that many. + */ +export function ilength(iterable: Iterable, limit = 1000000): number { + let result = 0; + + for (let _ of iterable) { + if (result >= limit) { + return Infinity; + } + result += 1; + } + + return result; +} + /** * Iterate over natural integers * @@ -451,12 +470,12 @@ export function iunique( ): Iterable { return { [Symbol.iterator]: function* () { - let done: T[] = []; + let done: Set = new Set(); let n = limit; for (let value of iterable) { - if (done.indexOf(value) < 0) { + if (!done.has(value)) { if (n-- > 0) { - done.push(value); + done.add(value); yield value; } else { throw new Error("Unique count limit on iterator");