├── .nvmrc ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── test.yml ├── .envrc ├── .prettierrc ├── .gitignore ├── src ├── types.ts ├── index.ts ├── utils.ts ├── custom.ts ├── more-itertools.ts ├── builtins.ts └── itertools.ts ├── .release-it.json ├── tsup.config.ts ├── tsconfig.json ├── test-d └── inference.test-d.ts ├── vitest.config.ts ├── LICENSE ├── test ├── custom.test.ts ├── builtins.test.ts ├── more-itertools.test.ts └── itertools.test.ts ├── package.json ├── eslint.config.mjs ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.17.0 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [nvie] 2 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout node 2 | PATH_add ./bin 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.tsbuildinfo 3 | /coverage 4 | /dist 5 | /node_modules 6 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Predicate = (value: T, index: number) => boolean; 2 | export type Primitive = string | number | boolean; 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: tuesday 8 | time: "04:45" 9 | open-pull-requests-limit: 10 10 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "@release-it/keep-a-changelog": { 4 | "filename": "CHANGELOG.md", 5 | "addUnreleased": true 6 | } 7 | }, 8 | "github": { 9 | "release": true, 10 | "releaseName": "v${version}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | dts: true, 6 | splitting: true, 7 | clean: true, 8 | // target: /* what tsconfig specifies */, 9 | format: ["esm", "cjs"], 10 | sourcemap: true, 11 | }); 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "strict": true, 5 | "target": "es2022", 6 | "module": "esnext", 7 | "moduleResolution": "bundler", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "paths": { 12 | "~": ["./src"], 13 | "~/*": ["./src/*"] 14 | } 15 | }, 16 | "include": ["src", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /test-d/inference.test-d.ts: -------------------------------------------------------------------------------- 1 | import { partition } from "../dist"; 2 | import { expectType } from "tsd"; 3 | 4 | function isStr(x: unknown): x is string { 5 | return typeof x === "string"; 6 | } 7 | 8 | { 9 | // partition with type predicate 10 | const items: unknown[] = [1, 2, null, true, 0, "hi", false, -1]; 11 | const [strings, others] = partition(items, isStr); 12 | expectType(strings); 13 | expectType(others); 14 | } 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import tsconfigPaths from "vite-tsconfig-paths"; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | coverage: { 8 | provider: "istanbul", 9 | reporter: ["text", "html"], 10 | 11 | // Require 100% test coverage 12 | thresholds: { 13 | lines: 100, 14 | functions: 100, 15 | statements: 100, 16 | branches: 100, 17 | }, 18 | }, 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | all, 3 | any, 4 | contains, 5 | enumerate, 6 | every, 7 | filter, 8 | find, 9 | iter, 10 | map, 11 | max, 12 | min, 13 | range, 14 | reduce, 15 | some, 16 | sorted, 17 | sum, 18 | xrange, 19 | zip, 20 | zip3, 21 | } from "./builtins"; 22 | export { compact, compactObject, first, flatmap, icompact } from "./custom"; 23 | export { 24 | chain, 25 | compress, 26 | count, 27 | cycle, 28 | dropwhile, 29 | groupBy, 30 | groupby, 31 | icompress, 32 | ifilter, 33 | igroupby, 34 | imap, 35 | indexBy, 36 | islice, 37 | izip, 38 | izip2, 39 | izip3, 40 | izipLongest, 41 | izipLongest3, 42 | izipMany, 43 | permutations, 44 | repeat, 45 | takewhile, 46 | zipLongest, 47 | zipLongest3, 48 | zipMany, 49 | } from "./itertools"; 50 | export { 51 | chunked, 52 | dupes, 53 | flatten, 54 | heads, 55 | intersperse, 56 | itake, 57 | pairwise, 58 | partition, 59 | roundrobin, 60 | take, 61 | uniqueEverseen, 62 | uniqueJustseen, 63 | } from "./more-itertools"; 64 | export type { Predicate, Primitive } from "./types"; 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [ 11 | # From https://github.com/nodejs/Release 12 | "20.x", # EoL by 2026-04-30 13 | "22.x", # EoL by 2027-04-30 14 | "24.x", # EoL by 2028-04-30 15 | "latest", 16 | ] 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - name: Use Node ${{ matrix.node-version }} 22 | uses: actions/setup-node@v5 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Check TypeScript 30 | run: npx tsc 31 | 32 | - name: Run tests 33 | run: npm run test 34 | env: 35 | CI: true 36 | 37 | - name: Run type-level tests 38 | run: npm run test:types 39 | env: 40 | CI: true 41 | 42 | - name: Run linters 43 | run: npm run lint 44 | 45 | - name: Run NPM package lints 46 | run: | 47 | npm run build 48 | npm run lint:package 49 | # @arethetypeswrong/cli requires Node 18+, it seems? 50 | # See https://github.com/arethetypeswrong/arethetypeswrong.github.io/issues/52 51 | if: ${{ matrix.node-version == '18.x' || matrix.node-version == 'latest' }} 52 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Primitive } from "./types"; 2 | 3 | type CmpFn = (a: T, b: T) => number; 4 | 5 | export function keyToCmp(keyFn: (item: T) => Primitive): CmpFn { 6 | return (a: T, b: T) => { 7 | const ka = keyFn(a); 8 | const kb = keyFn(b); 9 | // istanbul ignore else -- @preserve 10 | if (typeof ka === "boolean" && typeof kb === "boolean") { 11 | return ka === kb ? 0 : !ka && kb ? -1 : 1; 12 | } else if (typeof ka === "number" && typeof kb === "number") { 13 | return ka - kb; 14 | } else if (typeof ka === "string" && typeof kb === "string") { 15 | return ka === kb ? 0 : ka < kb ? -1 : 1; 16 | } else { 17 | return -1; 18 | } 19 | }; 20 | } 21 | 22 | export function identityPredicate(x: unknown): boolean { 23 | return !!x; 24 | } 25 | 26 | export function numberIdentity(x: unknown): number { 27 | // istanbul ignore if -- @preserve 28 | if (typeof x !== "number") { 29 | throw new Error("Inputs must be numbers"); 30 | } 31 | return x; 32 | } 33 | 34 | export function primitiveIdentity

(x: P): P; 35 | export function primitiveIdentity(x: unknown): Primitive; 36 | export function primitiveIdentity(x: unknown): Primitive { 37 | // istanbul ignore if -- @preserve 38 | if (typeof x !== "string" && typeof x !== "number" && typeof x !== "boolean") { 39 | throw new Error("Please provide a key function that can establish object identity"); 40 | } 41 | return x; 42 | } 43 | -------------------------------------------------------------------------------- /test/custom.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { compact, compactObject, flatmap, repeat } from "~"; 4 | 5 | describe("compact", () => { 6 | it("compact w/ empty list", () => { 7 | expect(compact([])).toEqual([]); 8 | }); 9 | 10 | it("icompact removes nullish values", () => { 11 | expect(compact("abc")).toEqual(["a", "b", "c"]); 12 | expect(compact(["x", undefined])).toEqual(["x"]); 13 | expect(compact([0, null, undefined, NaN, Infinity])).toEqual([0, NaN, Infinity]); 14 | }); 15 | }); 16 | 17 | describe("compactObject", () => { 18 | it("compactObject w/ empty object", () => { 19 | expect(compactObject({})).toEqual({}); 20 | }); 21 | 22 | it("compactObject removes nullish values", () => { 23 | expect(compactObject({ a: 1, b: "foo", c: 0, d: null })).toEqual({ a: 1, b: "foo", c: 0 }); 24 | expect(compactObject({ a: undefined, b: false, c: 0, d: null })).toEqual({ b: false, c: 0 }); 25 | }); 26 | }); 27 | 28 | describe("flatmap", () => { 29 | it("flatmap w/ empty list", () => { 30 | expect(Array.from(flatmap([], (x) => [x]))).toEqual([]); 31 | }); 32 | 33 | it("flatmap works", () => { 34 | const dupeEvens = (x: number) => (x % 2 === 0 ? [x, x] : [x]); 35 | const triple = (x: T) => [x, x, x]; 36 | const nothin = () => []; 37 | expect(Array.from(flatmap([1, 2, 3, 4, 5], dupeEvens))).toEqual([1, 2, 2, 3, 4, 4, 5]); 38 | expect(Array.from(flatmap(["hi", "ha"], triple))).toEqual(["hi", "hi", "hi", "ha", "ha", "ha"]); 39 | expect(Array.from(flatmap(["hi", "ha"], nothin))).toEqual([]); 40 | }); 41 | 42 | it("flatmap example", () => { 43 | const repeatN = (n: number) => repeat(n, n); 44 | expect(Array.from(flatmap([0, 1, 2, 3, 4], repeatN))).toEqual([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itertools", 3 | "version": "2.6.0-0", 4 | "description": "A JavaScript port of Python's awesome itertools standard library", 5 | "license": "MIT", 6 | "type": "module", 7 | "main": "./dist/index.cjs", 8 | "types": "./dist/index.d.cts", 9 | "exports": { 10 | ".": { 11 | "import": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | }, 15 | "require": { 16 | "types": "./dist/index.d.cts", 17 | "module": "./dist/index.js", 18 | "default": "./dist/index.cjs" 19 | } 20 | } 21 | }, 22 | "scripts": { 23 | "build": "tsup", 24 | "format": "eslint --fix src/ test/ && prettier --write src/ test/", 25 | "lint": "npm run lint:eslint && npm run lint:prettier", 26 | "lint:eslint": "eslint --report-unused-disable-directives src/ test/", 27 | "lint:prettier": "prettier --list-different src/ test/", 28 | "lint:package": "publint --strict && attw --pack", 29 | "test": "vitest run --coverage", 30 | "test:types": "npm run build && tsd --typings ./dist/index.d.ts", 31 | "release": "npm run test && npm run lint && npm run build && npm run lint:package && release-it" 32 | }, 33 | "files": [ 34 | "dist/", 35 | "LICENSE", 36 | "README.md" 37 | ], 38 | "devDependencies": { 39 | "@arethetypeswrong/cli": "^0.18.2", 40 | "@eslint/js": "^9.35.0", 41 | "@release-it/keep-a-changelog": "^7.0.0", 42 | "@vitest/coverage-istanbul": "^3.2.4", 43 | "eslint": "^9.35.0", 44 | "eslint-plugin-import": "^2.32.0", 45 | "eslint-plugin-simple-import-sort": "^12.1.1", 46 | "fast-check": "^4.3.0", 47 | "prettier": "^3.6.2", 48 | "publint": "^0.3.13", 49 | "release-it": "^19.0.5", 50 | "tsd": "^0.33.0", 51 | "tsup": "^8.5.0", 52 | "typescript": "^5.9.2", 53 | "typescript-eslint": "^8.44.0", 54 | "vite-tsconfig-paths": "^5.1.4", 55 | "vitest": "^3.2.4" 56 | }, 57 | "author": "Vincent Driessen", 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/nvie/itertools.git" 61 | }, 62 | "homepage": "https://github.com/nvie/itertools#readme", 63 | "bugs": { 64 | "url": "https://github.com/nvie/itertools/issues" 65 | }, 66 | "keywords": [ 67 | "itertool", 68 | "itertools", 69 | "node-itertools" 70 | ], 71 | "githubUrl": "https://github.com/nvie/itertools", 72 | "sideEffects": false 73 | } 74 | -------------------------------------------------------------------------------- /src/custom.ts: -------------------------------------------------------------------------------- 1 | import { find } from "./builtins"; 2 | import { ifilter, imap } from "./itertools"; 3 | import { flatten } from "./more-itertools"; 4 | import type { Predicate } from "./types"; 5 | 6 | function isNullish(x: T): x is NonNullable { 7 | return x != null; 8 | } 9 | 10 | function isDefined(x: unknown): boolean { 11 | return x !== undefined; 12 | } 13 | 14 | /** 15 | * Returns an iterable, filtering out any "nullish" values from the iterable. 16 | * 17 | * >>> compact([1, 2, undefined, 3, null]) 18 | * [1, 2, 3] 19 | * 20 | * For an eager version, @see compact(). 21 | */ 22 | export function icompact(iterable: Iterable): IterableIterator { 23 | return ifilter(iterable, isNullish); 24 | } 25 | 26 | /** 27 | * Returns an array, filtering out any "nullish" values from the iterable. 28 | * 29 | * >>> compact([1, 2, undefined, 3, null]) 30 | * [1, 2, 3] 31 | * 32 | * For a lazy version, @see icompact(). 33 | */ 34 | export function compact(iterable: Iterable): T[] { 35 | return Array.from(icompact(iterable)); 36 | } 37 | 38 | /** 39 | * Removes all "nullish" values from the given object. Returns a new object. 40 | * 41 | * >>> compactObject({ a: 1, b: undefined, c: 0, d: null }) 42 | * { a: 1, c: 0 } 43 | * 44 | */ 45 | export function compactObject(obj: Record): Record { 46 | const result = {} as Record; 47 | for (const [key, value_] of Object.entries(obj)) { 48 | const value = value_ as V | null | undefined; 49 | if (value != null) { 50 | result[key as K] = value; 51 | } 52 | } 53 | return result; 54 | } 55 | 56 | /** 57 | * Almost an alias of find(). There only is a difference if no key fn is 58 | * provided. In that case, `find()` will return the first item in the iterable, 59 | * whereas `first()` will return the first non-`undefined` value in the 60 | * iterable. 61 | */ 62 | export function first(iterable: Iterable, keyFn?: Predicate): T | undefined { 63 | return find(iterable, keyFn ?? isDefined); 64 | } 65 | 66 | /** 67 | * Returns 0 or more values for every value in the given iterable. 68 | * Technically, it's just calling map(), followed by flatten(), but it's a very 69 | * useful operation if you want to map over a structure, but not have a 1:1 70 | * input-output mapping. Instead, if you want to potentially return 0 or more 71 | * values per input element, use flatmap(): 72 | * 73 | * For example, to return all numbers `n` in the input iterable `n` times: 74 | * 75 | * >>> const repeatN = n => repeat(n, n); 76 | * >>> [...flatmap([0, 1, 2, 3, 4], repeatN)] 77 | * [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] // note: no 0 78 | * 79 | */ 80 | export function flatmap(iterable: Iterable, mapper: (item: T) => Iterable): IterableIterator { 81 | return flatten(imap(iterable, mapper)); 82 | } 83 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from "typescript-eslint"; 2 | import simpleImportSort from "eslint-plugin-simple-import-sort"; 3 | import tsParser from "@typescript-eslint/parser"; 4 | import eslint from "@eslint/js"; 5 | 6 | export default tseslint.config( 7 | { ignores: ["dist/*", "coverage/*", "node_modules/*"] }, 8 | 9 | eslint.configs.recommended, 10 | tseslint.configs.strictTypeChecked, 11 | tseslint.configs.stylisticTypeChecked, 12 | 13 | { 14 | languageOptions: { 15 | parser: tsParser, 16 | ecmaVersion: "latest", 17 | sourceType: "module", 18 | 19 | // Each project's individual/local tsconfig.json defines the behavior 20 | // of the parser 21 | parserOptions: { 22 | project: ["./tsconfig.json"], 23 | }, 24 | }, 25 | }, 26 | 27 | // ----------------------------- 28 | // Enable these checks 29 | // ----------------------------- 30 | { 31 | plugins: { 32 | "simple-import-sort": simpleImportSort, 33 | }, 34 | 35 | rules: { 36 | eqeqeq: ["error", "always", { null: "ignore" }], 37 | "object-shorthand": "error", 38 | 39 | "@typescript-eslint/consistent-type-imports": "error", 40 | "@typescript-eslint/explicit-module-boundary-types": "error", 41 | "@typescript-eslint/no-explicit-any": "error", 42 | "@typescript-eslint/no-unnecessary-condition": ["error", { allowConstantLoopConditions: true }], 43 | "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }], 44 | "@typescript-eslint/no-unused-vars": [ 45 | "warn", 46 | { args: "all", argsIgnorePattern: "^_.*", varsIgnorePattern: "^_.*" }, 47 | ], 48 | "simple-import-sort/exports": "error", 49 | "simple-import-sort/imports": "error", 50 | 51 | // -------------------------------------------------------------- 52 | // "The Code is the To-Do List" 53 | // https://www.executeprogram.com/blog/the-code-is-the-to-do-list 54 | // -------------------------------------------------------------- 55 | "no-warning-comments": ["error", { terms: ["xxx"], location: "anywhere" }], 56 | }, 57 | }, 58 | 59 | // ------------------------------- 60 | // Disable these checks 61 | // ------------------------------- 62 | { 63 | rules: { 64 | //"@typescript-eslint/consistent-type-definitions": "off", 65 | //"@typescript-eslint/no-empty-function": "off", 66 | //"@typescript-eslint/no-inferrable-types": "off", 67 | "@typescript-eslint/no-non-null-assertion": "off", 68 | "@typescript-eslint/unified-signatures": "off", 69 | "@typescript-eslint/no-deprecated": "off", 70 | //"@typescript-eslint/use-unknown-in-catch-callback-variable": "off", 71 | //"no-constant-condition": "off", 72 | }, 73 | }, 74 | 75 | // Overrides for tests specifically 76 | { 77 | files: ["test/**"], 78 | 79 | // Relax ESLint a bit in tests 80 | rules: { 81 | "@typescript-eslint/no-confusing-void-expression": "off", 82 | //"@typescript-eslint/no-deprecated": "off", 83 | //"@typescript-eslint/no-explicit-any": "off", 84 | //"@typescript-eslint/no-unsafe-argument": "off", 85 | //"@typescript-eslint/no-unsafe-call": "off", 86 | //"@typescript-eslint/no-unsafe-member-access": "off", 87 | //"@typescript-eslint/only-throw-error": "off", 88 | }, 89 | }, 90 | ); 91 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [2.6.0-0] - 2025-09-11 4 | 5 | - Add new `xrange`, which is like `range` but returns an array directly 6 | instead of an iterable. 7 | 8 | ## [2.5.0] - 2025-09-10 9 | 10 | - New itertools: `groupBy` and `indexBy` 11 | - Renamed `groupby` to `igroupby` 12 | - Deprecated `groupby` (now an alias to `igroupby`) 13 | - Drop support for Node 18.x (it probably still works, but it's EoL) 14 | 15 | ## [2.4.1] - 2025-02-25 16 | 17 | - Use `bundler` module resolution setting (recommended setting for libraries 18 | that use a bundler) 19 | - Upgrade dev dependencies 20 | 21 | ## [2.4.0] - 2025-02-19 22 | 23 | - Add second param `index` to all predicates. This will make operations like 24 | partitioning a list based on the element position as easy as partitioning 25 | based on the element value, for example: 26 | ```ts 27 | const items = [1, 2, 3, 4, 5, 6, 7, 8, 9]; 28 | const [thirds, rest] = partition(items, (item, index) => index % 3 === 0); 29 | console.log(thirds); // [1, 4, 7] 30 | console.log(rest); // [2, 3, 5, 6, 8, 9] 31 | ``` 32 | - Officially drop Node 16 support (it may still work) 33 | 34 | ## [2.3.2] - 2024-05-27 35 | 36 | - Fix missing top-level exports for `izipLongest3` and `intersperse` 37 | 38 | ## [2.3.1] - 2024-04-05 39 | 40 | - Actually export the new itertool at the top level 41 | 42 | ## [2.3.0] - 2024-04-05 43 | 44 | - Add new `dupes(iterable, keyFn?)` function, which returns groups of all 45 | duplicate items in iterable. 46 | 47 | ## [2.2.5] - 2024-03-07 48 | 49 | - Add missing export for `repeat` 50 | 51 | ## [2.2.4] - 2024-02-19 52 | 53 | - Type output types of all itertools more precisely 54 | 55 | ## [2.2.3] - 2024-01-09 56 | 57 | Fixes a bug where some iterators would render an inputted generator unusable, 58 | causing it to no longer be consumable after the iterable returns. 59 | 60 | Example: 61 | 62 | ```tsx 63 | function* gen() { 64 | yield 1; 65 | yield 2; 66 | yield 3; 67 | yield 4; 68 | } 69 | 70 | const lazy = gen(); 71 | 72 | // [1, 2] 73 | Array.from(islice(lazy, 0, 2)); 74 | 75 | Array.from(lazy); 76 | // ❌ Previously: [] 77 | // ✅ Now correctly: [3, 4] 78 | ``` 79 | 80 | This bug only happened when the source was a generator. It did not happen on 81 | a normal iterable. 82 | 83 | Similar bugs were present in: 84 | 85 | - `find()` 86 | - `islice()` 87 | - `takewhile()` 88 | - `dropwhile()` 89 | 90 | No other iterables were affected by this bug. This is the same bug that was 91 | fixed in 2.2.2 for `reduce()`, so many thanks again for surfacing this edge 92 | case, @quangloc99! 🙏 93 | 94 | ## [2.2.2] - 2024-01-09 95 | 96 | - Fix `reduce()` bug where using it on a lazy generator would produce the wrong 97 | result (thanks for finding, @quangloc99 🙏!) 98 | 99 | ## [2.2.1] - 2024-01-04 100 | 101 | - Fix `islice()` regression where it wasn't stopping on infinite iterables 102 | (thanks for finding, @Kareem-Medhat 🙏!) 103 | 104 | ## [2.2.0] 105 | 106 | - Move to ESM by default 107 | - Drop support for node 12.x and 14.x 108 | (they probably still work, but they're EoL) 109 | 110 | ## [2.1.2] 111 | 112 | - Improve tree-shakability when used in bundlers 113 | 114 | ## [2.1.1] 115 | 116 | - Improve documentation 117 | - Fix a bug in `reduce()` in a very small edge case 118 | 119 | ## [2.1.0] 120 | 121 | The following functions retain richer type information about their arguments: 122 | 123 | - `ifilter()` 124 | - `filter()` 125 | - `partition()` 126 | 127 | For example, TypeScript will now know the following: 128 | 129 | ```ts 130 | const items = [3, "hi", -7, "foo", 13]; 131 | 132 | function isNum(value: unknown): value is number { 133 | return typeof value === "number"; 134 | } 135 | 136 | const numbers: number[] = filter(items, isNum); // ✅ 137 | 138 | const [numbers, strings] = partition(items, isNum); // ✅ 139 | // ^^^^^^^ ^^^^^^^ string[] 140 | // number[] 141 | ``` 142 | 143 | - Add new `find(iterable, pred)` function, which is almost the same as 144 | `first(iterable, pred)` but behaves slightly more intuitive in the case 145 | where no predicate function is given. 146 | 147 | - Fix bug in `chunked()` with `size=1` 148 | 149 | ## [2.0.0] 150 | 151 | **Breaking changes:** 152 | 153 | - Rewritten source code in TypeScript (instead of Flow) 154 | - Modern ESM and CJS dual exports (fully tree-shakable when using ESM) 155 | - Massively [reduced bundle size](https://bundlephobia.com/package/itertools@2.0.0) 156 | - Targeted ES2015 (instead of ES5) 157 | - Support only TypeScript versions >= 4.3 158 | - Drop Flow support\* 159 | - Drop Node 10.x support 160 | - `icompact`, `compact`, and `compactObject` functions will now also remove 161 | `null` values, not only `undefined` 162 | 163 | (\*: I'm still open to bundling Flow types within this package, but only if 164 | that can be supported in a maintenance-free way, for example by using a script 165 | that will generate `*.flow` files from TypeScript source files. If someone can 166 | add support for that, I'm open to pull requests! 🙏 ) 167 | 168 | ## [1.7.1] 169 | 170 | - Add missing re-export of `islice` at the top level 171 | 172 | ## [1.7.0] 173 | 174 | - TypeScript support! 175 | - Declare official support for Node 16.x 176 | - Drop support for Node 13.x (unstable release) 177 | 178 | ## [1.6.1] 179 | 180 | - Include an error code with every FlowFixMe suppression 181 | (Flow 0.132.x compatibility) 182 | 183 | ## [1.6.0] 184 | 185 | - New itertool: `heads()` 186 | 187 | ## [1.5.4] 188 | 189 | - Export `roundrobin()` at the top level 190 | 191 | ## [1.5.3] 192 | 193 | - Fix bug in `chunked()` when input is exactly dividable 194 | 195 | ## [1.5.2] 196 | 197 | - Export `count()` function at the top level 🤦‍♂️ 198 | 199 | ## [1.5.1] 200 | 201 | - Internal change to make the code Flow 0.105.x compatible. Basically stops 202 | using array spreads (`[...things]`) in favor of `Array.from()`. 203 | 204 | ## [1.5.0] 205 | 206 | - Remove direct code dependency on `regenerator-runtime` (let `@babel/runtime` 207 | manage it) 208 | 209 | ## [1.4.0] 210 | 211 | - Switch to Babel 7 212 | 213 | ## [1.3.2] 214 | 215 | - Export `filter` at the top level 216 | 217 | ## [1.3.1] 218 | 219 | - New build system 220 | - Cleaner NPM package contents 221 | 222 | ## [1.3.0] 223 | 224 | - Drop support for Node 7 225 | 226 | ## [1.2.2] 227 | 228 | - Make itertools.js fully [Flow Strict](https://flow.org/en/docs/strict/) 229 | 230 | ## [1.2.1] 231 | 232 | - Export `permutations()` at the top-level 233 | 234 | ## [1.2.0] 235 | 236 | - Add port of `groupby()` function (see #87, thanks @sgenoud!) 237 | 238 | ## [1.1.6] 239 | 240 | - declare library to be side effect free (to help optimize webpack v4 builds) 241 | 242 | ## [1.1.5] 243 | 244 | - Include regenerator runtime via babel-runtime 245 | 246 | ## [1.1.4] 247 | 248 | - Make `regenerator-runtime` a normal runtime dependency 249 | 250 | ## [1.1.3] 251 | 252 | - Lower required version of `regenerator-runtime` 253 | 254 | ## [1.1.2] 255 | 256 | - Properly declare dependency on `regenerator-runtime` 257 | 258 | ## [1.1.1] 259 | 260 | - Fix bug in `cycle()` with infinite inputs 261 | 262 | ## [1.1.0] 263 | 264 | Started keeping a CHANGELOG. 265 | -------------------------------------------------------------------------------- /src/more-itertools.ts: -------------------------------------------------------------------------------- 1 | import { iter, map } from "./builtins"; 2 | import { izip, repeat } from "./itertools"; 3 | import type { Predicate, Primitive } from "./types"; 4 | import { primitiveIdentity } from "./utils"; 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(iterable: Iterable, size: number): IterableIterator { 19 | if (size < 1) { 20 | throw new Error(`Invalid chunk size: ${size}`); 21 | } 22 | 23 | const it = iter(iterable); 24 | for (;;) { 25 | const chunk = take(size, it); 26 | if (chunk.length > 0) { 27 | yield chunk; 28 | } 29 | if (chunk.length < size) { 30 | return; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Return an iterator flattening one level of nesting in a list of lists: 37 | * 38 | * [...flatten([[0, 1], [2, 3]])] 39 | * // [0, 1, 2, 3] 40 | * 41 | */ 42 | export function* flatten(iterableOfIterables: Iterable>): IterableIterator { 43 | for (const iterable of iterableOfIterables) { 44 | for (const item of iterable) { 45 | yield item; 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Intersperse filler element `value` among the items in `iterable`. 52 | * 53 | * >>> [...intersperse(-1, range(1, 5))] 54 | * [1, -1, 2, -1, 3, -1, 4] 55 | * 56 | */ 57 | export function intersperse(value: V, iterable: Iterable): IterableIterator { 58 | const stream = flatten(izip(repeat(value), iterable)); 59 | stream.next(); // eat away and discard the first value from the output 60 | return stream; 61 | } 62 | 63 | /** 64 | * Returns an iterable containing only the first `n` elements of the given 65 | * iterable. 66 | */ 67 | export function* itake(n: number, iterable: Iterable): IterableIterator { 68 | const it = iter(iterable); 69 | let count = n; 70 | while (count-- > 0) { 71 | const s = it.next(); 72 | if (!s.done) { 73 | yield s.value; 74 | } else { 75 | // Iterable exhausted, quit early 76 | return; 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Returns an iterator of paired items, overlapping, from the original. When 83 | * the input iterable has a finite number of items `n`, the outputted iterable 84 | * will have `n - 1` items. 85 | * 86 | * >>> pairwise([8, 2, 0, 7]) 87 | * [(8, 2), (2, 0), (0, 7)] 88 | * 89 | */ 90 | export function* pairwise(iterable: Iterable): IterableIterator<[T, T]> { 91 | const it = iter(iterable); 92 | const first = it.next(); 93 | if (first.done) { 94 | return; 95 | } 96 | 97 | let r1: T = first.value; 98 | for (const r2 of it) { 99 | yield [r1, r2]; 100 | r1 = r2; 101 | } 102 | } 103 | 104 | /** 105 | * Returns a 2-tuple of arrays. Splits the elements in the input iterable into 106 | * either of the two arrays. Will fully exhaust the input iterable. The first 107 | * array contains all items that match the predicate, the second the rest: 108 | * 109 | * >>> const isOdd = x => x % 2 !== 0; 110 | * >>> const iterable = range(10); 111 | * >>> const [odds, evens] = partition(iterable, isOdd); 112 | * >>> odds 113 | * [1, 3, 5, 7, 9] 114 | * >>> evens 115 | * [0, 2, 4, 6, 8] 116 | * 117 | */ 118 | export function partition( 119 | iterable: Iterable, 120 | predicate: (item: T, index: number) => item is N, 121 | ): [N[], Exclude[]]; 122 | export function partition(iterable: Iterable, predicate: Predicate): [T[], T[]]; 123 | export function partition(iterable: Iterable, predicate: Predicate): [T[], T[]] { 124 | const good = []; 125 | const bad = []; 126 | 127 | let index = 0; 128 | for (const item of iterable) { 129 | if (predicate(item, index++)) { 130 | good.push(item); 131 | } else { 132 | bad.push(item); 133 | } 134 | } 135 | 136 | return [good, bad]; 137 | } 138 | 139 | /** 140 | * Yields the next item from each iterable in turn, alternating between them. 141 | * Continues until all items are exhausted. 142 | * 143 | * >>> [...roundrobin([1, 2, 3], [4], [5, 6, 7, 8])] 144 | * [1, 4, 5, 2, 6, 3, 7, 8] 145 | */ 146 | export function* roundrobin(...iters: Iterable[]): IterableIterator { 147 | // We'll only keep lazy versions of the input iterables in here that we'll 148 | // slowly going to exhaust. Once an iterable is exhausted, it will be 149 | // removed from this list. Once the entire list is empty, this algorithm 150 | // ends. 151 | const iterables: Iterator[] = map(iters, iter); 152 | 153 | while (iterables.length > 0) { 154 | let index = 0; 155 | while (index < iterables.length) { 156 | const it = iterables[index]; 157 | const result = it.next(); 158 | 159 | if (!result.done) { 160 | yield result.value; 161 | index++; 162 | } else { 163 | // This iterable is exhausted, make sure to remove it from the 164 | // list of iterables. We'll splice the array from under our 165 | // feet, and NOT advancing the index counter. 166 | iterables.splice(index, 1); // intentional side-effect! 167 | } 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * Yields the heads of all of the given iterables. This is almost like 174 | * `roundrobin()`, except that the yielded outputs are grouped in to the 175 | * "rounds": 176 | * 177 | * >>> [...heads([1, 2, 3], [4], [5, 6, 7, 8])] 178 | * [[1, 4, 5], [2, 6], [3, 7], [8]] 179 | * 180 | * This is also different from `zipLongest()`, since the number of items in 181 | * each round can decrease over time, rather than being filled with a filler. 182 | */ 183 | export function* heads(...iters: Iterable[]): IterableIterator { 184 | // We'll only keep lazy versions of the input iterables in here that we'll 185 | // slowly going to exhaust. Once an iterable is exhausted, it will be 186 | // removed from this list. Once the entire list is empty, this algorithm 187 | // ends. 188 | const iterables: Iterator[] = map(iters, iter); 189 | 190 | while (iterables.length > 0) { 191 | let index = 0; 192 | const round = []; 193 | while (index < iterables.length) { 194 | const it = iterables[index]; 195 | const result = it.next(); 196 | 197 | if (!result.done) { 198 | round.push(result.value); 199 | index++; 200 | } else { 201 | // This iterable is exhausted, make sure to remove it from the 202 | // list of iterables. We'll splice the array from under our 203 | // feet, and NOT advancing the index counter. 204 | iterables.splice(index, 1); // intentional side-effect! 205 | } 206 | } 207 | if (round.length > 0) { 208 | yield round; 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Non-lazy version of itake(). 215 | */ 216 | export function take(n: number, iterable: Iterable): T[] { 217 | return Array.from(itake(n, iterable)); 218 | } 219 | 220 | /** 221 | * Yield unique elements, preserving order. 222 | * 223 | * >>> [...uniqueEverseen('AAAABBBCCDAABBB')] 224 | * ['A', 'B', 'C', 'D'] 225 | * >>> [...uniqueEverseen('AbBCcAB', s => s.toLowerCase())] 226 | * ['A', 'b', 'C'] 227 | * 228 | */ 229 | export function* uniqueEverseen( 230 | iterable: Iterable, 231 | keyFn: (item: T) => Primitive = primitiveIdentity, 232 | ): IterableIterator { 233 | const seen = new Set(); 234 | for (const item of iterable) { 235 | const key = keyFn(item); 236 | if (!seen.has(key)) { 237 | seen.add(key); 238 | yield item; 239 | } 240 | } 241 | } 242 | 243 | /** 244 | * Yield only elements from the input that occur more than once. Needs to 245 | * consume the entire input before being able to produce the first result. 246 | * 247 | * >>> [...dupes('AAAABCDEEEFABG')] 248 | * [['A', 'A', 'A', 'A', 'A'], ['E', 'E', 'E'], ['B', 'B']] 249 | * >>> [...dupes('AbBCcAB', s => s.toLowerCase())] 250 | * [['b', 'B', 'B'], ['C', 'c'], ['A', 'A']] 251 | * 252 | */ 253 | export function dupes( 254 | iterable: Iterable, 255 | keyFn: (item: T) => Primitive = primitiveIdentity, 256 | ): IterableIterator { 257 | const multiples = new Map(); 258 | 259 | { 260 | const singles = new Map(); 261 | for (const item of iterable) { 262 | const key = keyFn(item); 263 | if (multiples.has(key)) { 264 | multiples.get(key)!.push(item); 265 | } else if (singles.has(key)) { 266 | multiples.set(key, [singles.get(key)!, item]); 267 | singles.delete(key); 268 | } else { 269 | singles.set(key, item); 270 | } 271 | } 272 | } 273 | 274 | return multiples.values(); 275 | } 276 | 277 | /** 278 | * Yields elements in order, ignoring serial duplicates. 279 | * 280 | * >>> [...uniqueJustseen('AAAABBBCCDAABBB')] 281 | * ['A', 'B', 'C', 'D', 'A', 'B'] 282 | * >>> [...uniqueJustseen('AbBCcAB', s => s.toLowerCase())] 283 | * ['A', 'b', 'C', 'A', 'B'] 284 | * 285 | */ 286 | export function* uniqueJustseen( 287 | iterable: Iterable, 288 | keyFn: (item: T) => Primitive = primitiveIdentity, 289 | ): IterableIterator { 290 | let last = undefined; 291 | for (const item of iterable) { 292 | const key = keyFn(item); 293 | if (key !== last) { 294 | yield item; 295 | last = key; 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/builtins.ts: -------------------------------------------------------------------------------- 1 | import { count, ifilter, imap, izip, izip3, takewhile } from "./itertools"; 2 | import type { Predicate, Primitive } from "./types"; 3 | import { identityPredicate, keyToCmp, numberIdentity, primitiveIdentity } from "./utils"; 4 | 5 | /** 6 | * Returns the first item in the iterable for which the predicate holds, if 7 | * any. If no predicate is given, it will return the first value returned by 8 | * the iterable. 9 | */ 10 | export function find(iterable: Iterable, predicate?: Predicate): T | undefined { 11 | const it = iter(iterable); 12 | if (predicate === undefined) { 13 | const value = it.next(); 14 | return value.done ? undefined : value.value; 15 | } else { 16 | let res: IteratorResult; 17 | let i = 0; 18 | while (!(res = it.next()).done) { 19 | const value = res.value; 20 | if (predicate(value, i++)) { 21 | return value; 22 | } 23 | } 24 | return undefined; 25 | } 26 | } 27 | 28 | /** 29 | * Returns true when all of the items in iterable are truthy. An optional key 30 | * function can be used to define what truthiness means for this specific 31 | * collection. 32 | * 33 | * Examples: 34 | * 35 | * all([]) // => true 36 | * all([0]) // => false 37 | * all([0, 1, 2]) // => false 38 | * all([1, 2, 3]) // => true 39 | * 40 | * Examples with using a key function: 41 | * 42 | * all([2, 4, 6], n => n % 2 === 0) // => true 43 | * all([2, 4, 5], n => n % 2 === 0) // => false 44 | * 45 | */ 46 | export function every(iterable: Iterable, predicate: Predicate = identityPredicate): boolean { 47 | let index = 0; 48 | for (const item of iterable) { 49 | if (!predicate(item, index++)) { 50 | return false; 51 | } 52 | } 53 | return true; 54 | } 55 | 56 | /** 57 | * Returns true when some of the items in iterable are truthy. An optional key 58 | * function can be used to define what truthiness means for this specific 59 | * collection. 60 | * 61 | * Examples: 62 | * 63 | * some([]) // => false 64 | * some([0]) // => false 65 | * some([0, 1, null, undefined]) // => true 66 | * 67 | * Examples with using a key function: 68 | * 69 | * some([1, 4, 5], n => n % 2 === 0) // => true 70 | * some([{name: 'Bob'}, {name: 'Alice'}], person => person.name.startsWith('C')) // => false 71 | * 72 | */ 73 | export function some(iterable: Iterable, predicate: Predicate = identityPredicate): boolean { 74 | let index = 0; 75 | for (const item of iterable) { 76 | if (predicate(item, index++)) { 77 | return true; 78 | } 79 | } 80 | return false; 81 | } 82 | 83 | /** 84 | * Alias of `every()`. 85 | */ 86 | export const all = every; 87 | 88 | /** 89 | * Alias of `some()`. 90 | */ 91 | export const any = some; 92 | 93 | /** 94 | * Returns true when any of the items in the iterable are equal to the target object. 95 | * 96 | * Examples: 97 | * 98 | * contains([], 'whatever') // => false 99 | * contains([3], 42) // => false 100 | * contains([3], 3) // => true 101 | * contains([0, 1, 2], 2) // => true 102 | * 103 | */ 104 | export function contains(haystack: Iterable, needle: T): boolean { 105 | return some(haystack, (x) => x === needle); 106 | } 107 | 108 | /** 109 | * Returns an iterable of enumeration pairs. Iterable must be a sequence, an 110 | * iterator, or some other object which supports iteration. The elements 111 | * produced by returns a tuple containing a counter value (starting from 0 by 112 | * default) and the values obtained from iterating over given iterable. 113 | * 114 | * Example: 115 | * 116 | * import { enumerate } from 'itertools'; 117 | * 118 | * console.log([...enumerate(['hello', 'world'])]); 119 | * // [0, 'hello'], [1, 'world']] 120 | */ 121 | export function* enumerate(iterable: Iterable, start = 0): IterableIterator<[number, T]> { 122 | let index: number = start; 123 | for (const value of iterable) { 124 | yield [index++, value]; 125 | } 126 | } 127 | 128 | /** 129 | * Non-lazy version of ifilter(). 130 | */ 131 | export function filter(iterable: Iterable, predicate: (item: T, index: number) => item is N): N[]; 132 | export function filter(iterable: Iterable, predicate: Predicate): T[]; 133 | export function filter(iterable: Iterable, predicate: Predicate): T[] { 134 | return Array.from(ifilter(iterable, predicate)); 135 | } 136 | 137 | /** 138 | * Returns an iterator object for the given iterable. This can be used to 139 | * manually get an iterator for any iterable datastructure. The purpose and 140 | * main use case of this function is to get a single iterator (a thing with 141 | * state, think of it as a "cursor") which can only be consumed once. 142 | */ 143 | export function iter(iterable: Iterable): IterableIterator { 144 | return iterable[Symbol.iterator]() as IterableIterator; 145 | // ^^^^^^^^^^^^^^^^^^^^^^ Not safe! 146 | } 147 | 148 | /** 149 | * Non-lazy version of imap(). 150 | */ 151 | export function map(iterable: Iterable, mapper: (item: T) => V): V[] { 152 | return Array.from(imap(iterable, mapper)); 153 | } 154 | 155 | /** 156 | * Return the largest item in an iterable. Only works for numbers, as ordering 157 | * is pretty poorly defined on any other data type in JS. The optional `keyFn` 158 | * argument specifies a one-argument ordering function like that used for 159 | * sorted(). 160 | * 161 | * If the iterable is empty, `undefined` is returned. 162 | * 163 | * If multiple items are maximal, the function returns either one of them, but 164 | * which one is not defined. 165 | */ 166 | export function max(iterable: Iterable, keyFn: (item: T) => number = numberIdentity): T | undefined { 167 | return reduce2(iterable, (x, y) => (keyFn(x) > keyFn(y) ? x : y)); 168 | } 169 | 170 | /** 171 | * Return the smallest item in an iterable. Only works for numbers, as 172 | * ordering is pretty poorly defined on any other data type in JS. The 173 | * optional `keyFn` argument specifies a one-argument ordering function like 174 | * that used for sorted(). 175 | * 176 | * If the iterable is empty, `undefined` is returned. 177 | * 178 | * If multiple items are minimal, the function returns either one of them, but 179 | * which one is not defined. 180 | */ 181 | export function min(iterable: Iterable, keyFn: (item: T) => number = numberIdentity): T | undefined { 182 | return reduce2(iterable, (x, y) => (keyFn(x) < keyFn(y) ? x : y)); 183 | } 184 | 185 | /** 186 | * Internal helper for the range function 187 | */ 188 | function range_(start: number, stop: number, step: number): IterableIterator { 189 | const counter = count(start, step); 190 | const pred = step >= 0 ? (n: number) => n < stop : (n: number) => n > stop; 191 | return takewhile(counter, pred); 192 | } 193 | 194 | /** 195 | * Returns an iterator producing all the numbers in the given range one by one, 196 | * starting from `start` (default 0), as long as `i < stop`, in increments of 197 | * `step` (default 1). 198 | * 199 | * `range(a)` is a convenient shorthand for `range(0, a)`. 200 | * 201 | * Various valid invocations: 202 | * 203 | * range(5) // [0, 1, 2, 3, 4] 204 | * range(2, 5) // [2, 3, 4] 205 | * range(0, 5, 2) // [0, 2, 4] 206 | * range(5, 0, -1) // [5, 4, 3, 2, 1] 207 | * range(-3) // [] 208 | * 209 | * For a positive `step`, the iterator will keep producing values `n` as long 210 | * as the stop condition `n < stop` is satisfied. 211 | * 212 | * For a negative `step`, the iterator will keep producing values `n` as long 213 | * as the stop condition `n > stop` is satisfied. 214 | * 215 | * The produced range will be empty if the first value to produce already does 216 | * not meet the value constraint. 217 | */ 218 | export function range(stop: number): IterableIterator; 219 | export function range(start: number, stop: number, step?: number): IterableIterator; 220 | export function range(startOrStop: number, definitelyStop?: number, step = 1): IterableIterator { 221 | if (definitelyStop !== undefined) { 222 | return range_(startOrStop /* as start */, definitelyStop, step); 223 | } else { 224 | return range_(0, startOrStop /* as stop */, step); 225 | } 226 | } 227 | 228 | export function xrange(stop: number): number[]; 229 | export function xrange(start: number, stop: number, step?: number): number[]; 230 | export function xrange(startOrStop: number, definitelyStop?: number, step = 1): number[] { 231 | if (definitelyStop !== undefined) { 232 | return Array.from(range_(startOrStop /* as start */, definitelyStop, step)); 233 | } else { 234 | return Array.from(range_(0, startOrStop /* as stop */, step)); 235 | } 236 | } 237 | 238 | /** 239 | * Apply function of two arguments cumulatively to the items of sequence, from 240 | * left to right, so as to reduce the sequence to a single value. For example: 241 | * 242 | * reduce([1, 2, 3, 4, 5], (x, y) => x + y, 0) 243 | * 244 | * calculates 245 | * 246 | * (((((0+1)+2)+3)+4)+5) 247 | * 248 | * The left argument, `x`, is the accumulated value and the right argument, 249 | * `y`, is the update value from the sequence. 250 | * 251 | * **Difference between `reduce()` and `reduce\_()`**: `reduce()` requires an 252 | * explicit initializer, whereas `reduce_()` will automatically use the first 253 | * item in the given iterable as the initializer. When using `reduce()`, the 254 | * initializer value is placed before the items of the sequence in the 255 | * calculation, and serves as a default when the sequence is empty. When using 256 | * `reduce_()`, and the given iterable is empty, then no default value can be 257 | * derived and `undefined` will be returned. 258 | */ 259 | export function reduce(iterable: Iterable, reducer: (agg: T, item: T, index: number) => T): T | undefined; 260 | export function reduce(iterable: Iterable, reducer: (agg: O, item: T, index: number) => O, start: O): O; 261 | export function reduce( 262 | iterable: Iterable, 263 | reducer: ((agg: T, item: T, index: number) => T) | ((agg: O, item: T, index: number) => O), 264 | start?: O, 265 | ): O | (T | undefined) { 266 | if (start === undefined) { 267 | return reduce2(iterable, reducer as (agg: T, item: T, index: number) => T); 268 | } else { 269 | return reduce3(iterable, reducer as (agg: O, item: T, index: number) => O, start); 270 | } 271 | } 272 | 273 | function reduce3(iterable: Iterable, reducer: (agg: O, item: T, index: number) => O, start: O): O { 274 | let output = start; 275 | let index = 0; 276 | for (const item of iterable) { 277 | output = reducer(output, item, index++); 278 | } 279 | return output; 280 | } 281 | 282 | function reduce2(iterable: Iterable, reducer: (agg: T, item: T, index: number) => T): T | undefined { 283 | const it = iter(iterable); 284 | const start = find(it); 285 | if (start === undefined) { 286 | return undefined; 287 | } else { 288 | return reduce3(it, reducer, start); 289 | } 290 | } 291 | 292 | /** 293 | * Return a new sorted list from the items in iterable. 294 | * 295 | * Has two optional arguments: 296 | * 297 | * * `keyFn` specifies a function of one argument providing a primitive 298 | * identity for each element in the iterable. that will be used to compare. 299 | * The default value is to use a default identity function that is only 300 | * defined for primitive types. 301 | * 302 | * * `reverse` is a boolean value. If `true`, then the list elements are 303 | * sorted as if each comparison were reversed. 304 | */ 305 | export function sorted( 306 | iterable: Iterable, 307 | keyFn: (item: T) => Primitive = primitiveIdentity, 308 | reverse = false, 309 | ): T[] { 310 | const result = Array.from(iterable); 311 | result.sort(keyToCmp(keyFn)); // sort in-place 312 | 313 | if (reverse) { 314 | result.reverse(); // reverse in-place 315 | } 316 | 317 | return result; 318 | } 319 | 320 | /** 321 | * Sums the items of an iterable from left to right and returns the total. The 322 | * sum will defaults to 0 if the iterable is empty. 323 | */ 324 | export function sum(iterable: Iterable): number { 325 | return reduce(iterable, (x, y) => x + y, 0); 326 | } 327 | 328 | /** 329 | * See izip. 330 | */ 331 | export function zip(xs: Iterable, ys: Iterable): [T1, T2][] { 332 | return Array.from(izip(xs, ys)); 333 | } 334 | 335 | /** 336 | * See izip3. 337 | */ 338 | export function zip3(xs: Iterable, ys: Iterable, zs: Iterable): [T1, T2, T3][] { 339 | return Array.from(izip3(xs, ys, zs)); 340 | } 341 | -------------------------------------------------------------------------------- /src/itertools.ts: -------------------------------------------------------------------------------- 1 | import { every, iter, range } from "./builtins"; 2 | import { flatten } from "./more-itertools"; 3 | import type { Predicate, Primitive } from "./types"; 4 | import { primitiveIdentity } from "./utils"; 5 | 6 | const SENTINEL = Symbol(); 7 | 8 | /** 9 | * Returns an iterator that returns elements from the first iterable until it 10 | * is exhausted, then proceeds to the next iterable, until all of the iterables 11 | * are exhausted. Used for treating consecutive sequences as a single 12 | * sequence. 13 | */ 14 | export function chain(...iterables: Iterable[]): IterableIterator { 15 | return flatten(iterables); 16 | } 17 | 18 | /** 19 | * Returns an iterator that counts up values starting with number `start` 20 | * (default 0), incrementing by `step`. To decrement, use a negative step 21 | * number. 22 | */ 23 | export function* count(start = 0, step = 1): IterableIterator { 24 | let n = start; 25 | for (;;) { 26 | yield n; 27 | n += step; 28 | } 29 | } 30 | 31 | /** 32 | * Non-lazy version of icompress(). 33 | */ 34 | export function compress(data: Iterable, selectors: Iterable): T[] { 35 | return Array.from(icompress(data, selectors)); 36 | } 37 | 38 | /** 39 | * Returns an iterator producing elements from the iterable and saving a copy 40 | * of each. When the iterable is exhausted, return elements from the saved 41 | * copy. Repeats indefinitely. 42 | */ 43 | export function* cycle(iterable: Iterable): IterableIterator { 44 | const saved = []; 45 | for (const element of iterable) { 46 | yield element; 47 | saved.push(element); 48 | } 49 | 50 | while (saved.length > 0) { 51 | for (const element of saved) { 52 | yield element; 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Returns an iterator that drops elements from the iterable as long as the 59 | * predicate is true; afterwards, returns every remaining element. Note, the 60 | * iterator does not produce any output until the predicate first becomes 61 | * false. 62 | */ 63 | export function* dropwhile(iterable: Iterable, predicate: Predicate): IterableIterator { 64 | let index = 0; 65 | const it = iter(iterable); 66 | let res: IteratorResult; 67 | while (!(res = it.next()).done) { 68 | const value = res.value; 69 | if (!predicate(value, index++)) { 70 | yield value; 71 | break; // we break, so we cannot use a for..of loop! 72 | } 73 | } 74 | 75 | for (const value of it) { 76 | yield value; 77 | } 78 | } 79 | 80 | /** @deprecated Please rename to `igroupby`, or use the new eager version `groupBy`. */ 81 | export const groupby = igroupby; 82 | 83 | export function* igroupby( 84 | iterable: Iterable, 85 | keyFn: (item: T) => K = primitiveIdentity, 86 | ): Generator<[K, Generator], undefined> { 87 | const it = iter(iterable); 88 | 89 | let currentValue: T; 90 | let currentKey: K = SENTINEL as unknown as K; 91 | // ^^^^^^^^^^^^^^^ Hack! 92 | let targetKey: K = currentKey; 93 | 94 | const grouper = function* grouper(tgtKey: K): Generator { 95 | while (currentKey === tgtKey) { 96 | yield currentValue; 97 | 98 | const nextVal = it.next(); 99 | if (nextVal.done) return; 100 | currentValue = nextVal.value; 101 | currentKey = keyFn(currentValue); 102 | } 103 | }; 104 | 105 | for (;;) { 106 | while (currentKey === targetKey) { 107 | const nextVal = it.next(); 108 | if (nextVal.done) { 109 | currentKey = SENTINEL as unknown as K; 110 | // ^^^^^^^^^^^^^^^ Hack! 111 | return; 112 | } 113 | currentValue = nextVal.value; 114 | currentKey = keyFn(currentValue); 115 | } 116 | 117 | targetKey = currentKey; 118 | yield [currentKey, grouper(targetKey)]; 119 | } 120 | } 121 | 122 | export function groupBy(iterable: Iterable, keyFn: (item: T) => K): Record { 123 | const result = {} as Record; 124 | for (const item of iterable) { 125 | const key = keyFn(item); 126 | if (!Object.hasOwn(result, key)) { 127 | result[key] = []; 128 | } 129 | result[key].push(item); 130 | } 131 | return result; 132 | } 133 | 134 | export function indexBy(iterable: Iterable, keyFn: (item: T) => K): Record { 135 | const result = {} as Record; 136 | for (const item of iterable) { 137 | const key = keyFn(item); 138 | result[key] = item; 139 | } 140 | return result; 141 | } 142 | 143 | /** 144 | * Returns an iterator that filters elements from data returning only those 145 | * that have a corresponding element in selectors that evaluates to `true`. 146 | * Stops when either the data or selectors iterables has been exhausted. 147 | */ 148 | export function* icompress(data: Iterable, selectors: Iterable): IterableIterator { 149 | for (const [d, s] of izip(data, selectors)) { 150 | if (s) { 151 | yield d; 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * Returns an iterator that filters elements from iterable returning only those 158 | * for which the predicate is true. 159 | */ 160 | export function ifilter(iterable: Iterable, predicate: (item: T) => item is N): IterableIterator; 161 | export function ifilter(iterable: Iterable, predicate: Predicate): IterableIterator; 162 | export function* ifilter(iterable: Iterable, predicate: Predicate): IterableIterator { 163 | let index = 0; 164 | for (const value of iterable) { 165 | if (predicate(value, index++)) { 166 | yield value; 167 | } 168 | } 169 | } 170 | 171 | /** 172 | * Returns an iterator that computes the given mapper function using arguments 173 | * from each of the iterables. 174 | */ 175 | export function* imap(iterable: Iterable, mapper: (item: T) => V): IterableIterator { 176 | for (const value of iterable) { 177 | yield mapper(value); 178 | } 179 | } 180 | 181 | /** 182 | * Returns an iterator that returns selected elements from the iterable. If 183 | * `start` is non-zero, then elements from the iterable are skipped until start 184 | * is reached. Then, elements are returned by making steps of `step` (defaults 185 | * to 1). If set to higher than 1, items will be skipped. If `stop` is 186 | * provided, then iteration continues until the iterator reached that index, 187 | * otherwise, the iterable will be fully exhausted. `islice()` does not 188 | * support negative values for `start`, `stop`, or `step`. 189 | */ 190 | export function islice(iterable: Iterable, stop: number): IterableIterator; 191 | export function islice( 192 | iterable: Iterable, 193 | start: number, 194 | stop?: number | null, 195 | step?: number, 196 | ): IterableIterator; 197 | export function* islice( 198 | iterable: Iterable, 199 | stopOrStart: number, 200 | possiblyStop?: number | null, 201 | step = 1, 202 | ): IterableIterator { 203 | let start, stop; 204 | if (possiblyStop !== undefined) { 205 | // islice(iterable, start, stop[, step]) 206 | start = stopOrStart; 207 | stop = possiblyStop; 208 | } else { 209 | // islice(iterable, stop) 210 | start = 0; 211 | stop = stopOrStart; 212 | } 213 | 214 | if (start < 0) throw new Error("start cannot be negative"); 215 | if (stop !== null && stop < 0) throw new Error("stop cannot be negative"); 216 | if (step <= 0) throw new Error("step cannot be negative"); 217 | 218 | let i = -1; 219 | const it = iter(iterable); 220 | let res: IteratorResult; 221 | while (true) { 222 | i++; 223 | if (stop !== null && i >= stop) return; // early returns, so we cannot use a for..of loop! 224 | 225 | res = it.next(); 226 | if (res.done) return; 227 | 228 | if (i < start) continue; 229 | if ((i - start) % step === 0) { 230 | yield res.value; 231 | } 232 | } 233 | } 234 | 235 | /** 236 | * Returns an iterator that aggregates elements from each of the iterables. 237 | * Used for lock-step iteration over several iterables at a time. When 238 | * iterating over two iterables, use `izip2`. When iterating over three 239 | * iterables, use `izip3`, etc. `izip` is an alias for `izip2`. 240 | */ 241 | export function* izip(xs: Iterable, ys: Iterable): IterableIterator<[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 | ): IterableIterator<[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 izip2 = izip; 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 | ): IterableIterator<[T1 | D, T2 | D]> { 292 | const filler_ = filler as D; 293 | const ixs = iter(xs); 294 | const iys = iter(ys); 295 | for (;;) { 296 | const x = ixs.next(); 297 | const y = iys.next(); 298 | if (x.done && y.done) { 299 | // All iterables exhausted 300 | return; 301 | } else { 302 | yield [!x.done ? x.value : filler_, !y.done ? y.value : filler_]; 303 | } 304 | } 305 | } 306 | 307 | /** 308 | * See izipLongest2, but for three. 309 | */ 310 | export function* izipLongest3( 311 | xs: Iterable, 312 | ys: Iterable, 313 | zs: Iterable, 314 | filler?: D, 315 | ): IterableIterator<[T1 | D, T2 | D, T3 | D]> { 316 | const filler_ = filler as D; 317 | const ixs = iter(xs); 318 | const iys = iter(ys); 319 | const izs = iter(zs); 320 | for (;;) { 321 | const x = ixs.next(); 322 | const y = iys.next(); 323 | const z = izs.next(); 324 | if (x.done && y.done && z.done) { 325 | // All iterables exhausted 326 | return; 327 | } else { 328 | yield [!x.done ? x.value : filler_, !y.done ? y.value : filler_, !z.done ? z.value : filler_]; 329 | } 330 | } 331 | } 332 | 333 | /** 334 | * Like the other izips (`izip`, `izip3`, etc), but generalized to take an 335 | * unlimited amount of input iterables. Think `izip(*iterables)` in Python. 336 | * 337 | * **Note:** Due to Flow type system limitations, you can only "generially" zip 338 | * iterables with homogeneous types, so you cannot mix types like like 339 | * you can with izip2(). 340 | */ 341 | export function* izipMany(...iters: Iterable[]): IterableIterator { 342 | // Make them all iterables 343 | const iterables = iters.map(iter); 344 | 345 | for (;;) { 346 | const heads: IteratorResult[] = iterables.map((xs) => xs.next()); 347 | if (every(heads, (h) => !h.done)) { 348 | yield heads.map((h) => h.value as T); 349 | } else { 350 | // One of the iterables exhausted 351 | return; 352 | } 353 | } 354 | } 355 | 356 | /** 357 | * Return successive `r`-length permutations of elements in the iterable. 358 | * 359 | * If `r` is not specified, then `r` defaults to the length of the iterable and 360 | * all possible full-length permutations are generated. 361 | * 362 | * Permutations are emitted in lexicographic sort order. So, if the input 363 | * iterable is sorted, the permutation tuples will be produced in sorted order. 364 | * 365 | * Elements are treated as unique based on their position, not on their value. 366 | * So if the input elements are unique, there will be no repeat values in each 367 | * permutation. 368 | */ 369 | export function* permutations(iterable: Iterable, r?: number): IterableIterator { 370 | const pool = Array.from(iterable); 371 | const n = pool.length; 372 | const x = r ?? n; 373 | 374 | if (x > n) { 375 | return; 376 | } 377 | 378 | let indices: number[] = Array.from(range(n)); 379 | const cycles: number[] = Array.from(range(n, n - x, -1)); 380 | const poolgetter = (i: number) => pool[i]; 381 | 382 | yield indices.slice(0, x).map(poolgetter); 383 | 384 | while (n > 0) { 385 | let cleanExit = true; 386 | for (const i of range(x - 1, -1, -1)) { 387 | cycles[i] -= 1; 388 | if (cycles[i] === 0) { 389 | indices = indices 390 | .slice(0, i) 391 | .concat(indices.slice(i + 1)) 392 | .concat(indices.slice(i, i + 1)); 393 | cycles[i] = n - i; 394 | } else { 395 | const j: number = cycles[i]; 396 | 397 | const [p, q] = [indices[indices.length - j], indices[i]]; 398 | indices[i] = p; 399 | indices[indices.length - j] = q; 400 | yield indices.slice(0, x).map(poolgetter); 401 | cleanExit = false; 402 | break; 403 | } 404 | } 405 | 406 | if (cleanExit) { 407 | return; 408 | } 409 | } 410 | } 411 | 412 | /** 413 | * Returns an iterator that produces values over and over again. Runs 414 | * indefinitely unless the times argument is specified. 415 | */ 416 | export function* repeat(thing: T, times?: number): IterableIterator { 417 | if (times === undefined) { 418 | for (;;) { 419 | yield thing; 420 | } 421 | } else { 422 | for (const _ of range(times)) { 423 | yield thing; 424 | } 425 | } 426 | } 427 | 428 | /** 429 | * Returns an iterator that produces elements from the iterable as long as the 430 | * predicate is true. 431 | */ 432 | export function* takewhile(iterable: Iterable, predicate: Predicate): IterableIterator { 433 | let index = 0; 434 | const it = iter(iterable); 435 | let res: IteratorResult; 436 | while (!(res = it.next()).done) { 437 | const value = res.value; 438 | if (!predicate(value, index++)) return; // early return, so we cannot use for..of loop! 439 | yield value; 440 | } 441 | } 442 | 443 | export function zipLongest2(xs: Iterable, ys: Iterable, filler?: D): [T1 | D, T2 | D][] { 444 | return Array.from(izipLongest2(xs, ys, filler)); 445 | } 446 | 447 | export function zipLongest3( 448 | xs: Iterable, 449 | ys: Iterable, 450 | zs: Iterable, 451 | filler?: D, 452 | ): [T1 | D, T2 | D, T3 | D][] { 453 | return Array.from(izipLongest3(xs, ys, zs, filler)); 454 | } 455 | 456 | export const izipLongest = izipLongest2; 457 | export const zipLongest = zipLongest2; 458 | 459 | export function zipMany(...iters: Iterable[]): T[][] { 460 | return Array.from(izipMany(...iters)); 461 | } 462 | -------------------------------------------------------------------------------- /test/builtins.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from "fast-check"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { 5 | contains, 6 | enumerate, 7 | every, 8 | filter, 9 | first, 10 | iter, 11 | map, 12 | max, 13 | min, 14 | range, 15 | reduce, 16 | some, 17 | sorted, 18 | sum, 19 | xrange, 20 | zip, 21 | zip3, 22 | } from "~"; 23 | 24 | const isEven = (n: number) => n % 2 === 0; 25 | const isEvenIndex = (_: unknown, index: number) => index % 2 === 0; 26 | 27 | function isNum(value: unknown): value is number { 28 | return typeof value === "number"; 29 | } 30 | 31 | function* gen(values: T[]): Iterable { 32 | for (const value of values) { 33 | yield value; 34 | } 35 | } 36 | 37 | function predicate(): fc.Arbitrary<(a: unknown) => boolean> { 38 | return fc.oneof( 39 | fc.constant(() => true), 40 | fc.constant(() => false), 41 | fc.constant((a: unknown) => JSON.stringify(a ?? "0").length > 10), 42 | fc.constant((a: unknown) => JSON.stringify(a ?? "0").length !== 0), 43 | fc.constant((a: unknown) => typeof a === "number"), 44 | fc.constant((a: unknown) => typeof a === "string"), 45 | fc.constant((a: unknown) => typeof a === "object"), 46 | fc.constant((a: unknown) => typeof a === "function"), 47 | fc.constant((a: unknown) => Array.isArray(a)), 48 | ); 49 | } 50 | 51 | describe("every", () => { 52 | it("every of empty list is true", () => { 53 | expect(every([])).toBe(true); 54 | }); 55 | 56 | it("every is true if every elements are truthy", () => { 57 | expect(every([1])).toBe(true); 58 | expect(every([1, 2, 3])).toBe(true); 59 | }); 60 | 61 | it("every is false if some elements are not truthy", () => { 62 | expect(every([0, 1])).toBe(false); 63 | expect(every([1, 2, undefined, 3, 4])).toBe(false); 64 | }); 65 | }); 66 | 67 | describe("some", () => { 68 | it("some of empty list is false", () => { 69 | expect(some([])).toBe(false); 70 | }); 71 | 72 | it("some is true if some elements are truthy", () => { 73 | expect(some([1, 2, 3])).toBe(true); 74 | expect(some([0, 1])).toBe(true); 75 | expect(some([1, 0])).toBe(true); 76 | expect(some([0, undefined, NaN, 1])).toBe(true); 77 | }); 78 | 79 | it("some is false if no elements are truthy", () => { 80 | expect(some([0, null, NaN, undefined])).toBe(false); 81 | }); 82 | }); 83 | 84 | describe("every vs some", () => { 85 | it("every is always true with empty list", () => { 86 | fc.assert( 87 | fc.property( 88 | predicate(), 89 | 90 | (pred) => { 91 | expect(every([], pred)).toBe(true); 92 | }, 93 | ), 94 | ); 95 | }); 96 | 97 | it("some is always false with empty list", () => { 98 | fc.assert( 99 | fc.property( 100 | predicate(), 101 | 102 | (pred) => { 103 | expect(some([], pred)).toBe(false); 104 | }, 105 | ), 106 | ); 107 | }); 108 | 109 | it("every and some complete each other", () => { 110 | fc.assert( 111 | fc.property( 112 | fc.array(fc.anything()), 113 | predicate(), 114 | 115 | (arr, pred) => { 116 | const inverse = (x: unknown) => !pred(x); 117 | expect(every(arr, pred)).toEqual(!some(arr, inverse)); 118 | expect(some(arr, pred)).toEqual(!every(arr, inverse)); 119 | }, 120 | ), 121 | ); 122 | }); 123 | }); 124 | 125 | describe("contains", () => { 126 | it("contains of empty list is false", () => { 127 | expect(contains([], 0)).toBe(false); 128 | expect(contains([], 1)).toBe(false); 129 | expect(contains([], null)).toBe(false); 130 | expect(contains([], undefined)).toBe(false); 131 | }); 132 | 133 | it("contains is true iff iterable contains the given exact value", () => { 134 | expect(contains([1], 1)).toBe(true); 135 | expect(contains([1], 2)).toBe(false); 136 | expect(contains([1, 2, 3], 1)).toBe(true); 137 | expect(contains([1, 2, 3], 2)).toBe(true); 138 | expect(contains([1, 2, 3], 3)).toBe(true); 139 | expect(contains([1, 2, 3], 4)).toBe(false); 140 | }); 141 | 142 | it("contains does not work for elements with identity equality", () => { 143 | expect(contains([{}], {})).toBe(false); 144 | expect(contains([{ x: 123 }], { x: 123 })).toBe(false); 145 | expect(contains([[1, 2, 3]], [1, 2, 3])).toBe(false); 146 | }); 147 | }); 148 | 149 | describe("enumerate", () => { 150 | it("enumerate empty list", () => { 151 | expect(Array.from(enumerate([]))).toEqual([]); 152 | }); 153 | 154 | it("enumerate attaches indexes", () => { 155 | // We'll have to wrap it in a take() call to avoid infinite-length arrays :) 156 | expect(Array.from(enumerate(["x"]))).toEqual([[0, "x"]]); 157 | expect(Array.from(enumerate(["even", "odd"]))).toEqual([ 158 | [0, "even"], 159 | [1, "odd"], 160 | ]); 161 | }); 162 | 163 | it("enumerate from 3 up", () => { 164 | expect(Array.from(enumerate("abc", 3))).toEqual([ 165 | [3, "a"], 166 | [4, "b"], 167 | [5, "c"], 168 | ]); 169 | }); 170 | }); 171 | 172 | describe("filter", () => { 173 | it("filters empty list", () => { 174 | expect(filter([], isEven)).toEqual([]); 175 | expect(filter([], isEvenIndex)).toEqual([]); 176 | }); 177 | 178 | it("ifilter works like Array.filter, but lazy", () => { 179 | expect(filter([0, 1, 2, 3], isEven)).toEqual([0, 2]); 180 | expect(filter([0, 1, 2, 3], isEvenIndex)).toEqual([0, 2]); 181 | expect(filter([9, 0, 1, 2, 3], isEvenIndex)).toEqual([9, 1, 3]); 182 | }); 183 | 184 | it("filter retains rich type info", () => { 185 | const filtered = filter([3, "hi", null, -7], isNum); 186 | expect(filtered).toEqual([3, -7]); 187 | // ^^^^^^^^ number[] 188 | }); 189 | }); 190 | 191 | describe("iter", () => { 192 | it("iter makes some iterable a one-time iterable", () => { 193 | expect( 194 | Array.from( 195 | iter( 196 | new Map([ 197 | [1, "x"], 198 | [2, "y"], 199 | [3, "z"], 200 | ]), 201 | ), 202 | ), 203 | ).toEqual([ 204 | [1, "x"], 205 | [2, "y"], 206 | [3, "z"], 207 | ]); 208 | expect(Array.from(iter([1, 2, 3]))).toEqual([1, 2, 3]); 209 | expect(Array.from(iter(new Set([1, 2, 3])))).toEqual([1, 2, 3]); 210 | }); 211 | 212 | it("iter results can be consumed only once", () => { 213 | const it = iter([1, 2, 3]); 214 | expect(it.next()).toEqual({ value: 1, done: false }); 215 | expect(it.next()).toEqual({ value: 2, done: false }); 216 | expect(it.next()).toEqual({ value: 3, done: false }); 217 | expect(it.next()).toEqual({ value: undefined, done: true }); 218 | 219 | // Keeps returning "done" when exhausted... 220 | expect(it.next()).toEqual({ value: undefined, done: true }); 221 | expect(it.next()).toEqual({ value: undefined, done: true }); 222 | // ... 223 | }); 224 | 225 | it("iter results can be consumed in pieces", () => { 226 | const it = iter([1, 2, 3, 4, 5]); 227 | expect(first(it)).toBe(1); 228 | expect(first(it)).toBe(2); 229 | expect(first(it)).toBe(3); 230 | expect(Array.from(it)).toEqual([4, 5]); 231 | 232 | // Keeps returning "[]" when exhausted... 233 | expect(Array.from(it)).toEqual([]); 234 | expect(Array.from(it)).toEqual([]); 235 | // ... 236 | }); 237 | 238 | it("wrapping iter()s has no effect on the iterator's state", () => { 239 | const originalIter = iter([1, 2, 3, 4, 5]); 240 | expect(first(originalIter)).toBe(1); 241 | 242 | const wrappedIter = iter(iter(iter(originalIter))); 243 | expect(first(wrappedIter)).toBe(2); 244 | expect(first(iter(wrappedIter))).toBe(3); 245 | expect(Array.from(iter(originalIter))).toEqual([4, 5]); 246 | 247 | // Keeps returning "[]" when exhausted... 248 | expect(Array.from(originalIter)).toEqual([]); 249 | expect(Array.from(wrappedIter)).toEqual([]); 250 | // ... 251 | }); 252 | }); 253 | 254 | describe("map", () => { 255 | it("map on empty iterable", () => { 256 | expect(map([], (x) => x)).toEqual([]); 257 | }); 258 | 259 | it("imap works like Array.map, but lazy", () => { 260 | expect(map([1, 2, 3], (x) => x)).toEqual([1, 2, 3]); 261 | expect(map([1, 2, 3], (x) => 2 * x)).toEqual([2, 4, 6]); 262 | expect(map([1, 2, 3], (x) => x.toString())).toEqual(["1", "2", "3"]); 263 | }); 264 | }); 265 | 266 | describe("max", () => { 267 | it("can't take max of empty list", () => { 268 | expect(max([])).toBeUndefined(); 269 | }); 270 | 271 | it("max of single-item array", () => { 272 | expect(max([1])).toEqual(1); 273 | expect(max([2])).toEqual(2); 274 | expect(max([5])).toEqual(5); 275 | }); 276 | 277 | it("max of multi-item array", () => { 278 | expect(max([1, 2, 3])).toEqual(3); 279 | expect(max([2, 2, 2, 2])).toEqual(2); 280 | expect(max([5, 4, 3, 2, 1])).toEqual(5); 281 | expect(max([-3, -2, -1])).toEqual(-1); 282 | }); 283 | 284 | it("max of multi-item array with key function", () => { 285 | expect(max([{ n: 2 }, { n: 3 }, { n: 1 }], (o) => o.n)).toEqual({ n: 3 }); 286 | }); 287 | }); 288 | 289 | describe("min", () => { 290 | it("can't take min of empty list", () => { 291 | expect(min([])).toBeUndefined(); 292 | }); 293 | 294 | it("min of single-item array", () => { 295 | expect(min([1])).toEqual(1); 296 | expect(min([2])).toEqual(2); 297 | expect(min([5])).toEqual(5); 298 | }); 299 | 300 | it("min of multi-item array", () => { 301 | expect(min([1, 2, 3])).toEqual(1); 302 | expect(min([2, 2, 2, 2])).toEqual(2); 303 | expect(min([5, 4, 3, 2, 1])).toEqual(1); 304 | expect(min([-3, -2, -1])).toEqual(-3); 305 | }); 306 | 307 | it("min of multi-item array with key function", () => { 308 | expect(min([{ n: 2 }, { n: 3 }, { n: 1 }], (o) => o.n)).toEqual({ n: 1 }); 309 | }); 310 | }); 311 | 312 | describe("range", () => { 313 | it("range with end", () => { 314 | expect(Array.from(range(0))).toEqual([]); 315 | expect(Array.from(range(1))).toEqual([0]); 316 | expect(Array.from(range(2))).toEqual([0, 1]); 317 | expect(Array.from(range(5))).toEqual([0, 1, 2, 3, 4]); 318 | expect(Array.from(range(-1))).toEqual([]); 319 | }); 320 | 321 | it("range with start and end", () => { 322 | expect(Array.from(range(3, 5))).toEqual([3, 4]); 323 | expect(Array.from(range(4, 7))).toEqual([4, 5, 6]); 324 | 325 | // If end < start, then range is empty 326 | expect(Array.from(range(5, 1))).toEqual([]); 327 | }); 328 | 329 | it("range with start, end, and step", () => { 330 | expect(Array.from(range(3, 9, 3))).toEqual([3, 6]); 331 | expect(Array.from(range(3, 10, 3))).toEqual([3, 6, 9]); 332 | expect(Array.from(range(5, 1, -1))).toEqual([5, 4, 3, 2]); 333 | expect(Array.from(range(5, -3, -2))).toEqual([5, 3, 1, -1]); 334 | }); 335 | }); 336 | 337 | describe("xrange", () => { 338 | it("xrange with end", () => { 339 | expect(xrange(0)).toEqual([]); 340 | expect(xrange(1)).toEqual([0]); 341 | expect(xrange(2)).toEqual([0, 1]); 342 | expect(xrange(5)).toEqual([0, 1, 2, 3, 4]); 343 | expect(xrange(-1)).toEqual([]); 344 | }); 345 | 346 | it("xrange with start and end", () => { 347 | expect(xrange(3, 5)).toEqual([3, 4]); 348 | expect(xrange(4, 7)).toEqual([4, 5, 6]); 349 | 350 | // If end < start, then range is empty 351 | expect(xrange(5, 1)).toEqual([]); 352 | }); 353 | 354 | it("xrange with start, end, and step", () => { 355 | expect(xrange(3, 9, 3)).toEqual([3, 6]); 356 | expect(xrange(3, 10, 3)).toEqual([3, 6, 9]); 357 | expect(xrange(5, 1, -1)).toEqual([5, 4, 3, 2]); 358 | expect(xrange(5, -3, -2)).toEqual([5, 3, 1, -1]); 359 | }); 360 | }); 361 | 362 | describe("reduce", () => { 363 | const adder = (x: number, y: number) => x + y; 364 | const firstOne = (x: unknown) => x; 365 | 366 | it("reduce without initializer", () => { 367 | expect(reduce([], adder)).toBeUndefined(); 368 | expect(reduce([1], adder)).toEqual(1); 369 | expect(reduce([1, 2], adder)).toEqual(3); 370 | }); 371 | 372 | it("reduce on empty list returns start value", () => { 373 | expect(reduce([], adder, 0)).toEqual(0); 374 | expect(reduce([], adder, 13)).toEqual(13); 375 | }); 376 | 377 | it("reduce on list with only one item", () => { 378 | expect(reduce([5], adder, 0)).toEqual(5); 379 | expect(reduce([5], adder, 13)).toEqual(18); 380 | }); 381 | 382 | it("reduce on list with multiple items", () => { 383 | expect(reduce([1, 2, 3, 4], adder, 0)).toEqual(10); 384 | expect(reduce([1, 2, 3, 4, 5], adder, 13)).toEqual(28); 385 | }); 386 | 387 | it("reduce on list with multiple items (no initializer)", () => { 388 | expect(reduce([13, 2, 3, 4], firstOne)).toEqual(13); 389 | expect(reduce([undefined, null, 1, 2, 3, 4], firstOne)).toEqual(undefined); 390 | }); 391 | 392 | it("reduce on lazy iterable", () => { 393 | expect(reduce(gen([1, 2, 3]), adder)).toEqual(6); 394 | }); 395 | 396 | it("reduce on lazy iterable", () => { 397 | expect(reduce(gen([1, 2, 3]), adder, 100)).toEqual(106); 398 | }); 399 | }); 400 | 401 | describe("sorted", () => { 402 | it("sorted w/ empty list", () => { 403 | expect(sorted([])).toEqual([]); 404 | }); 405 | 406 | it("sorted values", () => { 407 | expect(sorted([1, 2, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]); 408 | expect(sorted([2, 1, 3, 4, 5])).toEqual([1, 2, 3, 4, 5]); 409 | expect(sorted([5, 4, 3, 2, 1])).toEqual([1, 2, 3, 4, 5]); 410 | 411 | // Explicitly test numeral ordering... in plain JS the following is true: 412 | // [4, 44, 100, 80, 9].sort() ~~> [100, 4, 44, 80, 9] 413 | expect(sorted([4, 44, 100, 80, 9])).toEqual([4, 9, 44, 80, 100]); 414 | expect(sorted(["4", "44", "100", "80", "44", "9"])).toEqual(["100", "4", "44", "44", "80", "9"]); 415 | expect(sorted([false, true, true, false])).toEqual([false, false, true, true]); 416 | }); 417 | 418 | it("sorted does not modify input", () => { 419 | const values = [4, 0, -3, 7, 1]; 420 | expect(sorted(values)).toEqual([-3, 0, 1, 4, 7]); 421 | expect(values).toEqual([4, 0, -3, 7, 1]); 422 | }); 423 | 424 | it("sorted in reverse", () => { 425 | expect(sorted([2, 1, 3, 4, 5], undefined, true)).toEqual([5, 4, 3, 2, 1]); 426 | }); 427 | 428 | it("sorted on lazy iterable", () => { 429 | expect(sorted(gen([2, 3, 1, 2]))).toEqual([1, 2, 2, 3]); 430 | }); 431 | }); 432 | 433 | describe("sum", () => { 434 | it("sum w/ empty iterable", () => { 435 | expect(sum([])).toEqual(0); 436 | }); 437 | 438 | it("sum works", () => { 439 | expect(sum([1, 2, 3, 4, 5, 6])).toEqual(21); 440 | expect(sum([-3, -2, -1, 0, 1, 2, 3])).toEqual(0); 441 | expect(sum([0.1, 0.2, 0.3])).toBeCloseTo(0.6); 442 | }); 443 | }); 444 | 445 | describe("zip", () => { 446 | it("zip with empty iterable", () => { 447 | expect(zip([], [])).toEqual([]); 448 | expect(zip("abc", [])).toEqual([]); 449 | }); 450 | 451 | it("izip with two iterables", () => { 452 | expect(zip("abc", "ABC")).toEqual([ 453 | ["a", "A"], 454 | ["b", "B"], 455 | ["c", "C"], 456 | ]); 457 | }); 458 | 459 | it("izip with three iterables", () => { 460 | expect(zip3("abc", "ABC", [5, 4, 3])).toEqual([ 461 | ["a", "A", 5], 462 | ["b", "B", 4], 463 | ["c", "C", 3], 464 | ]); 465 | }); 466 | 467 | it("izip different input lengths", () => { 468 | // Shortest lengthed input determines result length 469 | expect(zip("a", "ABC")).toEqual([["a", "A"]]); 470 | expect(zip3("", "ABCD", "PQR")).toEqual([]); 471 | }); 472 | }); 473 | -------------------------------------------------------------------------------- /test/more-itertools.test.ts: -------------------------------------------------------------------------------- 1 | import * as fc from "fast-check"; 2 | import { describe, expect, it } from "vitest"; 3 | 4 | import { 5 | chunked, 6 | dupes, 7 | first, 8 | flatten, 9 | heads, 10 | intersperse, 11 | pairwise, 12 | partition, 13 | roundrobin, 14 | take, 15 | uniqueEverseen, 16 | uniqueJustseen, 17 | } from "~"; 18 | import { find, iter, range } from "~/builtins"; 19 | 20 | const isEven = (x: number) => x % 2 === 0; 21 | const isEvenIndex = (_: unknown, index: number) => index % 2 === 0; 22 | const isPositive = (x: number) => x >= 0; 23 | 24 | function isNum(value: unknown): value is number { 25 | return typeof value === "number"; 26 | } 27 | 28 | function* gen(values: T[]): Iterable { 29 | for (const value of values) { 30 | yield value; 31 | } 32 | } 33 | 34 | describe("chunked", () => { 35 | it("empty", () => { 36 | expect(Array.from(chunked([], 3))).toEqual([]); 37 | expect(Array.from(chunked([], 1337))).toEqual([]); 38 | }); 39 | 40 | it("fails with invalid chunk size", () => { 41 | expect(() => Array.from(chunked([3, 2, 1], 0))).toThrow(); 42 | expect(() => Array.from(chunked([3, 2, 1], -3))).toThrow(); 43 | }); 44 | 45 | it("works with chunk size of 1", () => { 46 | expect(Array.from(chunked([5, 4, 3, 2, 1], 1))).toEqual([[5], [4], [3], [2], [1]]); 47 | }); 48 | 49 | it("works with array smaller than chunk size", () => { 50 | expect(Array.from(chunked([1], 3))).toEqual([[1]]); 51 | }); 52 | 53 | it("works with array of values", () => { 54 | expect(Array.from(chunked([1, 2, 3, 4, 5], 3))).toEqual([ 55 | [1, 2, 3], 56 | [4, 5], 57 | ]); 58 | }); 59 | 60 | it("works with exactly chunkable list", () => { 61 | expect(Array.from(chunked([1, 2, 3, 4, 5, 6], 3))).toEqual([ 62 | [1, 2, 3], 63 | [4, 5, 6], 64 | ]); 65 | }); 66 | 67 | it("works with chunkable list with remainder", () => { 68 | const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 69 | 70 | expect(Array.from(chunked(numbers, 3))).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]); 71 | expect(Array.from(chunked(numbers, 5))).toEqual([ 72 | [1, 2, 3, 4, 5], 73 | [6, 7, 8, 9, 10], 74 | ]); 75 | expect(Array.from(chunked(numbers, 9999))).toEqual([numbers]); 76 | }); 77 | 78 | it("chunked on lazy iterable", () => { 79 | const lazy = gen([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); 80 | expect(Array.from(chunked(lazy, 3))).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]); 81 | expect(Array.from(chunked(lazy, 5))).toEqual([]); // lazy input all consumed 82 | }); 83 | 84 | it("no chunk will be larger than the chunk size", () => { 85 | fc.assert( 86 | fc.property( 87 | fc.array(fc.anything()), 88 | fc.integer({ min: 1 }), 89 | 90 | (input, chunkSize) => { 91 | const output = Array.from(chunked(input, chunkSize)); 92 | fc.pre(output.length > 0); 93 | 94 | const lastChunk = output.pop()!; 95 | expect(lastChunk.length).toBeGreaterThan(0); 96 | expect(lastChunk.length).toBeLessThanOrEqual(chunkSize); 97 | 98 | // The remaining chunks are all exactly the chunk size 99 | for (const chunk of output) { 100 | expect(chunk.length).toEqual(chunkSize); 101 | } 102 | }, 103 | ), 104 | ); 105 | }); 106 | 107 | it("chunks contain all elements, in the same order as the input", () => { 108 | fc.assert( 109 | fc.property( 110 | fc.array(fc.anything()), 111 | fc.integer({ min: 1 }), 112 | 113 | (input, chunkSize) => { 114 | const output = Array.from(chunked(input, chunkSize)); 115 | 116 | // Exact same elements as input array 117 | expect(output.flatMap((x) => x)).toEqual(input); 118 | }, 119 | ), 120 | ); 121 | }); 122 | }); 123 | 124 | describe("find", () => { 125 | it("returns nothing for an empty array", () => { 126 | expect(find([])).toBeUndefined(); 127 | expect(find([undefined, undefined])).toBeUndefined(); 128 | }); 129 | 130 | it("returns the first value in the array", () => { 131 | expect(find([3, "ohai"])).toBe(3); 132 | expect(find(["ohai", 3])).toBe("ohai"); 133 | }); 134 | 135 | it("find may returns falsey values too", () => { 136 | expect(find([0, 1, 2])).toBe(0); 137 | expect(find([false, true])).toBe(false); 138 | expect(find([null, false, true])).toBe(null); 139 | expect(find([undefined, 3, "ohai"])).toBe(undefined); 140 | expect(find([NaN, 3, "ohai"])).toBe(NaN); 141 | }); 142 | 143 | it("find uses a predicate if provided", () => { 144 | expect(find([0, 1, 2, 3, 4], (n) => !!n)).toBe(1); 145 | expect(find([0, 1, 2, 3, 4], (n) => n > 1)).toBe(2); 146 | expect(find([0, 1, 2, 3, 4], (n) => n < 0)).toBeUndefined(); 147 | expect(find([false, true], (x) => x)).toBe(true); 148 | }); 149 | 150 | it("find on lazy iterable", () => { 151 | const lazy = gen([1, 2, 3]); 152 | expect(find(lazy)).toBe(1); 153 | expect(find(lazy)).toBe(2); 154 | expect(find(lazy)).toBe(3); 155 | expect(find(lazy)).toBe(undefined); 156 | }); 157 | }); 158 | 159 | describe("first", () => { 160 | it("returns nothing for an empty array", () => { 161 | expect(first([])).toBeUndefined(); 162 | expect(first([undefined, undefined])).toBeUndefined(); 163 | }); 164 | 165 | it("returns the first value in the array", () => { 166 | expect(first([3, "ohai"])).toBe(3); 167 | expect(first(["ohai", 3])).toBe("ohai"); 168 | expect(first([undefined, 3, "ohai"])).toBe(3); 169 | }); 170 | 171 | it("find may returns falsey values too", () => { 172 | expect(first([0, 1, 2])).toBe(0); 173 | expect(first([false, true])).toBe(false); 174 | expect(first([null, false, true])).toBe(null); 175 | expect(first([NaN, 3, "ohai"])).toBe(NaN); 176 | }); 177 | 178 | it("find uses a predicate if provided", () => { 179 | expect(first([0, 1, 2, 3, 4], (n) => !!n)).toBe(1); 180 | expect(first([0, 1, 2, 3, 4], (n) => n > 1)).toBe(2); 181 | expect(first([0, 1, 2, 3, 4], (n) => n < 0)).toBeUndefined(); 182 | expect(first([false, true], (x) => x)).toBe(true); 183 | }); 184 | }); 185 | 186 | describe("flatten", () => { 187 | it("flatten w/ empty list", () => { 188 | expect(Array.from(flatten([]))).toEqual([]); 189 | expect(Array.from(flatten([[], [], [], [], []]))).toEqual([]); 190 | }); 191 | 192 | it("flatten works", () => { 193 | expect( 194 | Array.from( 195 | flatten([ 196 | [1, 2], 197 | [3, 4, 5], 198 | ]), 199 | ), 200 | ).toEqual([1, 2, 3, 4, 5]); 201 | expect(Array.from(flatten(["hi", "ha"]))).toEqual(["h", "i", "h", "a"]); 202 | }); 203 | }); 204 | 205 | describe("intersperse", () => { 206 | it("intersperse on empty sequence", () => { 207 | expect(Array.from(intersperse(0, []))).toEqual([]); 208 | }); 209 | 210 | it("intersperse", () => { 211 | expect(Array.from(intersperse(-1, [13]))).toEqual([13]); 212 | expect(Array.from(intersperse(null, [13, 14]))).toEqual([13, null, 14]); 213 | expect(Array.from(intersperse("foo", [1, 2, 3, 4]))).toEqual([1, "foo", 2, "foo", 3, "foo", 4]); 214 | }); 215 | 216 | it("intersperse (lazy)", () => { 217 | const lazy = gen([1, 2, 3, 4]); 218 | expect(Array.from(intersperse("foo", lazy))).toEqual([1, "foo", 2, "foo", 3, "foo", 4]); 219 | }); 220 | }); 221 | 222 | describe("itake", () => { 223 | it("itake is tested through take() tests", () => { 224 | // This is okay 225 | }); 226 | }); 227 | 228 | describe("pairwise", () => { 229 | it("does nothing for empty array", () => { 230 | expect(Array.from(pairwise([]))).toEqual([]); 231 | expect(Array.from(pairwise([1]))).toEqual([]); 232 | }); 233 | 234 | it("it returns pairs of input", () => { 235 | expect(Array.from(pairwise([0, 1, 2]))).toEqual([ 236 | [0, 1], 237 | [1, 2], 238 | ]); 239 | expect(Array.from(pairwise([1, 2]))).toEqual([[1, 2]]); 240 | expect(Array.from(pairwise([1, 2, 3, 4]))).toEqual([ 241 | [1, 2], 242 | [2, 3], 243 | [3, 4], 244 | ]); 245 | }); 246 | }); 247 | 248 | describe("partition", () => { 249 | it("partition empty list", () => { 250 | expect(partition([], isEven)).toEqual([[], []]); 251 | expect(partition([], isEvenIndex)).toEqual([[], []]); 252 | }); 253 | 254 | it("partition splits input list into two lists", () => { 255 | const values = [1, -2, 3, 4, 5, 6, 8, 8, 0, -2, -3]; 256 | expect(partition(values, isEven)).toEqual([ 257 | [-2, 4, 6, 8, 8, 0, -2], 258 | [1, 3, 5, -3], 259 | ]); 260 | expect(partition(values, isEvenIndex)).toEqual([ 261 | [1, 3, 5, 8, 0, -3], 262 | [-2, 4, 6, 8, -2], 263 | ]); 264 | expect(partition(values, isPositive)).toEqual([ 265 | [1, 3, 4, 5, 6, 8, 8, 0], 266 | [-2, -2, -3], 267 | ]); 268 | }); 269 | 270 | it("partition retains rich type info", () => { 271 | const values = ["hi", 3, null, "foo", -7]; 272 | const [good, bad] = partition(values, isNum); 273 | expect(good).toEqual([3, -7]); 274 | // ^^^^ number[] 275 | expect(bad).toEqual(["hi", null, "foo"]); 276 | // ^^^ (string | null)[] 277 | }); 278 | }); 279 | 280 | describe("roundrobin", () => { 281 | it("roundrobin on empty list", () => { 282 | expect(Array.from(roundrobin())).toEqual([]); 283 | expect(Array.from(roundrobin([]))).toEqual([]); 284 | expect(Array.from(roundrobin([], []))).toEqual([]); 285 | expect(Array.from(roundrobin([], [], []))).toEqual([]); 286 | expect(Array.from(roundrobin([], [], [], []))).toEqual([]); 287 | }); 288 | 289 | it("roundrobin on equally sized lists", () => { 290 | expect(Array.from(roundrobin([1], [2], [3]))).toEqual([1, 2, 3]); 291 | expect(Array.from(roundrobin([1, 2], [3, 4]))).toEqual([1, 3, 2, 4]); 292 | expect(Array.from(roundrobin("foo", "bar")).join("")).toEqual("fboaor"); 293 | }); 294 | 295 | it("roundrobin on unequally sized lists", () => { 296 | expect(Array.from(roundrobin([1], [], [2, 3, 4]))).toEqual([1, 2, 3, 4]); 297 | expect(Array.from(roundrobin([1, 2, 3, 4, 5], [6, 7]))).toEqual([1, 6, 2, 7, 3, 4, 5]); 298 | expect(Array.from(roundrobin([1, 2, 3], [4], [5, 6, 7, 8]))).toEqual([1, 4, 5, 2, 6, 3, 7, 8]); 299 | }); 300 | }); 301 | 302 | describe("heads", () => { 303 | it("heads on empty list", () => { 304 | expect(Array.from(heads())).toEqual([]); 305 | expect(Array.from(heads([]))).toEqual([]); 306 | expect(Array.from(heads([], []))).toEqual([]); 307 | expect(Array.from(heads([], [], []))).toEqual([]); 308 | expect(Array.from(heads([], [], [], []))).toEqual([]); 309 | }); 310 | 311 | it("heads on equally sized lists", () => { 312 | expect(Array.from(heads([1], [2], [3]))).toEqual([[1, 2, 3]]); 313 | expect(Array.from(heads([1, 2], [3, 4]))).toEqual([ 314 | [1, 3], 315 | [2, 4], 316 | ]); 317 | expect(Array.from(heads("foo", "bar")).map((s) => s.join(""))).toEqual(["fb", "oa", "or"]); 318 | }); 319 | 320 | it("heads on unequally sized lists", () => { 321 | expect(Array.from(heads([1], [], [2, 3, 4]))).toEqual([[1, 2], [3], [4]]); 322 | expect(Array.from(heads([1, 2, 3, 4, 5], [6, 7]))).toEqual([[1, 6], [2, 7], [3], [4], [5]]); 323 | expect(Array.from(heads([1, 2, 3], [4], [5, 6, 7, 8]))).toEqual([[1, 4, 5], [2, 6], [3, 7], [8]]); 324 | }); 325 | }); 326 | 327 | describe("take", () => { 328 | it("take on empty array", () => { 329 | expect(take(0, [])).toEqual([]); 330 | expect(take(1, [])).toEqual([]); 331 | expect(take(99, [])).toEqual([]); 332 | }); 333 | 334 | it("take on infinite input", () => { 335 | expect(take(5, Math.PI.toString())).toEqual(["3", ".", "1", "4", "1"]); 336 | }); 337 | 338 | it("take on infinite input", () => { 339 | expect(take(0, range(999)).length).toEqual(0); 340 | expect(take(1, range(999)).length).toEqual(1); 341 | expect(take(99, range(999)).length).toEqual(99); 342 | }); 343 | 344 | it("take multiple times from collection will create new iterators every time", () => { 345 | const coll = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; 346 | expect(take(0, coll)).toEqual([]); 347 | expect(take(3, coll)).toEqual([0, 1, 2]); 348 | expect(take(3, coll)).toEqual([0, 1, 2]); 349 | expect(take(3, coll)).toEqual([0, 1, 2]); 350 | expect(take(5, coll)).toEqual([0, 1, 2, 3, 4]); 351 | }); 352 | 353 | it("take multiple times from an iterator", () => { 354 | const it = iter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 355 | expect(take(0, it)).toEqual([]); 356 | expect(take(3, it)).toEqual([0, 1, 2]); 357 | expect(take(3, it)).toEqual([3, 4, 5]); 358 | expect(take(5, it)).toEqual([6, 7, 8, 9]); 359 | }); 360 | 361 | it("take multiple times from lazy", () => { 362 | const lazy = gen([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); 363 | expect(take(0, lazy)).toEqual([]); 364 | expect(take(3, lazy)).toEqual([0, 1, 2]); 365 | expect(take(3, lazy)).toEqual([3, 4, 5]); 366 | expect(take(5, lazy)).toEqual([6, 7, 8, 9]); 367 | }); 368 | }); 369 | 370 | describe("uniqueJustseen", () => { 371 | it("uniqueJustseen w/ empty list", () => { 372 | expect(Array.from(uniqueJustseen([]))).toEqual([]); 373 | }); 374 | 375 | it("uniqueJustseen", () => { 376 | expect(Array.from(uniqueJustseen([1, 2, 3, 4, 5]))).toEqual([1, 2, 3, 4, 5]); 377 | expect(Array.from(uniqueJustseen([1, 1, 1, 2, 2]))).toEqual([1, 2]); 378 | expect(Array.from(uniqueJustseen([1, 1, 1, 2, 2, 1, 1, 1, 1]))).toEqual([1, 2, 1]); 379 | }); 380 | 381 | it("uniqueEverseen with key function", () => { 382 | expect(Array.from(uniqueJustseen("AaABbBCcaABBb", (s) => s.toLowerCase()))).toEqual(["A", "B", "C", "a", "B"]); 383 | }); 384 | }); 385 | 386 | describe("uniqueEverseen", () => { 387 | it("uniqueEverseen w/ empty list", () => { 388 | expect(Array.from(uniqueEverseen([]))).toEqual([]); 389 | }); 390 | 391 | it("uniqueEverseen never emits dupes, but keeps input ordering", () => { 392 | expect(Array.from(uniqueEverseen([1, 2, 3, 4, 5]))).toEqual([1, 2, 3, 4, 5]); 393 | expect(Array.from(uniqueEverseen([1, 1, 1, 2, 2, 3, 1, 3, 0, 4]))).toEqual([1, 2, 3, 0, 4]); 394 | expect(Array.from(uniqueEverseen([1, 1, 1, 2, 2, 1, 1, 1, 1]))).toEqual([1, 2]); 395 | }); 396 | 397 | it("uniqueEverseen with key function", () => { 398 | expect(Array.from(uniqueEverseen("AAAABBBCCDAABBB"))).toEqual(["A", "B", "C", "D"]); 399 | expect(Array.from(uniqueEverseen("ABCcAb", (s) => s.toLowerCase()))).toEqual(["A", "B", "C"]); 400 | expect(Array.from(uniqueEverseen("AbCBBcAb", (s) => s.toLowerCase()))).toEqual(["A", "b", "C"]); 401 | }); 402 | }); 403 | 404 | describe("dupes", () => { 405 | it("dupes w/ empty list", () => { 406 | expect(Array.from(dupes([]))).toEqual([]); 407 | }); 408 | 409 | it("dupes on a list without dupes", () => { 410 | expect(Array.from(dupes([1, 2, 3, 4, 5]))).toEqual([]); 411 | }); 412 | 413 | it("dupes on a list with dupes", () => { 414 | expect(Array.from(dupes(Array.from("Hello")))).toEqual([["l", "l"]]); 415 | expect(Array.from(dupes(Array.from("AAAABCDEEEFABG")))).toEqual([ 416 | ["A", "A", "A", "A", "A"], 417 | ["E", "E", "E"], 418 | ["B", "B"], 419 | ]); 420 | }); 421 | 422 | it("dupes with a key function", () => { 423 | expect(Array.from(dupes(Array.from("AbBCcABdE"), (s) => s.toLowerCase()))).toEqual([ 424 | ["b", "B", "B"], 425 | ["C", "c"], 426 | ["A", "A"], 427 | ]); 428 | }); 429 | 430 | it("dupes with complex objects and a key function", () => { 431 | expect( 432 | Array.from( 433 | dupes( 434 | [ 435 | { name: "Charlie", surname: "X" }, 436 | { name: "Alice", surname: "Rubrik" }, 437 | { name: "Alice", surname: "Doe" }, 438 | { name: "Bob" }, 439 | ], 440 | (p) => p.name, 441 | ), 442 | ), 443 | ).toEqual([ 444 | [ 445 | { name: "Alice", surname: "Rubrik" }, 446 | { name: "Alice", surname: "Doe" }, 447 | ], 448 | ]); 449 | 450 | expect( 451 | Array.from( 452 | dupes( 453 | [{ name: "Bob" }, { name: "Alice", surname: "Rubrik" }, { name: "Alice", surname: "Doe" }, { name: "Bob" }], 454 | (p) => p.name, 455 | ), 456 | ), 457 | ).toEqual([ 458 | [ 459 | { name: "Alice", surname: "Rubrik" }, 460 | { name: "Alice", surname: "Doe" }, 461 | ], 462 | [{ name: "Bob" }, { name: "Bob" }], 463 | ]); 464 | }); 465 | }); 466 | -------------------------------------------------------------------------------- /test/itertools.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { 4 | all, 5 | chain, 6 | compress, 7 | count, 8 | cycle, 9 | dropwhile, 10 | groupBy, 11 | ifilter, 12 | igroupby, 13 | imap, 14 | indexBy, 15 | islice, 16 | permutations, 17 | range, 18 | repeat, 19 | take, 20 | takewhile, 21 | zipLongest, 22 | zipLongest3, 23 | zipMany, 24 | } from "~"; 25 | import { primitiveIdentity } from "~/utils"; 26 | 27 | const isEven = (x: number) => x % 2 === 0; 28 | const isEvenIndex = (_: unknown, index: number) => index % 2 === 0; 29 | const isPositive = (x: number) => x >= 0; 30 | 31 | function isNum(value: unknown): value is number { 32 | return typeof value === "number"; 33 | } 34 | 35 | function* gen(values: T[]): Iterable { 36 | for (const value of values) { 37 | yield value; 38 | } 39 | } 40 | 41 | describe("chain", () => { 42 | it("chains empty iterables", () => { 43 | expect(Array.from(chain([], []))).toEqual([]); 44 | }); 45 | 46 | it("chains iterables together", () => { 47 | expect(Array.from(chain(["foo"], []))).toEqual(["foo"]); 48 | expect(Array.from(chain([], ["bar"]))).toEqual(["bar"]); 49 | expect(Array.from(chain([], ["bar"], ["qux"]))).toEqual(["bar", "qux"]); 50 | expect(Array.from(chain(["foo", "bar"], ["qux"]))).toEqual(["foo", "bar", "qux"]); 51 | }); 52 | }); 53 | 54 | describe("compress", () => { 55 | it("compress on empty list", () => { 56 | expect(compress([], [])).toEqual([]); 57 | }); 58 | 59 | it("compress removes selected items", () => { 60 | expect(compress("abc", [])).toEqual([]); 61 | expect(compress("abc", [true])).toEqual(["a"]); 62 | expect(compress("abc", [false, false, false])).toEqual([]); 63 | expect(compress("abc", [true, false, true])).toEqual(["a", "c"]); 64 | }); 65 | }); 66 | 67 | describe("count", () => { 68 | it("default counter", () => { 69 | expect(take(6, count())).toEqual([0, 1, 2, 3, 4, 5]); 70 | }); 71 | 72 | it("counter from different start value", () => { 73 | expect(take(6, count(1))).toEqual([1, 2, 3, 4, 5, 6]); 74 | expect(take(6, count(-3))).toEqual([-3, -2, -1, 0, 1, 2]); 75 | }); 76 | 77 | it("counter backwards", () => { 78 | expect(take(6, count(4, -1))).toEqual([4, 3, 2, 1, 0, -1]); 79 | expect(take(5, count(-3, -2))).toEqual([-3, -5, -7, -9, -11]); 80 | }); 81 | }); 82 | 83 | describe("cycle", () => { 84 | it("cycle with empty list", () => { 85 | expect(Array.from(cycle([]))).toEqual([]); 86 | }); 87 | 88 | it("cycles", () => { 89 | // We'll have to wrap it in a take() call to avoid infinite-length arrays :) 90 | expect(take(3, cycle(["x"]))).toEqual(["x", "x", "x"]); 91 | expect(take(5, cycle(["even", "odd"]))).toEqual(["even", "odd", "even", "odd", "even"]); 92 | }); 93 | 94 | it("cycles with infinite iterable", () => { 95 | // Function `cycle` should properly work with infinite iterators (`repeat('x')` in this case) 96 | expect(take(3, cycle(repeat("x")))).toEqual(["x", "x", "x"]); 97 | }); 98 | }); 99 | 100 | describe("dropwhile", () => { 101 | it("dropwhile on empty list", () => { 102 | expect(Array.from(dropwhile([], isEven))).toEqual([]); 103 | expect(Array.from(dropwhile([], isEvenIndex))).toEqual([]); 104 | expect(Array.from(dropwhile([], isPositive))).toEqual([]); 105 | }); 106 | 107 | it("dropwhile on list", () => { 108 | expect(Array.from(dropwhile([1], isEven))).toEqual([1]); 109 | expect(Array.from(dropwhile([1], isEvenIndex))).toEqual([]); 110 | expect(Array.from(dropwhile([1], isPositive))).toEqual([]); 111 | 112 | expect(Array.from(dropwhile([-1, 0, 1], isEven))).toEqual([-1, 0, 1]); 113 | expect(Array.from(dropwhile([4, -1, 0, 1], isEven))).toEqual([-1, 0, 1]); 114 | expect(Array.from(dropwhile([-1, 0, 1], isEvenIndex))).toEqual([0, 1]); 115 | expect(Array.from(dropwhile([4, -1, 0, 1], isEvenIndex))).toEqual([-1, 0, 1]); 116 | expect(Array.from(dropwhile([-1, 0, 1], isPositive))).toEqual([-1, 0, 1]); 117 | expect(Array.from(dropwhile([7, -1, 0, 1], isPositive))).toEqual([-1, 0, 1]); 118 | 119 | expect(Array.from(dropwhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([7, 8, 10]); 120 | expect(Array.from(dropwhile([0, 2, 4, 6, 7, 8, 10], isEvenIndex))).toEqual([2, 4, 6, 7, 8, 10]); 121 | expect(Array.from(dropwhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))).toEqual([-2, 3, 4, 5, 6, 7]); 122 | }); 123 | 124 | it("dropwhile on lazy iterable", () => { 125 | const lazy = dropwhile(gen([0, 1, 2, -2, 3, 4, 5, 6, 7]), isPositive); 126 | expect(Array.from(lazy)).toEqual([-2, 3, 4, 5, 6, 7]); 127 | }); 128 | }); 129 | 130 | describe("igroupby", () => { 131 | const countValues = (grouped: Iterable<[K, Iterable]>) => 132 | Array.from(imap(grouped, ([k, v]) => [k, Array.from(v).length])); 133 | 134 | it("igroupby with empty list", () => { 135 | expect(Array.from(igroupby([]))).toEqual([]); 136 | }); 137 | 138 | it("groups elements", () => { 139 | expect(countValues(igroupby("aaabbbbcddddaa"))).toEqual([ 140 | ["a", 3], 141 | ["b", 4], 142 | ["c", 1], 143 | ["d", 4], 144 | ["a", 2], 145 | ]); 146 | }); 147 | 148 | it("groups element with key function", () => { 149 | expect(countValues(igroupby("aaaAbb"))).toEqual([ 150 | ["a", 3], 151 | ["A", 1], 152 | ["b", 2], 153 | ]); 154 | expect(countValues(igroupby("aaaAbb", (val) => val.toUpperCase()))).toEqual([ 155 | ["A", 4], 156 | ["B", 2], 157 | ]); 158 | }); 159 | 160 | it("handles not using the inner iterator", () => { 161 | expect(Array.from(imap(igroupby("aaabbbbcddddaa"), ([k]) => k))).toEqual(["a", "b", "c", "d", "a"]); 162 | }); 163 | 164 | it("handles using the inner iterator after the iteration has advanced", () => { 165 | expect(Array.from(igroupby("aaabb")).map(([, v]) => Array.from(v))).toEqual([[], []]); 166 | const it = igroupby("aaabbccc"); 167 | // Flow does not like that I use next on an iterable (it is actually 168 | // a generator but the Generator type is awful. 169 | 170 | const [, v1] = it.next().value!; 171 | const [, v2] = it.next().value!; 172 | const [, v3] = it.next().value!; 173 | 174 | expect([...v1]).toEqual([]); 175 | expect([...v2]).toEqual([]); 176 | expect(v3.next().value!).toEqual("c"); 177 | Array.from(it); // exhaust the igroupby iterator 178 | expect([...v3]).toEqual([]); 179 | }); 180 | }); 181 | 182 | describe("groupBy", () => { 183 | it("groupBy with empty list", () => { 184 | expect(groupBy([], () => 0)).toEqual({}); 185 | }); 186 | 187 | it("groupBy uniqueness counting", () => { 188 | expect(Object.keys(groupBy("aaabb", primitiveIdentity)).length).toEqual(2); 189 | }); 190 | 191 | it("groups elements", () => { 192 | expect(groupBy("aaabbbbcddddaa", primitiveIdentity)).toEqual({ 193 | a: ["a", "a", "a", "a", "a"], 194 | b: ["b", "b", "b", "b"], 195 | c: ["c"], 196 | d: ["d", "d", "d", "d"], 197 | }); 198 | }); 199 | 200 | it("groups element with key function", () => { 201 | expect(groupBy("aaaAbb", primitiveIdentity)).toEqual({ 202 | a: ["a", "a", "a"], 203 | A: ["A"], 204 | b: ["b", "b"], 205 | }); 206 | expect(groupBy("aaaAbb", (val) => val.toUpperCase())).toEqual({ 207 | A: ["a", "a", "a", "A"], 208 | B: ["b", "b"], 209 | }); 210 | }); 211 | 212 | it("handles not using the inner iterator", () => { 213 | expect(Object.keys(groupBy("aaabbbbcddddaa", primitiveIdentity))).toEqual(["a", "b", "c", "d"]); 214 | }); 215 | }); 216 | 217 | describe("indexBy", () => { 218 | it("indexBy with empty list", () => { 219 | expect(indexBy([], () => 0)).toEqual({}); 220 | }); 221 | 222 | it("indexBy uniqueness counting", () => { 223 | expect(Object.keys(indexBy("aaabb", primitiveIdentity)).length).toEqual(2); 224 | }); 225 | 226 | it("indexes elements", () => { 227 | expect(indexBy("aaabbbbcddddaa", primitiveIdentity)).toEqual({ 228 | a: "a", 229 | b: "b", 230 | c: "c", 231 | d: "d", 232 | }); 233 | }); 234 | 235 | it("indexes elements with key function", () => { 236 | expect(indexBy("aaaAbb", primitiveIdentity)).toEqual({ 237 | a: "a", 238 | A: "A", 239 | b: "b", 240 | }); 241 | expect(indexBy("aaaAbb", (val) => val.toUpperCase())).toEqual({ 242 | A: "A", // Last element that maps to 'A' is the actual 'A' at position 3 243 | B: "b", // Last element that maps to 'B' is 'b' at position 5 244 | }); 245 | }); 246 | 247 | it("handles duplicate keys by keeping last value", () => { 248 | const numbers = [42, 13, 379, 7, 22, 3, 99]; 249 | expect(indexBy(numbers, (n) => n % 2)).toEqual({ 250 | 0: 22, 251 | 1: 99, 252 | }); 253 | }); 254 | 255 | it("works with numbers as keys", () => { 256 | const items = [ 257 | { pos: 0, value: "first" }, 258 | { pos: 1, value: "second" }, 259 | { pos: 2, value: "third" }, 260 | ]; 261 | expect(indexBy(items, (item) => item.pos)).toEqual({ 262 | 0: { pos: 0, value: "first" }, 263 | 1: { pos: 1, value: "second" }, 264 | 2: { pos: 2, value: "third" }, 265 | }); 266 | }); 267 | }); 268 | 269 | describe("icompress", () => { 270 | it("icompress is tested through compress() tests", () => { 271 | // This is okay 272 | }); 273 | }); 274 | 275 | describe("ifilter", () => { 276 | it("ifilter is tested through filter() tests (see builtins)", () => { 277 | // This is okay 278 | }); 279 | 280 | it("ifilter can handle infinite inputs", () => { 281 | expect(take(5, ifilter(range(9999), isEven))).toEqual([0, 2, 4, 6, 8]); 282 | expect(take(5, ifilter(range(9999), isEvenIndex))).toEqual([0, 2, 4, 6, 8]); 283 | }); 284 | 285 | it("ifilter retains rich type info", () => { 286 | const filtered = take(5, ifilter([3, "hi", null, -7], isNum)); 287 | expect(filtered).toEqual([3, -7]); 288 | // ^^^^^^^^ number[] 289 | }); 290 | }); 291 | 292 | describe("imap", () => { 293 | it("imap is tested through map() tests (see builtins)", () => { 294 | // This is okay 295 | }); 296 | 297 | it("...but imap can handle infinite inputs", () => { 298 | expect( 299 | take( 300 | 3, 301 | imap(range(9999), (x) => -x), 302 | ), 303 | ).toEqual([-0, -1, -2]); 304 | }); 305 | }); 306 | 307 | describe("islice", () => { 308 | it("islice an empty iterable", () => { 309 | expect(Array.from(islice([], 2))).toEqual([]); 310 | }); 311 | 312 | it("islice with arguments", () => { 313 | expect(Array.from(islice("ABCDEFG", /*stop*/ 2))).toEqual(["A", "B"]); 314 | expect(Array.from(islice("ABCDEFG", 2, 4))).toEqual(["C", "D"]); 315 | expect(Array.from(islice("ABCDEFG", /*start*/ 2, /*stop*/ undefined))).toEqual(["A", "B"]); 316 | expect(Array.from(islice("ABCDEFG", /*start*/ 2, /*stop*/ null))).toEqual(["C", "D", "E", "F", "G"]); 317 | expect(Array.from(islice("ABCDEFG", /*start*/ 0, /*stop*/ null, /*step*/ 2))).toEqual(["A", "C", "E", "G"]); 318 | expect(Array.from(islice("ABCDEFG", /*start*/ 1, /*stop*/ null, /*step*/ 2))).toEqual(["B", "D", "F"]); 319 | }); 320 | 321 | it("islice with infinite inputs", () => { 322 | expect(Array.from(islice(count(1), 0))).toEqual([]); 323 | expect(Array.from(islice(count(1), 0, 0))).toEqual([]); 324 | expect(Array.from(islice(count(1), 3, 2))).toEqual([]); 325 | expect(Array.from(islice(count(1), 5))).toEqual([1, 2, 3, 4, 5]); 326 | expect(Array.from(islice(count(1), 3, 7))).toEqual([4, 5, 6, 7]); 327 | expect(Array.from(islice(count(1), 4, 32, 5))).toEqual([5, 10, 15, 20, 25, 30]); 328 | expect(Array.from(islice(count(69), 18, 391, 81))).toEqual([87, 168, 249, 330, 411]); 329 | }); 330 | 331 | it("islice invalid stop argument", () => { 332 | expect(() => Array.from(islice("ABCDEFG", /*stop*/ -2))).toThrow(); 333 | expect(() => Array.from(islice("ABCDEFG", -2, -3))).toThrow(); 334 | expect(() => Array.from(islice("ABCDEFG", 0, 3, 0))).toThrow(); 335 | expect(() => Array.from(islice("ABCDEFG", 0, 3, -1))).toThrow(); 336 | }); 337 | 338 | it("continuation after islice", () => { 339 | const lazy = gen(take(10, count(1))); 340 | expect(Array.from(islice(lazy, 2, 5))).toEqual([3, 4, 5]); 341 | expect(Array.from(lazy)).toEqual([6, 7, 8, 9, 10]); 342 | }); 343 | }); 344 | 345 | describe("izip", () => { 346 | it("izip is tested through zip() tests (see builtins)", () => { 347 | // This is okay 348 | }); 349 | }); 350 | 351 | describe("izip3", () => { 352 | it("izip3 is tested through zip3() tests (see builtins)", () => { 353 | // This is okay 354 | }); 355 | }); 356 | 357 | describe("izipMany", () => { 358 | it("izipMany is tested through zipMany() tests", () => { 359 | // This is okay 360 | }); 361 | }); 362 | 363 | describe("izipLongest", () => { 364 | it("izipLongest is tested through zipLongest() tests", () => { 365 | // This is okay 366 | }); 367 | }); 368 | 369 | describe("izipLongest3", () => { 370 | it("izipLongest3 is tested through zipLongest3() tests", () => { 371 | // This is okay 372 | }); 373 | }); 374 | 375 | describe("permutations", () => { 376 | it("permutations of empty list", () => { 377 | expect(Array.from(permutations([]))).toEqual([[]]); 378 | }); 379 | 380 | it("permutations of unique values", () => { 381 | expect(Array.from(permutations([1, 2]))).toEqual([ 382 | [1, 2], 383 | [2, 1], 384 | ]); 385 | 386 | expect(Array.from(permutations([1, 2, 3]))).toEqual([ 387 | [1, 2, 3], 388 | [1, 3, 2], 389 | [2, 1, 3], 390 | [2, 3, 1], 391 | [3, 1, 2], 392 | [3, 2, 1], 393 | ]); 394 | 395 | // Duplicates have no effect on the results 396 | expect(Array.from(permutations([2, 2, 3]))).toEqual([ 397 | [2, 2, 3], 398 | [2, 3, 2], 399 | [2, 2, 3], 400 | [2, 3, 2], 401 | [3, 2, 2], 402 | [3, 2, 2], 403 | ]); 404 | }); 405 | 406 | it("permutations with r param", () => { 407 | // r too big 408 | expect(Array.from(permutations([1, 2], 5))).toEqual([]); 409 | 410 | // prettier-ignore 411 | expect(Array.from(permutations(range(4), 2))).toEqual([ 412 | [0, 1], [0, 2], [0, 3], [1, 0], [1, 2], [1, 3], 413 | [2, 0], [2, 1], [2, 3], [3, 0], [3, 1], [3, 2], 414 | ]); 415 | }); 416 | }); 417 | 418 | describe("repeat", () => { 419 | it("repeat indefinitely #1", () => { 420 | // practically limit it to something (in this case 99) 421 | const items = take(99, repeat(123)); 422 | expect(all(items, (n) => n === 123)).toEqual(true); 423 | }); 424 | 425 | it("repeat indefinitely #2", () => { 426 | const items = take(99, repeat("foo")); 427 | expect(all(items, (n) => n === "foo")).toEqual(true); 428 | }); 429 | 430 | it("repeat a fixed number of times", () => { 431 | const items = repeat("foo", 100); 432 | expect(all(items, (n) => n === "foo")).toEqual(true); 433 | }); 434 | }); 435 | 436 | describe("takewhile", () => { 437 | it("takewhile on empty list", () => { 438 | expect(Array.from(takewhile([], isEven))).toEqual([]); 439 | expect(Array.from(takewhile([], isEvenIndex))).toEqual([]); 440 | expect(Array.from(takewhile([], isPositive))).toEqual([]); 441 | }); 442 | 443 | it("takewhile on list", () => { 444 | expect(Array.from(takewhile([1], isEven))).toEqual([]); 445 | expect(Array.from(takewhile([1], isEvenIndex))).toEqual([1]); 446 | expect(Array.from(takewhile([1], isPositive))).toEqual([1]); 447 | 448 | expect(Array.from(takewhile([-1, 0, 1], isEven))).toEqual([]); 449 | expect(Array.from(takewhile([-1, 0, 1], isEvenIndex))).toEqual([-1]); 450 | expect(Array.from(takewhile([-1, 0, 1], isPositive))).toEqual([]); 451 | 452 | expect(Array.from(takewhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([0, 2, 4, 6]); 453 | expect(Array.from(takewhile([0, 2, 4, 6, 7, 8, 10], isEvenIndex))).toEqual([0]); 454 | expect(Array.from(takewhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))).toEqual([0, 1, 2]); 455 | }); 456 | 457 | it("takewhile on lazy iterable", () => { 458 | const lazy = gen([0, 1, 2, -2, 4, 6, 8, 7]); 459 | const lazy1 = takewhile(lazy, isPositive); 460 | const lazy2 = takewhile(lazy, isEvenIndex); 461 | const lazy3 = takewhile(lazy, isEven); 462 | 463 | expect(Array.from(lazy1)).toEqual([0, 1, 2]); 464 | 465 | // By now, the -2 from the original input has been consumed, but we should 466 | // be able to continue pulling more values from the same input 467 | expect(Array.from(lazy2)).toEqual([4]); 468 | 469 | // By now, the 6 from the original input has been consumed, but we should 470 | // be able to continue pulling more values from the same input 471 | expect(Array.from(lazy3)).toEqual([8]); 472 | }); 473 | }); 474 | 475 | describe("zipMany", () => { 476 | it("zipMany with empty iterable", () => { 477 | expect(zipMany([])).toEqual([]); 478 | expect(zipMany([], [])).toEqual([]); 479 | }); 480 | 481 | it("zipMany takes any number of (homogenous) iterables", () => { 482 | expect(zipMany("abc", "ABC")).toEqual([ 483 | ["a", "A"], 484 | ["b", "B"], 485 | ["c", "C"], 486 | ]); 487 | expect(zipMany("abc", "ABC", "pqrs", "xyz")).toEqual([ 488 | ["a", "A", "p", "x"], 489 | ["b", "B", "q", "y"], 490 | ["c", "C", "r", "z"], 491 | ]); 492 | }); 493 | }); 494 | 495 | describe("zipLongest", () => { 496 | it("zipLongest with empty iterable", () => { 497 | expect(zipLongest([], [])).toEqual([]); 498 | }); 499 | 500 | it("zipLongest with two iterables", () => { 501 | expect(zipLongest("abc", "")).toEqual([ 502 | ["a", undefined], 503 | ["b", undefined], 504 | ["c", undefined], 505 | ]); 506 | expect(zipLongest("x", "abc")).toEqual([ 507 | ["x", "a"], 508 | [undefined, "b"], 509 | [undefined, "c"], 510 | ]); 511 | expect(zipLongest("x", "abc", /* filler */ 0)).toEqual([ 512 | ["x", "a"], 513 | [0, "b"], 514 | [0, "c"], 515 | ]); 516 | }); 517 | }); 518 | 519 | describe("zipLongest3", () => { 520 | it("zipLongest3 with empty iterable", () => { 521 | expect(zipLongest3([], [], [])).toEqual([]); 522 | }); 523 | 524 | it("zipLongest3 with two iterables", () => { 525 | expect(zipLongest3("abc", "", [1, 2, 3])).toEqual([ 526 | ["a", undefined, 1], 527 | ["b", undefined, 2], 528 | ["c", undefined, 3], 529 | ]); 530 | expect(zipLongest3("x", "abc", [1, 2, 3])).toEqual([ 531 | ["x", "a", 1], 532 | [undefined, "b", 2], 533 | [undefined, "c", 3], 534 | ]); 535 | expect(zipLongest3("x", "abc", [1, 2], /* filler */ 0)).toEqual([ 536 | ["x", "a", 1], 537 | [0, "b", 2], 538 | [0, "c", 0], 539 | ]); 540 | }); 541 | }); 542 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://img.shields.io/npm/v/itertools.svg)](https://www.npmjs.com/package/itertools) 2 | [![Test Status](https://github.com/nvie/itertools/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/nvie/itertools.js/actions) 3 | [![Bundle size for itertools](https://pkg-size.dev/badge/bundle/2130)](https://pkg-size.dev/itertools) 4 | 5 | A JavaScript port of Python's awesome 6 | [itertools](https://docs.python.org/library/itertools.html) standard library. 7 | 8 | Usage example: 9 | 10 | ```ts 11 | >>> import { izip, cycle } from 'itertools'; 12 | >>> 13 | >>> const xs = [1, 2, 3, 4]; 14 | >>> const ys = ['hello', 'there']; 15 | >>> for (const [x, y] of izip(xs, cycle(ys))) { 16 | >>> console.log(x, y); 17 | >>> } 18 | 1 'hello' 19 | 2 'there' 20 | 3 'hello' 21 | 4 'there' 22 | ``` 23 | 24 | ## About argument order 25 | 26 | In Python, many of the itertools take a function as an argument. In the JS 27 | port of these we initially kept these orderings the same to stick closely to 28 | the Python functions, but in practice, it turns out to be more pragmatic to 29 | flip them, so the function gets to be the second param. Example: 30 | 31 | In Python: 32 | 33 | ```python 34 | map(fn, items) 35 | ``` 36 | 37 | But in JavaScript: 38 | 39 | ```python 40 | map(items, fn) 41 | ``` 42 | 43 | The rationale for this flipping of argument order is because in practice, the 44 | function bodies can span multiple lines, in which case the following block will 45 | remain aesthetically pleasing: 46 | 47 | ```ts 48 | import { map } from "itertools"; 49 | 50 | const numbers = [1, 2, 3]; 51 | const squares = map(numbers, (n) => { 52 | // 53 | // Do something wild with these numbers here 54 | // 55 | // ... 56 | return n * n; 57 | }); 58 | ``` 59 | 60 | ## API 61 | 62 | The `itertools` package consists of a few building blocks: 63 | 64 | - [Ports of builtins](#ports-of-builtins) 65 | - [Ports of itertools](#ports-of-itertools) 66 | - [Ports of more-itertools](#ports-of-more-itertools) 67 | - [Additions](#additions) 68 | 69 | ### Ports of builtins 70 | 71 | - [every](#every) 72 | - [some](#some) 73 | - [contains](#contains) 74 | - [enumerate](#enumerate) 75 | - [filter](#filter) 76 | - [iter](#iter) 77 | - [map](#map) 78 | - [max](#max) 79 | - [min](#min) 80 | - [range](#range) 81 | - [reduce](#reduce) 82 | - [sorted](#sorted) 83 | - [xrange](#xrange) 84 | - [sum](#sum) 85 | - [zip](#zip) 86 | - [zip3](#zip3) 87 | 88 | --- 89 | 90 | # every(iterable: Iterable<T>, keyFn?: Predicate<T>): boolean [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 91 | 92 | Returns true when every of the items in iterable are truthy. An optional key 93 | function can be used to define what truthiness means for this specific 94 | collection. 95 | 96 | Examples: 97 | 98 | ```ts 99 | every([]); // => true 100 | every([0]); // => false 101 | every([0, 1, 2]); // => false 102 | every([1, 2, 3]); // => true 103 | ``` 104 | 105 | Examples with using a key function: 106 | 107 | ```ts 108 | every([2, 4, 6], (n) => n % 2 === 0); // => true 109 | every([2, 4, 5], (n) => n % 2 === 0); // => false 110 | ``` 111 | 112 | --- 113 | 114 | # some(iterable: Iterable<T>, keyFn?: Predicate<T>): boolean [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 115 | 116 | Returns true when some of the items in iterable are truthy. An optional key 117 | function can be used to define what truthiness means for this specific 118 | collection. 119 | 120 | Examples: 121 | 122 | ```ts 123 | some([]); // => false 124 | some([0]); // => false 125 | some([0, 1, null, undefined]); // => true 126 | ``` 127 | 128 | Examples with using a key function: 129 | 130 | ```ts 131 | some([1, 4, 5], (n) => n % 2 === 0); // => true 132 | some([{ name: "Bob" }, { name: "Alice" }], (person) => person.name.startsWith("C")); // => false 133 | ``` 134 | 135 | --- 136 | 137 | # contains(haystack: Iterable<T>, needle: T): boolean [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 138 | 139 | Returns true when some of the items in the iterable are equal to the target 140 | object. 141 | 142 | Examples: 143 | 144 | ```ts 145 | contains([], "whatever"); // => false 146 | contains([3], 42); // => false 147 | contains([3], 3); // => true 148 | contains([0, 1, 2], 2); // => true 149 | ``` 150 | 151 | --- 152 | 153 | # enumerate(iterable: Iterable<T>, start: number = 0): Iterable<[number, T]> [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 154 | 155 | Returns an iterable of enumeration pairs. Iterable must be a sequence, an 156 | iterator, or some other object which supports iteration. The elements produced 157 | by returns a tuple containing a counter value (starting from 0 by default) and 158 | the values obtained from iterating over given iterable. 159 | 160 | Example: 161 | 162 | ```ts 163 | import { enumerate } from "itertools"; 164 | 165 | console.log([...enumerate(["hello", "world"])]); 166 | // [0, 'hello'], [1, 'world']] 167 | ``` 168 | 169 | --- 170 | 171 | # filter(iterable: Iterable<T>, predicate: Predicate<T>): T[] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 172 | 173 | Eager version of [ifilter](#ifilter). 174 | 175 | --- 176 | 177 | # iter(iterable: Iterable<T>): Iterator<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 178 | 179 | Returns an iterator object for the given iterable. This can be used to 180 | manually get an iterator for any iterable datastructure. The purpose and main 181 | use case of this function is to get a single iterator (a thing with state, 182 | think of it as a "cursor") which can only be consumed once. 183 | 184 | --- 185 | 186 | # map(iterable: _Iterable<T>_, mapper: _(item: T) => V_): _V[]_ [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 187 | 188 | Eager version of [imap](#imap). 189 | 190 | --- 191 | 192 | # max(iterable: Iterable<T>, keyFn?: (item: T) => number): T | undefined [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 193 | 194 | Return the largest item in an iterable. Only works for numbers, as ordering is 195 | pretty poorly defined on any other data type in JS. The optional `keyFn` 196 | argument specifies a one-argument ordering function like that used for 197 | [sorted](#sorted). 198 | 199 | If the iterable is empty, `undefined` is returned. 200 | 201 | If multiple items are maximal, the function returns either one of them, but 202 | which one is not defined. 203 | 204 | --- 205 | 206 | # min(iterable: Iterable<T>, keyFn?: (item: T) => number): T | undefined [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 207 | 208 | Return the smallest item in an iterable. Only works for numbers, as ordering 209 | is pretty poorly defined on any other data type in JS. The optional `keyFn` 210 | argument specifies a one-argument ordering function like that used for 211 | [sorted](#sorted). 212 | 213 | If the iterable is empty, `undefined` is returned. 214 | 215 | If multiple items are minimal, the function returns either one of them, but 216 | which one is not defined. 217 | 218 | --- 219 | 220 | # range(stop: number): Iterable<number> [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source")
221 | # range(start: number, stop: number, step: number = 1): Iterable<number> [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 222 | 223 | Returns an iterator producing all the numbers in the given range one by one, 224 | starting from `start` (default 0), as long as `i < stop`, in increments of 225 | `step` (default 1). 226 | 227 | `range(a)` is a convenient shorthand for `range(0, a)`. 228 | 229 | Various valid invocations: 230 | 231 | range(5) // 0, 1, 2, 3, 4 232 | range(0, 5) // 0, 1, 2, 3, 4 233 | range(0, 5, 2) // 0, 2, 4 234 | range(5, 0, -1) // 5, 4, 3, 2, 1 235 | range(5, 0) // (empty) 236 | range(-3) // (empty) 237 | 238 | For a positive `step`, the iterator will keep producing values `n` as long as 239 | the stop condition `n < stop` is satisfied. 240 | 241 | For a negative `step`, the iterator will keep producing values `n` as long as 242 | the stop condition `n > stop` is satisfied. 243 | 244 | The produced range will be empty if the first value to produce already does not 245 | meet the value constraint. 246 | 247 | --- 248 | 249 | # xrange(stop: number): number[] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source")
250 | # xrange(start: number, stop: number, step: number = 1): number[] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 251 | 252 | Returns an array with all the numbers in the given range, starting from 253 | `start` (default 0), as long as `i < stop`, in increments of `step` 254 | (default 1). 255 | 256 | `xrange(5)` is a convenient shorthand for `Array.from(range(5))`. 257 | 258 | Various valid invocations: 259 | 260 | xrange(5) // [0, 1, 2, 3, 4] 261 | xrange(2, 5) // [2, 3, 4] 262 | xrange(0, 5, 2) // [0, 2, 4] 263 | xrange(5, 0, -1) // [5, 4, 3, 2, 1] 264 | xrange(5, 0) // [] 265 | 266 | For a positive `step`, the iterator will keep producing values `n` as long as 267 | the stop condition `n < stop` is satisfied. 268 | 269 | For a negative `step`, the iterator will keep producing values `n` as long as 270 | the stop condition `n > stop` is satisfied. 271 | 272 | The produced range will be empty if the first value to produce already does not 273 | meet the value constraint. 274 | 275 | Don't use this on large or infinite ranges, as it will allocate a large array 276 | in memory. 277 | 278 | --- 279 | 280 | # reduce(iterable: Iterable<T>, reducer: (O, T, number) => O, start: O): O [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source")
281 | # reduce(iterable: Iterable<T>, reducer: (T, T, number) => T): T | undefined [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 282 | 283 | Apply function of two arguments cumulatively to the items of sequence, from 284 | left to right, so as to reduce the sequence to a single value. For example: 285 | 286 | ```ts 287 | reduce([1, 2, 3, 4, 5], (total, x) => total + x, 0); 288 | ``` 289 | 290 | calculates 291 | 292 | (((((0+1)+2)+3)+4)+5) 293 | 294 | The left argument, `total`, is the accumulated value and the right argument, 295 | `x`, is the update value from the sequence. 296 | 297 | Without an explicit initializer arg: 298 | 299 | ```ts 300 | reduce([1, 2, 3, 4, 5], (total, x) => total + x); 301 | ``` 302 | 303 | it calculates 304 | 305 | ((((1+2)+3)+4)+5) 306 | 307 | --- 308 | 309 | # sorted(iterable: Iterable<T>, keyFn?: (item: T) => Primitive, reverse?: boolean): T[] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 310 | 311 | Return a new sorted list from the items in iterable. 312 | 313 | Has two optional arguments: 314 | 315 | - `keyFn` specifies a function of one argument providing a primitive identity 316 | for each element in the iterable. that will be used to compare. The default 317 | value is to use a default identity function that is only defined for 318 | primitive types. 319 | 320 | - `reverse` is a boolean value. If `true`, then the list elements are sorted 321 | as if each comparison were reversed. 322 | 323 | --- 324 | 325 | # sum(iterable: Iterable<number>): number [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 326 | 327 | Sums the items of an iterable from left to right and returns the total. The 328 | sum will defaults to 0 if the iterable is empty. 329 | 330 | --- 331 | 332 | # zip(xs: Iterable<T1>, ys: Iterable<T2>): [T1, T2][] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source")
333 | # zip3(xs: Iterable<T1>, ys: Iterable<T2>, zs: Iterable<T3>): [T1, T2, T3][] [<>](https://github.com/nvie/itertools.js/blob/master/src/builtins.js "Source") 334 | 335 | Eager version of [izip](#izip) / [izip3](#izip3). 336 | 337 | ### Ports of itertools 338 | 339 | - [chain](#chain) 340 | - [compress](#compress) 341 | - [count](#count) 342 | - [cycle](#cycle) 343 | - [dropwhile](#dropwhile) 344 | - [groupBy](#groupBy) 345 | - [icompress](#icompress) 346 | - [ifilter](#ifilter) 347 | - [igroupby](#igroupby) 348 | - [imap](#imap) 349 | - [indexBy](#indexBy) 350 | - [islice](#islice) 351 | - [izip](#izip) 352 | - [izip3](#izip3) 353 | - [izipLongest](#izipLongest) 354 | - [izipMany](#izipMany) 355 | - [permutations](#permutations) 356 | - [repeat](#repeat) 357 | - [takewhile](#takewhile) 358 | - [zipLongest](#zipLongest) 359 | - [zipMany](#zipMany) 360 | 361 | --- 362 | 363 | # chain(...iterables: Iterable<T>[]): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 364 | 365 | Returns an iterator that returns elements from the first iterable until it is 366 | exhausted, then proceeds to the next iterable, until all of the iterables are 367 | exhausted. Used for treating consecutive sequences as a single sequence. 368 | 369 | --- 370 | 371 | # compress(iterable: Iterable<T>, selectors: Iterable<boolean>): T[] [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 372 | 373 | Eager version of [icompress](#icompress). 374 | 375 | --- 376 | 377 | # count(start: number, step: number): Iterable<number> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 378 | 379 | Returns an iterator that counts up values starting with number `start` (default 380 | 0), incrementing by `step`. To decrement, use a negative step number. 381 | 382 | --- 383 | 384 | # cycle(iterable: Iterable<T>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 385 | 386 | Returns an iterator producing elements from the iterable and saving a copy of 387 | each. When the iterable is exhausted, return elements from the saved copy. 388 | Repeats indefinitely. 389 | 390 | --- 391 | 392 | # dropwhile(iterable: Iterable<T>, predicate: (item: T) => boolean): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 393 | 394 | Returns an iterator that drops elements from the iterable as long as the 395 | predicate is true; afterwards, returns every remaining element. **Note:** the 396 | iterator does not produce any output until the predicate first becomes false. 397 | 398 | --- 399 | 400 | # groupBy(iterable: Iterable<T>, keyFn: (item: T) => K): Record<K, T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 401 | 402 | Groups elements of the iterable into a record based on the key function. Each 403 | key maps to an array of all elements that share the same key. 404 | 405 | ```ts 406 | const users = [ 407 | { name: "Alice", department: "Engineering" }, 408 | { name: "Bob", department: "Sales" }, 409 | { name: "Charlie", department: "Engineering" }, 410 | ]; 411 | 412 | groupBy(users, (user) => user.department); 413 | // { 414 | // 'Engineering': [ 415 | // { name: 'Alice', department: 'Engineering' }, 416 | // { name: 'Charlie', department: 'Engineering' } 417 | // ], 418 | // 'Sales': [ 419 | // { name: 'Bob', department: 'Sales' } 420 | // ] 421 | // } 422 | ``` 423 | 424 | --- 425 | 426 | # icompress(iterable: Iterable<T>, selectors: Iterable<boolean>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 427 | 428 | Returns an iterator that filters elements from data returning only those that 429 | have a corresponding element in selectors that evaluates to `true`. Stops when 430 | either the data or selectors iterables has been exhausted. 431 | 432 | --- 433 | 434 | # ifilter(iterable: Iterable<T>, predicate: Predicate<T>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 435 | 436 | Returns an iterator that filters elements from iterable returning only those 437 | for which the predicate is true. 438 | 439 | --- 440 | 441 | # igroupby(iterable: Iterable<T>, keyFn: (item: T) => Primitive): Iterable<[Primitive, Iterable<T>]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 442 | 443 | Make an Iterable that returns consecutive keys and groups from the iterable. 444 | The key is a function computing a key value for each element. If not specified, 445 | key defaults to an identity function and returns the element unchanged. 446 | Generally, the iterable needs to already be sorted on the same key function. 447 | 448 | The operation of `igroupby()` is similar to the `uniq` filter in Unix. It 449 | generates a break or new group every time the value of the key function changes 450 | (which is why it is usually necessary to have sorted the data using the same 451 | key function). That behavior differs from `SQL`’s `GROUP BY` which aggregates 452 | common elements regardless of their input order. 453 | 454 | The returned group is itself an iterator that shares the underlying iterable 455 | with `igroupby()`. Because the source is shared, when the `igroupby()` object is 456 | advanced, the previous group is no longer visible. So, if that data is needed 457 | later, it should be stored as an array. 458 | 459 | --- 460 | 461 | # imap(iterable: Iterable<T>, mapper: (item: T) => V): Iterable<V> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 462 | 463 | Returns an iterator that computes the given mapper function using arguments 464 | from each of the iterables. 465 | 466 | --- 467 | 468 | # indexBy(iterable: Iterable<T>, keyFn: (item: T) => K): Record<K, T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 469 | 470 | Creates an index (record) from the iterable where each key maps to the last 471 | element that produced that key. If multiple elements produce the same key, only 472 | the last one is kept. 473 | 474 | ```ts 475 | const users = [ 476 | { id: 1, name: "Alice" }, 477 | { id: 2, name: "Bob" }, 478 | { id: 3, name: "Charlie" }, 479 | ]; 480 | 481 | indexBy(users, (user) => user.id); 482 | // { 483 | // 1: { id: 1, name: 'Alice' }, 484 | // 2: { id: 2, name: 'Bob' }, 485 | // 3: { id: 3, name: 'Charlie' } 486 | // } 487 | ``` 488 | 489 | --- 490 | 491 | # islice(iterable: Iterable<T>[start: number], stop: number[, step: number]): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 492 | 493 | Returns an iterator that returns selected elements from the iterable. If 494 | `start` is non-zero, then elements from the iterable are skipped until start is 495 | reached. Then, elements are returned by making steps of `step` (defaults to 496 | 1). If set to higher than 1, items will be skipped. If `stop` is provided, 497 | then iteration continues until the iterator reached that index, otherwise, the 498 | iterable will be fully exhausted. `islice()` does not support negative values 499 | for `start`, `stop`, or `step`. 500 | 501 | --- 502 | 503 | # izip(xs: Iterable<T1>, ys: Iterable<T2>): Iterable<[T1, T2]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source")
504 | # izip3(xs: Iterable<T1>, ys: Iterable<T2>, zs: Iterable<T3>): Iterable<[T1, T2, T3]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 505 | 506 | Returns an iterator that aggregates elements from each of the iterables. Used 507 | for lock-step iteration over several iterables at a time. When iterating over 508 | two iterables, use `izip2`. When iterating over three iterables, use `izip3`, 509 | etc. `izip` is an alias for `izip2`. 510 | 511 | --- 512 | 513 | # izipLongest(xs: Iterable<T1>, ys: Iterable<T2>, filler?: D): Iterable<[T1 | D, T2 | D]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source")
514 | # izipLongest3(xs: Iterable<T1>, ys: Iterable<T2>, zs: Iterable<T3>, filler?: D): Iterable<[T1 | D, T2 | D, T3 | D]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 515 | 516 | Returns an iterator that aggregates elements from each of the iterables. If the 517 | iterables are of uneven length, missing values are filled-in with fillvalue. 518 | Iteration continues until the longest iterable is exhausted. 519 | 520 | --- 521 | 522 | # izipMany(...iters: Iterable<T>[]): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 523 | 524 | Like the other izips (`izip`, `izip3`, etc), but generalized to take an 525 | unlimited amount of input iterables. Think `izip(*iterables)` in Python. 526 | 527 | --- 528 | 529 | # permutations(iterable: Iterable<T>, r: number = undefined): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 530 | 531 | Return successive `r`-length permutations of elements in the iterable. 532 | 533 | If `r` is not specified, then `r` defaults to the length of the iterable and 534 | all possible full-length permutations are generated. 535 | 536 | Permutations are emitted in lexicographic sort order. So, if the input 537 | iterable is sorted, the permutation tuples will be produced in sorted order. 538 | 539 | Elements are treated as unique based on their position, not on their value. So 540 | if the input elements are unique, there will be no repeat values in each 541 | permutation. 542 | 543 | --- 544 | 545 | # repeat(thing: T, times: number = undefined): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 546 | 547 | Returns an iterator that produces values over and over again. Runs 548 | indefinitely unless the times argument is specified. 549 | 550 | --- 551 | 552 | # takewhile(iterable: Iterable<T>, predicate: (item: T) => boolean): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 553 | 554 | Returns an iterator that produces elements from the iterable as long as the 555 | predicate is true. 556 | 557 | --- 558 | 559 | # zipLongest(xs: Iterable<T1>, ys: Iterable<T2>, filler?: D): [T1 | D, T2 | D][] [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source")
560 | # zipLongest3(xs: Iterable<T1>, ys: Iterable<T2>, zs: Iterable<T3>, filler?: D): [T1 | D, T2 | D, T3 | D][] [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 561 | 562 | Eager version of [izipLongest](#izipLongest) and friends. 563 | 564 | --- 565 | 566 | # zipMany(...iters: Iterable<T>[]): T[][] [<>](https://github.com/nvie/itertools.js/blob/master/src/itertools.js "Source") 567 | 568 | Eager version of [izipMany](#izipMany). 569 | 570 | --- 571 | 572 | ### Ports of more-itertools 573 | 574 | - [chunked](#chunked) 575 | - [flatten](#flatten) 576 | - [intersperse](#intersperse) 577 | - [itake](#itake) 578 | - [pairwise](#pairwise) 579 | - [partition](#partition) 580 | - [roundrobin](#roundrobin) 581 | - [heads](#heads) 582 | - [take](#take) 583 | - [uniqueEverseen](#uniqueEverseen) 584 | - [uniqueJustseen](#uniqueJustseen) 585 | - [dupes](#dupes) 586 | 587 | --- 588 | 589 | # chunked(iterable: Iterable<T>, size: number): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 590 | 591 | Break iterable into lists of length `size`: 592 | 593 | >>> [...chunked([1, 2, 3, 4, 5, 6], 3)] 594 | [[1, 2, 3], [4, 5, 6]] 595 | 596 | If the length of iterable is not evenly divisible by `size`, the last returned 597 | list will be shorter: 598 | 599 | >>> [...chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)] 600 | [[1, 2, 3], [4, 5, 6], [7, 8]] 601 | 602 | --- 603 | 604 | # flatten(iterableOfIterables: Iterable<Iterable<T>>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 605 | 606 | Return an iterator flattening one level of nesting in a list of lists: 607 | 608 | >>> [...flatten([[0, 1], [2, 3]])] 609 | [0, 1, 2, 3] 610 | 611 | --- 612 | 613 | # intersperse(value: T, iterable: Iterable<T>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 614 | 615 | Intersperse filler element `value` among the items in `iterable`. 616 | 617 | >>> [...intersperse(-1, range(1, 5))] 618 | [1, -1, 2, -1, 3, -1, 4] 619 | 620 | --- 621 | 622 | # itake(n: number, iterable: Iterable<T>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 623 | 624 | Returns an iterable containing only the first `n` elements of the given 625 | iterable. 626 | 627 | --- 628 | 629 | # pairwise(iterable: Iterable<T>): Iterable<[T, T]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 630 | 631 | Returns an iterator of paired items, overlapping, from the original. When the 632 | input iterable has a finite number of items `n`, the outputted iterable will 633 | have `n - 1` items. 634 | 635 | >>> pairwise([8, 2, 0, 7]) 636 | [(8, 2), (2, 0), (0, 7)] 637 | 638 | --- 639 | 640 | # partition(iterable: Iterable<T>, predicate: Predicate<T>): [T[], T[]] [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 641 | 642 | Returns a 2-tuple of arrays. Splits the elements in the input iterable into 643 | either of the two arrays. Will fully exhaust the input iterable. The first 644 | array contains all items that match the predicate, the second the rest: 645 | 646 | >>> const isOdd = x => x % 2 !== 0; 647 | >>> const iterable = range(10); 648 | >>> const [odds, evens] = partition(iterable, isOdd); 649 | >>> odds 650 | [1, 3, 5, 7, 9] 651 | >>> evens 652 | [0, 2, 4, 6, 8] 653 | 654 | --- 655 | 656 | # roundrobin(...iterables: Iterable<T>[]): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 657 | 658 | Yields the next item from each iterable in turn, alternating between them. 659 | Continues until all items are exhausted. 660 | 661 | >>> [...roundrobin([1, 2, 3], [4], [5, 6, 7, 8])] 662 | [1, 4, 5, 2, 6, 3, 7, 8] 663 | 664 | --- 665 | 666 | # heads(...iterables: Iterable<T>[]): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 667 | 668 | Like `roundrobin()`, but will group the output per "round". 669 | 670 | >>> [...heads([1, 2, 3], [4], [5, 6, 7, 8])] 671 | [[1, 4, 5], [2, 6], [3, 7], [8]] 672 | 673 | --- 674 | 675 | # take(n: number, iterable: Iterable<T>): T[] [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 676 | 677 | Eager version of [itake](#itake). 678 | 679 | --- 680 | 681 | # uniqueEverseen(iterable: Iterable<T>, keyFn?: (item: T) => Primitive): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 682 | 683 | Yield unique elements, preserving order. 684 | 685 | >>> [...uniqueEverseen('AAAABBBCCDAABBB')] 686 | ['A', 'B', 'C', 'D'] 687 | >>> [...uniqueEverseen('AbBCcAB', s => s.toLowerCase())] 688 | ['A', 'b', 'C'] 689 | 690 | --- 691 | 692 | # uniqueJustseen(iterable: Iterable<T>, keyFn?: (item: T) => Primitive): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 693 | 694 | Yields elements in order, ignoring serial duplicates. 695 | 696 | >>> [...uniqueJustseen('AAAABBBCCDAABBB')] 697 | ['A', 'B', 'C', 'D', 'A', 'B'] 698 | >>> [...uniqueJustseen('AbBCcAB', s => s.toLowerCase())] 699 | ['A', 'b', 'C', 'A', 'B'] 700 | 701 | --- 702 | 703 | # dupes(iterable: Iterable<T>, keyFn?: (item: T) => Primitive): Iterable<T[]> [<>](https://github.com/nvie/itertools.js/blob/master/src/more-itertools.js "Source") 704 | 705 | Yield only elements from the input that occur more than once. Needs to consume the entire input before being able to produce the first result. 706 | 707 | >>> [...dupes('AAAABCDEEEFABG')] 708 | [['A', 'A', 'A', 'A', 'A'], ['E', 'E', 'E'], ['B', 'B']] 709 | >>> [...dupes('AbBCcAB', s => s.toLowerCase())] 710 | [['b', 'B', 'B'], ['C', 'c'], ['A', 'A']] 711 | 712 | --- 713 | 714 | ### Additions 715 | 716 | - [compact](#compact) 717 | - [compactObject](#compactObject) 718 | - [find](#find) 719 | - [first](#first) 720 | - [flatmap](#flatmap) 721 | - [icompact](#icompact) 722 | 723 | --- 724 | 725 | # compact(iterable: Iterable<T | null | undefined>): T[] [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 726 | 727 | Eager version of [icompact](#icompact). 728 | 729 | --- 730 | 731 | # compactObject(obj: Record<K, V | null | undefined>): Record<K, V> [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 732 | 733 | Removes all "nullish" values from the given object. Returns a new object. 734 | 735 | >>> compactObject({ a: 1, b: undefined, c: 0, d: null }) 736 | { a: 1, c: 0, d: null } 737 | 738 | --- 739 | 740 | # find(iterable: Iterable<T>, keyFn?: Predicate<T>): T | undefined [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 741 | 742 | Returns the first item in the iterable for which the predicate holds, if any. 743 | If no such item exists, `undefined` is returned. If no default predicate is 744 | given, the first value from the iterable is returned. 745 | 746 | --- 747 | 748 | # first(iterable: Iterable<T>, keyFn?: Predicate<T>): T | undefined [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 749 | 750 | Almost the same as `find()`, except when no explicit predicate function is 751 | given. `find()` will always return the first value in the iterable, whereas 752 | `first()` will return the first non-`undefined` value in the iterable. 753 | 754 | Prefer using `find()`, as its behavior is more intuitive and predictable. 755 | 756 | --- 757 | 758 | # flatmap(iterable: Iterable<T>, mapper: (item: T) => Iterable<S>): Iterable<S> [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 759 | 760 | Returns 0 or more values for every value in the given iterable. Technically, 761 | it's just calling map(), followed by flatten(), but it's a very useful 762 | operation if you want to map over a structure, but not have a 1:1 input-output 763 | mapping. Instead, if you want to potentially return 0 or more values per input 764 | element, use flatmap(): 765 | 766 | For example, to return all numbers `n` in the input iterable `n` times: 767 | 768 | >>> const repeatN = n => repeat(n, n); 769 | >>> [...flatmap([0, 1, 2, 3, 4], repeatN)] 770 | [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] // note: no 0 771 | 772 | --- 773 | 774 | # icompact(iterable: Iterable<T | null | undefined>): Iterable<T> [<>](https://github.com/nvie/itertools.js/blob/master/src/custom.js "Source") 775 | 776 | Returns an iterable, filtering out any "nullish" values from the input 777 | iterable. 778 | 779 | >>> compact([1, 2, undefined, 3]) 780 | [1, 2, 3] 781 | --------------------------------------------------------------------------------