├── .travis.yml ├── .prettierrc ├── jest.config.js ├── .gitignore ├── tsconfig.build-esm.json ├── scripts ├── markdown-toc-all.sh └── mark-classes-pure.sh ├── tsconfig.build-cjs.json ├── tslint.json ├── src ├── index.ts ├── propertyNames.ts ├── types.ts ├── util.ts ├── core.ts ├── iterables.ts ├── reducers.ts ├── chain.ts └── transducers.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── docs ├── benchmarks.md └── api.md ├── README.md └── __tests__ └── test.ts /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # NPM/Yarn 5 | node_modules 6 | yarn-debug.log 7 | yarn-error.log 8 | 9 | dist 10 | -------------------------------------------------------------------------------- /tsconfig.build-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "es2015", 5 | "outDir": "dist/esm" 6 | }, 7 | "extends": "./tsconfig.build-cjs.json" 8 | } 9 | -------------------------------------------------------------------------------- /scripts/markdown-toc-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Needed for lint-staged if multiple .md files are changed since markdown-toc 4 | # only accepts a single file argument. 5 | for var in "$@" 6 | do 7 | yarn markdown-toc -i "$var" 8 | done 9 | -------------------------------------------------------------------------------- /tsconfig.build-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "inlineSources": true, 4 | "noEmit": false, 5 | "outDir": "dist/cjs", 6 | "sourceMap": true 7 | }, 8 | "exclude": ["__tests__/**/*"], 9 | "extends": "./tsconfig.json" 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": [true, "never-prefix"], 5 | "max-classes-per-file": false, 6 | "no-bitwise": false, 7 | "no-this-assignment": [true, { "allow-destructuring": true }], 8 | "object-literal-sort-keys": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | chainFrom, 3 | TransducerBuilder, 4 | transducerBuilder, 5 | TransformChain, 6 | } from "./chain"; 7 | export { transduce } from "./core"; 8 | export { lazyTransduce, range, repeat, iterate, cycle } from "./iterables"; 9 | export * from "./reducers"; 10 | export * from "./transducers"; 11 | export * from "./types"; 12 | export { compose, isReduced, reduced } from "./util"; 13 | -------------------------------------------------------------------------------- /src/propertyNames.ts: -------------------------------------------------------------------------------- 1 | // By using these constants rather than string literals everywhere, we save 2 | // multiple kilobytes of script size after minification, since each instance of 3 | // these properties is represented by a single character variable rather than a 4 | // roughly 20 character string constant. 5 | 6 | export const INIT = "@@transducer/init"; 7 | export const RESULT = "@@transducer/result"; 8 | export const STEP = "@@transducer/step"; 9 | 10 | export const REDUCED = "@@transducer/reduced"; 11 | export const VALUE = "@@transducer/value"; 12 | -------------------------------------------------------------------------------- /scripts/mark-classes-pure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This replaces "/** @class */" with "/**@__PURE__*/" in all .js files in the 4 | # dist directory. This is useful because when TypeScript compiles classes to 5 | # ES5, it marks them with /** @class */, but UglifyJS looks for /**@__PURE__*/ 6 | # when performing dead code removal, such as during tree shaking. 7 | # 8 | # Intentionally removes an extra space to keep the same length, to maintain 9 | # accuracy of sourcemaps. 10 | 11 | find dist | grep '\.js$' | xargs perl -p -i -e 's~/\*\* \@class \*/ ~/\*\*\@__PURE__\*/~g' 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "lib": ["es5", "es2015.iterable", "es2015.collection"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmit": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitReturns": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "es5" 15 | }, 16 | "include": ["src/**/*", "__tests__/**/*"] 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Philipson 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 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Reduced { 2 | ["@@transducer/reduced"]: boolean; 3 | ["@@transducer/value"]: T; 4 | } 5 | 6 | export type MaybeReduced = T | Reduced; 7 | 8 | /** 9 | * Reducers are allowed to indicate that no further computation is needed by 10 | * returning a Reduced result. 11 | */ 12 | export type QuittingReducer = ( 13 | result: TResult, 14 | input: TInput, 15 | ) => MaybeReduced; 16 | 17 | export type Transducer = ( 18 | xf: CompletingTransformer, 19 | ) => CompletingTransformer; 20 | 21 | export interface CompletingTransformer { 22 | ["@@transducer/init"](): TResult; 23 | ["@@transducer/step"]( 24 | result: TResult, 25 | input: TInput, 26 | ): MaybeReduced; 27 | ["@@transducer/result"](result: TResult): TCompleteResult; 28 | } 29 | 30 | export type Transformer = CompletingTransformer< 31 | TResult, 32 | TResult, 33 | TInput 34 | >; 35 | 36 | export type Comparator = (a: T, b: T) => number; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transducist", 3 | "version": "2.2.0", 4 | "description": "Ergonomic JavaScript/TypeScript transducers for beginners and experts.", 5 | "module": "dist/esm/index.js", 6 | "main": "dist/cjs/index.js", 7 | "types": "dist/esm/index.d.ts", 8 | "files": [ 9 | "dist/" 10 | ], 11 | "sideEffects": false, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/dphilipson/transducist.git" 15 | }, 16 | "keywords": [ 17 | "transducers", 18 | "typescript", 19 | "functional", 20 | "iterators", 21 | "lazy", 22 | "chain" 23 | ], 24 | "homepage": "https://github.com/dphilipson/transducist", 25 | "bugs": { 26 | "url": "https://github.com/dphilipson/transducist/issues", 27 | "email": "david.philipson@gmail.com" 28 | }, 29 | "author": "David Philipson (http://dphil.me)", 30 | "license": "MIT", 31 | "scripts": { 32 | "build": "yarn run clean && tsc -p tsconfig.build-esm.json && tsc -p tsconfig.build-cjs.json && ./scripts/mark-classes-pure.sh", 33 | "clean": "rm -rf dist/*", 34 | "format-file": "prettier --write", 35 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 36 | "generate-toc": "git ls-files | egrep '\\.md$' | xargs scripts/markdown-toc-all.sh", 37 | "jest": "jest", 38 | "lint-file": "tslint", 39 | "lint": "tslint --project .", 40 | "prepublishOnly": "yarn run test && yarn run build", 41 | "test": "yarn run lint && tsc && yarn run jest" 42 | }, 43 | "husky": { 44 | "hooks": { 45 | "pre-commit": "lint-staged" 46 | } 47 | }, 48 | "lint-staged": { 49 | "**/*.{js,json}": [ 50 | "yarn run format-file", 51 | "git add" 52 | ], 53 | "**/*.ts": [ 54 | "yarn run lint-file --fix", 55 | "yarn run format-file", 56 | "git add" 57 | ], 58 | "*.md": [ 59 | "./scripts/markdown-toc-all.sh", 60 | "yarn run format-file", 61 | "git add" 62 | ] 63 | }, 64 | "devDependencies": { 65 | "@types/jest": "^24.0.23", 66 | "husky": "^3.1.0", 67 | "jest": "^24.9.0", 68 | "lint-staged": "^9.5.0", 69 | "markdown-toc": "^1.2.0", 70 | "prettier": "^1.19.1", 71 | "ts-jest": "^24.2.0", 72 | "tslint": "^5.20.1", 73 | "tslint-config-prettier": "^1.18.0", 74 | "typescript": "^3.7.3" 75 | }, 76 | "dependencies": {} 77 | } 78 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import { REDUCED, VALUE } from "./propertyNames"; 2 | import { MaybeReduced, Reduced, Transducer } from "./types"; 3 | 4 | export function reduced(result: T): Reduced { 5 | return { 6 | [REDUCED]: true, 7 | [VALUE]: result, 8 | }; 9 | } 10 | 11 | export function isReduced(result: MaybeReduced): result is Reduced { 12 | return result && (result as any)[REDUCED] === true; 13 | } 14 | 15 | export function ensureReduced(result: MaybeReduced): Reduced { 16 | return isReduced(result) ? result : reduced(result); 17 | } 18 | 19 | export function unreduced(result: MaybeReduced): T { 20 | return isReduced(result) ? result[VALUE] : result; 21 | } 22 | 23 | export function compose(): Transducer; 24 | export function compose(f0: Transducer): Transducer; 25 | export function compose( 26 | f0: Transducer, 27 | f1: Transducer, 28 | ): Transducer; 29 | export function compose( 30 | f0: Transducer, 31 | f1: Transducer, 32 | f2: Transducer, 33 | ): Transducer; 34 | export function compose( 35 | f0: Transducer, 36 | f1: Transducer, 37 | f2: Transducer, 38 | f3: Transducer, 39 | ): Transducer; 40 | export function compose( 41 | f0: Transducer, 42 | f1: Transducer, 43 | f2: Transducer, 44 | f3: Transducer, 45 | f4: Transducer, 46 | ): Transducer; 47 | export function compose( 48 | f0: Transducer, 49 | f1: Transducer, 50 | f2: Transducer, 51 | f3: Transducer, 52 | f4: Transducer, 53 | f5: Transducer, 54 | ): Transducer; 55 | export function compose( 56 | f0: Transducer, 57 | f1: Transducer, 58 | f2: Transducer, 59 | f3: Transducer, 60 | f4: Transducer, 61 | f5: Transducer, 62 | f6: Transducer, 63 | ): Transducer; 64 | export function compose( 65 | f0: Transducer, 66 | f1: Transducer, 67 | f2: Transducer, 68 | f3: Transducer, 69 | f4: Transducer, 70 | f5: Transducer, 71 | f6: Transducer, 72 | f7: Transducer, 73 | ): Transducer; 74 | export function compose( 75 | f0: Transducer, 76 | f1: Transducer, 77 | f2: Transducer, 78 | f3: Transducer, 79 | f4: Transducer, 80 | f5: Transducer, 81 | f6: Transducer, 82 | f7: Transducer, 83 | ...rest: Array> 84 | ): Transducer; 85 | export function compose(...fs: any[]): any { 86 | return (x: any) => { 87 | let result = x; 88 | for (let i = fs.length - 1; i >= 0; i--) { 89 | result = fs[i](result); 90 | } 91 | return result; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { getIterator } from "./iterables"; 2 | import { INIT, RESULT, STEP } from "./propertyNames"; 3 | import { 4 | CompletingTransformer, 5 | MaybeReduced, 6 | QuittingReducer, 7 | Transducer, 8 | Transformer, 9 | } from "./types"; 10 | import { isReduced, unreduced } from "./util"; 11 | 12 | export function transduce( 13 | collection: Iterable, 14 | transform: Transducer, 15 | reducer: CompletingTransformer, 16 | ): TCompleteResult; 17 | export function transduce( 18 | collection: Iterable, 19 | transform: Transducer, 20 | reducer: QuittingReducer, 21 | initialValue: TResult, 22 | ): TResult; 23 | export function transduce( 24 | collection: Iterable, 25 | transform: Transducer, 26 | reducer: 27 | | CompletingTransformer 28 | | QuittingReducer, 29 | initialValue?: TResult, 30 | ): TCompleteResult { 31 | let transformer: CompletingTransformer; 32 | if (typeof reducer === "function") { 33 | // Type coercion because in this branch, TResult and TCompleteResult are 34 | // the same, but the checker doesn't know that. 35 | transformer = new ReducerWrappingTransformer( 36 | reducer, 37 | initialValue!, 38 | ) as any; 39 | } else { 40 | transformer = reducer; 41 | } 42 | return reduceWithTransformer(collection, transform(transformer)); 43 | } 44 | 45 | function reduceWithTransformer( 46 | collection: Iterable, 47 | f: CompletingTransformer, 48 | ): TCompleteResult { 49 | const uncompleteResult = reduceWithFunction( 50 | collection, 51 | f[STEP].bind(f), 52 | f[INIT](), 53 | ); 54 | return f[RESULT](unreduced(uncompleteResult)); 55 | } 56 | 57 | export function reduceWithFunction( 58 | collection: Iterable, 59 | f: QuittingReducer, 60 | initialValue: TResult, 61 | ): MaybeReduced { 62 | const iterator = getIterator(collection); 63 | let result = initialValue; 64 | while (true) { 65 | const input = iterator.next(); 66 | if (input.done) { 67 | return result; 68 | } 69 | const next = f(result, input.value); 70 | if (isReduced(next)) { 71 | return next; 72 | } else { 73 | result = next; 74 | } 75 | } 76 | } 77 | 78 | class ReducerWrappingTransformer 79 | implements Transformer { 80 | public readonly [STEP]: QuittingReducer; 81 | 82 | constructor( 83 | f: QuittingReducer, 84 | private readonly initialValue: TResult, 85 | ) { 86 | this[STEP] = f; 87 | } 88 | 89 | public [INIT](): TResult { 90 | return this.initialValue; 91 | } 92 | 93 | public [RESULT](result: TResult): TResult { 94 | return result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /docs/benchmarks.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | 4 | 5 | - [Description](#description) 6 | - [Results](#results) 7 | - [Interpretation](#interpretation) 8 | 9 | 10 | 11 | ## Description 12 | 13 | These benchmarks were generated by running a computation equivalent to the 14 | following in various libraries on Node 10.40.0 on a 2015 MacBook Pro: 15 | 16 | ```ts 17 | chainFrom([1, 2 /** ... */, , n]) 18 | .map(x => x * 2) 19 | .filter(x => x % 5 !== 0) 20 | .map(x => x + 1) 21 | .filter(x => x % 2 === 0) 22 | .toArray(); 23 | ``` 24 | 25 | The benchmark source code can be found 26 | [here](https://github.com/dphilipson/transducist-benchmarks/blob/master/src/index.ts). 27 | 28 | ## Results 29 | 30 | ``` 31 | n = 10 32 | ------ 33 | transducist x 1,092,348 ops/sec ±1.34% (89 runs sampled) 34 | lodash (without chain) x 1,324,101 ops/sec ±0.74% (89 runs sampled) 35 | lodash (with chain) x 362,054 ops/sec ±2.80% (83 runs sampled) 36 | ramda x 476,214 ops/sec ±0.55% (93 runs sampled) 37 | lazy.js x 2,652,169 ops/sec ±1.22% (90 runs sampled) 38 | transducers.js x 1,099,321 ops/sec ±0.60% (90 runs sampled) 39 | transducers-js x 687,221 ops/sec ±0.96% (93 runs sampled) 40 | native array methods x 3,685,880 ops/sec ±0.45% (94 runs sampled) 41 | hand-optimized loop x 19,250,719 ops/sec ±2.01% (89 runs sampled) 42 | 43 | n = 100 44 | ------- 45 | transducist x 240,872 ops/sec ±0.88% (93 runs sampled) 46 | lodash (without chain) x 197,158 ops/sec ±0.60% (94 runs sampled) 47 | lodash (with chain) x 142,932 ops/sec ±0.61% (94 runs sampled) 48 | ramda x 196,973 ops/sec ±0.84% (87 runs sampled) 49 | lazy.js x 478,335 ops/sec ±0.57% (94 runs sampled) 50 | transducers.js x 235,804 ops/sec ±0.69% (92 runs sampled) 51 | transducers-js x 164,375 ops/sec ±0.64% (92 runs sampled) 52 | native array methods x 195,732 ops/sec ±0.58% (94 runs sampled) 53 | hand-optimized loop x 2,654,405 ops/sec ±0.57% (89 runs sampled) 54 | 55 | n = 1,000 56 | --------- 57 | transducist x 25,987 ops/sec ±0.60% (91 runs sampled) 58 | lodash (without chain) x 19,804 ops/sec ±0.58% (93 runs sampled) 59 | lodash (with chain) x 18,537 ops/sec ±0.82% (88 runs sampled) 60 | ramda x 27,283 ops/sec ±0.80% (92 runs sampled) 61 | lazy.js x 56,104 ops/sec ±0.66% (94 runs sampled) 62 | transducers.js x 25,595 ops/sec ±0.88% (89 runs sampled) 63 | transducers-js x 19,098 ops/sec ±0.65% (94 runs sampled) 64 | native array methods x 19,179 ops/sec ±0.67% (91 runs sampled) 65 | hand-optimized loop x 247,362 ops/sec ±1.25% (90 runs sampled) 66 | 67 | n = 10,000 68 | ---------- 69 | transducist x 2,623 ops/sec ±0.81% (94 runs sampled) 70 | lodash (without chain) x 1,973 ops/sec ±0.54% (93 runs sampled) 71 | lodash (with chain) x 1,954 ops/sec ±0.68% (93 runs sampled) 72 | ramda x 2,980 ops/sec ±0.65% (93 runs sampled) 73 | lazy.js x 5,921 ops/sec ±0.61% (93 runs sampled) 74 | transducers.js x 2,724 ops/sec ±0.43% (94 runs sampled) 75 | transducers-js x 1,910 ops/sec ±0.93% (93 runs sampled) 76 | native array methods x 1,873 ops/sec ±0.92% (91 runs sampled) 77 | hand-optimized loop x 26,402 ops/sec ±0.58% (91 runs sampled) 78 | 79 | n = 100,000 80 | ----------- 81 | transducist x 261 ops/sec ±0.53% (87 runs sampled) 82 | lodash (without chain) x 141 ops/sec ±1.08% (79 runs sampled) 83 | lodash (with chain) x 138 ops/sec ±1.11% (77 runs sampled) 84 | ramda x 186 ops/sec ±1.17% (78 runs sampled) 85 | lazy.js x 594 ops/sec ±0.72% (90 runs sampled) 86 | transducers.js x 244 ops/sec ±0.52% (87 runs sampled) 87 | transducers-js x 184 ops/sec ±0.65% (84 runs sampled) 88 | native array methods x 33.03 ops/sec ±0.85% (57 runs sampled) 89 | hand-optimized loop x 2,428 ops/sec ±0.34% (96 runs sampled) 90 | ``` 91 | 92 | ## Interpretation 93 | 94 | - [Lazy.js](http://danieltao.com/lazy.js/) is the fastest by far, more than 95 | doubling the performance of any other library tested. 96 | - After Lazy.js, Transducist's performance is competitive or superior to all 97 | other libraries tested. In particular, its performance is close to that of 98 | [Ramda](https://ramdajs.com/) and 99 | [transducers.js](https://github.com/jlongster/transducers.js/). 100 | - Note, however, that while Ramda has comparable performance on this benchmark 101 | task, its typical usage does not provide short-circuiting. In particular, if 102 | the task were changed to add a `.take(10)` at the end of the chain, then 103 | Transducist would complete nearly instantly while Ramda would take just as 104 | long. 105 | - [Lodash](https://lodash.com/) performed surprisingly poorly, and its chained 106 | API is slower than its non-chained one at all element counts. 107 | - Native array methods are fast at low element counts, but are overtaken by 108 | chaining libraries at around 100 elements. 109 | - Of course, the fastest of all is writing an optimized loop by hand, which is 110 | roughly 5x as fast as Lazy.js and 10x as fast as the other competitive 111 | libraries. 112 | -------------------------------------------------------------------------------- /src/iterables.ts: -------------------------------------------------------------------------------- 1 | import { STEP, VALUE } from "./propertyNames"; 2 | import { toArray } from "./reducers"; 3 | import { Transducer, Transformer } from "./types"; 4 | import { isReduced } from "./util"; 5 | 6 | const ITERATOR_SYMBOL = 7 | typeof Symbol !== "undefined" ? Symbol.iterator : ("@@iterator" as any); 8 | 9 | /** 10 | * For compatibility with environments where common types aren't iterable. 11 | */ 12 | export function getIterator(collection: Iterable): Iterator { 13 | const anyCollection: any = collection; 14 | if (anyCollection[ITERATOR_SYMBOL]) { 15 | return anyCollection[ITERATOR_SYMBOL](); 16 | } else if ( 17 | Array.isArray(anyCollection) || 18 | typeof anyCollection === "string" 19 | ) { 20 | // Treat a string like an array of characters. 21 | return new ArrayIterator(anyCollection as T[]); 22 | } else { 23 | throw new Error( 24 | "Cannot get iterator of non iterable value: " + anyCollection, 25 | ); 26 | } 27 | } 28 | 29 | /** 30 | * Iterator for arrays in environments without Iterable. 31 | */ 32 | class ArrayIterator implements Iterator { 33 | private i: number = 0; 34 | 35 | constructor(private readonly array: T[]) {} 36 | 37 | public [ITERATOR_SYMBOL]() { 38 | return this; 39 | } 40 | 41 | public next(): IteratorResult { 42 | const { i, array } = this; 43 | if (i < array.length) { 44 | this.i++; 45 | return { done: false, value: array[i] }; 46 | } else { 47 | return { done: true } as any; 48 | } 49 | } 50 | } 51 | 52 | class RangeIterator implements Iterator { 53 | private readonly end: number; 54 | private readonly step: number; 55 | private i: number; 56 | 57 | constructor(startOrEnd: number, end?: number, step?: number) { 58 | if (end == null) { 59 | this.i = 0; 60 | this.end = startOrEnd; 61 | this.step = 1; 62 | } else if (step == null) { 63 | this.i = startOrEnd; 64 | this.end = end; 65 | this.step = 1; 66 | } else { 67 | this.i = startOrEnd; 68 | this.end = end; 69 | this.step = step; 70 | } 71 | } 72 | 73 | public [ITERATOR_SYMBOL]() { 74 | return this; 75 | } 76 | 77 | public next(): IteratorResult { 78 | const { i, end, step } = this; 79 | if ((step > 0 && i < end) || (step < 0 && i > end)) { 80 | const result = { done: false, value: i }; 81 | this.i += step; 82 | return result; 83 | } else { 84 | return { done: true } as any; 85 | } 86 | } 87 | } 88 | 89 | export function range( 90 | startOrEnd: number, 91 | end?: number, 92 | step?: number, 93 | ): Iterable { 94 | if (step === 0) { 95 | throw new Error("Step in range() cannot be 0"); 96 | } 97 | return { 98 | [ITERATOR_SYMBOL]: () => new RangeIterator(startOrEnd, end, step), 99 | } as any; 100 | } 101 | 102 | class RepeatIterator implements Iterator { 103 | private i = 0; 104 | 105 | constructor( 106 | private readonly value: T, 107 | private readonly count: number = Number.POSITIVE_INFINITY, 108 | ) {} 109 | 110 | public [ITERATOR_SYMBOL]() { 111 | return this; 112 | } 113 | 114 | public next(): IteratorResult { 115 | if (this.count == null || this.i < this.count) { 116 | this.i++; 117 | return { done: false, value: this.value }; 118 | } else { 119 | return { done: true } as any; 120 | } 121 | } 122 | } 123 | 124 | export function repeat(value: T, count?: number): Iterable { 125 | if (count != null && count < 0) { 126 | throw new Error("Repeat count cannot be negative"); 127 | } 128 | return { [ITERATOR_SYMBOL]: () => new RepeatIterator(value, count) } as any; 129 | } 130 | 131 | class IterateIterator implements Iterator { 132 | private hasEmittedInitialValue = false; 133 | private lastValue: T; 134 | 135 | constructor( 136 | initialValue: T, 137 | private readonly getNextValue: (value: T) => T, 138 | ) { 139 | this.lastValue = initialValue; 140 | } 141 | 142 | public [ITERATOR_SYMBOL]() { 143 | return this; 144 | } 145 | 146 | public next(): IteratorResult { 147 | if (!this.hasEmittedInitialValue) { 148 | this.hasEmittedInitialValue = true; 149 | return { done: false, value: this.lastValue }; 150 | } else { 151 | this.lastValue = this.getNextValue(this.lastValue); 152 | return { done: false, value: this.lastValue }; 153 | } 154 | } 155 | } 156 | 157 | export function iterate( 158 | initialValue: T, 159 | getNextValue: (lastValue: T) => T, 160 | ): Iterable { 161 | return { 162 | [ITERATOR_SYMBOL]: () => 163 | new IterateIterator(initialValue, getNextValue), 164 | } as any; 165 | } 166 | 167 | class CycleIterator implements Iterator { 168 | private readonly valueIterator: Iterator; 169 | private readonly values: T[] = []; 170 | private hasConsumedIterator = false; 171 | private i = 0; 172 | 173 | constructor(values: Iterable) { 174 | this.valueIterator = (values as any)[ITERATOR_SYMBOL](); 175 | } 176 | 177 | public [ITERATOR_SYMBOL]() { 178 | return this; 179 | } 180 | 181 | public next(): IteratorResult { 182 | if (!this.hasConsumedIterator) { 183 | const result = this.valueIterator.next(); 184 | const { done, value } = result; 185 | if (!done) { 186 | this.values.push(value); 187 | return result; 188 | } else { 189 | this.hasConsumedIterator = true; 190 | return this.next(); 191 | } 192 | } else if (this.values.length === 0) { 193 | return { done: true } as any; 194 | } else { 195 | const value = this.values[this.i]; 196 | this.i = (this.i + 1) % this.values.length; 197 | return { done: false, value }; 198 | } 199 | } 200 | } 201 | 202 | export function cycle(values: Iterable): Iterable { 203 | return { [ITERATOR_SYMBOL]: () => new CycleIterator(values) as any } as any; 204 | } 205 | 206 | /** 207 | * An iterable which enables lazy consumption of the output of a 208 | * transducer-based transform. 209 | */ 210 | class TransducerIterable implements Iterator { 211 | private readonly xfToArray: Transformer; 212 | private upcoming: Iterator = new ArrayIterator([]); 213 | private hasSeenEnd: boolean = false; 214 | 215 | constructor( 216 | private readonly iterator: Iterator, 217 | xf: Transducer, 218 | ) { 219 | this.xfToArray = xf(toArray()); 220 | } 221 | 222 | public [ITERATOR_SYMBOL]() { 223 | return this; 224 | } 225 | 226 | public next(): IteratorResult { 227 | while (true) { 228 | const backlogged = this.upcoming.next(); 229 | if (!backlogged.done) { 230 | return backlogged; 231 | } else if (this.hasSeenEnd) { 232 | return { done: true } as any; 233 | } else { 234 | const { done, value } = this.iterator.next(); 235 | if (done) { 236 | return { done } as any; 237 | } else { 238 | let outValues = this.xfToArray[STEP]([], value); 239 | if (isReduced(outValues)) { 240 | this.hasSeenEnd = true; 241 | outValues = outValues[VALUE]; 242 | } 243 | this.upcoming = new ArrayIterator(outValues); 244 | } 245 | } 246 | } 247 | } 248 | } 249 | 250 | export function lazyTransduce( 251 | collection: Iterable, 252 | transform: Transducer, 253 | ): IterableIterator { 254 | // We can't satisfy the IterableIterator interface while functioning in 255 | // environments without Symbol, hence the cast. 256 | return new TransducerIterable(getIterator(collection), transform) as any; 257 | } 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transducist 2 | 3 | Ergonomic JavaScript/TypeScript transducers for beginners and experts. 4 | 5 | [![Build 6 | Status](https://travis-ci.org/dphilipson/transducist.svg?branch=master)](https://travis-ci.org/dphilipson/transducist) 7 | 8 | ## Table of Contents 9 | 10 | 11 | 12 | - [Introduction](#introduction) 13 | - [Goals](#goals) 14 | - [Installation](#installation) 15 | - [Basic Usage](#basic-usage) 16 | - [Iterable utilities](#iterable-utilities) 17 | - [Advanced Usage](#advanced-usage) 18 | - [Using custom transducers](#using-custom-transducers) 19 | - [Using custom reductions](#using-custom-reductions) 20 | - [Creating a standalone transducer](#creating-a-standalone-transducer) 21 | - [Bundle Size and Tree Shaking](#bundle-size-and-tree-shaking) 22 | - [Benchmarks](#benchmarks) 23 | - [API](#api) 24 | 25 | 26 | 27 | ## Introduction 28 | 29 | This library will let you write code that looks like this: 30 | 31 | ```ts 32 | // Let's find 100 people who have a parent named Brad who runs Haskell projects 33 | // so we can ask them about their dads Brads' monads. 34 | const result = chainFrom(haskellProjects) 35 | .map(project => project.owner) 36 | .filter(owner => owner.name === "Brad") 37 | .flatMap(owner => owner.children) 38 | .take(100) 39 | .toArray(); 40 | ``` 41 | 42 | This computation is very efficient because no intermediate arrays are created 43 | and work stops early once 100 people are found. 44 | 45 | You might be thinking that this looks very similar to [chains in 46 | Lodash](https://lodash.com/docs/4.17.4#chain) or various other libraries that 47 | offer a similar API. But this library is different because it's implemented with 48 | transducers and exposes all benefits of the transducer protocol, such as being 49 | able to easily add novel transformation types to the middle of a chain and 50 | producing logic applicable to any data structure, not just arrays. 51 | 52 | Never heard of a transducer? Check the links in the 53 | [transducers-js](https://github.com/cognitect-labs/transducers-js#transducers-js) 54 | readme for an introduction to the concept, but note that **you don't need to 55 | understand anything about transducers to use this library**. 56 | 57 | ## Goals 58 | 59 | Provide an API for using transducers that is… 60 | 61 | - **…easy** to use even **without transducer knowledge or experience**. If you 62 | haven't yet wrapped your head around transducers or need to share a codebase 63 | with others who haven't, the basic chaining API is fully usable without ever 64 | seeing a reference to transducers or anything more advanced than `map` and 65 | `filter`. However, it is also… 66 | 67 | - …able to reap the **full benefits of transducers** for those who are 68 | familiar with them. By using the general purpose `.compose()` to place 69 | custom transducers in the middle of a chain, any kind of novel transform can 70 | be added while still maintaining the efficiency bonuses of laziness and 71 | short-circuiting. Further, the library can also be used to construct 72 | standalone transducers which may be used elsewhere by other libraries that 73 | incorporate transducers into their API. 74 | 75 | - **…fast**! Transducist performs efficient computations by never creating 76 | more objects than necessary. [See the 77 | benchmarks](https://github.com/dphilipson/transducist/blob/master/docs/benchmarks.md#benchmarks) 78 | for details. 79 | 80 | - **…typesafe**. Transducist is written in TypeScript and is designed to be 81 | fully typesafe without requiring you to manually specify type parameters 82 | everywhere. 83 | 84 | - **…small**. Transducist is less than 4kB gzipped, and can be made even 85 | smaller through [tree shaking](#bundle-size-and-tree-shaking). 86 | 87 | ## Installation 88 | 89 | With Yarn: 90 | 91 | ``` 92 | yarn add transducist 93 | ``` 94 | 95 | With NPM: 96 | 97 | ``` 98 | npm install transducist 99 | ``` 100 | 101 | This library, with the exception of the functions which relate to `Set` and 102 | `Map`, works fine on ES5 without any polyfills or transpilation, but its 103 | TypeScript definitions depend on ES6 definitions for the `Iterable` type. If you 104 | use TypeScript in your project, you must make definitions for these types 105 | available by doing one of the following: 106 | 107 | - In `tsconfig.json`, set `"target"` to `"es6"` or higher. 108 | - In `tsconfig.json`, set `"libs"` to include `"es2015.iterable"` or something 109 | that includes it 110 | - Add the definitions by some other means, such as importing types for 111 | `es6-shim`. 112 | 113 | Furthermore, the methods `toSet`, `toMap`, and `toMapGroupBy` assume the 114 | presence of ES6 `Set` and `Map` classes in your environment. If you wish to use 115 | these methods, you must ensure your environment has these classes or provide a 116 | polyfill. 117 | 118 | ## Basic Usage 119 | 120 | Import with 121 | 122 | ```ts 123 | import { chainFrom } from "transducist"; 124 | ``` 125 | 126 | Start a chain by calling `chainFrom()` on any iterable, such as an array, a 127 | string, or an ES6 `Set`. 128 | 129 | ```ts 130 | const result = chainFrom(["a", "bb", "ccc", "dddd", "eeeee"]); 131 | ``` 132 | 133 | Then follow up with any number of transforms. 134 | 135 | ```ts 136 | .map(s => s.toUpperCase()) 137 | .filter(s => s.length % 2 === 1) 138 | .take(2) 139 | ``` 140 | 141 | To finish the chain and get a result out, call a method which terminates the 142 | chain and produces a result. 143 | 144 | ```ts 145 | .toArray(); // -> ["A", "CCC"] 146 | ``` 147 | 148 | Other terminating methods include `.forEach()`, `.find()`, and `.toSet()`, among 149 | many others. For a particularly interesting one, see 150 | [`.toMapGroupBy()`](https://github.com/dphilipson/transducist/blob/master/docs/api.md#tomapgroupbygetkey-transformer). 151 | 152 | For a list of all possible transformations and terminations, [see the full API 153 | docs](https://github.com/dphilipson/transducist/blob/master/docs/api.md#api). 154 | 155 | ### Iterable utilities 156 | 157 | Transducist also comes with a handful of iterable helpers for common sequences, 158 | which are often useful as the starter for a chain. For example: 159 | 160 | ```ts 161 | chainFrom(range(5)) 162 | .map(i => i * i) 163 | .toArray(); // -> [0, 1, 4, 9, 16] 164 | 165 | chainFrom(repeat("x", 5)).joinToString(""); // -> "xxxxx" 166 | ``` 167 | 168 | All such iterables generate values only when needed, which means they can 169 | represent even infinite sequences: 170 | 171 | ```ts 172 | chainFrom(range(0, Number.POSITIVE_INFINITY)) 173 | .map(n => n * n) 174 | .takeWhile(n => n < 20) 175 | .toArray(); // -> [0, 1, 4, 9, 16] 176 | ``` 177 | 178 | For a full list of iterables, [see the Iterables 179 | docs](https://github.com/dphilipson/transducist/blob/master/docs/api.md#iterables). 180 | 181 | ## Advanced Usage 182 | 183 | These advanced usage patterns make use of transducers. If you aren't familiar 184 | with transducers yet, see the links in the 185 | [transducers-js](https://github.com/cognitect-labs/transducers-js#transducers-js) 186 | readme for an introduction. 187 | 188 | ### Using custom transducers 189 | 190 | Arbitrary objects that satisfy the [transducer 191 | protocol](https://github.com/cognitect-labs/transducers-js#the-transducer-protocol) 192 | can be added to the chain using the `.compose()` method, allowing you to write 193 | new types of transforms that can be included in the middle of the chain without 194 | losing the benefits of early termination and no intermediate array creation. 195 | This includes transducers defined by other libraries, so we could for instance 196 | reuse a transducer from 197 | [`transducers.js`](https://github.com/jlongster/transducers.js/) as follows: 198 | 199 | ```ts 200 | import { chainFrom } from "transducist"; 201 | import { cat } from "transducers.js"; 202 | 203 | const result = chainFrom([[1, 2], [3, 4, 5], [6]]) 204 | .drop(1) 205 | .compose(cat) 206 | .map(x => 10 * x) 207 | .toArray(); // -> [30, 40, 50, 60]; 208 | ``` 209 | 210 | All of this library's transformation methods are implemented internally with 211 | calls to `.compose()`. 212 | 213 | ### Using custom reductions 214 | 215 | Similarly, arbitrary terminating operations can be introduced using the 216 | `.reduce()` method, which can accept not only a plain reducer function (that is, 217 | a function of the form `(acc, x) => acc`) but also any object satisfying the 218 | [transformer 219 | protocol](https://github.com/cognitect-labs/transducers-js#transformer-protocol). 220 | All of this library's termination methods are implemented internally with a call 221 | to `.reduce()` (with the single exception of `.toIterator()`). 222 | 223 | ### Creating a standalone transducer 224 | 225 | It is also possible to use a chaining API to define a transducer without using 226 | it in a computation, so it can be passed around and consumed by other APIs which 227 | understand the transducer protocol, such as 228 | [transduce-stream](https://github.com/transduce/transduce-stream). This is done 229 | by starting the chain by calling `transducerBuilder()` and calling `.build()` 230 | when done, for example: 231 | 232 | ```ts 233 | import { chainFrom, transducerBuilder } from "transducist"; 234 | 235 | const firstThreeOdds = transducerBuilder() 236 | .filter(n => n % 2 === 1) 237 | .take(3) 238 | .build(); 239 | ``` 240 | 241 | Since this returns a transducer, we can also use it ourselves with `.compose()`: 242 | 243 | ```ts 244 | const result = chainFrom([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 245 | .compose(firstThreeOdds) 246 | .toArray(); // -> [1, 3, 5] 247 | ``` 248 | 249 | This is a good way to factor out a transformation for reuse. 250 | 251 | ## Bundle Size and Tree Shaking 252 | 253 | If you are using a bundler which supports tree shaking (e.g. Webpack 4+, Rollup) 254 | and are looking to decrease bundle size, Transducist also provides an alternate 255 | API to allow you to only pay for the functions you actually use, which 256 | incidentally is similar to the API provided by more typical transducer 257 | libraries. All chain methods are also available as standalone functions and can 258 | be used as follows: 259 | 260 | ```ts 261 | import { compose, filter, map, toArray, transduce } from "transducist"; 262 | 263 | transduce( 264 | [1, 2, 3, 4, 5], 265 | compose( 266 | filter(x => x > 2), 267 | map(x => 2 * x), 268 | ), 269 | toArray(), 270 | ); // -> [6, 8, 10] 271 | ``` 272 | 273 | which is equivalent to the fluent version: 274 | 275 | ```ts 276 | import { chainFrom } from "transducist"; 277 | 278 | chainFrom([1, 2, 3, 4, 5]) 279 | .filter(x => x > 2) 280 | .map(x => 2 * x) 281 | .toArray(); // -> [6, 8, 10] 282 | ``` 283 | 284 | However, the standalone function version of this example adds a mere 1.64 kB to 285 | bundle size (pre-gzip), compared to the chained version which adds 11.1 kB (as 286 | of version 1.0.0). Note that after gzipping, the fluent version is below 4kB as 287 | well. 288 | 289 | For details, [see the tree shaking 290 | API](https://github.com/dphilipson/transducist/blob/master/docs/api.md#tree-shakeable-api) 291 | section of the API docs. 292 | 293 | ## Benchmarks 294 | 295 | [View the 296 | benchmarks.](https://github.com/dphilipson/transducist/blob/master/docs/benchmarks.md#benchmarks) 297 | 298 | ## API 299 | 300 | [View the full API 301 | docs.](https://github.com/dphilipson/transducist/blob/master/docs/api.md#api) 302 | 303 | Copyright © 2017 David Philipson 304 | -------------------------------------------------------------------------------- /src/reducers.ts: -------------------------------------------------------------------------------- 1 | import { INIT, RESULT, STEP, VALUE } from "./propertyNames"; 2 | import { filter, remove } from "./transducers"; 3 | import { Comparator, CompletingTransformer, Transformer } from "./types"; 4 | import { isReduced, reduced } from "./util"; 5 | 6 | // Transformers with no parameters, such as the one for count() here, are 7 | // created the first time they are called so they can be tree shaken if unused. 8 | // Tree shaking does not remove top-level object literal constants if they have 9 | // computed keys. 10 | 11 | let countTransformer: Transformer | undefined; 12 | 13 | export function count(): Transformer { 14 | if (!countTransformer) { 15 | countTransformer = { 16 | [INIT]: () => 0, 17 | [RESULT]: (result: number) => result, 18 | [STEP]: (result: number) => result + 1, 19 | }; 20 | } 21 | return countTransformer; 22 | } 23 | 24 | export function every(pred: (item: T) => boolean): Transformer { 25 | return remove(pred)(isEmpty()); 26 | } 27 | 28 | export function find( 29 | pred: (item: T) => item is U, 30 | ): Transformer; 31 | export function find(pred: (item: T) => boolean): Transformer; 32 | export function find(pred: (item: T) => boolean): Transformer { 33 | return filter(pred)(first()); 34 | } 35 | 36 | let firstTransformer: Transformer | undefined; 37 | 38 | export function first(): Transformer { 39 | if (!firstTransformer) { 40 | firstTransformer = { 41 | [INIT]: () => null, 42 | [RESULT]: (result: any) => result, 43 | [STEP]: (_: any, input: any) => reduced(input), 44 | }; 45 | } 46 | return firstTransformer; 47 | } 48 | 49 | class ForEachTransformer implements Transformer { 50 | constructor(private readonly f: (input: T) => void) {} 51 | 52 | public [INIT]() { 53 | return undefined; 54 | } 55 | 56 | public [RESULT]() { 57 | return undefined; 58 | } 59 | 60 | public [STEP](_: void, input: T) { 61 | return this.f(input); 62 | } 63 | } 64 | 65 | export function forEach(f: (input: T) => void): Transformer { 66 | return new ForEachTransformer(f); 67 | } 68 | 69 | let isEmptyTransformer: Transformer | undefined; 70 | 71 | export function isEmpty(): Transformer { 72 | if (!isEmptyTransformer) { 73 | isEmptyTransformer = { 74 | [INIT]: () => true, 75 | [RESULT]: (result: boolean) => result, 76 | [STEP]: () => reduced(false), 77 | }; 78 | } 79 | return isEmptyTransformer; 80 | } 81 | 82 | class JoinToString implements CompletingTransformer { 83 | constructor(private readonly separator: string) {} 84 | 85 | public [INIT]() { 86 | return []; 87 | } 88 | 89 | public [RESULT](result: any[]) { 90 | return result.join(this.separator); 91 | } 92 | 93 | public [STEP](result: any[], input: any) { 94 | result.push(input); 95 | return result; 96 | } 97 | } 98 | 99 | export function joinToString( 100 | separator: string, 101 | ): CompletingTransformer { 102 | return new JoinToString(separator); 103 | } 104 | 105 | let isNotEmptyTransformer: Transformer | undefined; 106 | 107 | export function some(pred: (item: T) => boolean): Transformer { 108 | if (!isNotEmptyTransformer) { 109 | isNotEmptyTransformer = { 110 | [INIT]: () => false, 111 | [RESULT]: (result: boolean) => result, 112 | [STEP]: () => reduced(true), 113 | }; 114 | } 115 | return filter(pred)(isNotEmptyTransformer); 116 | } 117 | 118 | let toArrayTransformer: Transformer | undefined; 119 | 120 | export function toArray(): Transformer { 121 | if (!toArrayTransformer) { 122 | toArrayTransformer = { 123 | [INIT]: () => [], 124 | [RESULT]: (result: any[]) => result, 125 | [STEP]: (result: any[], input: any) => { 126 | result.push(input); 127 | return result; 128 | }, 129 | }; 130 | } 131 | return toArrayTransformer; 132 | } 133 | 134 | class ToMap implements Transformer, T> { 135 | constructor( 136 | private readonly getKey: (item: T) => K, 137 | private readonly getValue: (item: T) => V, 138 | ) {} 139 | 140 | public [INIT](): Map { 141 | return new Map(); 142 | } 143 | 144 | public [RESULT](result: Map): Map { 145 | return result; 146 | } 147 | 148 | public [STEP](result: Map, item: T): Map { 149 | result.set(this.getKey(item), this.getValue(item)); 150 | return result; 151 | } 152 | } 153 | 154 | export function toMap( 155 | getKey: (item: T) => K, 156 | getValue: (item: T) => V, 157 | ): Transformer, T> { 158 | return new ToMap(getKey, getValue); 159 | } 160 | 161 | class InProgressTransformer { 162 | private result: TResult; 163 | private isReduced = false; 164 | 165 | public constructor( 166 | private readonly xf: CompletingTransformer< 167 | TResult, 168 | TCompleteResult, 169 | TInput 170 | >, 171 | ) { 172 | this.result = xf[INIT](); 173 | } 174 | 175 | public step(input: TInput): void { 176 | if (!this.isReduced) { 177 | const newResult = this.xf[STEP](this.result, input); 178 | if (isReduced(newResult)) { 179 | this.result = newResult[VALUE]; 180 | this.isReduced = true; 181 | } else { 182 | this.result = newResult; 183 | } 184 | } 185 | } 186 | 187 | public getResult(): TCompleteResult { 188 | return this.xf[RESULT](this.result); 189 | } 190 | } 191 | 192 | class ToMapGroupBy 193 | implements 194 | CompletingTransformer< 195 | Map>, 196 | Map, 197 | T 198 | > { 199 | constructor( 200 | private readonly getKey: (item: T) => K, 201 | private readonly xf: CompletingTransformer, 202 | ) {} 203 | 204 | public [INIT](): Map> { 205 | return new Map(); 206 | } 207 | 208 | public [RESULT]( 209 | result: Map>, 210 | ): Map { 211 | const completeResult = new Map(); 212 | const entries = result.entries(); 213 | for (let step = entries.next(); !step.done; step = entries.next()) { 214 | const [key, value] = step.value; 215 | completeResult.set(key, value.getResult()); 216 | } 217 | return completeResult; 218 | } 219 | 220 | public [STEP]( 221 | result: Map>, 222 | item: T, 223 | ): Map> { 224 | const key = this.getKey(item); 225 | if (!result.has(key)) { 226 | result.set(key, new InProgressTransformer(this.xf)); 227 | } 228 | result.get(key)!.step(item); 229 | return result; 230 | } 231 | } 232 | 233 | export function toMapGroupBy( 234 | getKey: (item: T) => K, 235 | ): Transformer, T>; 236 | export function toMapGroupBy( 237 | getKey: (item: T) => K, 238 | transformer: CompletingTransformer, 239 | ): Transformer, T>; 240 | export function toMapGroupBy( 241 | getKey: (item: T) => K, 242 | transformer: CompletingTransformer = toArray(), 243 | ): Transformer, T> { 244 | return new ToMapGroupBy(getKey, transformer); 245 | } 246 | 247 | class ToObject 248 | implements Transformer, T> { 249 | constructor( 250 | private readonly getKey: (item: T) => K, 251 | private readonly getValue: (item: T) => V, 252 | ) {} 253 | 254 | public [INIT](): Record { 255 | return {} as any; 256 | } 257 | 258 | public [RESULT](result: Record): Record { 259 | return result; 260 | } 261 | 262 | public [STEP](result: Record, item: T): Record { 263 | result[this.getKey(item)] = this.getValue(item); 264 | return result; 265 | } 266 | } 267 | 268 | export function toObject( 269 | getKey: (item: T) => K, 270 | getValue: (item: T) => V, 271 | ): Transformer, T> { 272 | return new ToObject(getKey, getValue); 273 | } 274 | 275 | class ToObjectGroupBy 276 | implements 277 | CompletingTransformer< 278 | Record>, 279 | Record, 280 | T 281 | > { 282 | constructor( 283 | private readonly getKey: (item: T) => K, 284 | private readonly xf: CompletingTransformer, 285 | ) {} 286 | 287 | public [INIT](): Record> { 288 | return {} as any; 289 | } 290 | 291 | public [RESULT]( 292 | result: Record>, 293 | ): Record { 294 | const completeResult: Record = {} as any; 295 | Object.keys(result).forEach( 296 | key => 297 | ((completeResult as any)[key] = (result as any)[ 298 | key 299 | ].getResult()), 300 | ); 301 | return completeResult; 302 | } 303 | 304 | public [STEP]( 305 | result: Record>, 306 | item: T, 307 | ): Record> { 308 | const key = this.getKey(item); 309 | if (!result[key]) { 310 | result[key] = new InProgressTransformer(this.xf); 311 | } 312 | result[key].step(item); 313 | return result; 314 | } 315 | } 316 | 317 | export function toObjectGroupBy( 318 | getKey: (item: T) => K, 319 | ): Transformer, V>; 320 | export function toObjectGroupBy( 321 | getKey: (item: T) => K, 322 | transformer: CompletingTransformer, 323 | ): Transformer, T>; 324 | export function toObjectGroupBy( 325 | getKey: (item: T) => K, 326 | transformer: CompletingTransformer = toArray(), 327 | ): Transformer, T> { 328 | return new ToObjectGroupBy(getKey, transformer); 329 | } 330 | 331 | let toSetTransformer: Transformer, any> | undefined; 332 | 333 | export function toSet(): Transformer, T> { 334 | if (!toSetTransformer) { 335 | toSetTransformer = { 336 | [INIT]: () => new Set(), 337 | [RESULT]: (result: Set) => result, 338 | [STEP]: (result: Set, input: any) => { 339 | result.add(input); 340 | return result; 341 | }, 342 | }; 343 | } 344 | return toSetTransformer; 345 | } 346 | 347 | let averageTransformer: 348 | | CompletingTransformer<[number, number], number | null, number> 349 | | undefined; 350 | 351 | export function toAverage(): CompletingTransformer< 352 | [number, number], 353 | number | null, 354 | number 355 | > { 356 | if (!averageTransformer) { 357 | averageTransformer = { 358 | [INIT]: () => [0, 0], 359 | [RESULT]: (result: [number, number]) => 360 | result[1] === 0 ? null : result[0] / result[1], 361 | [STEP]: (result: [number, number], input: number) => { 362 | result[0] += input; 363 | result[1]++; 364 | return result; 365 | }, 366 | }; 367 | } 368 | return averageTransformer; 369 | } 370 | 371 | class Min implements Transformer { 372 | constructor(private readonly comparator: Comparator) {} 373 | 374 | public [INIT]() { 375 | return null; 376 | } 377 | 378 | public [RESULT](result: T | null) { 379 | return result; 380 | } 381 | 382 | public [STEP](result: T | null, input: T) { 383 | return result === null || this.comparator(input, result) < 0 384 | ? input 385 | : result; 386 | } 387 | } 388 | 389 | function invertComparator(comparator: Comparator): Comparator { 390 | return (a, b) => -comparator(a, b); 391 | } 392 | 393 | const NATURAL_COMPARATOR: Comparator = (a: number, b: number) => { 394 | if (a < b) { 395 | return -1; 396 | } else { 397 | return a > b ? 1 : 0; 398 | } 399 | }; 400 | 401 | export function max(): Transformer; 402 | export function max(comparator: Comparator): Transformer; 403 | export function max( 404 | comparator: Comparator = NATURAL_COMPARATOR, 405 | ): Transformer { 406 | return new Min(invertComparator(comparator)); 407 | } 408 | 409 | export function min(): Transformer; 410 | export function min(comparator: Comparator): Transformer; 411 | export function min( 412 | comparator: Comparator = NATURAL_COMPARATOR, 413 | ): Transformer { 414 | return new Min(comparator); 415 | } 416 | 417 | let sumTransformer: Transformer | undefined; 418 | 419 | export function sum(): Transformer { 420 | if (!sumTransformer) { 421 | sumTransformer = { 422 | [INIT]: () => 0, 423 | [RESULT]: (result: number) => result, 424 | [STEP]: (result: number, input: number) => { 425 | return result + input; 426 | }, 427 | }; 428 | } 429 | return sumTransformer; 430 | } 431 | -------------------------------------------------------------------------------- /src/chain.ts: -------------------------------------------------------------------------------- 1 | import { transduce } from "./core"; 2 | import { lazyTransduce } from "./iterables"; 3 | import { 4 | count, 5 | every, 6 | find, 7 | first, 8 | forEach, 9 | isEmpty, 10 | joinToString, 11 | max, 12 | min, 13 | some, 14 | sum, 15 | toArray, 16 | toAverage, 17 | toMap, 18 | toMapGroupBy, 19 | toObject, 20 | toObjectGroupBy, 21 | toSet, 22 | } from "./reducers"; 23 | import { 24 | dedupe, 25 | drop, 26 | dropWhile, 27 | filter, 28 | flatMap, 29 | flatten, 30 | interpose, 31 | map, 32 | mapIndexed, 33 | partitionAll, 34 | partitionBy, 35 | remove, 36 | take, 37 | takeNth, 38 | takeWhile, 39 | } from "./transducers"; 40 | import { 41 | Comparator, 42 | CompletingTransformer, 43 | QuittingReducer, 44 | Transducer, 45 | } from "./types"; 46 | 47 | export interface TransformChain { 48 | // tslint:disable: member-ordering 49 | compose(transducer: Transducer): TransformChain; 50 | 51 | dedupe(): TransformChain; 52 | drop(n: number): TransformChain; 53 | dropWhile(pred: (item: T) => boolean): TransformChain; 54 | filter(pred: (item: T) => item is U): TransformChain; 55 | filter(pred: (item: T) => boolean): TransformChain; 56 | flatMap(f: (item: T) => Iterable): TransformChain; 57 | flatten: T extends Iterable ? () => TransformChain : void; 58 | interpose(separator: T): TransformChain; 59 | map(f: (item: T) => U): TransformChain; 60 | mapIndexed(f: (item: T, index: number) => U): TransformChain; 61 | partitionAll(n: number): TransformChain; 62 | partitionBy(pred: (item: T) => any): TransformChain; 63 | remove( 64 | pred: (item: T) => item is U, 65 | ): TransformChain>; 66 | remove(pred: (item: T) => boolean): TransformChain; 67 | removeAbsent(): TransformChain>; 68 | take(n: number): TransformChain; 69 | takeNth(n: number): TransformChain; 70 | takeWhile(pred: (item: T) => item is U): TransformChain; 71 | takeWhile(pred: (item: T) => boolean): TransformChain; 72 | 73 | reduce( 74 | reducer: QuittingReducer, 75 | initialValue: TResult, 76 | ): TResult; 77 | reduce( 78 | transformer: CompletingTransformer, 79 | ): TCompleteResult; 80 | 81 | average: T extends number ? () => number | null : void; 82 | count(): number; 83 | every(pred: (item: T) => boolean): boolean; 84 | find(pred: (item: T) => item is U): U | null; 85 | find(pred: (item: T) => boolean): T | null; 86 | first(): T | null; 87 | forEach(f: (item: T) => void): void; 88 | isEmpty(): boolean; 89 | joinToString(separator: string): string; 90 | max: T extends number 91 | ? (comparator?: Comparator) => number | null 92 | : (comparator: Comparator) => T | null; 93 | min: T extends number 94 | ? (comparator?: Comparator) => number | null 95 | : (comparator: Comparator) => T | null; 96 | some(pred: (item: T) => boolean): boolean; 97 | sum: T extends number ? () => number : void; 98 | toArray(): T[]; 99 | toMap(getKey: (item: T) => K, getValue: (item: T) => V): Map; 100 | toMapGroupBy(getKey: (item: T) => K): Map; 101 | toMapGroupBy( 102 | getKey: (item: T) => K, 103 | transformer: CompletingTransformer, 104 | ): Map; 105 | toObject( 106 | getKey: (item: T) => K, 107 | getValue: (item: T) => V, 108 | ): Record; 109 | toObjectGroupBy( 110 | getKey: (item: T) => K, 111 | ): Record; 112 | toObjectGroupBy( 113 | getKey: (item: T) => K, 114 | transformer: CompletingTransformer, 115 | ): Record; 116 | toSet(): Set; 117 | 118 | toIterator(): IterableIterator; 119 | // tslint:enable: member-ordering 120 | } 121 | 122 | export interface TransducerBuilder { 123 | // tslint:disable: member-ordering 124 | compose(transducer: Transducer): TransducerBuilder; 125 | 126 | dedupe(): TransducerBuilder; 127 | drop(n: number): TransducerBuilder; 128 | dropWhile(pred: (item: T) => boolean): TransducerBuilder; 129 | filter( 130 | pred: (item: T) => item is U, 131 | ): TransducerBuilder; 132 | filter(pred: (item: T) => boolean): TransducerBuilder; 133 | flatMap(f: (item: T) => Iterable): TransducerBuilder; 134 | flatten: T extends Iterable 135 | ? () => TransducerBuilder 136 | : void; 137 | interpose(separator: T): TransducerBuilder; 138 | map(f: (item: T) => U): TransducerBuilder; 139 | mapIndexed( 140 | f: (item: T, index: number) => U, 141 | ): TransducerBuilder; 142 | partitionAll(n: number): TransducerBuilder; 143 | partitionBy(pred: (item: T) => boolean): TransducerBuilder; 144 | remove( 145 | pred: (item: T) => item is U, 146 | ): TransducerBuilder>; 147 | remove(pred: (item: T) => boolean): TransducerBuilder; 148 | removeAbsent(): TransducerBuilder>; 149 | take(n: number): TransducerBuilder; 150 | takeNth(n: number): TransducerBuilder; 151 | takeWhile( 152 | pred: (item: T) => item is U, 153 | ): TransducerBuilder; 154 | takeWhile(pred: (item: T) => boolean): TransducerBuilder; 155 | 156 | build(): Transducer; 157 | // tslint:disable: member-ordering 158 | } 159 | 160 | export function chainFrom(collection: Iterable): TransformChain { 161 | return new TransducerChain(collection) as any; 162 | } 163 | 164 | export function transducerBuilder(): TransducerBuilder { 165 | return new TransducerChain([]) as any; 166 | } 167 | 168 | type CombinedBuilder = TransformChain & 169 | TransducerBuilder; 170 | 171 | class TransducerChain implements CombinedBuilder { 172 | private readonly transducers: Array> = []; 173 | 174 | constructor(private readonly collection: Iterable) {} 175 | 176 | public compose(transducer: Transducer): CombinedBuilder { 177 | this.transducers.push(transducer); 178 | return this as any; 179 | } 180 | 181 | public build(): Transducer { 182 | return (x: any) => { 183 | let result = x; 184 | for (let i = this.transducers.length - 1; i >= 0; i--) { 185 | result = this.transducers[i](result); 186 | } 187 | return result; 188 | }; 189 | } 190 | 191 | // ----- Composing transducers ----- 192 | 193 | public dedupe(): CombinedBuilder { 194 | return this.compose(dedupe()); 195 | } 196 | 197 | public drop(n: number): CombinedBuilder { 198 | return this.compose(drop(n)); 199 | } 200 | 201 | public dropWhile(pred: (item: T) => boolean): CombinedBuilder { 202 | return this.compose(dropWhile(pred)); 203 | } 204 | 205 | public filter(pred: (item: T) => boolean): CombinedBuilder { 206 | return this.compose(filter(pred)); 207 | } 208 | 209 | public flatMap(f: (item: T) => Iterable): CombinedBuilder { 210 | return this.compose(flatMap(f)); 211 | } 212 | 213 | // @ts-ignore 214 | public flatten(): CombinedBuilder { 215 | return this.compose(flatten() as any); 216 | } 217 | 218 | public interpose(separator: T): CombinedBuilder { 219 | return this.compose(interpose(separator)); 220 | } 221 | 222 | public map(f: (item: T) => U): CombinedBuilder { 223 | return this.compose(map(f)); 224 | } 225 | 226 | public mapIndexed( 227 | f: (item: T, index: number) => U, 228 | ): CombinedBuilder { 229 | return this.compose(mapIndexed(f)); 230 | } 231 | 232 | public partitionAll(n: number): CombinedBuilder { 233 | return this.compose(partitionAll(n)); 234 | } 235 | 236 | public partitionBy(f: (item: T) => any): CombinedBuilder { 237 | return this.compose(partitionBy(f)); 238 | } 239 | 240 | public remove( 241 | pred: (item: T) => item is U, 242 | ): CombinedBuilder>; 243 | public remove(pred: (item: T) => boolean): CombinedBuilder; 244 | public remove(pred: (item: T) => boolean): CombinedBuilder { 245 | return this.compose(remove(pred)); 246 | } 247 | 248 | public removeAbsent(): CombinedBuilder> { 249 | return this.remove(item => item == null) as CombinedBuilder< 250 | TBase, 251 | NonNullable 252 | >; 253 | } 254 | 255 | public take(n: number): CombinedBuilder { 256 | return this.compose(take(n)); 257 | } 258 | 259 | public takeNth(n: number): CombinedBuilder { 260 | return this.compose(takeNth(n)); 261 | } 262 | 263 | public takeWhile(pred: (item: T) => boolean): CombinedBuilder { 264 | return this.compose(takeWhile(pred)); 265 | } 266 | 267 | // ----- Reductions ----- 268 | 269 | public reduce( 270 | reducer: QuittingReducer, 271 | initialValue: TResult, 272 | ): TResult; 273 | public reduce( 274 | reducer: CompletingTransformer, 275 | ): TCompleteResult; 276 | public reduce( 277 | reducer: 278 | | QuittingReducer 279 | | CompletingTransformer, 280 | initialValue?: TResult, 281 | ): TCompleteResult { 282 | if (typeof reducer === "function") { 283 | // Type coercion because in this branch, TResult and TCompleteResult are 284 | // the same, but the checker doesn't know that. 285 | return transduce( 286 | this.collection, 287 | this.build(), 288 | reducer, 289 | initialValue!, 290 | ) as any; 291 | } else { 292 | return transduce(this.collection, this.build(), reducer); 293 | } 294 | } 295 | 296 | // @ts-ignore 297 | public average(): number | null { 298 | return this.reduce(toAverage() as any); 299 | } 300 | 301 | public count(): number { 302 | return this.reduce(count()); 303 | } 304 | 305 | public every(pred: (item: T) => boolean): boolean { 306 | return this.reduce(every(pred)); 307 | } 308 | 309 | public find(pred: (item: T) => boolean): T | null { 310 | return this.reduce(find(pred)); 311 | } 312 | 313 | public first(): T | null { 314 | return this.reduce(first()); 315 | } 316 | 317 | public forEach(f: (item: T) => void): void { 318 | this.reduce(forEach(f)); 319 | } 320 | 321 | public isEmpty(): boolean { 322 | return this.reduce(isEmpty()); 323 | } 324 | 325 | public joinToString(separator: string): string { 326 | return this.reduce(joinToString(separator)); 327 | } 328 | 329 | // @ts-ignore 330 | public max(comparator: Comparator): T | null { 331 | return this.reduce(max(comparator)); 332 | } 333 | 334 | // @ts-ignore 335 | public min(comparator: Comparator): T | null { 336 | return this.reduce(min(comparator)); 337 | } 338 | 339 | public some(pred: (item: T) => boolean): boolean { 340 | return this.reduce(some(pred)); 341 | } 342 | 343 | // @ts-ignore 344 | public sum(): number { 345 | return this.reduce(sum() as any); 346 | } 347 | 348 | public toArray(): T[] { 349 | return this.reduce(toArray()); 350 | } 351 | 352 | public toMap( 353 | getKey: (item: T) => K, 354 | getValue: (item: T) => V, 355 | ): Map { 356 | return this.reduce(toMap(getKey, getValue)); 357 | } 358 | 359 | public toMapGroupBy(getKey: (item: T) => K): Map; 360 | public toMapGroupBy( 361 | getKey: (item: T) => K, 362 | transformer: CompletingTransformer, 363 | ): Map; 364 | public toMapGroupBy( 365 | getKey: (item: T) => K, 366 | transformer?: CompletingTransformer, 367 | ): Map { 368 | return this.reduce(toMapGroupBy(getKey, transformer as any)); 369 | } 370 | 371 | public toObject( 372 | getKey: (item: T) => K, 373 | getValue: (item: T) => V, 374 | ): Record { 375 | return this.reduce(toObject(getKey, getValue)); 376 | } 377 | 378 | public toObjectGroupBy( 379 | getKey: (item: T) => K, 380 | ): Record; 381 | public toObjectGroupBy( 382 | getKey: (item: T) => K, 383 | transformer: CompletingTransformer, 384 | ): Record; 385 | public toObjectGroupBy( 386 | getKey: (item: T) => K, 387 | transformer?: CompletingTransformer, 388 | ): Record { 389 | return this.reduce(toObjectGroupBy(getKey, transformer as any)); 390 | } 391 | 392 | public toSet(): Set { 393 | return this.reduce(toSet()); 394 | } 395 | 396 | public toIterator(): IterableIterator { 397 | return lazyTransduce(this.collection, this.build()); 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/transducers.ts: -------------------------------------------------------------------------------- 1 | import { reduceWithFunction } from "./core"; 2 | import { INIT, RESULT, STEP, VALUE } from "./propertyNames"; 3 | import { 4 | CompletingTransformer, 5 | MaybeReduced, 6 | QuittingReducer, 7 | Transducer, 8 | } from "./types"; 9 | import { ensureReduced, isReduced, reduced, unreduced } from "./util"; 10 | 11 | // It seems like there should be a way to factor out the repeated logic between 12 | // all of these transformer classes, but every attempt thus far has 13 | // significantly damaged performance. These functions are the bottleneck of the 14 | // code, so any added layers of indirection have a nontrivial perf cost. 15 | 16 | interface ValueWrapper { 17 | value: T; 18 | } 19 | 20 | function updateValue>( 21 | result: TWrapper, 22 | newValue: MaybeReduced, 23 | ): MaybeReduced { 24 | if (isReduced(newValue)) { 25 | result.value = newValue[VALUE]; 26 | return reduced(result); 27 | } else { 28 | result.value = newValue; 29 | return result; 30 | } 31 | } 32 | 33 | interface DedupeState extends ValueWrapper { 34 | last: T | {}; 35 | } 36 | 37 | class Dedupe 38 | implements 39 | CompletingTransformer, TCompleteResult, TInput> { 40 | constructor( 41 | private readonly xf: CompletingTransformer< 42 | TResult, 43 | TCompleteResult, 44 | TInput 45 | >, 46 | ) {} 47 | 48 | public [INIT](): DedupeState { 49 | return { last: {}, value: this.xf[INIT]() }; 50 | } 51 | 52 | public [RESULT](result: DedupeState): TCompleteResult { 53 | return this.xf[RESULT](result.value); 54 | } 55 | 56 | public [STEP]( 57 | result: DedupeState, 58 | input: TInput, 59 | ): MaybeReduced> { 60 | if (input !== result.last) { 61 | result.last = input; 62 | return updateValue(result, this.xf[STEP](result.value, input)); 63 | } else { 64 | return result; 65 | } 66 | } 67 | } 68 | 69 | export function dedupe(): Transducer { 70 | return xf => new Dedupe(xf); 71 | } 72 | 73 | class Drop 74 | implements CompletingTransformer { 75 | private i = 0; 76 | 77 | constructor( 78 | private readonly xf: CompletingTransformer< 79 | TResult, 80 | TCompleteResult, 81 | TInput 82 | >, 83 | private readonly n: number, 84 | ) {} 85 | 86 | public [INIT](): TResult { 87 | return this.xf[INIT](); 88 | } 89 | 90 | public [RESULT](result: TResult): TCompleteResult { 91 | return this.xf[RESULT](result); 92 | } 93 | 94 | public [STEP](result: TResult, input: TInput): MaybeReduced { 95 | return this.i++ < this.n ? result : this.xf[STEP](result, input); 96 | } 97 | } 98 | 99 | export function drop(n: number): Transducer { 100 | return xf => new Drop(xf, n); 101 | } 102 | 103 | interface DropWhileState extends ValueWrapper { 104 | isDoneDropping: boolean; 105 | } 106 | 107 | class DropWhile 108 | implements 109 | CompletingTransformer< 110 | DropWhileState, 111 | TCompleteResult, 112 | TInput 113 | > { 114 | constructor( 115 | private readonly xf: CompletingTransformer< 116 | TResult, 117 | TCompleteResult, 118 | TInput 119 | >, 120 | private readonly pred: (item: TInput) => boolean, 121 | ) {} 122 | 123 | public [INIT](): DropWhileState { 124 | return { value: this.xf[INIT](), isDoneDropping: false }; 125 | } 126 | 127 | public [RESULT](result: DropWhileState): TCompleteResult { 128 | return this.xf[RESULT](result.value); 129 | } 130 | 131 | public [STEP]( 132 | result: DropWhileState, 133 | input: TInput, 134 | ): MaybeReduced> { 135 | if (result.isDoneDropping) { 136 | return updateValue(result, this.xf[STEP](result.value, input)); 137 | } else { 138 | if (this.pred(input)) { 139 | return result; 140 | } else { 141 | result.isDoneDropping = true; 142 | return updateValue(result, this.xf[STEP](result.value, input)); 143 | } 144 | } 145 | } 146 | } 147 | 148 | export function dropWhile(pred: (item: T) => boolean): Transducer { 149 | return xf => new DropWhile(xf, pred); 150 | } 151 | 152 | class Filter 153 | implements CompletingTransformer { 154 | constructor( 155 | private readonly xf: CompletingTransformer< 156 | TResult, 157 | TCompleteResult, 158 | TInput 159 | >, 160 | private readonly pred: (item: TInput) => boolean, 161 | ) {} 162 | 163 | public [INIT](): TResult { 164 | return this.xf[INIT](); 165 | } 166 | 167 | public [RESULT](result: TResult): TCompleteResult { 168 | return this.xf[RESULT](result); 169 | } 170 | 171 | public [STEP](result: TResult, input: TInput): MaybeReduced { 172 | return this.pred(input) ? this.xf[STEP](result, input) : result; 173 | } 174 | } 175 | export function filter( 176 | pred: (item: T) => item is U, 177 | ): Transducer; 178 | export function filter(pred: (item: T) => boolean): Transducer; 179 | export function filter(pred: (item: T) => boolean): Transducer { 180 | return xf => new Filter(xf, pred); 181 | } 182 | 183 | class Flatten< 184 | TResult, 185 | TCompleteResult, 186 | TInput extends Iterable, 187 | TOutput 188 | > implements CompletingTransformer { 189 | private readonly step: QuittingReducer; 190 | 191 | constructor( 192 | private readonly xf: CompletingTransformer< 193 | TResult, 194 | TCompleteResult, 195 | TOutput 196 | >, 197 | ) { 198 | this.step = xf[STEP].bind(xf); 199 | } 200 | 201 | public [INIT](): TResult { 202 | return this.xf[INIT](); 203 | } 204 | 205 | public [RESULT](result: TResult): TCompleteResult { 206 | return this.xf[RESULT](result); 207 | } 208 | 209 | public [STEP](result: TResult, input: TInput): MaybeReduced { 210 | return reduceWithFunction(input, this.step, result); 211 | } 212 | } 213 | 214 | export function flatten(): Transducer, T> { 215 | return xf => new Flatten(xf); 216 | } 217 | 218 | export function flatMap(f: (item: T) => Iterable): Transducer { 219 | return xf => new MapTransformer(new Flatten(xf), f); 220 | } 221 | 222 | class Interpose 223 | implements CompletingTransformer { 224 | private isStarted = false; 225 | 226 | constructor( 227 | private readonly xf: CompletingTransformer< 228 | TResult, 229 | TCompleteResult, 230 | TInput 231 | >, 232 | private readonly separator: TInput, 233 | ) {} 234 | 235 | public [INIT](): TResult { 236 | return this.xf[INIT](); 237 | } 238 | 239 | public [RESULT](result: TResult): TCompleteResult { 240 | return this.xf[RESULT](result); 241 | } 242 | 243 | public [STEP](result: TResult, input: TInput): MaybeReduced { 244 | if (this.isStarted) { 245 | const withSeparator = this.xf[STEP](result, this.separator); 246 | if (isReduced(withSeparator)) { 247 | return withSeparator; 248 | } else { 249 | return this.xf[STEP](withSeparator, input); 250 | } 251 | } else { 252 | this.isStarted = true; 253 | return this.xf[STEP](result, input); 254 | } 255 | } 256 | } 257 | 258 | export function interpose(separator: T): Transducer { 259 | return xf => new Interpose(xf, separator); 260 | } 261 | 262 | // Not named Map to avoid confusion with the native Map class. 263 | class MapTransformer 264 | implements CompletingTransformer { 265 | constructor( 266 | private readonly xf: CompletingTransformer< 267 | TResult, 268 | TCompleteResult, 269 | TOutput 270 | >, 271 | private readonly f: (item: TInput) => TOutput, 272 | ) {} 273 | 274 | public [INIT](): TResult { 275 | return this.xf[INIT](); 276 | } 277 | 278 | public [RESULT](result: TResult): TCompleteResult { 279 | return this.xf[RESULT](result); 280 | } 281 | 282 | public [STEP](result: TResult, input: TInput): MaybeReduced { 283 | return this.xf[STEP](result, this.f(input)); 284 | } 285 | } 286 | 287 | export function map(f: (item: T) => U): Transducer { 288 | return xf => new MapTransformer(xf, f); 289 | } 290 | 291 | interface MapIndexedState extends ValueWrapper { 292 | i: number; 293 | } 294 | 295 | class MapIndexed 296 | implements 297 | CompletingTransformer< 298 | MapIndexedState, 299 | TCompleteResult, 300 | TInput 301 | > { 302 | constructor( 303 | private readonly xf: CompletingTransformer< 304 | TResult, 305 | TCompleteResult, 306 | TOutput 307 | >, 308 | private readonly f: (item: TInput, index: number) => TOutput, 309 | ) {} 310 | 311 | public [INIT](): MapIndexedState { 312 | return { value: this.xf[INIT](), i: 0 }; 313 | } 314 | 315 | public [RESULT](result: MapIndexedState): TCompleteResult { 316 | return this.xf[RESULT](result.value); 317 | } 318 | 319 | public [STEP]( 320 | result: MapIndexedState, 321 | input: TInput, 322 | ): MaybeReduced> { 323 | return updateValue( 324 | result, 325 | this.xf[STEP](result.value, this.f(input, result.i++)), 326 | ); 327 | } 328 | } 329 | 330 | export function mapIndexed( 331 | f: (item: T, index: number) => U, 332 | ): Transducer { 333 | return xf => new MapIndexed(xf, f); 334 | } 335 | 336 | class PartitionAll 337 | implements CompletingTransformer { 338 | private buffer: TInput[] = []; 339 | 340 | constructor( 341 | private readonly xf: CompletingTransformer< 342 | TResult, 343 | TCompleteResult, 344 | TInput[] 345 | >, 346 | private readonly n: number, 347 | ) {} 348 | 349 | public [INIT](): TResult { 350 | return this.xf[INIT](); 351 | } 352 | 353 | public [RESULT](result: TResult): TCompleteResult { 354 | if (this.buffer.length > 0) { 355 | result = unreduced(this.xf[STEP](result, this.buffer)); 356 | this.buffer = []; 357 | } 358 | return this.xf[RESULT](result); 359 | } 360 | 361 | public [STEP](result: TResult, input: TInput): MaybeReduced { 362 | this.buffer.push(input); 363 | if (this.buffer.length === this.n) { 364 | const newResult = this.xf[STEP](result, this.buffer); 365 | this.buffer = []; 366 | return newResult; 367 | } else { 368 | return result; 369 | } 370 | } 371 | } 372 | 373 | export function partitionAll(n: number): Transducer { 374 | if (n === 0) { 375 | throw new Error("Size in partitionAll() cannot be 0"); 376 | } else if (n < 0) { 377 | throw new Error("Size in partitionAll() cannot be negative"); 378 | } 379 | return xf => new PartitionAll(xf, n); 380 | } 381 | 382 | interface PartitionByState extends ValueWrapper { 383 | buffer: TInput[]; 384 | lastKey: any; 385 | } 386 | 387 | const INITIAL_LAST_KEY = {}; 388 | 389 | class PartitionBy 390 | implements 391 | CompletingTransformer< 392 | PartitionByState, 393 | TCompleteResult, 394 | TInput 395 | > { 396 | constructor( 397 | private readonly xf: CompletingTransformer< 398 | TResult, 399 | TCompleteResult, 400 | TInput[] 401 | >, 402 | private readonly f: (item: TInput) => any, 403 | ) {} 404 | 405 | public [INIT](): PartitionByState { 406 | return { 407 | value: this.xf[INIT](), 408 | buffer: [], 409 | lastKey: INITIAL_LAST_KEY, 410 | }; 411 | } 412 | 413 | public [RESULT]( 414 | result: PartitionByState, 415 | ): TCompleteResult { 416 | if (result.buffer.length > 0) { 417 | result.value = unreduced( 418 | this.xf[STEP](result.value, result.buffer), 419 | ); 420 | result.buffer = []; 421 | } 422 | return this.xf[RESULT](result.value); 423 | } 424 | 425 | public [STEP]( 426 | result: PartitionByState, 427 | input: TInput, 428 | ): MaybeReduced> { 429 | const key = this.f(input); 430 | const { value, buffer, lastKey } = result; 431 | result.lastKey = key; 432 | let newResult: MaybeReduced>; 433 | if (lastKey === INITIAL_LAST_KEY || lastKey === key) { 434 | newResult = result; 435 | } else { 436 | newResult = updateValue(result, this.xf[STEP](value, buffer)); 437 | unreduced(newResult).buffer = []; 438 | } 439 | unreduced(newResult).buffer.push(input); 440 | return newResult; 441 | } 442 | } 443 | 444 | export function partitionBy(f: (item: T) => any): Transducer { 445 | return xf => new PartitionBy(xf, f); 446 | } 447 | 448 | class Take 449 | implements CompletingTransformer { 450 | private i = 0; 451 | 452 | constructor( 453 | private readonly xf: CompletingTransformer< 454 | TResult, 455 | TCompleteResult, 456 | TInput 457 | >, 458 | private readonly n: number, 459 | ) {} 460 | 461 | public [INIT](): TResult { 462 | return this.xf[INIT](); 463 | } 464 | 465 | public [RESULT](result: TResult): TCompleteResult { 466 | return this.xf[RESULT](result); 467 | } 468 | 469 | public [STEP](result: TResult, input: TInput): MaybeReduced { 470 | // Written this way to avoid pulling one more element than necessary. 471 | if (this.n <= 0) { 472 | return reduced(result); 473 | } 474 | const next = this.xf[STEP](result, input); 475 | return this.i++ < this.n - 1 ? next : ensureReduced(next); 476 | } 477 | } 478 | 479 | export function remove( 480 | pred: (item: T) => item is U, 481 | ): Transducer>; 482 | export function remove(pred: (item: T) => boolean): Transducer; 483 | export function remove(pred: (item: T) => boolean): Transducer { 484 | return filter(item => !pred(item)); 485 | } 486 | 487 | export function take(n: number): Transducer { 488 | return xf => new Take(xf, n); 489 | } 490 | 491 | interface TakeNthState extends ValueWrapper { 492 | i: number; 493 | } 494 | 495 | class TakeNth 496 | implements 497 | CompletingTransformer, TCompleteResult, TInput> { 498 | constructor( 499 | private readonly xf: CompletingTransformer< 500 | TResult, 501 | TCompleteResult, 502 | TInput 503 | >, 504 | private readonly n: number, 505 | ) {} 506 | 507 | public [INIT](): TakeNthState { 508 | return { value: this.xf[INIT](), i: 0 }; 509 | } 510 | 511 | public [RESULT](result: TakeNthState): TCompleteResult { 512 | return this.xf[RESULT](result.value); 513 | } 514 | 515 | public [STEP]( 516 | result: TakeNthState, 517 | input: TInput, 518 | ): MaybeReduced> { 519 | return result.i++ % this.n === 0 520 | ? updateValue(result, this.xf[STEP](result.value, input)) 521 | : result; 522 | } 523 | } 524 | 525 | export function takeNth(n: number): Transducer { 526 | if (n === 0) { 527 | throw new Error("Step in takeNth() cannot be 0"); 528 | } else if (n < 0) { 529 | throw new Error("Step in takeNth() cannot be negative"); 530 | } 531 | return xf => new TakeNth(xf, n); 532 | } 533 | 534 | class TakeWhile 535 | implements CompletingTransformer { 536 | constructor( 537 | private readonly xf: CompletingTransformer< 538 | TResult, 539 | TCompleteResult, 540 | TInput 541 | >, 542 | private readonly pred: (item: TInput) => boolean, 543 | ) {} 544 | 545 | public [INIT](): TResult { 546 | return this.xf[INIT](); 547 | } 548 | 549 | public [RESULT](result: TResult): TCompleteResult { 550 | return this.xf[RESULT](result); 551 | } 552 | 553 | public [STEP](result: TResult, input: TInput): MaybeReduced { 554 | return this.pred(input) 555 | ? this.xf[STEP](result, input) 556 | : reduced(result); 557 | } 558 | } 559 | 560 | export function takeWhile( 561 | pred: (item: T) => item is U, 562 | ): Transducer; 563 | export function takeWhile(pred: (item: T) => boolean): Transducer; 564 | export function takeWhile(pred: (item: T) => boolean): Transducer { 565 | return xf => new TakeWhile(xf, pred); 566 | } 567 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## Table of Contents 4 | 5 | 6 | 7 | - [Starting a chain](#starting-a-chain) 8 | - [`chainFrom(iterable)`](#chainfromiterable) 9 | - [`transducerBuilder()`](#transducerbuilder) 10 | - [Transformation methods](#transformation-methods) 11 | - [`.dedupe()`](#dedupe) 12 | - [`.drop(n)`](#dropn) 13 | - [`.dropWhile(pred)`](#dropwhilepred) 14 | - [`.filter(pred)`](#filterpred) 15 | - [`.flatMap(f)`](#flatmapf) 16 | - [`.flatten()`](#flatten) 17 | - [`.interpose(separator)`](#interposeseparator) 18 | - [`.map(f)`](#mapf) 19 | - [`.mapIndexed(f)`](#mapindexedf) 20 | - [`.partitionAll(n)`](#partitionalln) 21 | - [`.partitionBy(f)`](#partitionbyf) 22 | - [`.remove(pred)`](#removepred) 23 | - [`.removeAbsent()`](#removeabsent) 24 | - [`.take(n)`](#taken) 25 | - [`.takeNth(n)`](#takenthn) 26 | - [`.takeWhile(pred)`](#takewhilepred) 27 | - [`.compose(transducer)`](#composetransducer) 28 | - [Ending a chain](#ending-a-chain) 29 | - [`.average()`](#average) 30 | - [`.count()`](#count) 31 | - [`.every(pred)`](#everypred) 32 | - [`.find(pred)`](#findpred) 33 | - [`.first()`](#first) 34 | - [`.forEach(f)`](#foreachf) 35 | - [`.isEmpty()`](#isempty) 36 | - [`.joinToString(separator)`](#jointostringseparator) 37 | - [`.max(comparator?)`](#maxcomparator) 38 | - [`.min(comparator?)`](#mincomparator) 39 | - [`.some(pred)`](#somepred) 40 | - [`.sum()`](#sum) 41 | - [`.toArray()`](#toarray) 42 | - [`.toMap(getKey, getValue)`](#tomapgetkey-getvalue) 43 | - [`.toMapGroupBy(getKey, transformer?)`](#tomapgroupbygetkey-transformer) 44 | - [`.toObject(getKey, getValue)`](#toobjectgetkey-getvalue) 45 | - [`.toObjectGroupBy(getKey, transformer?)`](#toobjectgroupbygetkey-transformer) 46 | - [`.toSet()`](#toset) 47 | - [`.toIterator()`](#toiterator) 48 | - [`.reduce(reducer, intialValue?)`](#reducereducer-intialvalue) 49 | - [Iterables](#iterables) 50 | - [`cycle(iterable)`](#cycleiterable) 51 | - [`iterate(initialValue, f)`](#iterateinitialvalue-f) 52 | - [`repeat(value, count?)`](#repeatvalue-count) 53 | - [`range(start?, end, ste?)`](#rangestart-end-ste) 54 | - [Utility functions](#utility-functions) 55 | - [`isReduced(result)`](#isreducedresult) 56 | - [`reduced(result)`](#reducedresult) 57 | - [Tree shakeable API](#tree-shakeable-api) 58 | - [`compose(f1, f2, ...)`](#composef1-f2-) 59 | - [`transduce(iterable, transducer, transformer)`](#transduceiterable-transducer-transformer) 60 | - [`lazyTransduce(iterable, transducer)`](#lazytransduceiterable-transducer) 61 | 62 | 63 | 64 | ## Starting a chain 65 | 66 | ### `chainFrom(iterable)` 67 | 68 | Starts a chain. Any number of transformation methods may be added, after which a 69 | termination method should be called to produce a result. No computation is done 70 | until a termination method is called. 71 | 72 | The argument may be any iterable, such as an array, a string, or an ES6 `Set`. 73 | This is back-compatible with older browsers which did not implement the 74 | `Iterable` interface for arrays and strings. 75 | 76 | ### `transducerBuilder()` 77 | 78 | Starts a chain for constructing a new transducer. Any number of transformation 79 | methods may be added, after which `.build()` should be called to produce a 80 | transducer. 81 | 82 | ## Transformation methods 83 | 84 | Any number of these methods may be called on a chain to add transformations in 85 | sequence. 86 | 87 | ### `.dedupe()` 88 | 89 | Removes elements that are equal to the proceeding element (using `===` for 90 | equality). For example: 91 | 92 | ```ts 93 | chainFrom([1, 2, 2, 3, 3, 3]) 94 | .dedupe() 95 | .toArray(); // -> [1, 2, 3] 96 | ``` 97 | 98 | ### `.drop(n)` 99 | 100 | Skips the first `n` elements. If there are fewer than `n` elements, then skip 101 | all of them. If `n` is negative, then leave the elements unchanged (same as 102 | `0`). 103 | 104 | For example: 105 | 106 | ```ts 107 | chainFrom([1, 2, 3, 4, 5]) 108 | .drop(3) 109 | .toArray(); // -> [4, 5] 110 | ``` 111 | 112 | ### `.dropWhile(pred)` 113 | 114 | Skips elements as long as the predicate `pred` holds. For example: 115 | 116 | ```ts 117 | chainFrom([1, 2, 3, 4, 5]) 118 | .dropWhile(n => n < 3) 119 | .toArray(); // -> [3, 4, 5] 120 | ``` 121 | 122 | ### `.filter(pred)` 123 | 124 | Keeps only the elements matching the predicate `pred`. For example: 125 | 126 | ```ts 127 | chainFrom([1, 2, 3, 4]) 128 | .filter(x => x % 2 === 1) 129 | .toArray(); // -> [1, 3] 130 | ``` 131 | 132 | ### `.flatMap(f)` 133 | 134 | For `f` a function which maps each element to an iterable, applies `f` to each 135 | element and concatenates the results. For example: 136 | 137 | ```ts 138 | const authors = [ 139 | { name: "cbrontë", books: ["Jane Eyre", "Shirley"] }, 140 | { name: "mshelley", books: ["Frankenstein"] }, 141 | ]; 142 | 143 | chainFrom(authors) 144 | .flatMap(author => author.books) 145 | .toArray(); 146 | // -> ["Jane Eyre", "Shirley", "Frankenstein"] 147 | ``` 148 | 149 | ### `.flatten()` 150 | 151 | For a chain of iterables, concatenates the contents of each iterable. For example: 152 | 153 | ```ts 154 | chainFrom([[0, 1], [2], [], [3, 4]]) 155 | .flatten() 156 | .toArray(); // [0, 1, 2, 3, 4] 157 | ``` 158 | 159 | ### `.interpose(separator)` 160 | 161 | Inserts `separator` between each pair of elements. For example: 162 | 163 | ```ts 164 | chainFrom([1, 2, 3, 4, 5]) 165 | .interpose(0) 166 | .toArray(); 167 | // -> [1, 0, 2, 0, 3, 0, 4, 0, 5] 168 | ``` 169 | 170 | ### `.map(f)` 171 | 172 | Transforms each element by applying `f` to it. For example: 173 | 174 | ```ts 175 | chainFrom([1, 2, 3]) 176 | .map(x => x * 2) 177 | .toArray(); // -> [2, 4, 6] 178 | ``` 179 | 180 | ### `.mapIndexed(f)` 181 | 182 | Transforms each element by applying `f` to the element and the current index in 183 | the sequence. For example: 184 | 185 | ```ts 186 | chainFrom(["a", "b", "c"]) 187 | .mapIndexed((x, i) => [x, i]) 188 | .toArray(); // -> [["a", 0], ["b", 1], ["c", 2]] 189 | ``` 190 | 191 | ### `.partitionAll(n)` 192 | 193 | Groups elements into arrays of `n` elements. If the number of elements does not 194 | divide perfectly by `n`, the last array will have fewer than `n` elements. 195 | Throws if `n` is nonpositive. For example: 196 | 197 | ```ts 198 | chainFrom([1, 2, 3, 4, 5]) 199 | .partitionAll(2) 200 | .toArray(); 201 | // -> [[1, 2], [3, 4], [5]] 202 | ``` 203 | 204 | ### `.partitionBy(f)` 205 | 206 | Groups consecutive elements for which `f` returns the same value (as determined 207 | by `===`) into arrays. For example: 208 | 209 | ```ts 210 | chainFrom(["a", "ab", "bc", "c", "cd", "cde"]) 211 | .partitionBy(s => s[0]) 212 | .toArray(); 213 | // -> [["a", "ab"], ["bc"], ["c", "cd", "cde"]] 214 | ``` 215 | 216 | ### `.remove(pred)` 217 | 218 | Like `filter()`, but removes the elements matching `pred` instead. For example: 219 | 220 | ```ts 221 | chainFrom([1, 2, 3, 4]) 222 | .remove(x => x % 2 === 1) 223 | .toArray(); // -> [2, 4] 224 | ``` 225 | 226 | ### `.removeAbsent()` 227 | 228 | Removes `null` and `undefined` elements (but not other falsy values). For 229 | example: 230 | 231 | ```ts 232 | chainFrom([0, 1, null, 2, undefined, 3]) 233 | .removeAbsent() 234 | .toArray(); // -> [0, 1, 2, 3] 235 | ``` 236 | 237 | ### `.take(n)` 238 | 239 | Takes the first `n` elements and drops the rest. An essential operation for 240 | efficiency, because it stops computations from occurring on more elements of the 241 | input than needed to produce `n` results. If there are less than `n` elements, 242 | then leave all of them unchanged. If `n` is negative, then take none of them 243 | (same as `0`). For example: 244 | 245 | ```ts 246 | chainFrom([1, 2, 3, 4, 5]) 247 | .take(3) 248 | .toArray(); // -> [1, 2, 3] 249 | ``` 250 | 251 | ### `.takeNth(n)` 252 | 253 | Takes every `n`th element, starting from the first one. In other words, it takes 254 | the elements whose indices are multiples of `n`. Throws if `n` is nonpositive. 255 | For example: 256 | 257 | ```ts 258 | chainFrom([1, 2, 3, 4, 5, 6]) 259 | .takeNth(2) 260 | .toArray(); // [1, 3, 5] 261 | ``` 262 | 263 | ### `.takeWhile(pred)` 264 | 265 | Takes elements as long as the predicate `pred` holds, then drops the rest. Like 266 | `take()`, stops unnecessary computations on elements after `pred` fails. For 267 | example: 268 | 269 | ```ts 270 | chainFrom([1, 2, 3, 4, 5]) 271 | .takeWhile(n => n < 3) 272 | .toArray(); // -> [1, 2] 273 | ``` 274 | 275 | ### `.compose(transducer)` 276 | 277 | Add an arbitrary transducer to the chain. `transducer` should be a function 278 | which implements the [transducer 279 | protocol](https://github.com/cognitect-labs/transducers-js#the-transducer-protocol), 280 | meaning it is a function which takes a `Transformer` and returns another 281 | `Transformer`. This is the most general transformation, and it is used by this 282 | library internally to implement all the others. For example usage, see the 283 | [Using custom 284 | transducers](https://github.com/dphilipson/transducist#using-custom-transducers) 285 | section of the main readme. 286 | 287 | ## Ending a chain 288 | 289 | The following methods terminate a chain started with `chainFrom`, performing the 290 | calculations and producing a result. 291 | 292 | ### `.average()` 293 | 294 | For a chain of numbers, return their average, or `null` if there are no 295 | elements. For example: 296 | 297 | ```ts 298 | chainFrom(["a", "bb", "ccc"]) 299 | .map(s => s.length) 300 | .average(); // -> 2 301 | ``` 302 | 303 | ### `.count()` 304 | 305 | Returns the number of elements. For example: 306 | 307 | ```ts 308 | chainFrom([1, 2, 3, 4, 5]) 309 | .filter(x => x % 2 === 1) 310 | .count(); // -> 3 311 | ``` 312 | 313 | ### `.every(pred)` 314 | 315 | Returns `true` if all elements satisfy the predicate `pred`, or `false` 316 | otherwise. Short-circuits computation once a failure is found. Note that this is 317 | equivalent to `.remove(pred).isEmpty()`. 318 | 319 | Example: 320 | 321 | ```ts 322 | chainFrom([1, 2, 3, 4, 5]) 323 | .map(n => 10 * n) 324 | .every(n => n > 3); // -> true 325 | 326 | chainFrom([1, 2, 3, 4, 5]) 327 | .map(n => 10 * n) 328 | .every(n => n < 30); // -> false 329 | ``` 330 | 331 | ### `.find(pred)` 332 | 333 | Returns the first element of the result which satisfies the predicate `pred`, or 334 | `null` if no such element exists. Short-circuits computation once a match is 335 | found. Note that this is equivalent to `.filter(pred).first()`. 336 | 337 | Example: 338 | 339 | ```ts 340 | chainFrom([1, 2, 3, 4, 5]) 341 | .map(x => x * 10) 342 | .find(x => x % 6 === 0); // -> 30 343 | ``` 344 | 345 | ### `.first()` 346 | 347 | Returns the first element of the result, or `null` if there are no elements. 348 | Short-circuits computation after reading the first element. 349 | 350 | Example: 351 | 352 | ```ts 353 | chainFrom([1, 2, 3, 4, 5]) 354 | .map(x => x * 10) 355 | .first(); // -> 10 356 | ``` 357 | 358 | ### `.forEach(f)` 359 | 360 | Calls `f` on each element of the result, presumably for side-effects. For 361 | example: 362 | 363 | ```ts 364 | chainFrom([1, 2, 3, 4, 5]) 365 | .map(x => x * 10) 366 | .forEach(x => console.log(x)); 367 | // Prints 10, 20, 30, 40, 50 368 | ``` 369 | 370 | ### `.isEmpty()` 371 | 372 | Returns `true` if there are any elements, else `false`. Short-circuits 373 | computation after reading one element. For example: 374 | 375 | ```ts 376 | chainFrom([1, 2, 3, 4, 5]) 377 | .filter(n => n > 10) 378 | .isEmpty(); // -> true 379 | 380 | chainFrom([1, 2, 3, 4, 5]) 381 | .filter(n => n % 2 === 0) 382 | .isEmpty(); // -> false 383 | ``` 384 | 385 | ### `.joinToString(separator)` 386 | 387 | Returns a string obtained by concatenating the elements together as strings with 388 | the separator between them. For example: 389 | 390 | ```ts 391 | chainFrom([1, 2, 3, 4, 5]) 392 | .filter(n => n % 2 === 1) 393 | .joinToString(" -> "); // -> "1 -> 3 -> 5" 394 | ``` 395 | 396 | Not called `toString()` in order to avoid clashing with the `Object` prototype 397 | method. 398 | 399 | ### `.max(comparator?)` 400 | 401 | Returns the maximum element, according to the comparator. If the elements are 402 | numbers, then this may be called without providing a comparator, in which case 403 | the natural comparator is used. Returns `null` if there are no elements. 404 | 405 | Example: 406 | 407 | ```ts 408 | chainFrom(["a", "bb", "ccc"]) 409 | .map(s => s.length) 410 | .max(); // -> 3 411 | ``` 412 | 413 | ### `.min(comparator?)` 414 | 415 | Returns the minimum element, according to the comparator. If the elements are 416 | numbers, then this may be called without providing a comparator, in which case 417 | the natural comparator is used. Returns `null` if there are no elements. 418 | 419 | Example: 420 | 421 | ```ts 422 | chainFrom(["a", "bb", "ccc"]) 423 | .map(s => s.length) 424 | .min()); // -> 1 425 | ``` 426 | 427 | ### `.some(pred)` 428 | 429 | Returns `true` if any element satisfies the predicate `pred`, or `false` 430 | otherwise. Short-circuits computation once a match is found. Note that this is 431 | equivalent to `.filter(pred).isEmpty() === false`. 432 | 433 | Example: 434 | 435 | ```ts 436 | chainFrom([1, 2, 3, 4, 5]) 437 | .map(n => 10 * n) 438 | .some(n => n === 30); // -> true 439 | 440 | chainFrom([1, 2, 3, 4, 5]) 441 | .map(n => 10 * n) 442 | .some(n => n === 1); // -> false 443 | ``` 444 | 445 | ### `.sum()` 446 | 447 | For a chain of numbers, return their sum. If the input is empty, return `0`. For 448 | example: 449 | 450 | ```ts 451 | chainFrom(["a", "bb", "ccc"]) 452 | .map(s => s.length) 453 | .sum(); // -> 6 454 | ``` 455 | 456 | ### `.toArray()` 457 | 458 | Returns an array of the results. See any of the above examples. 459 | 460 | ### `.toMap(getKey, getValue)` 461 | 462 | Returns an ES6 `Map`, each of whose key-value pairs is generated by calling the 463 | provided funtions on each element in the result. If multiple elements produce 464 | the same key, then the value produced by the latest element will override the 465 | earlier ones. Similar to [`.toObject()`](#toobjectgetkey-getvalue), except that 466 | the `getKey` function is permitted to return any type of value, not just a 467 | string. 468 | 469 | This function assumes that `Map` is present in your environment. **If you call 470 | this function, you are responsible for providing a polyfill if your environment 471 | does not natively support `Map`.** 472 | 473 | Example: 474 | 475 | ```ts 476 | const authors = [ 477 | { name: "cbrontë", books: ["Jane Eyre", "Shirley"] }, 478 | { name: "mshelley", books: ["Frankenstein"] }, 479 | ]; 480 | 481 | chainFrom(authors).toMap( 482 | a => a.id, 483 | a => a.books.length, 484 | ); 485 | // -> Map{ "cbrontë" -> 2, "mshelley" -> 4 } 486 | ``` 487 | 488 | ### `.toMapGroupBy(getKey, transformer?)` 489 | 490 | Produces an ES6 `Map` by grouping together all elements for which `getKey` 491 | returns the same value under that key. By default, each key corresponds to an 492 | array of the elements which produced that key. 493 | 494 | Optionally, a transformer may be passed as the second argument to configure the 495 | reduction behavior of the values. All chain-ending methods in this section other 496 | than `.toIterator()` have a standalone variant which produces a transformer (for 497 | details, see the section [Tree shakeable API](#tree-shakeable-api)), which opens 498 | many possibilities. Some advanced examples are shown below. 499 | 500 | Similar to [`.toObjectGroupBy()`](#toobjectgroupbygetkey-transformer), except 501 | that the `getKey` function is permitted to return any type of value, not just a 502 | string. 503 | 504 | This function assumes that `Map` is present in your environment. **If you call 505 | this function, you are responsible for providing a polyfill if your environment 506 | does not natively support `Map`.** 507 | 508 | Examples: 509 | 510 | ```ts 511 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toMapGroupBy(s => s[0]); 512 | // -> Map{ "a" -> ["a", "aa", "aaa"], "b" -> ["b", "bc"] } 513 | 514 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toMapGroupBy(s => s[0], count()); 515 | // -> Map{ "a" -> 3, "b" -> 2 } 516 | 517 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toMapGroupBy( 518 | s => s[0], 519 | some(s => s.length === 3), 520 | ); 521 | // -> Map{ "a" -> true, "b" -> false } 522 | 523 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toMapGroupBy( 524 | s => s[0], 525 | map((s: string) => s.length)(toAverage()), 526 | ); 527 | // -> Map{ "a" -> 2, "b" -> 1.5 } 528 | ``` 529 | 530 | ### `.toObject(getKey, getValue)` 531 | 532 | Returns an object, each of whose key-value pairs is generated by calling the 533 | provided functions on each element in the result. If multiple elements produce 534 | the same key, then the value produced by the latest element will override the 535 | earlier ones. Similar to [`.toMap()`](#tomapgetkey-getvalue), except that the 536 | `getKey` function is required to return a string. 537 | 538 | Example: 539 | 540 | ```ts 541 | const authors = [ 542 | { name: "cbrontë", books: ["Jane Eyre", "Shirley"] }, 543 | { name: "mshelley", books: ["Frankenstein"] }, 544 | ]; 545 | 546 | chainFrom(authors).toObject( 547 | a => a.id, 548 | a => a.books.length, 549 | ); 550 | // -> { cbrontë: 2, mshelley: 1 } 551 | ``` 552 | 553 | ### `.toObjectGroupBy(getKey, transformer?)` 554 | 555 | Produces an object by grouping together all elements for which `getKey` returns 556 | the same value under that key. By default, each key corresponds to an array of 557 | the elements which produced that key. 558 | 559 | Optionally, a transformer may be passed as the second argument to configure the 560 | reduction behavior of the values. All chain-ending methods in this section other 561 | than `.toIterator()` have a standalone variant which produces a transformer (for 562 | details, see the section [Tree-shakeable API](#tree-shakeable-api)), which opens 563 | many possibilities. Some advanced examples are shown below. 564 | 565 | Similar to [`.toMapGroupBy()`](#tomapgroupbygetkey-transformer), except that the 566 | `getKey` function is required to return a string. 567 | 568 | Examples: 569 | 570 | ```ts 571 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toObjectGroupBy(s => s[0]); 572 | // -> { a: ["a", "aa", "aaa"], b: ["b", "bc"] } 573 | 574 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toObjectGroupBy(s => s[0], count()); 575 | // -> { a: 3, b: 2 } 576 | 577 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toObjectGroupBy( 578 | s => s[0], 579 | some(s => s.length === 3), 580 | ); 581 | // -> { a: true, b: false } 582 | 583 | chainFrom(["a", "b", "aa", "aaa", "bc"]).toMapGroupBy( 584 | s => s[0], 585 | map((s: string) => s.length)(toAverage()), 586 | ); 587 | // -> { a: 2, b: 1.5 } 588 | ``` 589 | 590 | ### `.toSet()` 591 | 592 | Returns an ES6 `Set` of the results. 593 | 594 | This function assumes that `Set` is present in your environment. **If you call 595 | this function, you are responsible for providing a polyfill if your environment 596 | does not natively support `Set`.** 597 | 598 | ### `.toIterator()` 599 | 600 | Returns an iterator. Elements of the input iterator are not read until this 601 | iterator is read, and then only as many as needed to compute the number of 602 | results requested. This is the primary way of reading results lazily. 603 | 604 | Example: 605 | 606 | ```ts 607 | const iterator = chainFrom([1, 2, 3, 4, 5]) 608 | .map(x => x * 10) 609 | .toIterator(); 610 | console.log(iterator.next().value()); // Prints 10 611 | // So far, the map function has only been called once. 612 | ``` 613 | 614 | ### `.reduce(reducer, intialValue?)` 615 | 616 | Reduces the elements according to the reducer, and returns the result. `reducer` 617 | may be either a plain function of the form `(acc, x) => acc` or a transformer as 618 | defined by the [transformer 619 | protocol](https://github.com/cognitect-labs/transducers-js#transformer-protocol). 620 | This is the most general way to terminate a chain, and all the others (except 621 | for `toIterator`) are implemented using it. 622 | 623 | Example of using a plain function reducer: 624 | 625 | ```ts 626 | chainFrom([1, 2, 3, 4, 5]) 627 | .map(x => x * 10) 628 | .reduce((acc, x) => acc + x, 0); // -> 150 629 | ``` 630 | 631 | A handful of pre-made transformers are provided by this library to be used with 632 | `reduce()`. They are described in the next section. 633 | 634 | ## Iterables 635 | 636 | Helper functions for producing iterables, often useful for starting chains. 637 | 638 | ### `cycle(iterable)` 639 | 640 | Returns an iterable which repeats the elements of the input iterable 641 | indefinitely. If the input iterable is empty, then produces an empty iterable. For example: 642 | 643 | ```ts 644 | chainFrom(cycle(["a", "b", "c"])) 645 | .take(7) 646 | .toArray(); // -> ["a", "b", "c", "a", "b", "c", "a"] 647 | 648 | chainFrom(cycle([])).toArray(); // -> [] 649 | ``` 650 | 651 | ### `iterate(initialValue, f)` 652 | 653 | Returns an iterable which emits `initialValue`, `f(initialValue)`, 654 | `f(f(initialValue))`, and so on. For example: 655 | 656 | ```ts 657 | chainFrom(iterate(1, x => 2 * x)) 658 | .take(5) 659 | .toArray(); // -> [1, 2, 4, 8, 16] 660 | ``` 661 | 662 | ### `repeat(value, count?)` 663 | 664 | Returns an iterable which emits `value` repeatedly. If `count` is provided, then 665 | emits `value` that many times. If `count` is omitted, then emits `value` 666 | indefinitely. Throws if `count` is negative. For example: 667 | 668 | ```ts 669 | chainFrom(repeat("x")) 670 | .take(3) 671 | .toArray(); // -> ["x", "x", "x"] 672 | 673 | chainFrom(repeat("x", 3)).toArray(); // -> ["x", "x", "x"] 674 | ``` 675 | 676 | ### `range(start?, end, ste?)` 677 | 678 | Returns an iterable which outputs values from `start` inclusive to `end` 679 | exclusive, incrementing by `step` each time. `start` and `step` may be omitted, 680 | and default to `0` and `1` respectively. 681 | 682 | If step is positive, then outputs values incrementing upwards from `start` until 683 | the last value less than `end`. If step is negative, then outputs values 684 | incrementing downwards from `start` until the last value greater than `end`. 685 | 686 | A `start` greater than or equal to `end` for positive `step`, or a `start` less 687 | than or equal to `end` for a negative `step`, is permitted, and produces an 688 | empty iterator. 689 | 690 | Throws an error if `step` is zero. 691 | 692 | Example: 693 | 694 | ```ts 695 | chainFrom(range(3)) 696 | .map(i => "String #" + i) 697 | .toArray(); // -> ["String #0", "String #1", "String #2"] 698 | 699 | chainFrom(range(10, 15)).toArray(); // -> [10, 11, 12, 13, 14] 700 | 701 | chainFrom(range(10, 15, 2)).toArray(); // -> [10, 12, 14] 702 | 703 | chainFrom(range(15, 10, -2)).toArray(); // -> [15, 13, 11] 704 | ``` 705 | 706 | Values are produced lazily, so for example the following will return quickly and not 707 | use up all your memory: 708 | 709 | ```ts 710 | chainFrom(range(1000000000000)) 711 | .take(3) 712 | .toArray(); // -> [0, 1, 2] 713 | ``` 714 | 715 | ## Utility functions 716 | 717 | ### `isReduced(result)` 718 | 719 | Returns true if `result` is a reduced value as described by the [transducer 720 | protocol](https://github.com/cognitect-labs/transducers-js#reduced). 721 | 722 | ### `reduced(result)` 723 | 724 | Returns a reduced value of `result`, as described by the [transducer 725 | protocol](https://github.com/cognitect-labs/transducers-js#reduced). Can be 726 | returned by a reducer or a transformer to short-circuit computation. 727 | 728 | ## Tree shakeable API 729 | 730 | As discussed in the readme section [Bundle Size and Tree 731 | Shaking](https://github.com/dphilipson/transducist#bundle-size-and-tree-shaking), 732 | Transducist also provides standalone functions with the same behavior as the 733 | chain, for the purposes of reducing bundle size. In particular, all chain 734 | methods (except `toIterator()`) have a standalone function of the same name. 735 | 736 | For those familiar with the [transducer 737 | protocol](https://github.com/cognitect-labs/transducers-js#transformer-protocol), 738 | the standalone functions corresponding to the transform methods (e.g. `map()`, 739 | `take()`) each produce a transducer, while the standalone functions 740 | corresponding to the end of the chain (e.g. `toArray()`, `count()`) each produce 741 | a transformer. 742 | 743 | In addition to the standalone functions whose names match the methods listed 744 | above, the tree-shakeable API is completed by the functions below. 745 | 746 | ### `compose(f1, f2, ...)` 747 | 748 | Composes any number of transducers together to produce a new transducer. This is 749 | actually just ordinary function composition, although its TypeScript typings are 750 | specialized for transducers in particular. 751 | 752 | ### `transduce(iterable, transducer, transformer)` 753 | 754 | (Or: `transduce(iterable, transducer, reducer, initialValue)`) 755 | 756 | Starting from the iterable, applies the transformations specified by the 757 | transducer and then uses the transformer to construct a final result. 758 | 759 | Rather than providing a transformer as the third argument, this may also be 760 | called by passing an ordinary reducer function and an initial value as the final 761 | two arguments, where a reducer is a plain function of the form `(acc, x) => acc`. 762 | 763 | Example: 764 | 765 | ```ts 766 | import { compose, filter, map, toArray, transduce } from "transducist"; 767 | 768 | transduce( 769 | [1, 2, 3, 4, 5], 770 | compose( 771 | filter(x => x > 2), 772 | map(x => 2 * x), 773 | ), 774 | toArray(), 775 | ); // -> [6, 8, 10] 776 | ``` 777 | 778 | which is equivalent to the chained version: 779 | 780 | ```ts 781 | import { chainFrom } from "transducist"; 782 | 783 | chainFrom([1, 2, 3, 4, 5]) 784 | .filter(x => x > 2) 785 | .map(x => 2 * x) 786 | .toArray(); // -> [6, 8, 10] 787 | ``` 788 | 789 | ### `lazyTransduce(iterable, transducer)` 790 | 791 | Returns an iterator which lazily performs the transformations specified by the 792 | transducer. This is the standalone version of ending a chain with 793 | [`.toIterator()`](#toiterator). That is, the following are equivalent: 794 | 795 | ```ts 796 | import { compose, filter, map, lazyTransduce } from "transducist"; 797 | 798 | lazyTransduce( 799 | [1, 2, 3, 4, 5], 800 | compose( 801 | filter(x => x > 2), 802 | map(x => 2 * x), 803 | ), 804 | ); 805 | ``` 806 | 807 | ```ts 808 | import { chainFrom } from "transducist"; 809 | 810 | chainFrom([1, 2, 3, 4, 5]) 811 | .filter(x => x > 2) 812 | .map(x => 2 * x) 813 | .toIterator(); 814 | ``` 815 | 816 | Copyright © 2017 David Philipson 817 | -------------------------------------------------------------------------------- /__tests__/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | chainFrom, 3 | count, 4 | cycle, 5 | first, 6 | iterate, 7 | range, 8 | repeat, 9 | transducerBuilder, 10 | Transformer, 11 | } from "../src/index"; 12 | 13 | describe("transformer chain", () => { 14 | it("should apply transformations in order", () => { 15 | const input = [1, 2, 3, 4, 5]; 16 | const inc = (n: number) => n + 1; 17 | const isEven = (n: number) => n % 2 === 0; 18 | const result1 = chainFrom(input) 19 | .map(inc) 20 | .filter(isEven) 21 | .toArray(); 22 | const result2 = chainFrom(input) 23 | .filter(isEven) 24 | .map(inc) 25 | .toArray(); 26 | expect(result1).toEqual([2, 4, 6]); 27 | expect(result2).toEqual([3, 5]); 28 | }); 29 | }); 30 | 31 | // ----- Transformations ----- 32 | 33 | describe("compose()", () => { 34 | it("should apply the specified transform", () => { 35 | const transducer = transducerBuilder() 36 | .map(s => s.length) 37 | .build(); 38 | const result = chainFrom(["a", "bb", "ccc"]) 39 | .compose(transducer) 40 | .toArray(); 41 | expect(result).toEqual([1, 2, 3]); 42 | }); 43 | }); 44 | 45 | describe("dedupe()", () => { 46 | it("should remove consecutive duplicates", () => { 47 | const result = chainFrom([1, 2, 2, 3, 3, 3]) 48 | .dedupe() 49 | .toArray(); 50 | expect(result).toEqual([1, 2, 3]); 51 | }); 52 | }); 53 | 54 | describe("drop()", () => { 55 | it("should drop the first n elements", () => { 56 | const result = chainFrom([1, 2, 3, 4, 5]) 57 | .drop(2) 58 | .toArray(); 59 | expect(result).toEqual([3, 4, 5]); 60 | }); 61 | 62 | it("should drop everything if n is greater than the length", () => { 63 | const result = chainFrom([1, 2, 3, 4, 5]) 64 | .drop(7) 65 | .toArray(); 66 | expect(result).toEqual([]); 67 | }); 68 | 69 | it("should drop nothing if n is 0", () => { 70 | const result = chainFrom([1, 2, 3, 4, 5]) 71 | .drop(0) 72 | .toArray(); 73 | expect(result).toEqual([1, 2, 3, 4, 5]); 74 | }); 75 | 76 | it("should drop nothing if n is negative", () => { 77 | const result = chainFrom([1, 2, 3, 4, 5]) 78 | .drop(-2) 79 | .toArray(); 80 | expect(result).toEqual([1, 2, 3, 4, 5]); 81 | }); 82 | }); 83 | 84 | describe("dropWhile()", () => { 85 | it("should drop elements until the predicate fails", () => { 86 | const result = chainFrom([1, 2, 3, 4, 5]) 87 | .dropWhile(n => n < 3) 88 | .toArray(); 89 | expect(result).toEqual([3, 4, 5]); 90 | }); 91 | }); 92 | 93 | describe("filter()", () => { 94 | it("should remove elements not matching the filter", () => { 95 | const result = chainFrom([1, 2, 3, 4, 5]) 96 | .filter(n => n % 2 === 0) 97 | .toArray(); 98 | expect(result).toEqual([2, 4]); 99 | }); 100 | }); 101 | 102 | describe("flatMap()", () => { 103 | it("should map then concatenate elements", () => { 104 | const result = chainFrom(["a", "bb", "ccc"]) 105 | .flatMap(s => s.split("")) 106 | .toArray(); 107 | expect(result).toEqual(["a", "b", "b", "c", "c", "c"]); 108 | }); 109 | 110 | it("should work when mapping to iterables", () => { 111 | const result = chainFrom(["a", "bb", "ccc"]) 112 | .flatMap(s => range(s.length)) 113 | .toArray(); 114 | expect(result).toEqual([0, 0, 1, 0, 1, 2]); 115 | }); 116 | 117 | it("should work when mapping to strings", () => { 118 | const result = chainFrom(["a", "bb", "ccc"]) 119 | .flatMap(s => s) 120 | .toArray(); 121 | expect(result).toEqual(["a", "b", "b", "c", "c", "c"]); 122 | }); 123 | 124 | it("should consume iterators only as much as necessary", () => { 125 | const iterators = [range(3), range(3), range(3)].map( 126 | getIterableIterator, 127 | ); 128 | const result = chainFrom([0, 1, 2]) 129 | .flatMap(n => iterators[n]) 130 | .take(5) 131 | .toArray(); 132 | expect(result).toEqual([0, 1, 2, 0, 1]); 133 | expect(iterators[0].next().done).toEqual(true); 134 | expect(iterators[1].next().value).toEqual(2); 135 | expect(iterators[2].next().value).toEqual(0); 136 | }); 137 | }); 138 | 139 | describe("flatten()", () => { 140 | it("should concatenate arrays", () => { 141 | const result = chainFrom([["a", "b"], ["c", "d"], [], ["e"]]) 142 | .flatten() 143 | .toArray(); 144 | expect(result).toEqual(["a", "b", "c", "d", "e"]); 145 | }); 146 | 147 | it("should concatenate iterables", () => { 148 | const result = chainFrom([range(2), range(2, 4), range(4, 5)]) 149 | .flatten() 150 | .toArray(); 151 | expect(result).toEqual([0, 1, 2, 3, 4]); 152 | }); 153 | 154 | it("should consume iterators only as much as necessary", () => { 155 | const iterators = [range(3), range(3), range(3)].map( 156 | getIterableIterator, 157 | ); 158 | const result = chainFrom(iterators) 159 | .flatten() 160 | .take(5) 161 | .toArray(); 162 | expect(result).toEqual([0, 1, 2, 0, 1]); 163 | expect(iterators[0].next().done).toEqual(true); 164 | expect(iterators[1].next().value).toEqual(2); 165 | expect(iterators[2].next().value).toEqual(0); 166 | }); 167 | }); 168 | 169 | describe("interpose()", () => { 170 | it("should insert the separator between elements", () => { 171 | const result = chainFrom([1, 2, 3]) 172 | .interpose(0) 173 | .toArray(); 174 | expect(result).toEqual([1, 0, 2, 0, 3]); 175 | }); 176 | }); 177 | 178 | describe("map()", () => { 179 | it("should map over elements", () => { 180 | const result = chainFrom(["a", "bb", "ccc"]) 181 | .map(s => s.length) 182 | .toArray(); 183 | expect(result).toEqual([1, 2, 3]); 184 | }); 185 | }); 186 | 187 | describe("mapIndexed()", () => { 188 | it("should map over elements with their indices", () => { 189 | const result = chainFrom([10, 10, 10]) 190 | .mapIndexed((x, i) => x * i) 191 | .toArray(); 192 | expect(result).toEqual([0, 10, 20]); 193 | }); 194 | }); 195 | 196 | describe("partitionAll()", () => { 197 | it("should group elements by the specified size", () => { 198 | const result = chainFrom([1, 2, 3, 4, 5]) 199 | .partitionAll(2) 200 | .toArray(); 201 | expect(result).toEqual([[1, 2], [3, 4], [5]]); 202 | }); 203 | 204 | it("should throw if n is 0", () => { 205 | expect(() => chainFrom([1, 2, 3, 4, 5]).partitionAll(0)).toThrowError( 206 | /0/, 207 | ); 208 | }); 209 | 210 | it("should throw if n is negative", () => { 211 | expect(() => chainFrom([1, 2, 3, 4, 5]).partitionAll(-2)).toThrowError( 212 | /negative/, 213 | ); 214 | }); 215 | }); 216 | 217 | describe("partitionBy()", () => { 218 | it("should group elements with the same function value", () => { 219 | const result = chainFrom(["a", "b", "cc", "dd", "e"]) 220 | .partitionBy(s => s.length) 221 | .toArray(); 222 | expect(result).toEqual([["a", "b"], ["cc", "dd"], ["e"]]); 223 | }); 224 | }); 225 | 226 | describe("remove()", () => { 227 | it("should remove elements matching the filter", () => { 228 | const result = chainFrom([1, 2, 3, 4, 5]) 229 | .remove(n => n % 2 === 0) 230 | .toArray(); 231 | expect(result).toEqual([1, 3, 5]); 232 | }); 233 | }); 234 | 235 | describe("removeAbsent()", () => { 236 | it("should remove null and undefined elements", () => { 237 | const result = chainFrom([1, null, 2, undefined, 3]) 238 | .removeAbsent() 239 | .toArray(); 240 | expect(result).toEqual([1, 2, 3]); 241 | }); 242 | 243 | it("should preserve other falsy elements", () => { 244 | const result = chainFrom([false, null, 0, undefined, "", NaN]) 245 | .removeAbsent() 246 | .toArray(); 247 | expect(result).toEqual([false, 0, "", NaN]); 248 | }); 249 | }); 250 | 251 | describe("take()", () => { 252 | it("should take the first n elements", () => { 253 | const result = chainFrom([1, 2, 3, 4, 5]) 254 | .take(3) 255 | .toArray(); 256 | expect(result).toEqual([1, 2, 3]); 257 | }); 258 | 259 | it("should terminate after pulling n elements", () => { 260 | const iterator = getIterableIterator(range(1, 5)); 261 | const result = chainFrom(iterator) 262 | .take(2) 263 | .toArray(); 264 | expect(result).toEqual([1, 2]); 265 | expect(iterator.next().value).toEqual(3); 266 | }); 267 | 268 | it("should take all elements if n is greater than length", () => { 269 | const result = chainFrom([1, 2, 3, 4, 5]) 270 | .take(7) 271 | .toArray(); 272 | expect(result).toEqual([1, 2, 3, 4, 5]); 273 | }); 274 | 275 | it("should return empty if n is 0", () => { 276 | const result = chainFrom([1, 2, 3, 4, 5]) 277 | .take(0) 278 | .toArray(); 279 | expect(result).toEqual([]); 280 | }); 281 | 282 | it("should return empty if n is negative", () => { 283 | const result = chainFrom([1, 2, 3, 4, 5]) 284 | .take(-2) 285 | .toArray(); 286 | expect(result).toEqual([]); 287 | }); 288 | }); 289 | 290 | describe("takeNth()", () => { 291 | it("should take every nth element", () => { 292 | const result = chainFrom([1, 2, 3, 4, 5]) 293 | .takeNth(2) 294 | .toArray(); 295 | expect(result).toEqual([1, 3, 5]); 296 | }); 297 | 298 | it("should throw if n is 0", () => { 299 | expect(() => chainFrom([1, 2, 3, 4, 5]).takeNth(0)).toThrow(/0/); 300 | }); 301 | 302 | it("should throw if n is negative", () => { 303 | expect(() => chainFrom([1, 2, 3, 4, 5]).takeNth(-2)).toThrow( 304 | /negative/, 305 | ); 306 | }); 307 | }); 308 | 309 | describe("takeWhile()", () => { 310 | it("should take elements until the predicate fails", () => { 311 | const result = chainFrom([1, 2, 3, 4, 5]) 312 | .takeWhile(n => n < 3) 313 | .toArray(); 314 | expect(result).toEqual([1, 2]); 315 | }); 316 | 317 | it("should terminate after the predicate fails", () => { 318 | const iterator = getIterableIterator(range(1, 5)); 319 | const result = chainFrom(iterator) 320 | .takeWhile(n => n < 3) 321 | .toArray(); 322 | expect(result).toEqual([1, 2]); 323 | expect(iterator.next().value).toEqual(4); 324 | }); 325 | }); 326 | 327 | // ----- Reductions ----- 328 | 329 | describe("reduce()", () => { 330 | const aPush = (array: T[], x: T): T[] => { 331 | array.push(x); 332 | return array; 333 | }; 334 | 335 | const transformer: Transformer = { 336 | ["@@transducer/init"]: () => [], 337 | ["@@transducer/result"]: x => x, 338 | ["@@transducer/step"]: aPush, 339 | }; 340 | 341 | it("should use a reducer and initial value", () => { 342 | const result = chainFrom([1, 2, 3]) 343 | .map(n => 2 * n) 344 | .reduce(aPush, []); 345 | expect(result).toEqual([2, 4, 6]); 346 | }); 347 | 348 | it("should use a transformer and no initial value", () => { 349 | const result = chainFrom([1, 2, 3]) 350 | .map(n => 2 * n) 351 | .reduce(transformer); 352 | expect(result).toEqual([2, 4, 6]); 353 | }); 354 | }); 355 | 356 | describe("count()", () => { 357 | it("should return the number of elements", () => { 358 | const result = chainFrom([1, 2, 3, 4, 5]) 359 | .filter(n => n < 3) 360 | .count(); 361 | expect(result).toEqual(2); 362 | }); 363 | }); 364 | 365 | describe("every()", () => { 366 | it("should return true if all elements match the predicate", () => { 367 | const result = chainFrom([1, 2, 3, 4, 5]) 368 | .map(n => 10 * n) 369 | .every(n => n > 3); 370 | expect(result).toEqual(true); 371 | }); 372 | 373 | it("should return false if any element fails the predicate", () => { 374 | const result = chainFrom([1, 2, 3, 4, 5]) 375 | .map(n => 10 * n) 376 | .every(n => n < 30); 377 | expect(result).toEqual(false); 378 | }); 379 | 380 | it("should short-circuit if a failure is found", () => { 381 | const iterator = getIterableIterator(range(1, 5)); 382 | const result = chainFrom(iterator) 383 | .map(n => 10 * n) 384 | .every(n => n < 30); 385 | expect(result).toEqual(false); 386 | expect(iterator.next().value).toEqual(4); 387 | }); 388 | }); 389 | 390 | describe("find()", () => { 391 | const input = [1, 2, 3, 4, 5]; 392 | 393 | it("should return the first element matching the predicate", () => { 394 | const result = chainFrom(input).find(x => x > 2); 395 | expect(result).toEqual(3); 396 | }); 397 | 398 | it("should return null if there are no matching elements", () => { 399 | const result = chainFrom(input) 400 | .map(x => x * 2) 401 | .find(x => x % 2 === 1); 402 | expect(result).toBeNull(); 403 | }); 404 | 405 | it("should terminate computation upon finding a match", () => { 406 | const iterator = getIterableIterator(range(1, 5)); 407 | const result = chainFrom(iterator) 408 | .map(x => 10 * x) 409 | .find(x => x === 20); 410 | expect(result).toEqual(20); 411 | expect(iterator.next().value).toEqual(3); 412 | }); 413 | }); 414 | 415 | describe("first()", () => { 416 | const input = [1, 2, 3, 4, 5]; 417 | 418 | it("should return the first element if it exists", () => { 419 | const result = chainFrom(input) 420 | .map(x => 2 * x) 421 | .drop(2) 422 | .first(); 423 | expect(result).toEqual(6); 424 | }); 425 | 426 | it("should return null if there are no elements", () => { 427 | const result = chainFrom(input) 428 | .filter(n => n > 10) 429 | .first(); 430 | expect(result).toBeNull(); 431 | }); 432 | 433 | it("should terminate computation", () => { 434 | const iterator = getIterableIterator(range(1, 5)); 435 | const result = chainFrom(iterator) 436 | .map(x => 10 * x) 437 | .first(); 438 | expect(result).toEqual(10); 439 | expect(iterator.next().value).toEqual(2); 440 | }); 441 | }); 442 | 443 | describe("forEach()", () => { 444 | it("should call the provided function on each input", () => { 445 | const input = ["a", "bb", "ccc"]; 446 | const result: number[] = []; 447 | chainFrom(input) 448 | .map(s => s.length) 449 | .forEach(n => result.push(n)); 450 | expect(result).toEqual([1, 2, 3]); 451 | }); 452 | }); 453 | 454 | describe("isEmpty()", () => { 455 | it("should return true if there are no elements", () => { 456 | const result = chainFrom([1, 2, 3, 4, 5]) 457 | .filter(n => n > 10) 458 | .isEmpty(); 459 | expect(result).toEqual(true); 460 | }); 461 | 462 | it("should return false if there are any elements", () => { 463 | const result = chainFrom([1, 2, 3, 4, 5]) 464 | .filter(n => n % 2 === 0) 465 | .isEmpty(); 466 | expect(result).toEqual(false); 467 | }); 468 | 469 | it("should terminate after one element", () => { 470 | const iterator = getIterableIterator(range(1, 5)); 471 | const result = chainFrom(iterator) 472 | .map(n => 10 * n) 473 | .isEmpty(); 474 | expect(result).toEqual(false); 475 | expect(iterator.next().value).toEqual(2); 476 | }); 477 | }); 478 | 479 | describe("joinToString()", () => { 480 | it("should concatenate the elements into a string with the separator", () => { 481 | const result = chainFrom([1, 2, 3, 4, 5]) 482 | .filter(n => n % 2 === 1) 483 | .joinToString(" -> "); 484 | expect(result).toEqual("1 -> 3 -> 5"); 485 | }); 486 | 487 | it("should work if the separator is the empty string", () => { 488 | const result = chainFrom([1, 2, 3, 4, 5]) 489 | .filter(n => n % 2 === 1) 490 | .joinToString(""); 491 | expect(result).toEqual("135"); 492 | }); 493 | }); 494 | 495 | describe("some()", () => { 496 | it("should return true if any element matches the predicate", () => { 497 | const result = chainFrom([1, 2, 3, 4, 5]) 498 | .map(n => 10 * n) 499 | .some(n => n === 30); 500 | expect(result).toEqual(true); 501 | }); 502 | 503 | it("should return false if no element matches the predicate", () => { 504 | const result = chainFrom([1, 2, 3, 4, 5]) 505 | .map(n => 10 * n) 506 | .some(n => n === 1); 507 | expect(result).toEqual(false); 508 | }); 509 | 510 | it("should short-circuit if a match is found", () => { 511 | const iterator = getIterableIterator(range(1, 5)); 512 | const result = chainFrom(iterator) 513 | .map(n => 10 * n) 514 | .some(n => n === 30); 515 | expect(result).toEqual(true); 516 | expect(iterator.next().value).toEqual(4); 517 | }); 518 | }); 519 | 520 | describe("toArray()", () => { 521 | const input = ["a", "bb", "ccc"]; 522 | 523 | it("should return an input array if no transforms", () => { 524 | const result = chainFrom(input).toArray(); 525 | expect(result).toEqual(input); 526 | }); 527 | 528 | it("should convert iterable input to an array", () => { 529 | const iterator = input[Symbol.iterator](); 530 | const result = chainFrom(iterator).toArray(); 531 | expect(result).toEqual(input); 532 | }); 533 | }); 534 | 535 | describe("toMap()", () => { 536 | it("should make a map using the provided functions", () => { 537 | const input: Array<[boolean, number]> = [ 538 | [false, 0], 539 | [true, 1], 540 | ]; 541 | const result = chainFrom(input).toMap( 542 | x => x[0], 543 | x => x[1], 544 | ); 545 | expect(result).toEqual(new Map(input)); 546 | }); 547 | 548 | it("should replace earlier values with later ones at the same key", () => { 549 | const input: Array<[string, number]> = [ 550 | ["a", 1], 551 | ["b", 1], 552 | ["a", 2], 553 | ]; 554 | const result = chainFrom(input).toMap( 555 | x => x[0], 556 | x => x[1], 557 | ); 558 | expect(result).toEqual( 559 | new Map([ 560 | ["a", 2], 561 | ["b", 1], 562 | ]), 563 | ); 564 | }); 565 | }); 566 | 567 | describe("toMapGroupBy()", () => { 568 | it("should group into arrays by default", () => { 569 | const input = ["a", "b", "aa", "aaa", "bc"]; 570 | const result = chainFrom(input).toMapGroupBy(s => s[0]); 571 | expect(result).toEqual( 572 | new Map([ 573 | ["a", ["a", "aa", "aaa"]], 574 | ["b", ["b", "bc"]], 575 | ]), 576 | ); 577 | }); 578 | 579 | it("should use the provided transformer", () => { 580 | const input = ["a", "b", "aa", "aaa", "bc"]; 581 | const result = chainFrom(input).toMapGroupBy(s => s[0], count()); 582 | expect(result).toEqual( 583 | new Map([ 584 | ["a", 3], 585 | ["b", 2], 586 | ]), 587 | ); 588 | }); 589 | 590 | it("should respect when provided transformer returns reduced", () => { 591 | const input = ["a", "b", "aa", "aaa", "bc"]; 592 | const firstTransformer = first(); 593 | const stepSpy = jest.spyOn(firstTransformer, "@@transducer/step"); 594 | try { 595 | const result = chainFrom(input).toMapGroupBy( 596 | s => s[0], 597 | firstTransformer, 598 | ); 599 | expect(result).toEqual( 600 | new Map([ 601 | ["a", "a"], 602 | ["b", "b"], 603 | ]), 604 | ); 605 | expect(stepSpy).toHaveBeenCalledTimes(2); 606 | } finally { 607 | stepSpy.mockRestore(); 608 | } 609 | }); 610 | }); 611 | 612 | describe("toObject()", () => { 613 | it("should make an object using the provided functions", () => { 614 | const input = ["a", "bb", "ccc"]; 615 | const result = chainFrom(input).toObject( 616 | s => s, 617 | s => s.length, 618 | ); 619 | expect(result).toEqual({ a: 1, bb: 2, ccc: 3 }); 620 | }); 621 | 622 | it("should replace earlier values with later ones at the same key", () => { 623 | const input: Array<[string, number]> = [ 624 | ["a", 1], 625 | ["b", 1], 626 | ["a", 2], 627 | ]; 628 | const result = chainFrom(input).toObject( 629 | x => x[0], 630 | x => x[1], 631 | ); 632 | expect(result).toEqual({ a: 2, b: 1 }); 633 | }); 634 | }); 635 | 636 | describe("toObjectGroupBy()", () => { 637 | it("should group into arrays by default", () => { 638 | const input = ["a", "b", "aa", "aaa", "bc"]; 639 | const result = chainFrom(input).toObjectGroupBy(s => s[0]); 640 | expect(result).toEqual({ a: ["a", "aa", "aaa"], b: ["b", "bc"] }); 641 | }); 642 | 643 | it("should use the provided transformer", () => { 644 | const input = ["a", "b", "aa", "aaa", "bc"]; 645 | const result = chainFrom(input).toObjectGroupBy(s => s[0], count()); 646 | expect(result).toEqual({ a: 3, b: 2 }); 647 | }); 648 | 649 | it("should respect when provided transformer returns reduced", () => { 650 | const input = ["a", "b", "aa", "aaa", "bc"]; 651 | const firstTransformer = first(); 652 | const stepSpy = jest.spyOn(firstTransformer, "@@transducer/step"); 653 | try { 654 | const result = chainFrom(input).toObjectGroupBy( 655 | s => s[0], 656 | firstTransformer, 657 | ); 658 | expect(result).toEqual({ a: "a", b: "b" }); 659 | expect(stepSpy).toHaveBeenCalledTimes(2); 660 | } finally { 661 | stepSpy.mockRestore(); 662 | } 663 | }); 664 | }); 665 | 666 | describe("toSet()", () => { 667 | it("should produce a set", () => { 668 | const result = chainFrom([0, 1, 3]) 669 | .map(n => n % 3) 670 | .toSet(); 671 | expect(result).toEqual(new Set([0, 1])); 672 | }); 673 | }); 674 | 675 | describe("toIterator()", () => { 676 | it("should return an iterable whose @@iterator is itself", () => { 677 | const iterator = chainFrom([1, 2, 3]) 678 | .map(n => 2 * n) 679 | .toIterator(); 680 | expect(iterator[Symbol.iterator]()).toBe(iterator); 681 | }); 682 | 683 | it("should return an iterator of the elements", () => { 684 | const iterator = chainFrom([1, 2, 3]) 685 | .map(n => 2 * n) 686 | .toIterator(); 687 | const result = Array.from(iterator); 688 | expect(result).toEqual([2, 4, 6]); 689 | }); 690 | 691 | it("should respect early termination", () => { 692 | const iterator = getIterableIterator(range(1, 5)); 693 | const truncatedIterator = chainFrom(iterator) 694 | .take(2) 695 | .toIterator(); 696 | const result = Array.from(truncatedIterator); 697 | expect(result).toEqual([1, 2]); 698 | expect(iterator.next().value).toEqual(3); 699 | }); 700 | 701 | it("should work with flatMap()", () => { 702 | // This tests that the iterator works with transducers that produce 703 | // multiple outputs for one input. 704 | const iterator = chainFrom(["a", "bb", "ccc"]) 705 | .flatMap(s => s.split("")) 706 | .toIterator(); 707 | const result = Array.from(iterator); 708 | expect(result).toEqual(["a", "b", "b", "c", "c", "c"]); 709 | }); 710 | 711 | it("should work when iterating strings", () => { 712 | const iterator = chainFrom("hello") 713 | .filter(c => c !== "l") 714 | .toIterator(); 715 | const result = Array.from(iterator); 716 | expect(result).toEqual(["h", "e", "o"]); 717 | }); 718 | }); 719 | 720 | describe("average()", () => { 721 | it("should average the elements", () => { 722 | const result = chainFrom([1, 2, 3, 4, 5]).average(); 723 | expect(result).toEqual(3); 724 | }); 725 | 726 | it("should return null on empty input", () => { 727 | const input: number[] = []; 728 | const result = chainFrom(input).average(); 729 | expect(result).toBeNull(); 730 | }); 731 | }); 732 | 733 | describe("max()", () => { 734 | it("should take the max of numbers", () => { 735 | const result = chainFrom([3, 4, 5, 1, 2]).max(); 736 | expect(result).toEqual(5); 737 | }); 738 | 739 | it("should return null on empty input", () => { 740 | const input: number[] = []; 741 | const result = chainFrom(input).max(); 742 | expect(result).toBeNull(); 743 | }); 744 | 745 | it("should use the comparator if provided", () => { 746 | const input: Array<[string, number]> = [ 747 | ["a", 2], 748 | ["b", 1], 749 | ["c", 3], 750 | ]; 751 | const result = chainFrom(input).max((a, b) => (a[1] < b[1] ? -1 : 1)); 752 | expect(result).toEqual(["c", 3]); 753 | }); 754 | }); 755 | 756 | describe("min()", () => { 757 | it("should take the min of numbers", () => { 758 | const result = chainFrom([3, 4, 5, 1, 2]).min(); 759 | expect(result).toEqual(1); 760 | }); 761 | 762 | it("should return null on empty input", () => { 763 | const input: number[] = []; 764 | const result = chainFrom(input).min(); 765 | expect(result).toBeNull(); 766 | }); 767 | 768 | it("should use the comparator if provided", () => { 769 | const input: Array<[string, number]> = [ 770 | ["a", 2], 771 | ["b", 1], 772 | ["c", 3], 773 | ]; 774 | const result = chainFrom(input).min((a, b) => (a[1] < b[1] ? -1 : 1)); 775 | expect(result).toEqual(["b", 1]); 776 | }); 777 | }); 778 | 779 | describe("sum()", () => { 780 | it("should sum the elements", () => { 781 | const result = chainFrom([1, 2, 3, 4, 5]).sum(); 782 | expect(result).toEqual(15); 783 | }); 784 | 785 | it("should return 0 on empty input", () => { 786 | const input: number[] = []; 787 | const result = chainFrom(input).sum(); 788 | expect(result).toEqual(0); 789 | }); 790 | }); 791 | 792 | describe("transducer builder", () => { 793 | it("should return the identity if no transforms provided", () => { 794 | const transducer = transducerBuilder().build(); 795 | const result = chainFrom([1, 2, 3]) 796 | .compose(transducer) 797 | .toArray(); 798 | expect(result).toEqual([1, 2, 3]); 799 | }); 800 | 801 | it("should return the composition of the specified operations", () => { 802 | const transducer = transducerBuilder() 803 | .map(x => x + 1) 804 | .filter(x => x % 2 === 0) 805 | .build(); 806 | const result = chainFrom([1, 2, 3, 4, 5]) 807 | .compose(transducer) 808 | .toArray(); 809 | expect(result).toEqual([2, 4, 6]); 810 | }); 811 | }); 812 | 813 | // ----- Iterables ----- 814 | 815 | describe("range()", () => { 816 | it("should iterate from 0 to end with single argument", () => { 817 | expect(Array.from(range(5))).toEqual([0, 1, 2, 3, 4]); 818 | }); 819 | 820 | it("should iterate from start to end with two arguments", () => { 821 | expect(Array.from(range(2, 5))).toEqual([2, 3, 4]); 822 | }); 823 | 824 | it("should iterate in steps with three arguments", () => { 825 | expect(Array.from(range(2, 7, 2))).toEqual([2, 4, 6]); 826 | }); 827 | 828 | it("should iterate backwards if the step is negative", () => { 829 | expect(Array.from(range(7, 2, -2))).toEqual([7, 5, 3]); 830 | }); 831 | 832 | it("should be empty if start is at least end and step is positive", () => { 833 | expect(Array.from(range(2, 2))).toEqual([]); 834 | expect(Array.from(range(3, 2))).toEqual([]); 835 | }); 836 | 837 | it("should be empty if start is at most end and step is negative", () => { 838 | expect(Array.from(range(2, 2, -1))).toEqual([]); 839 | expect(Array.from(range(2, 3, -1))).toEqual([]); 840 | }); 841 | 842 | it("should throw if step is 0", () => { 843 | expect(() => range(1, 5, 0)).toThrowError(/0/); 844 | }); 845 | }); 846 | 847 | describe("repeat()", () => { 848 | it("should repeat the input indefinitely with one argument", () => { 849 | expect( 850 | chainFrom(repeat("x")) 851 | .take(3) 852 | .toArray(), 853 | ).toEqual(["x", "x", "x"]); 854 | }); 855 | 856 | it("should repeat the value count times with two arguments", () => { 857 | expect(Array.from(repeat("x", 3))).toEqual(["x", "x", "x"]); 858 | }); 859 | 860 | it("should produce an empty iterator if count is 0", () => { 861 | expect(Array.from(repeat("x", 0))).toEqual([]); 862 | }); 863 | 864 | it("should throw an error if count is negative", () => { 865 | expect(() => repeat("x", -1)).toThrowError(/negative/); 866 | }); 867 | }); 868 | 869 | describe("iterate()", () => { 870 | it("should generate a sequence by repeatedly calling the function", () => { 871 | expect( 872 | chainFrom(iterate(1, x => x * 2)) 873 | .take(5) 874 | .toArray(), 875 | ).toEqual([1, 2, 4, 8, 16]); 876 | }); 877 | }); 878 | 879 | describe("cycle()", () => { 880 | it("should repeat an iterable indefinitely", () => { 881 | expect( 882 | chainFrom(cycle(["a", "b", "c"])) 883 | .take(7) 884 | .toArray(), 885 | ).toEqual(["a", "b", "c", "a", "b", "c", "a"]); 886 | }); 887 | 888 | it("should produce an empty iterator if input is empty", () => { 889 | expect(Array.from(cycle([]))).toEqual([]); 890 | }); 891 | }); 892 | 893 | function getIterableIterator(iterable: Iterable): IterableIterator { 894 | return iterable[Symbol.iterator]() as IterableIterator; 895 | } 896 | --------------------------------------------------------------------------------