├── .gitignore ├── types.ts ├── deno.jsonc ├── .github └── workflows │ ├── jsr.yml │ └── test.yml ├── mod.ts ├── LICENSE ├── LICENSE.orig ├── utils.ts ├── README.md ├── custom.test.ts ├── custom.ts ├── more-itertools.ts ├── more-itertools.test.ts ├── builtins.ts ├── builtins.test.ts ├── itertools.test.ts └── itertools.ts /.gitignore: -------------------------------------------------------------------------------- 1 | deno.lock 2 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export type Maybe = T | void; 2 | export type Predicate = (v: T) => boolean; 3 | export type Primitive = string | number | boolean; 4 | 5 | // https://github.com/Microsoft/TypeScript/issues/26705#issuecomment-416573802 6 | export type IsNullable = undefined extends T ? K : never; 7 | export type NullableKeys = { 8 | [K in keyof T]-?: IsNullable; 9 | }[keyof T]; 10 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lambdalisue/itertools", 3 | "version": "0.0.0", 4 | "exports": "./mod.ts", 5 | "tasks": { 6 | "test": "deno test -A --parallel --doc --shuffle", 7 | "check": "deno check **/*.ts" 8 | }, 9 | "imports": { 10 | "@std/assert": "jsr:@std/assert@^0.221.0", 11 | "@std/expect": "jsr:@std/expect@^0.221.0", 12 | "@std/testing": "jsr:@std/testing@^0.221.0", 13 | "https://deno.land/x/itertools@$MODULE_VERSION/": "./" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/jsr.yml: -------------------------------------------------------------------------------- 1 | name: jsr 2 | 3 | env: 4 | DENO_VERSION: 1.x 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: ${{ env.DENO_VERSION }} 25 | - name: Publish 26 | run: | 27 | deno run -A jsr:@david/publish-on-tag@0.1.3 28 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { 2 | all, 3 | any, 4 | contains, 5 | enumerate, 6 | filter, 7 | iter, 8 | map, 9 | max, 10 | min, 11 | range, 12 | reduce, 13 | sorted, 14 | sum, 15 | zip, 16 | zip3, 17 | } from "./builtins.ts"; 18 | export { 19 | chain, 20 | compress, 21 | count, 22 | cycle, 23 | dropwhile, 24 | groupby, 25 | icompress, 26 | ifilter, 27 | imap, 28 | izip, 29 | izip2, 30 | izip3, 31 | izipLongest, 32 | izipMany, 33 | permutations, 34 | takewhile, 35 | zipLongest, 36 | zipMany, 37 | } from "./itertools.ts"; 38 | export { 39 | chunked, 40 | flatten, 41 | heads, 42 | itake, 43 | pairwise, 44 | partition, 45 | roundrobin, 46 | take, 47 | uniqueEverseen, 48 | uniqueJustseen, 49 | } from "./more-itertools.ts"; 50 | export { compact, compactObject, first, flatmap, icompact } from "./custom.ts"; 51 | 52 | export type { Predicate, Primitive } from "./types.ts"; 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | env: 4 | DENO_VERSION: 1.x 5 | 6 | on: 7 | schedule: 8 | - cron: "0 7 * * 0" 9 | push: 10 | branches: 11 | - main 12 | pull_request: 13 | workflow_dispatch: 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: denoland/setup-deno@v1 21 | with: 22 | deno-version: ${{ env.DENO_VERSION }} 23 | - name: Format 24 | run: | 25 | deno fmt --check 26 | - name: Lint 27 | run: deno lint 28 | - name: Type check 29 | run: deno task check 30 | 31 | test: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: denoland/setup-deno@v1 36 | with: 37 | deno-version: ${{ env.DENO_VERSION }} 38 | - name: Test 39 | run: | 40 | deno task test 41 | timeout-minutes: 5 42 | - name: JSR publish (dry-run) 43 | run: | 44 | deno publish --dry-run 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Alisue 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /LICENSE.orig: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Vincent Driessen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | import type { Primitive } from "./types.ts"; 2 | 3 | type CmpFn = (a: T, b: T) => number; 4 | 5 | export function keyToCmp(keyFn: (v: T) => Primitive): CmpFn { 6 | return (a: T, b: T) => { 7 | const ka = keyFn(a); 8 | const kb = keyFn(b); 9 | if (typeof ka === "boolean" && typeof kb === "boolean") { 10 | return ka === kb ? 0 : !ka && kb ? -1 : 1; 11 | } else if (typeof ka === "number" && typeof kb === "number") { 12 | return ka - kb; 13 | } else if (typeof ka === "string" && typeof kb === "string") { 14 | return ka === kb ? 0 : ka < kb ? -1 : 1; 15 | } else { 16 | return -1; 17 | } 18 | }; 19 | } 20 | 21 | export function identityPredicate(x: T): boolean { 22 | return !!x; 23 | } 24 | 25 | export function numberIdentity(x: T): number { 26 | if (typeof x !== "number") { 27 | throw new Error("Inputs must be numbers"); 28 | } 29 | return x; 30 | } 31 | 32 | export function primitiveIdentity(x: T): Primitive { 33 | if ( 34 | typeof x !== "string" && typeof x !== "number" && typeof x !== "boolean" 35 | ) { 36 | throw new Error( 37 | "Please provide a key function that can establish object identity", 38 | ); 39 | } 40 | return x; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # itertools 2 | 3 | [![jsr](https://img.shields.io/jsr/v/%40lambdalisue/itertools?logo=javascript&logoColor=white)](https://jsr.io/@lambdalisue/itertools) 4 | [![denoland](https://img.shields.io/github/v/release/lambdalisue/deno-itertools?logo=deno&label=denoland)](https://github.com/lambdalisue/deno-itertools/releases) 5 | [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/itertools/mod.ts) 6 | [![Test](https://github.com/lambdalisue/deno-itertools/actions/workflows/test.yml/badge.svg)](https://github.com/lambdalisue/deno-itertools/actions/workflows/test.yml) 7 | 8 | A TypeScript port of Python's awesome [itertools] standard library. 9 | 10 | This is an standalone version of [nvie/itertools.js] for using under the [Deno] 11 | environment. 12 | 13 | [Deno]: https://deno.land/ 14 | [itertools]: https://docs.python.org/3/library/itertools.html 15 | [more-itertools]: https://pypi.org/project/more-itertools/ 16 | [nvie/itertools.js]: https://github.com/nvie/itertools.js 17 | 18 | ## Example 19 | 20 | ```typescript 21 | import { enumerate } from "https://deno.land/x/itertools@$MODULE_VERSION/mod.ts"; 22 | 23 | console.log([...enumerate(["hello", "world"])]); 24 | // [0, 'hello'], [1, 'world'] 25 | ``` 26 | 27 | This module provides more functions ported from Python's builtin functions, 28 | [itertools], [more-itertools], and more. In other words, all functions provided 29 | by the original [nvie/itertools.js]. 30 | 31 | See 32 | [API documentation](https://doc.deno.land/https/deno.land/x/itertools/mod.ts) 33 | for more details. 34 | 35 | ## License 36 | 37 | The code follows MIT license written in [LICENSE](./LICENSE). Contributors need 38 | to agree that any modifications sent in this repository follow the license. 39 | 40 | The original license is written in [LICENSE.orig](./LICENSE.orig). 41 | -------------------------------------------------------------------------------- /custom.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "@std/testing/bdd"; 2 | import { expect } from "@std/expect"; 3 | import { compact, compactObject, flatmap } from "./custom.ts"; 4 | import { repeat } from "./itertools.ts"; 5 | 6 | describe("compact", () => { 7 | it("compact w/ empty list", () => { 8 | expect(compact([])).toEqual([]); 9 | }); 10 | 11 | it("icompact removes undefined values", () => { 12 | expect(compact("abc")).toEqual(["a", "b", "c"]); 13 | expect(compact(["x", undefined])).toEqual(["x"]); 14 | expect(compact([0, null, undefined, NaN, Infinity])).toEqual([ 15 | 0, 16 | null, 17 | NaN, 18 | Infinity, 19 | ]); 20 | }); 21 | }); 22 | 23 | describe("compactObject", () => { 24 | it("compactObject w/ empty object", () => { 25 | expect(compactObject({})).toEqual({}); 26 | }); 27 | 28 | it("compactObject removes undefined values", () => { 29 | expect(compactObject({ a: 1, b: "foo", c: 0 })).toEqual({ 30 | a: 1, 31 | b: "foo", 32 | c: 0, 33 | }); 34 | expect(compactObject({ a: undefined, b: false, c: 0 })).toEqual({ 35 | b: false, 36 | c: 0, 37 | }); 38 | }); 39 | }); 40 | 41 | describe("flatmap", () => { 42 | it("flatmap w/ empty list", () => { 43 | expect(Array.from(flatmap([], (x) => [x]))).toEqual([]); 44 | }); 45 | 46 | it("flatmap works", () => { 47 | const dupeEvens = (x: number) => (x % 2 === 0 ? [x, x] : [x]); 48 | const triple = (x: string) => [x, x, x]; 49 | const nothin = () => []; 50 | expect(Array.from(flatmap([1, 2, 3, 4, 5], dupeEvens))).toEqual([ 51 | 1, 52 | 2, 53 | 2, 54 | 3, 55 | 4, 56 | 4, 57 | 5, 58 | ]); 59 | expect(Array.from(flatmap(["hi", "ha"], triple))).toEqual([ 60 | "hi", 61 | "hi", 62 | "hi", 63 | "ha", 64 | "ha", 65 | "ha", 66 | ]); 67 | expect(Array.from(flatmap(["hi", "ha"], nothin))).toEqual([]); 68 | }); 69 | 70 | it("flatmap example", () => { 71 | const repeatN = (n: number) => repeat(n, n); 72 | expect(Array.from(flatmap([0, 1, 2, 3, 4], repeatN))).toEqual([ 73 | 1, 74 | 2, 75 | 2, 76 | 3, 77 | 3, 78 | 3, 79 | 4, 80 | 4, 81 | 4, 82 | 4, 83 | ]); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /custom.ts: -------------------------------------------------------------------------------- 1 | import { imap } from "./itertools.ts"; 2 | import { flatten } from "./more-itertools.ts"; 3 | import type { Maybe, NullableKeys, Predicate } from "./types.ts"; 4 | 5 | function isDefined(x: T): boolean { 6 | return x !== undefined; 7 | } 8 | 9 | /** 10 | * Returns an iterable, filtering out any `undefined` values from the input 11 | * iterable. This function is useful to convert a list of `Maybe`'s to 12 | * a list of `T`'s, discarding all the undefined values: 13 | * 14 | * >>> compact([1, 2, undefined, 3]) 15 | * [1, 2, 3] 16 | */ 17 | export function* icompact(iterable: Iterable>): Iterable { 18 | for (const item of iterable) { 19 | if (typeof item !== "undefined") { 20 | yield item; 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * See icompact(). 27 | */ 28 | export function compact(iterable: Iterable>): Array { 29 | return Array.from(icompact(iterable)); 30 | } 31 | 32 | /** 33 | * Removes all undefined values from the given object. Returns a new object. 34 | * 35 | * >>> compactObject({ a: 1, b: undefined, c: 0 }) 36 | * { a: 1, c: 0 } 37 | */ 38 | export function compactObject>( 39 | obj: O, 40 | ): Omit> { 41 | // deno-lint-ignore no-explicit-any 42 | const result: any = {}; 43 | for (const [key, value] of Object.entries(obj)) { 44 | if (typeof value !== "undefined") { 45 | result[key] = value; 46 | } 47 | } 48 | return result; 49 | } 50 | 51 | /** 52 | * Returns the first item in the iterable for which the predicate holds, if 53 | * any. If no such item exists, `undefined` is returned. The default 54 | * predicate is any defined value. 55 | */ 56 | export function first( 57 | iterable: Iterable, 58 | keyFn?: Predicate, 59 | ): Maybe { 60 | const fn = keyFn || isDefined; 61 | for (const value of iterable) { 62 | if (fn(value)) { 63 | return value; 64 | } 65 | } 66 | return undefined; 67 | } 68 | 69 | /** 70 | * Returns 0 or more values for every value in the given iterable. 71 | * Technically, it's just calling map(), followed by flatten(), but it's a very 72 | * useful operation if you want to map over a structure, but not have a 1:1 73 | * input-output mapping. Instead, if you want to potentially return 0 or more 74 | * values per input element, use flatmap(): 75 | * 76 | * For example, to return all numbers `n` in the input iterable `n` times: 77 | * 78 | * >>> const repeatN = n => repeat(n, n); 79 | * >>> [...flatmap([0, 1, 2, 3, 4], repeatN)] 80 | * [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] // note: no 0 81 | */ 82 | export function flatmap( 83 | iterable: Iterable, 84 | mapper: (v: T) => Iterable, 85 | ): Iterable { 86 | return flatten(imap(iterable, mapper)); 87 | } 88 | -------------------------------------------------------------------------------- /more-itertools.ts: -------------------------------------------------------------------------------- 1 | import { iter, map } from "./builtins.ts"; 2 | import { izip, repeat } from "./itertools.ts"; 3 | import type { Predicate, Primitive } from "./types.ts"; 4 | import { primitiveIdentity } from "./utils.ts"; 5 | 6 | /** 7 | * Break iterable into lists of length `size`: 8 | * 9 | * [...chunked([1, 2, 3, 4, 5, 6], 3)] 10 | * // [[1, 2, 3], [4, 5, 6]] 11 | * 12 | * If the length of iterable is not evenly divisible by `size`, the last returned 13 | * list will be shorter: 14 | * 15 | * [...chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)] 16 | * // [[1, 2, 3], [4, 5, 6], [7, 8]] 17 | */ 18 | export function* chunked( 19 | iterable: Iterable, 20 | size: number, 21 | ): Iterable> { 22 | const it = iter(iterable); 23 | const r = it.next(); 24 | if (r.done) { 25 | return; 26 | } 27 | 28 | let chunk = [r.value]; 29 | 30 | for (const item of it) { 31 | chunk.push(item); 32 | 33 | if (chunk.length === size) { 34 | yield chunk; 35 | chunk = []; 36 | } 37 | } 38 | 39 | // Yield the remainder, if there is any 40 | if (chunk.length > 0) { 41 | yield chunk; 42 | } 43 | } 44 | 45 | /** 46 | * Return an iterator flattening one level of nesting in a list of lists: 47 | * 48 | * [...flatten([[0, 1], [2, 3]])] 49 | * // [0, 1, 2, 3] 50 | */ 51 | export function* flatten( 52 | iterableOfIterables: Iterable>, 53 | ): Iterable { 54 | for (const iterable of iterableOfIterables) { 55 | for (const item of iterable) { 56 | yield item; 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Intersperse filler element `value` among the items in `iterable`. 63 | * 64 | * >>> [...intersperse(-1, range(1, 5))] 65 | * [1, -1, 2, -1, 3, -1, 4] 66 | */ 67 | export function intersperse( 68 | value: T, 69 | iterable: Iterable, 70 | ): Iterable { 71 | const stream = flatten(izip(repeat(value), iterable)); 72 | take(1, stream); // eat away and discard the first value from the output 73 | return stream; 74 | } 75 | 76 | /** 77 | * Returns an iterable containing only the first `n` elements of the given 78 | * iterable. 79 | */ 80 | export function* itake(n: number, iterable: Iterable): Iterable { 81 | const it = iter(iterable); 82 | let count = n; 83 | while (count-- > 0) { 84 | const s = it.next(); 85 | if (!s.done) { 86 | yield s.value; 87 | } else { 88 | // Iterable exhausted, quit early 89 | return; 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Returns an iterator of paired items, overlapping, from the original. When 96 | * the input iterable has a finite number of items `n`, the outputted iterable 97 | * will have `n - 1` items. 98 | * 99 | * >>> pairwise([8, 2, 0, 7]) 100 | * [(8, 2), (2, 0), (0, 7)] 101 | */ 102 | export function* pairwise(iterable: Iterable): Iterable<[T, T]> { 103 | const it = iter(iterable); 104 | const r = it.next(); 105 | if (r.done) { 106 | return; 107 | } 108 | 109 | let r1 = r.value; 110 | for (const r2 of it) { 111 | yield [r1, r2]; 112 | r1 = r2; 113 | } 114 | } 115 | 116 | /** 117 | * Returns a 2-tuple of arrays. Splits the elements in the input iterable into 118 | * either of the two arrays. Will fully exhaust the input iterable. The first 119 | * array contains all items that match the predicate, the second the rest: 120 | * 121 | * >>> const isOdd = x => x % 2 !== 0; 122 | * >>> const iterable = range(10); 123 | * >>> const [odds, evens] = partition(iterable, isOdd); 124 | * >>> odds 125 | * [1, 3, 5, 7, 9] 126 | * >>> evens 127 | * [0, 2, 4, 6, 8] 128 | */ 129 | export function partition( 130 | iterable: Iterable, 131 | predicate: Predicate, 132 | ): [Array, Array] { 133 | const good = []; 134 | const bad = []; 135 | 136 | for (const item of iterable) { 137 | if (predicate(item)) { 138 | good.push(item); 139 | } else { 140 | bad.push(item); 141 | } 142 | } 143 | 144 | return [good, bad]; 145 | } 146 | 147 | /** 148 | * Yields the next item from each iterable in turn, alternating between them. 149 | * Continues until all items are exhausted. 150 | * 151 | * >>> [...roundrobin([1, 2, 3], [4], [5, 6, 7, 8])] 152 | * [1, 4, 5, 2, 6, 3, 7, 8] 153 | */ 154 | export function* roundrobin(...iters: Array>): Iterable { 155 | // We'll only keep lazy versions of the input iterables in here that we'll 156 | // slowly going to exhaust. Once an iterable is exhausted, it will be 157 | // removed from this list. Once the entire list is empty, this algorithm 158 | // ends. 159 | const iterables: Array> = map(iters, iter); 160 | 161 | while (iterables.length > 0) { 162 | let index = 0; 163 | while (index < iterables.length) { 164 | const it = iterables[index]; 165 | const result = it.next(); 166 | 167 | if (!result.done) { 168 | yield result.value; 169 | index++; 170 | } else { 171 | // This iterable is exhausted, make sure to remove it from the 172 | // list of iterables. We'll splice the array from under our 173 | // feet, and NOT advancing the index counter. 174 | iterables.splice(index, 1); // intentional side-effect! 175 | } 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * Yields the heads of all of the given iterables. This is almost like 182 | * `roundrobin()`, except that the yielded outputs are grouped in to the 183 | * "rounds": 184 | * 185 | * >>> [...heads([1, 2, 3], [4], [5, 6, 7, 8])] 186 | * [[1, 4, 5], [2, 6], [3, 7], [8]] 187 | * 188 | * This is also different from `zipLongest()`, since the number of items in 189 | * each round can decrease over time, rather than being filled with a filler. 190 | */ 191 | export function* heads(...iters: Array>): Iterable> { 192 | // We'll only keep lazy versions of the input iterables in here that we'll 193 | // slowly going to exhaust. Once an iterable is exhausted, it will be 194 | // removed from this list. Once the entire list is empty, this algorithm 195 | // ends. 196 | const iterables: Array> = map(iters, iter); 197 | 198 | while (iterables.length > 0) { 199 | let index = 0; 200 | const round = []; 201 | while (index < iterables.length) { 202 | const it = iterables[index]; 203 | const result = it.next(); 204 | 205 | if (!result.done) { 206 | round.push(result.value); 207 | index++; 208 | } else { 209 | // This iterable is exhausted, make sure to remove it from the 210 | // list of iterables. We'll splice the array from under our 211 | // feet, and NOT advancing the index counter. 212 | iterables.splice(index, 1); // intentional side-effect! 213 | } 214 | } 215 | if (round.length > 0) { 216 | yield round; 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * Non-lazy version of itake(). 223 | */ 224 | export function take(n: number, iterable: Iterable): Array { 225 | return Array.from(itake(n, iterable)); 226 | } 227 | 228 | /** 229 | * Yield unique elements, preserving order. 230 | * 231 | * >>> [...uniqueEverseen('AAAABBBCCDAABBB')] 232 | * ['A', 'B', 'C', 'D'] 233 | * >>> [...uniqueEverseen('AbBCcAB', s => s.toLowerCase())] 234 | * ['A', 'b', 'C'] 235 | */ 236 | export function* uniqueEverseen( 237 | iterable: Iterable, 238 | keyFn: (v: T) => Primitive = primitiveIdentity, 239 | ): Iterable { 240 | const seen = new Set(); 241 | for (const item of iterable) { 242 | const key = keyFn(item); 243 | if (!seen.has(key)) { 244 | seen.add(key); 245 | yield item; 246 | } 247 | } 248 | } 249 | 250 | /** 251 | * Yields elements in order, ignoring serial duplicates. 252 | * 253 | * >>> [...uniqueJustseen('AAAABBBCCDAABBB')] 254 | * ['A', 'B', 'C', 'D', 'A', 'B'] 255 | * >>> [...uniqueJustseen('AbBCcAB', s => s.toLowerCase())] 256 | * ['A', 'b', 'C', 'A', 'B'] 257 | */ 258 | export function* uniqueJustseen( 259 | iterable: Iterable, 260 | keyFn: (v: T) => Primitive = primitiveIdentity, 261 | ): Iterable { 262 | let last = undefined; 263 | for (const item of iterable) { 264 | const key = keyFn(item); 265 | if (key !== last) { 266 | yield item; 267 | last = key; 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /more-itertools.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "@std/testing/bdd"; 2 | import { expect } from "@std/expect"; 3 | import { range } from "./builtins.ts"; 4 | import { first } from "./custom.ts"; 5 | import { 6 | chunked, 7 | flatten, 8 | heads, 9 | intersperse, 10 | pairwise, 11 | partition, 12 | roundrobin, 13 | take, 14 | uniqueEverseen, 15 | uniqueJustseen, 16 | } from "./more-itertools.ts"; 17 | 18 | const isEven = (x: number) => x % 2 === 0; 19 | const isPositive = (x: number) => x >= 0; 20 | 21 | describe("chunked", () => { 22 | it("does nothing for empty array", () => { 23 | expect(Array.from(chunked([], 3))).toEqual([]); 24 | }); 25 | 26 | it("works with array smaller than chunk size", () => { 27 | expect(Array.from(chunked([1], 3))).toEqual([[1]]); 28 | }); 29 | 30 | it("works with array of values", () => { 31 | expect(Array.from(chunked([1, 2, 3, 4, 5], 3))).toEqual([ 32 | [1, 2, 3], 33 | [4, 5], 34 | ]); 35 | }); 36 | 37 | it("works with exactly chunkable list", () => { 38 | expect(Array.from(chunked([1, 2, 3, 4, 5, 6], 3))).toEqual([ 39 | [1, 2, 3], 40 | [4, 5, 6], 41 | ]); 42 | }); 43 | }); 44 | 45 | describe("first", () => { 46 | it("returns nothing for an empty array", () => { 47 | expect(first([])).toBeUndefined(); 48 | expect(first([undefined, undefined])).toBeUndefined(); 49 | }); 50 | 51 | it("returns the first value in the array", () => { 52 | expect(first([3, "ohai"])).toBe(3); 53 | expect(first([undefined, 3, "ohai"])).toBe(3); 54 | expect(first(["ohai", 3])).toBe("ohai"); 55 | }); 56 | 57 | it("first may returns falsey values too", () => { 58 | expect(first([0, 1, 2])).toBe(0); 59 | expect(first([false, true])).toBe(false); 60 | }); 61 | 62 | it("first uses a predicate if provided", () => { 63 | expect(first([0, 1, 2, 3, 4], (n) => !!n)).toBe(1); 64 | expect(first([0, 1, 2, 3, 4], (n) => n > 1)).toBe(2); 65 | expect(first([0, 1, 2, 3, 4], (n) => n < 0)).toBeUndefined(); 66 | expect(first([false, true], (x) => x)).toBe(true); 67 | }); 68 | }); 69 | 70 | describe("flatten", () => { 71 | it("flatten w/ empty list", () => { 72 | expect(Array.from(flatten([]))).toEqual([]); 73 | expect(Array.from(flatten([[], [], [], [], []]))).toEqual([]); 74 | }); 75 | 76 | it("flatten works", () => { 77 | expect( 78 | Array.from( 79 | flatten([ 80 | [1, 2], 81 | [3, 4, 5], 82 | ]), 83 | ), 84 | ).toEqual([1, 2, 3, 4, 5]); 85 | expect(Array.from(flatten(["hi", "ha"]))).toEqual(["h", "i", "h", "a"]); 86 | }); 87 | }); 88 | 89 | describe("intersperse", () => { 90 | it("intersperse on empty sequence", () => { 91 | expect(Array.from(intersperse(0, []))).toEqual([]); 92 | }); 93 | 94 | it("intersperse", () => { 95 | expect(Array.from(intersperse(-1, [13]))).toEqual([13]); 96 | expect(Array.from(intersperse(null, [13, 14]))).toEqual([13, null, 14]); 97 | expect(Array.from(intersperse("foo", [1, 2, 3, 4]))).toEqual([ 98 | 1, 99 | "foo", 100 | 2, 101 | "foo", 102 | 3, 103 | "foo", 104 | 4, 105 | ]); 106 | }); 107 | }); 108 | 109 | describe("itake", () => { 110 | it("itake is tested through take() tests", () => { 111 | // This is okay 112 | }); 113 | }); 114 | 115 | describe("pairwise", () => { 116 | it("does nothing for empty array", () => { 117 | expect(Array.from(pairwise([]))).toEqual([]); 118 | expect(Array.from(pairwise([1]))).toEqual([]); 119 | }); 120 | 121 | it("it returns pairs of input", () => { 122 | expect(Array.from(pairwise([0, 1, 2]))).toEqual([ 123 | [0, 1], 124 | [1, 2], 125 | ]); 126 | expect(Array.from(pairwise([1, 2]))).toEqual([[1, 2]]); 127 | expect(Array.from(pairwise([1, 2, 3, 4]))).toEqual([ 128 | [1, 2], 129 | [2, 3], 130 | [3, 4], 131 | ]); 132 | }); 133 | }); 134 | 135 | describe("partition", () => { 136 | it("partition empty list", () => { 137 | expect(partition([], isEven)).toEqual([[], []]); 138 | }); 139 | 140 | it("partition splits input list into two lists", () => { 141 | const values = [1, -2, 3, 4, 5, 6, 8, 8, 0, -2, -3]; 142 | expect(partition(values, isEven)).toEqual([ 143 | [-2, 4, 6, 8, 8, 0, -2], 144 | [1, 3, 5, -3], 145 | ]); 146 | expect(partition(values, isPositive)).toEqual([ 147 | [1, 3, 4, 5, 6, 8, 8, 0], 148 | [-2, -2, -3], 149 | ]); 150 | }); 151 | }); 152 | 153 | describe("roundrobin", () => { 154 | it("roundrobin on empty list", () => { 155 | expect(Array.from(roundrobin())).toEqual([]); 156 | expect(Array.from(roundrobin([]))).toEqual([]); 157 | expect(Array.from(roundrobin([], []))).toEqual([]); 158 | expect(Array.from(roundrobin([], [], []))).toEqual([]); 159 | expect(Array.from(roundrobin([], [], [], []))).toEqual([]); 160 | }); 161 | 162 | it("roundrobin on equally sized lists", () => { 163 | expect(Array.from(roundrobin([1], [2], [3]))).toEqual([1, 2, 3]); 164 | expect(Array.from(roundrobin([1, 2], [3, 4]))).toEqual([1, 3, 2, 4]); 165 | expect(Array.from(roundrobin("foo", "bar")).join("")).toEqual("fboaor"); 166 | }); 167 | 168 | it("roundrobin on unequally sized lists", () => { 169 | expect(Array.from(roundrobin([1], [], [2, 3, 4]))).toEqual([1, 2, 3, 4]); 170 | expect(Array.from(roundrobin([1, 2, 3, 4, 5], [6, 7]))).toEqual([ 171 | 1, 172 | 6, 173 | 2, 174 | 7, 175 | 3, 176 | 4, 177 | 5, 178 | ]); 179 | expect(Array.from(roundrobin([1, 2, 3], [4], [5, 6, 7, 8]))).toEqual([ 180 | 1, 181 | 4, 182 | 5, 183 | 2, 184 | 6, 185 | 3, 186 | 7, 187 | 8, 188 | ]); 189 | }); 190 | }); 191 | 192 | describe("heads", () => { 193 | it("heads on empty list", () => { 194 | expect(Array.from(heads())).toEqual([]); 195 | expect(Array.from(heads([]))).toEqual([]); 196 | expect(Array.from(heads([], []))).toEqual([]); 197 | expect(Array.from(heads([], [], []))).toEqual([]); 198 | expect(Array.from(heads([], [], [], []))).toEqual([]); 199 | }); 200 | 201 | it("heads on equally sized lists", () => { 202 | expect(Array.from(heads([1], [2], [3]))).toEqual([[1, 2, 3]]); 203 | expect(Array.from(heads([1, 2], [3, 4]))).toEqual([ 204 | [1, 3], 205 | [2, 4], 206 | ]); 207 | expect(Array.from(heads("foo", "bar")).map((s) => s.join(""))).toEqual([ 208 | "fb", 209 | "oa", 210 | "or", 211 | ]); 212 | }); 213 | 214 | it("heads on unequally sized lists", () => { 215 | expect(Array.from(heads([1], [], [2, 3, 4]))).toEqual([[1, 2], [3], [4]]); 216 | expect(Array.from(heads([1, 2, 3, 4, 5], [6, 7]))).toEqual([ 217 | [1, 6], 218 | [2, 7], 219 | [3], 220 | [4], 221 | [5], 222 | ]); 223 | expect(Array.from(heads([1, 2, 3], [4], [5, 6, 7, 8]))).toEqual([ 224 | [1, 4, 5], 225 | [2, 6], 226 | [3, 7], 227 | [8], 228 | ]); 229 | }); 230 | }); 231 | 232 | describe("take", () => { 233 | it("take on empty array", () => { 234 | expect(take(0, [])).toEqual([]); 235 | expect(take(1, [])).toEqual([]); 236 | expect(take(99, [])).toEqual([]); 237 | }); 238 | 239 | it("take on infinite input", () => { 240 | expect(take(5, Math.PI.toString())).toEqual(["3", ".", "1", "4", "1"]); 241 | }); 242 | 243 | it("take on infinite input", () => { 244 | expect(take(0, range(999)).length).toEqual(0); 245 | expect(take(1, range(999)).length).toEqual(1); 246 | expect(take(99, range(999)).length).toEqual(99); 247 | }); 248 | }); 249 | 250 | describe("uniqueJustseen", () => { 251 | it("uniqueJustseen w/ empty list", () => { 252 | expect(Array.from(uniqueJustseen([]))).toEqual([]); 253 | }); 254 | 255 | it("uniqueJustseen", () => { 256 | expect(Array.from(uniqueJustseen([1, 2, 3, 4, 5]))).toEqual([ 257 | 1, 258 | 2, 259 | 3, 260 | 4, 261 | 5, 262 | ]); 263 | expect(Array.from(uniqueJustseen([1, 1, 1, 2, 2]))).toEqual([1, 2]); 264 | expect(Array.from(uniqueJustseen([1, 1, 1, 2, 2, 1, 1, 1, 1]))).toEqual([ 265 | 1, 266 | 2, 267 | 1, 268 | ]); 269 | }); 270 | 271 | it("uniqueEverseen with key function", () => { 272 | expect(Array.from(uniqueJustseen("AaABbBCcaABBb", (s) => s.toLowerCase()))) 273 | .toEqual(["A", "B", "C", "a", "B"]); 274 | }); 275 | }); 276 | 277 | describe("uniqueEverseen", () => { 278 | it("uniqueEverseen w/ empty list", () => { 279 | expect(Array.from(uniqueEverseen([]))).toEqual([]); 280 | }); 281 | 282 | it("uniqueEverseen never emits dupes, but keeps input ordering", () => { 283 | expect(Array.from(uniqueEverseen([1, 2, 3, 4, 5]))).toEqual([ 284 | 1, 285 | 2, 286 | 3, 287 | 4, 288 | 5, 289 | ]); 290 | expect(Array.from(uniqueEverseen([1, 1, 1, 2, 2, 3, 1, 3, 0, 4]))).toEqual([ 291 | 1, 292 | 2, 293 | 3, 294 | 0, 295 | 4, 296 | ]); 297 | expect(Array.from(uniqueEverseen([1, 1, 1, 2, 2, 1, 1, 1, 1]))).toEqual([ 298 | 1, 299 | 2, 300 | ]); 301 | }); 302 | 303 | it("uniqueEverseen with key function", () => { 304 | expect(Array.from(uniqueEverseen("AAAABBBCCDAABBB"))).toEqual([ 305 | "A", 306 | "B", 307 | "C", 308 | "D", 309 | ]); 310 | expect(Array.from(uniqueEverseen("ABCcAb", (s) => s.toLowerCase()))) 311 | .toEqual(["A", "B", "C"]); 312 | expect(Array.from(uniqueEverseen("AbCBBcAb", (s) => s.toLowerCase()))) 313 | .toEqual(["A", "b", "C"]); 314 | }); 315 | }); 316 | -------------------------------------------------------------------------------- /builtins.ts: -------------------------------------------------------------------------------- 1 | import { first } from "./custom.ts"; 2 | import { count, ifilter, imap, izip, izip3, takewhile } from "./itertools.ts"; 3 | import type { Maybe, Predicate, Primitive } from "./types.ts"; 4 | import { 5 | identityPredicate, 6 | keyToCmp, 7 | numberIdentity, 8 | primitiveIdentity, 9 | } from "./utils.ts"; 10 | 11 | /** 12 | * Returns true when all of the items in iterable are truthy. An optional key 13 | * function can be used to define what truthiness means for this specific 14 | * collection. 15 | * 16 | * Examples: 17 | * 18 | * all([]) // => true 19 | * all([0]) // => false 20 | * all([0, 1, 2]) // => false 21 | * all([1, 2, 3]) // => true 22 | * 23 | * Examples with using a key function: 24 | * 25 | * all([2, 4, 6], n => n % 2 === 0) // => true 26 | * all([2, 4, 5], n => n % 2 === 0) // => false 27 | */ 28 | export function all( 29 | iterable: Iterable, 30 | keyFn: Predicate = identityPredicate, 31 | ): boolean { 32 | for (const item of iterable) { 33 | if (!keyFn(item)) { 34 | return false; 35 | } 36 | } 37 | return true; 38 | } 39 | 40 | /** 41 | * Returns true when any of the items in iterable are truthy. An optional key 42 | * function can be used to define what truthiness means for this specific 43 | * collection. 44 | * 45 | * Examples: 46 | * 47 | * any([]) // => false 48 | * any([0]) // => false 49 | * any([0, 1, null, undefined]) // => true 50 | * 51 | * Examples with using a key function: 52 | * 53 | * any([1, 4, 5], n => n % 2 === 0) // => true 54 | * any([{name: 'Bob'}, {name: 'Alice'}], person => person.name.startsWith('C')) // => false 55 | */ 56 | export function any( 57 | iterable: Iterable, 58 | keyFn: Predicate = identityPredicate, 59 | ): boolean { 60 | for (const item of iterable) { 61 | if (keyFn(item)) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | } 67 | 68 | /** 69 | * Returns true when any of the items in the iterable are equal to the target object. 70 | * 71 | * Examples: 72 | * 73 | * contains([], 'whatever') // => false 74 | * contains([3], 42) // => false 75 | * contains([3], 3) // => true 76 | * contains([0, 1, 2], 2) // => true 77 | */ 78 | export function contains(haystack: Iterable, needle: T): boolean { 79 | return any(haystack, (x) => x === needle); 80 | } 81 | 82 | /** 83 | * Returns an iterable of enumeration pairs. Iterable must be a sequence, an 84 | * iterator, or some other object which supports iteration. The elements 85 | * produced by returns a tuple containing a counter value (starting from 0 by 86 | * default) and the values obtained from iterating over given iterable. 87 | * 88 | * Example: 89 | * 90 | * import { enumerate } from 'itertools'; 91 | * 92 | * console.log([...enumerate(['hello', 'world'])]); 93 | * // [0, 'hello'], [1, 'world']] 94 | */ 95 | export function* enumerate( 96 | iterable: Iterable, 97 | start = 0, 98 | ): Iterable<[number, T]> { 99 | let index: number = start; 100 | for (const value of iterable) { 101 | yield [index++, value]; 102 | } 103 | } 104 | 105 | /** 106 | * Non-lazy version of ifilter(). 107 | */ 108 | export function filter( 109 | iterable: Iterable, 110 | predicate: Predicate, 111 | ): Array { 112 | return Array.from(ifilter(iterable, predicate)); 113 | } 114 | 115 | /** 116 | * Returns an iterator object for the given iterable. This can be used to 117 | * manually get an iterator for any iterable datastructure. The purpose and 118 | * main use case of this function is to get a single iterator (a thing with 119 | * state, think of it as a "cursor") which can only be consumed once. 120 | */ 121 | export function iter( 122 | iterable: Iterable | IterableIterator, 123 | ): IterableIterator { 124 | const inner = iterable[Symbol.iterator](); 125 | const it = { 126 | next(): IteratorResult { 127 | return inner.next(); 128 | }, 129 | 130 | [Symbol.iterator]() { 131 | return it; 132 | }, 133 | }; 134 | return it; 135 | } 136 | 137 | /** 138 | * Non-lazy version of imap(). 139 | */ 140 | export function map( 141 | iterable: Iterable, 142 | mapper: (v: T) => V, 143 | ): Array { 144 | return Array.from(imap(iterable, mapper)); 145 | } 146 | 147 | /** 148 | * Return the largest item in an iterable. Only works for numbers, as ordering 149 | * is pretty poorly defined on any other data type in JS. The optional `keyFn` 150 | * argument specifies a one-argument ordering function like that used for 151 | * sorted(). 152 | * 153 | * If the iterable is empty, `undefined` is returned. 154 | * 155 | * If multiple items are maximal, the function returns either one of them, but 156 | * which one is not defined. 157 | */ 158 | export function max( 159 | iterable: Iterable, 160 | keyFn: (v: T) => number = numberIdentity, 161 | ): Maybe { 162 | return reduce_(iterable, (x, y) => (keyFn(x) > keyFn(y) ? x : y)); 163 | } 164 | 165 | /** 166 | * Return the smallest item in an iterable. Only works for numbers, as 167 | * ordering is pretty poorly defined on any other data type in JS. The 168 | * optional `keyFn` argument specifies a one-argument ordering function like 169 | * that used for sorted(). 170 | * 171 | * If the iterable is empty, `undefined` is returned. 172 | * 173 | * If multiple items are minimal, the function returns either one of them, but 174 | * which one is not defined. 175 | */ 176 | export function min( 177 | iterable: Iterable, 178 | keyFn: (v: T) => number = numberIdentity, 179 | ): Maybe { 180 | return reduce_(iterable, (x, y) => (keyFn(x) < keyFn(y) ? x : y)); 181 | } 182 | 183 | /** 184 | * Internal helper for the range function 185 | */ 186 | function _range(start: number, stop: number, step: number): Iterable { 187 | if (step === 0) { 188 | throw new Error("range() arg 3 must not be zero"); 189 | } 190 | const counter = count(start, step); 191 | const pred = step > 0 ? (n: number) => n < stop : (n: number) => n > stop; 192 | return takewhile(counter, pred); 193 | } 194 | 195 | /** 196 | * Returns an iterator producing all the numbers in the given range one by one, 197 | * starting from `start` (default 0), as long as `i < stop`, in increments of 198 | * `step` (default 1). 199 | * 200 | * `range(a)` is a convenient shorthand for `range(0, a)`. 201 | * 202 | * Various valid invocations: 203 | * 204 | * range(5) // [0, 1, 2, 3, 4] 205 | * range(2, 5) // [2, 3, 4] 206 | * range(0, 5, 2) // [0, 2, 4] 207 | * range(5, 0, -1) // [5, 4, 3, 2, 1] 208 | * range(-3) // [] 209 | * 210 | * For a positive `step`, the iterator will keep producing values `n` as long 211 | * as the stop condition `n < stop` is satisfied. 212 | * 213 | * For a negative `step`, the iterator will keep producing values `n` as long 214 | * as the stop condition `n > stop` is satisfied. 215 | * 216 | * The produced range will be empty if the first value to produce already does 217 | * not meet the value constraint. 218 | */ 219 | export function range(a: number, ...rest: Array): Iterable { 220 | const args = [a, ...rest]; // "a" was only used by Flow to make at least one value mandatory 221 | switch (args.length) { 222 | case 1: 223 | return _range(0, args[0], 1); 224 | case 2: 225 | return _range(args[0], args[1], 1); 226 | case 3: 227 | return _range(args[0], args[1], args[2]); 228 | /* istanbul ignore next */ 229 | default: 230 | throw new Error("invalid number of arguments"); 231 | } 232 | } 233 | 234 | /** 235 | * Apply function of two arguments cumulatively to the items of sequence, from 236 | * left to right, so as to reduce the sequence to a single value. For example: 237 | * 238 | * reduce([1, 2, 3, 4, 5], (x, y) => x + y, 0) 239 | * 240 | * calculates 241 | * 242 | * (((((0+1)+2)+3)+4)+5) 243 | * 244 | * The left argument, `x`, is the accumulated value and the right argument, 245 | * `y`, is the update value from the sequence. 246 | * 247 | * **Difference between `reduce()` and `reduce\_()`**: `reduce()` requires an 248 | * explicit initializer, whereas `reduce_()` will automatically use the first 249 | * item in the given iterable as the initializer. When using `reduce()`, the 250 | * initializer value is placed before the items of the sequence in the 251 | * calculation, and serves as a default when the sequence is empty. When using 252 | * `reduce_()`, and the given iterable is empty, then no default value can be 253 | * derived and `undefined` will be returned. 254 | */ 255 | export function reduce( 256 | iterable: Iterable, 257 | reducer: (a: O, v: T, i: number) => O, 258 | start: O, 259 | ): O { 260 | let output = start; 261 | for (const [index, item] of enumerate(iterable)) { 262 | output = reducer(output, item, index); 263 | } 264 | return output; 265 | } 266 | 267 | /** 268 | * See reduce(). 269 | */ 270 | export function reduce_( 271 | iterable: Iterable, 272 | reducer: (a: T, v: T, i: number) => T, 273 | ): Maybe { 274 | const it = iter(iterable); 275 | const start = first(it); 276 | if (start === undefined) { 277 | return undefined; 278 | } else { 279 | return reduce(it, reducer, start); 280 | } 281 | } 282 | 283 | /** 284 | * Return a new sorted list from the items in iterable. 285 | * 286 | * Has two optional arguments: 287 | * 288 | * * `keyFn` specifies a function of one argument providing a primitive 289 | * identity for each element in the iterable. that will be used to compare. 290 | * The default value is to use a default identity function that is only 291 | * defined for primitive types. 292 | * 293 | * * `reverse` is a boolean value. If `true`, then the list elements are 294 | * sorted as if each comparison were reversed. 295 | */ 296 | export function sorted( 297 | iterable: Iterable, 298 | keyFn: (v: T) => Primitive = primitiveIdentity, 299 | reverse = false, 300 | ): Array { 301 | const result = Array.from(iterable); 302 | result.sort(keyToCmp(keyFn)); // sort in-place 303 | 304 | if (reverse) { 305 | result.reverse(); // reverse in-place 306 | } 307 | 308 | return result; 309 | } 310 | 311 | /** 312 | * Sums the items of an iterable from left to right and returns the total. The 313 | * sum will defaults to 0 if the iterable is empty. 314 | */ 315 | export function sum(iterable: Iterable): number { 316 | return reduce(iterable, (x, y) => x + y, 0); 317 | } 318 | 319 | /** 320 | * See izip. 321 | */ 322 | export function zip( 323 | xs: Iterable, 324 | ys: Iterable, 325 | ): Array<[T1, T2]> { 326 | return Array.from(izip(xs, ys)); 327 | } 328 | 329 | /** 330 | * See izip3. 331 | */ 332 | export function zip3( 333 | xs: Iterable, 334 | ys: Iterable, 335 | zs: Iterable, 336 | ): Array<[T1, T2, T3]> { 337 | return Array.from(izip3(xs, ys, zs)); 338 | } 339 | -------------------------------------------------------------------------------- /builtins.test.ts: -------------------------------------------------------------------------------- 1 | import { assertThrows } from "@std/assert"; 2 | import { describe, it } from "@std/testing/bdd"; 3 | import { expect } from "@std/expect"; 4 | import { 5 | all, 6 | any, 7 | contains, 8 | enumerate, 9 | filter, 10 | iter, 11 | map, 12 | max, 13 | min, 14 | range, 15 | reduce, 16 | sorted, 17 | sum, 18 | zip, 19 | zip3, 20 | } from "./builtins.ts"; 21 | import { first } from "./custom.ts"; 22 | 23 | const isEven = (n: number) => n % 2 === 0; 24 | 25 | describe("all", () => { 26 | it("all of empty list is true", () => { 27 | expect(all([])).toBe(true); 28 | }); 29 | 30 | it("all is true if all elements are truthy", () => { 31 | expect(all([1])).toBe(true); 32 | expect(all([1, 2, 3])).toBe(true); 33 | }); 34 | 35 | it("all is false if any elements are not truthy", () => { 36 | expect(all([0, 1])).toBe(false); 37 | expect(all([1, 2, undefined, 3, 4])).toBe(false); 38 | }); 39 | }); 40 | 41 | describe("any", () => { 42 | it("any of empty list is false", () => { 43 | expect(any([])).toBe(false); 44 | }); 45 | 46 | it("any is true if any elements are truthy", () => { 47 | expect(any([1, 2, 3])).toBe(true); 48 | expect(any([0, 1])).toBe(true); 49 | expect(any([1, 0])).toBe(true); 50 | expect(any([0, undefined, NaN, 1])).toBe(true); 51 | }); 52 | 53 | it("any is false if no elements are truthy", () => { 54 | expect(any([0, null, NaN, undefined])).toBe(false); 55 | }); 56 | }); 57 | 58 | describe("contains", () => { 59 | it("contains of empty list is false", () => { 60 | expect(contains([], 0)).toBe(false); 61 | expect(contains([], 1)).toBe(false); 62 | expect(contains([], null)).toBe(false); 63 | expect(contains([], undefined)).toBe(false); 64 | }); 65 | 66 | it("contains is true iff iterable contains the given exact value", () => { 67 | expect(contains([1], 1)).toBe(true); 68 | expect(contains([1], 2)).toBe(false); 69 | expect(contains([1, 2, 3], 1)).toBe(true); 70 | expect(contains([1, 2, 3], 2)).toBe(true); 71 | expect(contains([1, 2, 3], 3)).toBe(true); 72 | expect(contains([1, 2, 3], 4)).toBe(false); 73 | }); 74 | 75 | it("contains does not work for elements with identity equality", () => { 76 | expect(contains([{}], {})).toBe(false); 77 | expect(contains([{ x: 123 }], { x: 123 })).toBe(false); 78 | expect(contains([[1, 2, 3]], [1, 2, 3])).toBe(false); 79 | }); 80 | }); 81 | 82 | describe("enumerate", () => { 83 | it("enumerate empty list", () => { 84 | expect(Array.from(enumerate([]))).toEqual([]); 85 | }); 86 | 87 | it("enumerate attaches indexes", () => { 88 | // We'll have to wrap it in a take() call to avoid infinite-length arrays :) 89 | expect(Array.from(enumerate(["x"]))).toEqual([[0, "x"]]); 90 | expect(Array.from(enumerate(["even", "odd"]))).toEqual([ 91 | [0, "even"], 92 | [1, "odd"], 93 | ]); 94 | }); 95 | 96 | it("enumerate from 3 up", () => { 97 | expect(Array.from(enumerate("abc", 3))).toEqual([ 98 | [3, "a"], 99 | [4, "b"], 100 | [5, "c"], 101 | ]); 102 | }); 103 | }); 104 | 105 | describe("filter", () => { 106 | it("filters empty list", () => { 107 | expect(filter([], isEven)).toEqual([]); 108 | }); 109 | 110 | it("ifilter works like Array.filter, but lazy", () => { 111 | expect(filter([0, 1, 2, 3], isEven)).toEqual([0, 2]); 112 | }); 113 | }); 114 | 115 | describe("iter", () => { 116 | it("iter makes any iterable a one-time iterable", () => { 117 | expect( 118 | Array.from( 119 | iter( 120 | new Map([ 121 | [1, "x"], 122 | [2, "y"], 123 | [3, "z"], 124 | ]), 125 | ), 126 | ), 127 | ).toEqual([ 128 | [1, "x"], 129 | [2, "y"], 130 | [3, "z"], 131 | ]); 132 | expect(Array.from(iter([1, 2, 3]))).toEqual([1, 2, 3]); 133 | expect(Array.from(iter(new Set([1, 2, 3])))).toEqual([1, 2, 3]); 134 | }); 135 | 136 | it("iter results can be consumed only once", () => { 137 | const it = iter([1, 2, 3]); 138 | expect(it.next()).toEqual({ value: 1, done: false }); 139 | expect(it.next()).toEqual({ value: 2, done: false }); 140 | expect(it.next()).toEqual({ value: 3, done: false }); 141 | expect(it.next()).toEqual({ value: undefined, done: true }); 142 | 143 | // Keeps returning "done" when exhausted... 144 | expect(it.next()).toEqual({ value: undefined, done: true }); 145 | expect(it.next()).toEqual({ value: undefined, done: true }); 146 | // ... 147 | }); 148 | 149 | it("iter results can be consumed in pieces", () => { 150 | const it = iter([1, 2, 3, 4, 5]); 151 | expect(first(it)).toBe(1); 152 | expect(first(it)).toBe(2); 153 | expect(first(it)).toBe(3); 154 | expect(Array.from(it)).toEqual([4, 5]); 155 | 156 | // Keeps returning "[]" when exhausted... 157 | expect(Array.from(it)).toEqual([]); 158 | expect(Array.from(it)).toEqual([]); 159 | // ... 160 | }); 161 | 162 | it("wrapping iter()s has no effect on the iterator's state", () => { 163 | const originalIter = iter([1, 2, 3, 4, 5]); 164 | expect(first(originalIter)).toBe(1); 165 | 166 | const wrappedIter = iter(iter(iter(originalIter))); 167 | expect(first(wrappedIter)).toBe(2); 168 | expect(first(iter(wrappedIter))).toBe(3); 169 | expect(Array.from(iter(originalIter))).toEqual([4, 5]); 170 | 171 | // Keeps returning "[]" when exhausted... 172 | expect(Array.from(originalIter)).toEqual([]); 173 | expect(Array.from(wrappedIter)).toEqual([]); 174 | // ... 175 | }); 176 | }); 177 | 178 | describe("map", () => { 179 | it("map on empty iterable", () => { 180 | expect(map([], (x) => x)).toEqual([]); 181 | }); 182 | 183 | it("imap works like Array.map, but lazy", () => { 184 | expect(map([1, 2, 3], (x) => x)).toEqual([1, 2, 3]); 185 | expect(map([1, 2, 3], (x) => 2 * x)).toEqual([2, 4, 6]); 186 | expect(map([1, 2, 3], (x) => x.toString())).toEqual(["1", "2", "3"]); 187 | }); 188 | }); 189 | 190 | describe("max", () => { 191 | it("can't take max of empty list", () => { 192 | expect(max([])).toBeUndefined(); 193 | }); 194 | 195 | it("max of single-item array", () => { 196 | expect(max([1])).toEqual(1); 197 | expect(max([2])).toEqual(2); 198 | expect(max([5])).toEqual(5); 199 | }); 200 | 201 | it("max of multi-item array", () => { 202 | expect(max([1, 2, 3])).toEqual(3); 203 | expect(max([2, 2, 2, 2])).toEqual(2); 204 | expect(max([5, 4, 3, 2, 1])).toEqual(5); 205 | expect(max([-3, -2, -1])).toEqual(-1); 206 | }); 207 | 208 | it("max of multi-item array with key function", () => { 209 | expect(max([{ n: 2 }, { n: 3 }, { n: 1 }], (o) => o.n)).toEqual({ n: 3 }); 210 | }); 211 | }); 212 | 213 | describe("min", () => { 214 | it("can't take min of empty list", () => { 215 | expect(min([])).toBeUndefined(); 216 | }); 217 | 218 | it("min of single-item array", () => { 219 | expect(min([1])).toEqual(1); 220 | expect(min([2])).toEqual(2); 221 | expect(min([5])).toEqual(5); 222 | }); 223 | 224 | it("min of multi-item array", () => { 225 | expect(min([1, 2, 3])).toEqual(1); 226 | expect(min([2, 2, 2, 2])).toEqual(2); 227 | expect(min([5, 4, 3, 2, 1])).toEqual(1); 228 | expect(min([-3, -2, -1])).toEqual(-3); 229 | }); 230 | 231 | it("min of multi-item array with key function", () => { 232 | expect(min([{ n: 2 }, { n: 3 }, { n: 1 }], (o) => o.n)).toEqual({ n: 1 }); 233 | }); 234 | }); 235 | 236 | describe("range", () => { 237 | it("range with end", () => { 238 | expect(Array.from(range(0))).toEqual([]); 239 | expect(Array.from(range(1))).toEqual([0]); 240 | expect(Array.from(range(2))).toEqual([0, 1]); 241 | expect(Array.from(range(5))).toEqual([0, 1, 2, 3, 4]); 242 | expect(Array.from(range(-1))).toEqual([]); 243 | }); 244 | 245 | it("range with start and end", () => { 246 | expect(Array.from(range(3, 5))).toEqual([3, 4]); 247 | expect(Array.from(range(4, 7))).toEqual([4, 5, 6]); 248 | 249 | // If end < start, then range is empty 250 | expect(Array.from(range(5, 1))).toEqual([]); 251 | }); 252 | 253 | it("range with start, end, and step", () => { 254 | expect(Array.from(range(3, 9, 3))).toEqual([3, 6]); 255 | expect(Array.from(range(3, 10, 3))).toEqual([3, 6, 9]); 256 | expect(Array.from(range(5, 1, -1))).toEqual([5, 4, 3, 2]); 257 | expect(Array.from(range(5, -3, -2))).toEqual([5, 3, 1, -1]); 258 | }); 259 | 260 | it("step cannot be 0", () => { 261 | assertThrows( 262 | () => { 263 | range(0, 1, 0); 264 | }, 265 | Error, 266 | "range() arg 3 must not be zero", 267 | ); 268 | }); 269 | }); 270 | 271 | describe("reduce", () => { 272 | const adder = (x: number, y: number) => x + y; 273 | 274 | it("reduce on empty list returns start value", () => { 275 | expect(reduce([], adder, 0)).toEqual(0); 276 | expect(reduce([], adder, 13)).toEqual(13); 277 | }); 278 | 279 | it("reduce on list with only one item", () => { 280 | expect(reduce([5], adder, 0)).toEqual(5); 281 | expect(reduce([5], adder, 13)).toEqual(18); 282 | }); 283 | 284 | it("reduce on list with multiple items", () => { 285 | expect(reduce([1, 2, 3, 4], adder, 0)).toEqual(10); 286 | expect(reduce([1, 2, 3, 4, 5], adder, 13)).toEqual(28); 287 | }); 288 | }); 289 | 290 | describe("sorted", () => { 291 | it("sorted w/ empty list", () => { 292 | expect(sorted([])).toEqual([]); 293 | }); 294 | 295 | it("sorted values", () => { 296 | expect(sorted([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]); 297 | expect(sorted([2, 1, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]); 298 | expect(sorted([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]); 299 | 300 | // Explicitly test numeral ordering... in plain JS the following is true: 301 | // [4, 44, 100, 80, 9].sort() ~~> [100, 4, 44, 80, 9] 302 | expect(sorted([4, 44, 100, 80, 9])).toEqual([4, 9, 44, 80, 100]); 303 | expect(sorted(["4", "44", "100", "80", "44", "9"])).toEqual([ 304 | "100", 305 | "4", 306 | "44", 307 | "44", 308 | "80", 309 | "9", 310 | ]); 311 | expect(sorted([false, true, true, false])).toEqual([ 312 | false, 313 | false, 314 | true, 315 | true, 316 | ]); 317 | }); 318 | 319 | it("sorted does not modify input", () => { 320 | const values = [4, 0, -3, 7, 1]; 321 | expect(sorted(values)).toEqual([-3, 0, 1, 4, 7]); 322 | expect(values).toEqual([4, 0, -3, 7, 1]); 323 | }); 324 | 325 | it("sorted in reverse", () => { 326 | expect(sorted([2, 1, 3, 4, 5], undefined, true)).toEqual([5, 4, 3, 2, 1]); 327 | }); 328 | }); 329 | 330 | describe("sum", () => { 331 | it("sum w/ empty iterable", () => { 332 | expect(sum([])).toEqual(0); 333 | }); 334 | 335 | it("sum works", () => { 336 | expect(sum([1, 2, 3, 4, 5, 6])).toEqual(21); 337 | expect(sum([-3, -2, -1, 0, 1, 2, 3])).toEqual(0); 338 | // expect(sum([0.1, 0.2, 0.3])).toBeCloseTo(0.6); 339 | }); 340 | }); 341 | 342 | describe("zip", () => { 343 | it("zip with empty iterable", () => { 344 | expect(zip([], [])).toEqual([]); 345 | expect(zip("abc", [])).toEqual([]); 346 | }); 347 | 348 | it("izip with two iterables", () => { 349 | expect(zip("abc", "ABC")).toEqual([ 350 | ["a", "A"], 351 | ["b", "B"], 352 | ["c", "C"], 353 | ]); 354 | }); 355 | 356 | it("izip with three iterables", () => { 357 | expect(zip3("abc", "ABC", [5, 4, 3])).toEqual([ 358 | ["a", "A", 5], 359 | ["b", "B", 4], 360 | ["c", "C", 3], 361 | ]); 362 | }); 363 | 364 | it("izip different input lengths", () => { 365 | // Shortest lengthed input determines result length 366 | expect(zip("a", "ABC")).toEqual([["a", "A"]]); 367 | expect(zip3("", "ABCD", "PQR")).toEqual([]); 368 | }); 369 | }); 370 | -------------------------------------------------------------------------------- /itertools.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from "@std/testing/bdd"; 2 | import { expect } from "@std/expect"; 3 | import { all, iter, range } from "./builtins.ts"; 4 | import { 5 | chain, 6 | compress, 7 | count, 8 | cycle, 9 | dropwhile, 10 | groupby, 11 | ifilter, 12 | imap, 13 | islice, 14 | permutations, 15 | repeat, 16 | takewhile, 17 | zipLongest, 18 | zipLongest3, 19 | zipMany, 20 | } from "./itertools.ts"; 21 | import { take } from "./more-itertools.ts"; 22 | 23 | const isEven = (x: number) => x % 2 === 0; 24 | const isPositive = (x: number) => x >= 0; 25 | 26 | describe("chain", () => { 27 | it("chains empty iterables", () => { 28 | expect(Array.from(chain([], []))).toEqual([]); 29 | }); 30 | 31 | it("chains iterables together", () => { 32 | expect(Array.from(chain(["foo"], []))).toEqual(["foo"]); 33 | expect(Array.from(chain([], ["bar"]))).toEqual(["bar"]); 34 | expect(Array.from(chain([], ["bar"], ["qux"]))).toEqual(["bar", "qux"]); 35 | expect(Array.from(chain(["foo", "bar"], ["qux"]))).toEqual([ 36 | "foo", 37 | "bar", 38 | "qux", 39 | ]); 40 | }); 41 | }); 42 | 43 | describe("compress", () => { 44 | it("compress on empty list", () => { 45 | expect(compress([], [])).toEqual([]); 46 | }); 47 | 48 | it("compress removes selected items", () => { 49 | expect(compress("abc", [])).toEqual([]); 50 | expect(compress("abc", [true])).toEqual(["a"]); 51 | expect(compress("abc", [false, false, false])).toEqual([]); 52 | expect(compress("abc", [true, false, true])).toEqual(["a", "c"]); 53 | }); 54 | }); 55 | 56 | describe("count", () => { 57 | it("default counter", () => { 58 | expect(take(6, count())).toEqual([0, 1, 2, 3, 4, 5]); 59 | }); 60 | 61 | it("counter from different start value", () => { 62 | expect(take(6, count(1))).toEqual([1, 2, 3, 4, 5, 6]); 63 | expect(take(6, count(-3))).toEqual([-3, -2, -1, 0, 1, 2]); 64 | }); 65 | 66 | it("counter backwards", () => { 67 | expect(take(6, count(4, -1))).toEqual([4, 3, 2, 1, 0, -1]); 68 | expect(take(5, count(-3, -2))).toEqual([-3, -5, -7, -9, -11]); 69 | }); 70 | }); 71 | 72 | describe("cycle", () => { 73 | it("cycle with empty list", () => { 74 | expect(Array.from(cycle([]))).toEqual([]); 75 | }); 76 | 77 | it("cycles", () => { 78 | // We'll have to wrap it in a take() call to avoid infinite-length arrays :) 79 | expect(take(3, cycle(["x"]))).toEqual(["x", "x", "x"]); 80 | expect(take(5, cycle(["even", "odd"]))).toEqual([ 81 | "even", 82 | "odd", 83 | "even", 84 | "odd", 85 | "even", 86 | ]); 87 | }); 88 | 89 | it("cycles with infinite iterable", () => { 90 | // Function `cycle` should properly work with infinite iterators (`repeat('x')` in this case) 91 | expect(take(3, cycle(repeat("x")))).toEqual(["x", "x", "x"]); 92 | }); 93 | }); 94 | 95 | describe("dropwhile", () => { 96 | it("dropwhile on empty list", () => { 97 | expect(Array.from(dropwhile([], isEven))).toEqual([]); 98 | expect(Array.from(dropwhile([], isPositive))).toEqual([]); 99 | }); 100 | 101 | it("dropwhile on list", () => { 102 | expect(Array.from(dropwhile([1], isEven))).toEqual([1]); 103 | expect(Array.from(dropwhile([1], isPositive))).toEqual([]); 104 | 105 | expect(Array.from(dropwhile([-1, 0, 1], isEven))).toEqual([-1, 0, 1]); 106 | expect(Array.from(dropwhile([4, -1, 0, 1], isEven))).toEqual([-1, 0, 1]); 107 | expect(Array.from(dropwhile([-1, 0, 1], isPositive))).toEqual([-1, 0, 1]); 108 | expect(Array.from(dropwhile([7, -1, 0, 1], isPositive))).toEqual([ 109 | -1, 110 | 0, 111 | 1, 112 | ]); 113 | 114 | expect(Array.from(dropwhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([ 115 | 7, 116 | 8, 117 | 10, 118 | ]); 119 | expect(Array.from(dropwhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))) 120 | .toEqual([-2, 3, 4, 5, 6, 7]); 121 | }); 122 | }); 123 | 124 | describe("groupby", () => { 125 | // deno-lint-ignore no-explicit-any 126 | const countValues = (grouped: Iterable) => 127 | Array.from(imap(grouped, ([k, v]) => [k, Array.from(v).length])); 128 | 129 | it("groupby with empty list", () => { 130 | expect(Array.from(groupby([]))).toEqual([]); 131 | }); 132 | 133 | it("groups elements", () => { 134 | expect(countValues(groupby("aaabbbbcddddaa"))).toEqual([ 135 | ["a", 3], 136 | ["b", 4], 137 | ["c", 1], 138 | ["d", 4], 139 | ["a", 2], 140 | ]); 141 | }); 142 | 143 | it("groups element with key function", () => { 144 | expect(countValues(groupby("aaaAbb"))).toEqual([ 145 | ["a", 3], 146 | ["A", 1], 147 | ["b", 2], 148 | ]); 149 | expect(countValues(groupby("aaaAbb", (val) => val.toUpperCase()))).toEqual([ 150 | ["A", 4], 151 | ["B", 2], 152 | ]); 153 | }); 154 | 155 | it("handles not using the inner iterator", () => { 156 | expect(Array.from(imap(groupby("aaabbbbcddddaa"), ([k]) => k))).toEqual([ 157 | "a", 158 | "b", 159 | "c", 160 | "d", 161 | "a", 162 | ]); 163 | }); 164 | 165 | it("handles using the inner iterator after the iteration has advanced", () => { 166 | expect(Array.from(groupby("aaabb")).map(([, v]) => Array.from(v))).toEqual([ 167 | [], 168 | [], 169 | ]); 170 | const it = iter(groupby("aaabbccc")); 171 | // Flow does not like that I use next on an iterable (it is actually 172 | // a generator but the Generator type is awful. 173 | 174 | // $FlowFixMe[prop-missing] 175 | const [, v1] = it.next().value; 176 | // $FlowFixMe[prop-missing] 177 | const [, v2] = it.next().value; 178 | // $FlowFixMe[prop-missing] 179 | const [, v3] = it.next().value; 180 | 181 | expect([...v1]).toEqual([]); 182 | expect([...v2]).toEqual([]); 183 | expect(v3.next().value).toEqual("c"); 184 | Array.from(it); // exhaust the groupby iterator 185 | expect([...v3]).toEqual([]); 186 | }); 187 | }); 188 | 189 | describe("icompress", () => { 190 | it("icompress is tested through compress() tests", () => { 191 | // This is okay 192 | }); 193 | }); 194 | 195 | describe("ifilter", () => { 196 | it("ifilter is tested through filter() tests (see builtins)", () => { 197 | // This is okay 198 | }); 199 | 200 | it("ifilter can handle infinite inputs", () => { 201 | expect(take(5, ifilter(range(9999), isEven))).toEqual([0, 2, 4, 6, 8]); 202 | }); 203 | }); 204 | 205 | describe("imap", () => { 206 | it("imap is tested through map() tests (see builtins)", () => { 207 | // This is okay 208 | }); 209 | 210 | it("...but imap can handle infinite inputs", () => { 211 | expect( 212 | take( 213 | 3, 214 | imap(range(9999), (x) => -x), 215 | ), 216 | ).toEqual([-0, -1, -2]); 217 | }); 218 | }); 219 | 220 | describe("islice", () => { 221 | it("islice an empty iterable", () => { 222 | expect(Array.from(islice([], 2))).toEqual([]); 223 | }); 224 | 225 | it("islice with arguments", () => { 226 | expect(Array.from(islice("ABCDEFG", /*stop*/ 2))).toEqual(["A", "B"]); 227 | expect(Array.from(islice("ABCDEFG", 2, 4))).toEqual(["C", "D"]); 228 | expect(Array.from(islice("ABCDEFG", /*start*/ 2, /*stop*/ null))).toEqual([ 229 | "C", 230 | "D", 231 | "E", 232 | "F", 233 | "G", 234 | ]); 235 | expect( 236 | Array.from(islice("ABCDEFG", /*start*/ 0, /*stop*/ null, /*step*/ 2)), 237 | ).toEqual(["A", "C", "E", "G"]); 238 | expect( 239 | Array.from(islice("ABCDEFG", /*start*/ 1, /*stop*/ null, /*step*/ 2)), 240 | ).toEqual(["B", "D", "F"]); 241 | }); 242 | }); 243 | 244 | describe("izip", () => { 245 | it("izip is tested through zip() tests (see builtins)", () => { 246 | // This is okay 247 | }); 248 | }); 249 | 250 | describe("izip3", () => { 251 | it("izip3 is tested through zip3() tests (see builtins)", () => { 252 | // This is okay 253 | }); 254 | }); 255 | 256 | describe("izipMany", () => { 257 | it("izipMany is tested through zipMany() tests", () => { 258 | // This is okay 259 | }); 260 | }); 261 | 262 | describe("izipLongest", () => { 263 | it("izipLongest is tested through zipLongest() tests", () => { 264 | // This is okay 265 | }); 266 | }); 267 | 268 | describe("permutations", () => { 269 | it("permutations of empty list", () => { 270 | expect(Array.from(permutations([]))).toEqual([[]]); 271 | }); 272 | 273 | it("permutations of unique values", () => { 274 | expect(Array.from(permutations([1, 2]))).toEqual([ 275 | [1, 2], 276 | [2, 1], 277 | ]); 278 | 279 | expect(Array.from(permutations([1, 2, 3]))).toEqual([ 280 | [1, 2, 3], 281 | [1, 3, 2], 282 | [2, 1, 3], 283 | [2, 3, 1], 284 | [3, 1, 2], 285 | [3, 2, 1], 286 | ]); 287 | 288 | // Duplicates have no effect on the results 289 | expect(Array.from(permutations([2, 2, 3]))).toEqual([ 290 | [2, 2, 3], 291 | [2, 3, 2], 292 | [2, 2, 3], 293 | [2, 3, 2], 294 | [3, 2, 2], 295 | [3, 2, 2], 296 | ]); 297 | }); 298 | 299 | it("permutations with r param", () => { 300 | // r too big 301 | expect(Array.from(permutations([1, 2], 5))).toEqual([]); 302 | 303 | // prettier-ignore 304 | expect(Array.from(permutations(range(4), 2))).toEqual([ 305 | [0, 1], 306 | [0, 2], 307 | [0, 3], 308 | [1, 0], 309 | [1, 2], 310 | [1, 3], 311 | [2, 0], 312 | [2, 1], 313 | [2, 3], 314 | [3, 0], 315 | [3, 1], 316 | [3, 2], 317 | ]); 318 | }); 319 | }); 320 | 321 | describe("repeat", () => { 322 | it("repeat indefinitely", () => { 323 | // practically limit it to something (in this case 99) 324 | const items1 = take(99, repeat(123)); 325 | expect(all(items1, (n) => n === 123)).toEqual(true); 326 | 327 | const items2 = take(99, repeat("foo")); 328 | expect(all(items2, (n) => n === "foo")).toEqual(true); 329 | }); 330 | 331 | it("repeat a fixed number of times", () => { 332 | const items = repeat("foo", 100); 333 | expect(all(items, (n) => n === "foo")).toEqual(true); 334 | }); 335 | }); 336 | 337 | describe("takewhile", () => { 338 | it("takewhile on empty list", () => { 339 | expect(Array.from(takewhile([], isEven))).toEqual([]); 340 | expect(Array.from(takewhile([], isPositive))).toEqual([]); 341 | }); 342 | 343 | it("takewhile on list", () => { 344 | expect(Array.from(takewhile([1], isEven))).toEqual([]); 345 | expect(Array.from(takewhile([1], isPositive))).toEqual([1]); 346 | 347 | expect(Array.from(takewhile([-1, 0, 1], isEven))).toEqual([]); 348 | expect(Array.from(takewhile([-1, 0, 1], isPositive))).toEqual([]); 349 | 350 | expect(Array.from(takewhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([ 351 | 0, 352 | 2, 353 | 4, 354 | 6, 355 | ]); 356 | expect(Array.from(takewhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))) 357 | .toEqual([0, 1, 2]); 358 | }); 359 | }); 360 | 361 | describe("zipMany", () => { 362 | it("zipMany with empty iterable", () => { 363 | expect(zipMany([])).toEqual([]); 364 | expect(zipMany([], [])).toEqual([]); 365 | }); 366 | 367 | it("zipMany takes any number of (homogenous) iterables", () => { 368 | expect(zipMany("abc", "ABC")).toEqual([ 369 | ["a", "A"], 370 | ["b", "B"], 371 | ["c", "C"], 372 | ]); 373 | expect(zipMany("abc", "ABC", "pqrs", "xyz")).toEqual([ 374 | ["a", "A", "p", "x"], 375 | ["b", "B", "q", "y"], 376 | ["c", "C", "r", "z"], 377 | ]); 378 | }); 379 | }); 380 | 381 | describe("zipLongest", () => { 382 | it("zipLongest with empty iterable", () => { 383 | expect(zipLongest([], [])).toEqual([]); 384 | }); 385 | 386 | it("zipLongest with two iterables", () => { 387 | expect(zipLongest("abc", "")).toEqual([ 388 | ["a", undefined], 389 | ["b", undefined], 390 | ["c", undefined], 391 | ]); 392 | expect(zipLongest("x", "abc")).toEqual([ 393 | ["x", "a"], 394 | [undefined, "b"], 395 | [undefined, "c"], 396 | ]); 397 | expect(zipLongest("x", "abc", /* filler */ 0)).toEqual([ 398 | ["x", "a"], 399 | [0, "b"], 400 | [0, "c"], 401 | ]); 402 | }); 403 | }); 404 | 405 | describe("zipLongest3", () => { 406 | it("zipLongest3 with empty iterable", () => { 407 | expect(zipLongest3([], [], [])).toEqual([]); 408 | }); 409 | 410 | it("zipLongest3 with two iterables", () => { 411 | expect(zipLongest3("abc", "", [1, 2, 3])).toEqual([ 412 | ["a", undefined, 1], 413 | ["b", undefined, 2], 414 | ["c", undefined, 3], 415 | ]); 416 | expect(zipLongest3("x", "abc", [1, 2, 3])).toEqual([ 417 | ["x", "a", 1], 418 | [undefined, "b", 2], 419 | [undefined, "c", 3], 420 | ]); 421 | expect(zipLongest3("x", "abc", [1, 2], /* filler */ 0)).toEqual([ 422 | ["x", "a", 1], 423 | [0, "b", 2], 424 | [0, "c", 0], 425 | ]); 426 | }); 427 | }); 428 | -------------------------------------------------------------------------------- /itertools.ts: -------------------------------------------------------------------------------- 1 | import { all, enumerate, iter, range } from "./builtins.ts"; 2 | import { flatten } from "./more-itertools.ts"; 3 | import type { Maybe, Predicate, Primitive } from "./types.ts"; 4 | import { primitiveIdentity } from "./utils.ts"; 5 | 6 | const SENTINEL = Symbol(); 7 | 8 | function composeAnd( 9 | f1: (v: number) => boolean, 10 | f2: (v: number) => boolean, 11 | ): (v: number) => boolean { 12 | return (n: number) => f1(n) && f2(n); 13 | } 14 | 15 | function slicePredicate( 16 | start: number, 17 | stop: number | null | undefined = undefined, 18 | step: number, 19 | ) { 20 | // If stop is not provided (= undefined), then interpret the start value as the stop value 21 | let _start = start, _stop = stop; 22 | const _step = step; 23 | if (_stop === undefined) { 24 | [_start, _stop] = [0, _start]; 25 | } 26 | 27 | let pred = (n: number) => n >= _start; 28 | 29 | if (_stop !== null) { 30 | const stopNotNull = _stop; 31 | pred = composeAnd(pred, (n: number) => n < stopNotNull); 32 | } 33 | 34 | if (_step > 1) { 35 | pred = composeAnd(pred, (n: number) => (n - _start) % _step === 0); 36 | } 37 | 38 | return pred; 39 | } 40 | 41 | /** 42 | * Returns an iterator that returns elements from the first iterable until it 43 | * is exhausted, then proceeds to the next iterable, until all of the iterables 44 | * are exhausted. Used for treating consecutive sequences as a single 45 | * sequence. 46 | */ 47 | export function chain(...iterables: Array>): Iterable { 48 | return flatten(iterables); 49 | } 50 | 51 | /** 52 | * Returns an iterator that counts up values starting with number `start` 53 | * (default 0), incrementing by `step`. To decrement, use a negative step 54 | * number. 55 | */ 56 | export function* count(start = 0, step = 1): Iterable { 57 | let n = start; 58 | for (;;) { 59 | yield n; 60 | n += step; 61 | } 62 | } 63 | 64 | /** 65 | * Non-lazy version of icompress(). 66 | */ 67 | export function compress( 68 | data: Iterable, 69 | selectors: Iterable, 70 | ): Array { 71 | return Array.from(icompress(data, selectors)); 72 | } 73 | 74 | /** 75 | * Returns an iterator producing elements from the iterable and saving a copy 76 | * of each. When the iterable is exhausted, return elements from the saved 77 | * copy. Repeats indefinitely. 78 | */ 79 | export function* cycle(iterable: Iterable): Iterable { 80 | const saved = []; 81 | for (const element of iterable) { 82 | yield element; 83 | saved.push(element); 84 | } 85 | 86 | while (saved.length > 0) { 87 | for (const element of saved) { 88 | yield element; 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * Returns an iterator that drops elements from the iterable as long as the 95 | * predicate is true; afterwards, returns every remaining element. Note, the 96 | * iterator does not produce any output until the predicate first becomes 97 | * false. 98 | */ 99 | export function* dropwhile( 100 | iterable: Iterable, 101 | predicate: Predicate, 102 | ): Iterable { 103 | const it = iter(iterable); 104 | for (const value of it) { 105 | if (!predicate(value)) { 106 | yield value; 107 | break; 108 | } 109 | } 110 | for (const value of it) { 111 | yield value; 112 | } 113 | } 114 | 115 | export function* groupby( 116 | iterable: Iterable, 117 | keyFn: (v: T) => Primitive = primitiveIdentity, 118 | ): Iterable<[Primitive, Iterable]> { 119 | const it = iter(iterable); 120 | 121 | let currentValue: T; 122 | // $FlowFixMe[incompatible-type] - deliberate use of the SENTINEL symbol 123 | let currentKey: Primitive | typeof SENTINEL = SENTINEL; 124 | let targetKey: Primitive | typeof SENTINEL = currentKey; 125 | 126 | const grouper = function* grouper(tgtKey: Primitive) { 127 | while (currentKey === tgtKey) { 128 | yield currentValue; 129 | 130 | const nextVal = it.next(); 131 | if (nextVal.done) return; 132 | currentValue = nextVal.value; 133 | currentKey = keyFn(currentValue); 134 | } 135 | }; 136 | 137 | for (;;) { 138 | while (currentKey === targetKey) { 139 | const nextVal = it.next(); 140 | if (nextVal.done) { 141 | // $FlowFixMe[incompatible-type] - deliberate use of the SENTINEL symbol 142 | currentKey = SENTINEL; 143 | break; 144 | } 145 | currentValue = nextVal.value; 146 | currentKey = keyFn(currentValue); 147 | } 148 | if (currentKey === SENTINEL) { 149 | return; 150 | } 151 | targetKey = currentKey; 152 | yield [currentKey, grouper(targetKey)]; 153 | } 154 | } 155 | 156 | /** 157 | * Returns an iterator that filters elements from data returning only those 158 | * that have a corresponding element in selectors that evaluates to `true`. 159 | * Stops when either the data or selectors iterables has been exhausted. 160 | */ 161 | export function* icompress( 162 | data: Iterable, 163 | selectors: Iterable, 164 | ): Iterable { 165 | for (const [d, s] of izip(data, selectors)) { 166 | if (s) { 167 | yield d; 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * Returns an iterator that filters elements from iterable returning only those 174 | * for which the predicate is true. 175 | */ 176 | export function* ifilter( 177 | iterable: Iterable, 178 | predicate: Predicate, 179 | ): Iterable { 180 | for (const value of iterable) { 181 | if (predicate(value)) { 182 | yield value; 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Returns an iterator that computes the given mapper function using arguments 189 | * from each of the iterables. 190 | */ 191 | export function* imap( 192 | iterable: Iterable, 193 | mapper: (v: T) => V, 194 | ): Iterable { 195 | for (const value of iterable) { 196 | yield mapper(value); 197 | } 198 | } 199 | 200 | /** 201 | * Returns an iterator that returns selected elements from the iterable. If 202 | * `start` is non-zero, then elements from the iterable are skipped until start 203 | * is reached. Then, elements are returned by making steps of `step` (defaults 204 | * to 1). If set to higher than 1, items will be skipped. If `stop` is 205 | * provided, then iteration continues until the iterator reached that index, 206 | * otherwise, the iterable will be fully exhausted. `islice()` does not 207 | * support negative values for `start`, `stop`, or `step`. 208 | */ 209 | export function* islice( 210 | iterable: Iterable, 211 | start: number, 212 | stop: number | null | undefined = undefined, 213 | step = 1, 214 | ): Iterable { 215 | /* istanbul ignore if */ 216 | if (start < 0) throw new Error("start cannot be negative"); 217 | /* istanbul ignore if */ 218 | if (typeof stop === "number" && stop < 0) { 219 | throw new Error("stop cannot be negative"); 220 | } 221 | /* istanbul ignore if */ 222 | if (step < 0) throw new Error("step cannot be negative"); 223 | 224 | const pred = slicePredicate(start, stop, step); 225 | for (const [i, value] of enumerate(iterable)) { 226 | if (pred(i)) { 227 | yield value; 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * Returns an iterator that aggregates elements from each of the iterables. 234 | * Used for lock-step iteration over several iterables at a time. When 235 | * iterating over two iterables, use `izip2`. When iterating over three 236 | * iterables, use `izip3`, etc. `izip` is an alias for `izip2`. 237 | */ 238 | export function* izip2( 239 | xs: Iterable, 240 | ys: Iterable, 241 | ): Iterable<[T1, T2]> { 242 | const ixs = iter(xs); 243 | const iys = iter(ys); 244 | for (;;) { 245 | const x = ixs.next(); 246 | const y = iys.next(); 247 | if (!x.done && !y.done) { 248 | yield [x.value, y.value]; 249 | } else { 250 | // One of the iterables exhausted 251 | return; 252 | } 253 | } 254 | } 255 | 256 | /** 257 | * Like izip2, but for three input iterables. 258 | */ 259 | export function* izip3( 260 | xs: Iterable, 261 | ys: Iterable, 262 | zs: Iterable, 263 | ): Iterable<[T1, T2, T3]> { 264 | const ixs = iter(xs); 265 | const iys = iter(ys); 266 | const izs = iter(zs); 267 | for (;;) { 268 | const x = ixs.next(); 269 | const y = iys.next(); 270 | const z = izs.next(); 271 | if (!x.done && !y.done && !z.done) { 272 | yield [x.value, y.value, z.value]; 273 | } else { 274 | // One of the iterables exhausted 275 | return; 276 | } 277 | } 278 | } 279 | 280 | export const izip = izip2; 281 | 282 | /** 283 | * Returns an iterator that aggregates elements from each of the iterables. If 284 | * the iterables are of uneven length, missing values are filled-in with 285 | * fillvalue. Iteration continues until the longest iterable is exhausted. 286 | */ 287 | export function* izipLongest2( 288 | xs: Iterable, 289 | ys: Iterable, 290 | filler?: D, 291 | ): Iterable<[T1 | D, T2 | D]> { 292 | const ixs = iter(xs); 293 | const iys = iter(ys); 294 | for (;;) { 295 | const x = ixs.next(); 296 | const y = iys.next(); 297 | if (x.done && y.done) { 298 | // All iterables exhausted 299 | return; 300 | } else { 301 | yield [ 302 | // deno-lint-ignore no-explicit-any 303 | !x.done ? x.value : filler as any, 304 | // deno-lint-ignore no-explicit-any 305 | !y.done ? y.value : filler as any, 306 | ]; 307 | } 308 | } 309 | } 310 | 311 | /** 312 | * See izipLongest2, but for three. 313 | */ 314 | export function* izipLongest3( 315 | xs: Iterable, 316 | ys: Iterable, 317 | zs: Iterable, 318 | filler?: D, 319 | ): Iterable<[T1 | D, T2 | D, T3 | D]> { 320 | const ixs = iter(xs); 321 | const iys = iter(ys); 322 | const izs = iter(zs); 323 | for (;;) { 324 | const x = ixs.next(); 325 | const y = iys.next(); 326 | const z = izs.next(); 327 | if (x.done && y.done && z.done) { 328 | // All iterables exhausted 329 | return; 330 | } else { 331 | yield [ 332 | // deno-lint-ignore no-explicit-any 333 | !x.done ? x.value : filler as any, 334 | // deno-lint-ignore no-explicit-any 335 | !y.done ? y.value : filler as any, 336 | // deno-lint-ignore no-explicit-any 337 | !z.done ? z.value : filler as any, 338 | ]; 339 | } 340 | } 341 | } 342 | 343 | /** 344 | * Like the other izips (`izip`, `izip3`, etc), but generalized to take an 345 | * unlimited amount of input iterables. Think `izip(*iterables)` in Python. 346 | * 347 | * **Note:** Due to Flow type system limitations, you can only "generially" zip 348 | * iterables with homogeneous types, so you cannot mix types like like 349 | * you can with izip2(). 350 | */ 351 | export function* izipMany(...iters: Array>): Iterable> { 352 | // Make them all iterables 353 | const iterables = iters.map(iter); 354 | 355 | for (;;) { 356 | const heads: Array> = iterables.map((xs) => 357 | xs.next() 358 | ); 359 | if (all(heads, (h) => !h.done)) { 360 | // deno-lint-ignore no-explicit-any 361 | yield heads.map((h) => ((h.value as any) as T)); 362 | } else { 363 | // One of the iterables exhausted 364 | return; 365 | } 366 | } 367 | } 368 | 369 | /** 370 | * Return successive `r`-length permutations of elements in the iterable. 371 | * 372 | * If `r` is not specified, then `r` defaults to the length of the iterable and 373 | * all possible full-length permutations are generated. 374 | * 375 | * Permutations are emitted in lexicographic sort order. So, if the input 376 | * iterable is sorted, the permutation tuples will be produced in sorted order. 377 | * 378 | * Elements are treated as unique based on their position, not on their value. 379 | * So if the input elements are unique, there will be no repeat values in each 380 | * permutation. 381 | */ 382 | export function* permutations( 383 | iterable: Iterable, 384 | r: Maybe, 385 | ): Iterable> { 386 | const pool = Array.from(iterable); 387 | const n = pool.length; 388 | const x = r === undefined ? n : r; 389 | 390 | if (x > n) { 391 | return; 392 | } 393 | 394 | let indices: Array = Array.from(range(n)); 395 | const cycles: Array = Array.from(range(n, n - x, -1)); 396 | const poolgetter = (i: number) => pool[i]; 397 | 398 | yield indices.slice(0, x).map(poolgetter); 399 | 400 | while (n > 0) { 401 | let cleanExit = true; 402 | for (const i of range(x - 1, -1, -1)) { 403 | cycles[i] -= 1; 404 | if (cycles[i] === 0) { 405 | indices = indices 406 | .slice(0, i) 407 | .concat(indices.slice(i + 1)) 408 | .concat(indices.slice(i, i + 1)); 409 | cycles[i] = n - i; 410 | } else { 411 | const j: number = cycles[i]; 412 | 413 | const [p, q] = [indices[indices.length - j], indices[i]]; 414 | indices[i] = p; 415 | indices[indices.length - j] = q; 416 | yield indices.slice(0, x).map(poolgetter); 417 | cleanExit = false; 418 | break; 419 | } 420 | } 421 | 422 | if (cleanExit) { 423 | return; 424 | } 425 | } 426 | } 427 | 428 | /** 429 | * Returns an iterator that produces values over and over again. Runs 430 | * indefinitely unless the times argument is specified. 431 | */ 432 | export function* repeat(thing: T, times?: number): Iterable { 433 | if (times === undefined) { 434 | for (;;) { 435 | yield thing; 436 | } 437 | } else { 438 | // eslint-disable-next-line no-unused-vars 439 | for (const _ of range(times)) { 440 | yield thing; 441 | } 442 | } 443 | } 444 | 445 | /** 446 | * Returns an iterator that produces elements from the iterable as long as the 447 | * predicate is true. 448 | */ 449 | export function* takewhile( 450 | iterable: Iterable, 451 | predicate: Predicate, 452 | ): Iterable { 453 | for (const value of iterable) { 454 | if (!predicate(value)) return; 455 | yield value; 456 | } 457 | } 458 | 459 | export function zipLongest2( 460 | xs: Iterable, 461 | ys: Iterable, 462 | filler?: D, 463 | ): Array<[T1 | D, T2 | D]> { 464 | return Array.from(izipLongest2(xs, ys, filler)); 465 | } 466 | 467 | export function zipLongest3( 468 | xs: Iterable, 469 | ys: Iterable, 470 | zs: Iterable, 471 | filler?: D, 472 | ): Array<[T1 | D, T2 | D, T3 | D]> { 473 | return Array.from(izipLongest3(xs, ys, zs, filler)); 474 | } 475 | 476 | export const izipLongest = izipLongest2; 477 | export const zipLongest = zipLongest2; 478 | 479 | export function zipMany(...iters: Array>): Array> { 480 | return Array.from(izipMany(...iters)); 481 | } 482 | --------------------------------------------------------------------------------