From 5d66f45cb8a265cadd540631df831b9ad3f8d092 Mon Sep 17 00:00:00 2001 From: Jason Dent Date: Sat, 14 Dec 2019 19:29:31 +0100 Subject: [PATCH] Move Sequence Implimention into a class (#12) --- .vscode/launch.json | 55 ++-- package.json | 2 +- src/GenSequence.perf.ts | 37 ++- src/GenSequence.test.ts | 2 +- src/GenSequence.ts | 138 +--------- src/ImplSequence.ts | 126 +++++++++ src/index.ts | 2 +- src/operators/index.ts | 1 + src/operators/operators.ts | 253 +++++------------- ...perators.test.ts => operatorsBase.test.ts} | 4 +- src/operators/operatorsBase.ts | 223 +++++++++++++++ src/operators/types.ts | 5 - src/util/types.ts | 49 ++++ src/{operators => util}/util.ts | 4 + 14 files changed, 518 insertions(+), 383 deletions(-) create mode 100644 src/ImplSequence.ts create mode 100644 src/operators/index.ts rename src/operators/{operators.test.ts => operatorsBase.test.ts} (95%) create mode 100644 src/operators/operatorsBase.ts delete mode 100644 src/operators/types.ts create mode 100644 src/util/types.ts rename src/{operators => util}/util.ts (82%) diff --git a/.vscode/launch.json b/.vscode/launch.json index c719551..d8dbaa5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,44 +4,31 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { + { "type": "node", "request": "launch", - "name": "Launch Program", - "program": "${workspaceRoot}/lib/index.js", - "cwd": "${workspaceRoot}", - "outDir": "${workspaceRoot}/lib", - "sourceMaps": true + "name": "Jest current-file", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand", "${file}"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/.bin/jest", + } }, { "type": "node", - "request": "attach", - "name": "Attach to Process", - "port": 5858, - "outDir": "${workspaceRoot}/lib", - "sourceMaps": true - }, - { - // Name of configuration; appears in the launch configuration drop down menu. - "name": "Run mocha", - // Type of configuration. Possible values: "node", "mono". - "type": "node2", - // Request type "launch" or "attach" "request": "launch", - // Workspace relative or absolute path to the program. - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - // Automatically stop program after launch. - "stopOnEntry": false, - // Command line arguments passed to the program. - "args": ["--recursive", "lib/**/*.test.js"], - // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. - "cwd": "${workspaceRoot}", - // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. - "runtimeExecutable": null, - "outDir": "${workspaceRoot}/lib", - "sourceMaps": true, - // Environment variables passed to the program. - "env": { "NODE_ENV": "test"} - } + "name": "Jest All", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": ["--runInBand"], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/.bin/jest", + } + } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 2248a3d..db3b869 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "clean-build": "npm run clean && npm run build", "clean": "rimraf dist", "test": "jest src/**/*.test.ts src/*.test.ts", - "perf": "jest src/**/*.perf.ts src/*.perf.ts", + "perf": "jest src/*.perf.ts", "build": "tsc -p .", "watch": "tsc -w -p .", "coverage": "npm test -- --coverage", diff --git a/src/GenSequence.perf.ts b/src/GenSequence.perf.ts index 05fa00a..0065b1b 100644 --- a/src/GenSequence.perf.ts +++ b/src/GenSequence.perf.ts @@ -12,11 +12,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 100); const rExp = measure(fnExp, 100); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toEqual(rBase.result); - console.log('Simple Generator to an array' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(1.2); + assertExpectedRatio('Simple Generator to an array', rBase, rExp, 1.2); }); test('filter filter reduce', () => { @@ -36,11 +34,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 10); const rExp = measure(fnExp, 10); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toBe(rBase.result); - console.log('filter filter reduce' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(1.4); + assertExpectedRatio('filter filter reduce', rBase, rExp, 1.4); }); test('filter slice filter reduce', () => { @@ -64,11 +60,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 10); const rExp = measure(fnExp, 10); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toBe(rBase.result); - console.log('filter slice filter reduce' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(1); + assertExpectedRatio('filter slice filter reduce', rBase, rExp, 1); }); test('filter slice filter reduce (1000)', () => { @@ -92,11 +86,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 1000); const rExp = measure(fnExp, 1000); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toBe(rBase.result); - console.log('filter slice filter reduce (1000)' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(2); + assertExpectedRatio('filter slice filter reduce (1000)', rBase, rExp, 2, 3); }); test('filter slice filter first (1000)', () => { @@ -122,11 +114,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 1000); const rExp = measure(fnExp, 1000); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toBe(rBase.result); - console.log('filter slice filter first (1000)' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(3); + assertExpectedRatio('filter slice filter first (1000)', rBase, rExp, 0.5); }); test('concatMap', () => { @@ -156,11 +146,9 @@ describe('Performance Test', () => { } const rBase = measure(fnBase, 100); const rExp = measure(fnExp, 100); - const ratio = rExp.avg / rBase.avg; expect(rExp.result).toBe(rBase.result); - console.log('concatMap' + compareMeasurementsToString(rBase, rExp)); - expect(ratio).toBeLessThan(2); + assertExpectedRatio('concatMap', rBase, rExp, 1); }); }); @@ -209,19 +197,26 @@ function toMs(diff: [number, number]) { return diff[0] * 1e3 + diff[1] / 1e6; } -function compareMeasurementsToString(base: Measurement, comp: Measurement): string { +function assertExpectedRatio(testName: string, base: Measurement, comp: Measurement, expectedRatio: number, failRatio?: number) { + console.log(testName + compareMeasurementsToString(base, comp, expectedRatio)); + const ratio = comp.avg / base.avg; + expect(ratio).toBeLessThan(failRatio ?? expectedRatio); +} + +function compareMeasurementsToString(base: Measurement, comp: Measurement, expectedRatio: number): string { function fix(n: number | undefined) { if (n === undefined) { return '-'; } return n.toFixed(3); } + const ratio = (comp.avg || 0) / (base.avg || 1); return ` -\tbase\tcomp +\t\tbase\tcomp avg:\t${fix(base.avg)}\t${fix(comp.avg)} min:\t${fix(base.min)}\t${fix(comp.min)} max:\t${fix(base.max)}\t${fix(comp.max)} cnt:\t${base.count}\t${comp.count} -ratio:\t${fix((comp.avg || 0) / (base.avg || 1))} +ratio:\t${fix(expectedRatio)}\t${fix(ratio)}\t${ratio <= expectedRatio ? 'PASS' : 'FAIL'} `; } diff --git a/src/GenSequence.test.ts b/src/GenSequence.test.ts index 9b08810..ee6b9c2 100644 --- a/src/GenSequence.test.ts +++ b/src/GenSequence.test.ts @@ -1,5 +1,5 @@ import { genSequence, sequenceFromObject, sequenceFromRegExpMatch, objectToSequence, Sequence } from './GenSequence'; -import * as op from './operators/operators'; +import * as op from './operators/operatorsBase'; describe('GenSequence Tests', () => { diff --git a/src/GenSequence.ts b/src/GenSequence.ts index a1102eb..56ab950 100644 --- a/src/GenSequence.ts +++ b/src/GenSequence.ts @@ -1,49 +1,9 @@ -import { IterableLike, Maybe } from './operators/types'; -import { filter, skip, take, concat, concatMap, combine, map, scan, all, any, count, first, forEach, max, min, reduce } from './operators/operators'; +import { Sequence, GenIterable } from './util/types'; +import { toIterableIterator } from './util/util'; +import { ImplSequence } from './ImplSequence'; -export interface Sequence extends IterableLike { - next(): IteratorResult; - - //// Filters - /** keep values where the fnFilter(t) returns true */ - filter(fnFilter: (t: T) => boolean): Sequence; - skip(n: number): Sequence; - take(n: number): Sequence; - - //// Extenders - concat(j: Iterable): Sequence; - concatMap(fn: (t: T) => Iterable): Sequence; - - //// Mappers - combine(fn: (t: T, u?: U) => V, j: Iterable): Sequence; - /** map values from type T to type U */ - map(fnMap: (t: T) => U): Sequence; - scan(fnReduce: (previousValue: T, currentValue: T, currentIndex: number) => T, initialValue?: T): Sequence; - scan(fnReduce: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): Sequence; - - //// Reducers - all(fnFilter: (t: T) => boolean): boolean; - any(fnFilter: (t: T) => boolean): boolean; - count(): number; - first(fnFilter?: (t: T) => boolean, defaultValue?: T): Maybe; - first(fnFilter: (t: T) => boolean, defaultValue: T): T; - forEach(fn: (t: T, index: number) => void): void; - max(fnSelector?: (t: T) => T): Maybe; - max(fnSelector: (t: T) => U): Maybe; - min(fnSelector?: (t: T) => T): Maybe; - min(fnSelector: (t: T) => U): Maybe; - /** reduce function see Array.reduce */ - reduce(fnReduce: (previousValue: T, currentValue: T, currentIndex: number) => T): Maybe; - reduce(fnReduce: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): U; - reduceToSequence>(fnReduce: (previousValue: V, currentValue: T, currentIndex: number) => V, initialValue: V): Sequence; - reduceToSequence(fnReduce: (previousValue: GenIterable, currentValue: T, currentIndex: number) => GenIterable, initialValue: GenIterable): Sequence; - - //// Cast - toArray(): T[]; - toIterable(): IterableIterator; -} - -export interface GenIterable extends IterableLike {} +export { Sequence, GenIterable } from './util/types'; +export { toIterableIterator } from './util/util'; export interface SequenceCreator { (i: GenIterable): Sequence; @@ -53,88 +13,7 @@ export interface SequenceCreator { export function genSequence(i: () => GenIterable): Sequence; export function genSequence(i: GenIterable): Sequence; export function genSequence(i: (() => GenIterable) | GenIterable): Sequence { - const createIterable: () => GenIterable = (typeof i === "function") ? i : () => i; - - function fnNext() { - let iter: Maybe>; - return () => { - if(!iter) { - iter = createIterable()[Symbol.iterator](); - } - return iter.next(); - }; - } - - const seq = { - [Symbol.iterator]: () => createIterable()[Symbol.iterator](), - next: fnNext(), // late binding is intentional here. - - //// Filters - filter: (fnFilter: (t: T) => boolean) => genSequence(() => filter(fnFilter, createIterable())), - skip: (n: number) => { - return genSequence(() => skip(n, createIterable())); - }, - take: (n: number) => { - return genSequence(() => take(n, createIterable())); - }, - - //// Extenders - concat: (j: Iterable) => { - return genSequence(() => concat(createIterable(), j)); - }, - concatMap: (fn: (t: T) => Iterable) => { - return genSequence(() => concatMap(fn, createIterable())); - }, - - //// Mappers - combine: (fn: (t: T, u?: U) => V, j: Iterable) => { - return genSequence(() => combine(fn, createIterable(), j)); - }, - map: (fn: (t: T) => U) => genSequence(() => map(fn, createIterable())), - scan: (fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue: U) => { - return genSequence(() => scan(createIterable(), fnReduce, initValue)); - }, - - // Reducers - all: (fnFilter: (t: T) => boolean): boolean => { - return all(fnFilter, createIterable()); - }, - any: (fnFilter: (t: T) => boolean): boolean => { - return any(fnFilter, createIterable()); - }, - count: (): number => { - return count(createIterable()); - }, - first: (fnFilter: (t: T) => boolean, defaultValue: T): T => { - return first(fnFilter, defaultValue, createIterable()) as T; - }, - forEach: (fn: (t: T, index: number) => void): void => { - return forEach(fn, createIterable()); - }, - max: (fnSelector: (t: T) => U): Maybe => { - return max(fnSelector, createIterable()); - }, - min: (fnSelector: (t: T) => U): Maybe => { - return min(fnSelector, createIterable()); - }, - reduce: (fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue?: U) => { - return reduce(fnReduce, initValue!, createIterable()); - }, - reduceToSequence: ( - fnReduce: (previousValue: GenIterable, currentValue: T, currentIndex: number) => GenIterable, - initialValue: GenIterable - ): Sequence => { - return genSequence(reduce>(fnReduce, initialValue!, createIterable())); - }, - - //// Cast - toArray: () => [...createIterable()], - toIterable: () => { - return toIterableIterator(createIterable()); - }, - }; - - return seq; + return new ImplSequence(i); } // Collection of entry points into GenSequence @@ -144,11 +23,6 @@ export const GenSequence = { sequenceFromObject, }; -//// Cast -export function* toIterableIterator(i: Iterable) { - yield* i; -} - /** * alias of toIterableIterator */ diff --git a/src/ImplSequence.ts b/src/ImplSequence.ts new file mode 100644 index 0000000..9c5a54f --- /dev/null +++ b/src/ImplSequence.ts @@ -0,0 +1,126 @@ +import { Sequence, GenIterable, Maybe } from './util/types'; +import { filter, skip, take, concat, concatMap, combine, map, scan, all, any, count, first, forEach, max, min, reduce } from './operators'; +import { toIterableIterator } from './util/util'; + +type LazyIterable = (() => GenIterable) | GenIterable; + +export class ImplSequence implements Sequence { + private _iterator: Maybe>; + + constructor(private i: LazyIterable) { + } + + private get iter() { + return (typeof this.i === "function") ? this.i() : this.i; + } + + private get iterator() { + if (!this._iterator) { + this._iterator = this.iter[Symbol.iterator](); + } + return this._iterator; + } + + private inject(fn: (i: GenIterable) => GenIterable): LazyIterable { + const iter = this.i; + return () => fn(typeof iter === "function" ? iter() : iter); + } + + private chain(fn: (i: GenIterable) => GenIterable): ImplSequence { + return new ImplSequence(this.inject(fn)); + } + + [Symbol.iterator]() { + return this.iter[Symbol.iterator](); + } + + next(): IteratorResult { + return this.iterator.next(); + } + + //// Filters + filter(fnFilter: (t: T) => boolean): ImplSequence { + return this.chain(filter(fnFilter)); + } + + skip(n: number): ImplSequence { + return this.chain(skip(n)); + } + + take(n: number): ImplSequence { + return this.chain(take(n)); + } + + //// Extenders + concat(j: Iterable): ImplSequence { + return this.chain(concat(j)); + } + + concatMap(fn: (t: T) => Iterable): ImplSequence { + return this.chain(concatMap(fn)); + } + + //// Mappers + combine(fn: (t: T, u?: U) => V, j: Iterable): ImplSequence { + return this.chain(combine(fn, j)); + } + + map(fn: (t: T) => U): ImplSequence { + return this.chain(map(fn)); + } + + scan(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue: U): ImplSequence { + return this.chain(scan(fnReduce, initValue)); + } + + // Reducers + all(fnFilter: (t: T) => boolean): boolean { + return all(fnFilter)(this.iter); + } + + any(fnFilter: (t: T) => boolean): boolean { + return any(fnFilter)(this.iter); + } + + count(): number { + return count()(this.iter); + } + + first(fnFilter: (t: T) => boolean, defaultValue: T): T { + return first(fnFilter, defaultValue)(this.iter); + } + + forEach(fn: (t: T, index: number) => void): void { + return forEach(fn)(this.iter); + } + + max(fnSelector?: (t: T) => T): Maybe; + max(fnSelector: (t: T) => U): Maybe { + return max(fnSelector)(this.iter); + } + + min(fnSelector?: (t: T) => T): Maybe + min(fnSelector: (t: T) => U): Maybe { + return min(fnSelector)(this.iter); + } + + reduce(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue?: U) { + return reduce(fnReduce, initValue!)(this.iter); + } + + reduceToSequence( + fnReduce: (previousValue: GenIterable, currentValue: T, currentIndex: number) => GenIterable, + initialValue: GenIterable + ): ImplSequence { + return this.chain(reduce>(fnReduce, initialValue!)); + } + + //// Cast + toArray() { + return [...this.iter]; + } + + toIterable() { + return toIterableIterator(this.iter); + } +} diff --git a/src/index.ts b/src/index.ts index e1ae2fc..0d535ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export * from './GenSequence'; -export * from './operators/operators'; +export { Sequence } from './util/types'; diff --git a/src/operators/index.ts b/src/operators/index.ts new file mode 100644 index 0000000..4e26b60 --- /dev/null +++ b/src/operators/index.ts @@ -0,0 +1 @@ +export * from './operators'; diff --git a/src/operators/operators.ts b/src/operators/operators.ts index ffa22be..20649e0 100644 --- a/src/operators/operators.ts +++ b/src/operators/operators.ts @@ -1,220 +1,101 @@ -import { Maybe } from './types'; +import * as op from './operatorsBase'; + + +import { Maybe, IterableLike } from '../util/types'; + +type ChainFunction = (i: IterableLike) => IterableLike; +type ReduceFunction = (i: IterableLike) => U; /** * Operators used by Sequence */ //// Filters -export function* filter(fnFilter: (t: T) => boolean, i: Iterable) { - for (const v of i) { - if (fnFilter(v)) { - yield v; - } - } -} - -export function* skip(n: number, i: Iterable): IterableIterator { - let a = 0; - for (const t of i) { - if (a >= n) { - yield t; - } - a += 1; - } -} - - -export function* take(n: number, i: Iterable): IterableIterator { - let a = 0; - if (n) { - for (const t of i) { - if (a >= n) { - break; - } - yield t; - a += 1; - } - } +export function filter(fnFilter: (t: T) => boolean): ChainFunction { + return (i: IterableLike) => op.filter(fnFilter, i); +} + +export function skip(n: number): ChainFunction { + return (i: IterableLike) => op.skip(n, i); +} + +export function take(n: number): ChainFunction { + return (i: IterableLike) => op.take(n, i); } //// Extenders /** * Concat two iterables together */ -export function* concat(i: Iterable | IterableIterator, j: Iterable | IterableIterator): IterableIterator { - yield *i; - yield *j; +export function concat(j: IterableLike): ChainFunction { + return (i: IterableLike) => op.concat(i, j); } -export function* concatMap(fn: (t: T) => Iterable, i: Iterable | IterableIterator): IterableIterator { - for (const t of i) { - yield *fn(t); - } +export function concatMap(fn: (t: T) => IterableLike): ChainFunction { + return (i: IterableLike) => op.concatMap(fn, i); } //// Mappers /** * Combine two iterables together using fnMap function. */ -export function* combine( +export function combine( fnMap: (t: T, u?: U) => V, - i: Iterable | IterableIterator, - j: Iterable | IterableIterator -): IterableIterator { - const jit = j[Symbol.iterator](); - for (const r of i) { - const s = jit.next().value; - yield fnMap(r, s); - } + j: IterableLike +): ChainFunction { + return (i: IterableLike) => op.combine(fnMap, i, j); } /** * apply a mapping function to an Iterable. */ -export function map(fnMap: (t: T) => U): (i: Iterable) => IterableIterator; -export function map(fnMap: (t: T) => U, i: Iterable): IterableIterator; -export function map(fnMap: (t: T) => U, i?: Iterable): IterableIterator | ((i: Iterable) => IterableIterator) { - function* fn(fnMap: (t: T) => U, i: Iterable): IterableIterator { - for (const v of i) { - yield fnMap(v); - } - } - - if (i !== undefined) { - return fn(fnMap, i); - } - - return function(i: Iterable) { - return fn(fnMap, i); - }; -} - -export function scan(i: Iterable, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T): IterableIterator; -export function scan(i: Iterable, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue: T): IterableIterator; -export function scan(i: Iterable, fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue: U): IterableIterator; -export function* scan(i: Iterable, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue?: T): IterableIterator { - let index = 0; - if (initValue === undefined) { - // We need to create a new iterable to prevent for...of from restarting an array. - index = 1; - const iter = i[Symbol.iterator](); - let r = iter.next(); - if (!r.done) yield r.value; - initValue = r.value; - i = makeIterable(iter); - } - let prevValue: T = initValue!; - for (const t of i) { - const nextValue = fnReduce(prevValue, t, index); - yield nextValue; - prevValue = nextValue; - index += 1; - } +export function map(fnMap: (t: T) => U): ChainFunction { + return (i: IterableLike) => op.map(fnMap, i); +} + +export function scan(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T): ChainFunction; +export function scan(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue: T): ChainFunction; +export function scan(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue: U): ChainFunction +export function scan(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue?: T): ChainFunction { + return (i: IterableLike) => op.scan(i, fnReduce, initValue!); } //// Reducers -export function all(fn: (t: T) => boolean, i: Iterable): boolean { - for (const t of i) { - if (!fn(t)) { - return false; - } - } - return true; -} - -export function any(fn: (t: T) => boolean, i: Iterable): boolean { - for (const t of i) { - if (fn(t)) { - return true; - } - } - return false; -} - -export function count(i: Iterable): number { - return reduce(p => p + 1, 0, i); -} - -export function first(fn: (t: T) => boolean, defaultValue: T, i: Iterable): T; -export function first(fn: Maybe<(t: T) => boolean>, defaultValue: Maybe, i: Iterable): Maybe { - fn = fn || (() => true); - for (const t of i) { - if (fn(t)) { - return t; - } - } - return defaultValue; -} - -export function forEach(fn: (t: T, index: number) => void, i: Iterable) { - let index = 0; - for (const t of i) { - fn(t, index); - index += 1; - } -} - -export function max(selector: (t: T) => U, i: Iterable): Maybe; -export function max(selector: (t: T) => T = (t => t), i: Iterable): Maybe { - return reduce((p: T, c: T) => selector(c) > selector(p) ? c : p, undefined, i); -} - -export function min(selector: (t: T) => U, i: Iterable): Maybe; -export function min(selector: (t: T) => T = (t => t), i: Iterable): Maybe { - return reduce((p: T, c: T) => selector(c) < selector(p) ? c : p, undefined, i); -} - -export function reduce(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initialValue: U, i: Iterable): U; -export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: T, i: Iterable): T; -export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe, i: Iterable): Maybe; -export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe, i: Iterable): Maybe { - let index = 0; - if (initialValue === undefined) { - index = 1; - const r = i[Symbol.iterator]().next(); - initialValue = r.value; - } - let prevValue: T = initialValue!; - for (const t of i) { - const nextValue = fnReduce(prevValue, t, index); - prevValue = nextValue; - index += 1; - } - return prevValue; -} - -//// Utilities -/** - * Convert an Iterator into an IterableIterator - */ -export function makeIterable(i: Iterator | IterableIterator) { - function* iterate() { - for (let r = i.next(); ! r.done; r = i.next()) { - yield r.value; - } - } - return isIterable(i) ? i : iterate(); +export function all(fn: (t: T) => boolean): ReduceFunction { + return (i: IterableLike) => op.all(fn, i); } -export function isIterable(i: Iterator | IterableIterator): i is IterableIterator { - return !!(i as IterableIterator)[Symbol.iterator]; +export function any(fn: (t: T) => boolean): ReduceFunction { + return (i: IterableLike) => op.any(fn, i); } -/** - * Creates a scan function that can be used in a map function. - */ -export function scanMap(accFn: (acc: T, value: T) => T, init?: T): ((value: T) => T); -export function scanMap(accFn: (acc: U, value: T) => U, init: U): ((value: T) => U); -export function scanMap(accFn: (acc: T, value: T) => T, init?: T): ((value: T) => T) { - let acc = init; - let first = true; - return function(value: T): T { - if (first && acc === undefined) { - first = false; - acc = value; - return acc; - } - acc = accFn(acc as T, value); - return acc; - }; +export function count(): ReduceFunction { + return (i: IterableLike) => op.count(i); +} + +export function first(fn: (t: T) => boolean, defaultValue: T): ReduceFunction; +export function first(fn?: (t: T) => boolean, defaultValue?: T): ReduceFunction>; +export function first(fn: Maybe<(t: T) => boolean>, defaultValue: Maybe): ReduceFunction> { + return (i: IterableLike) => op.first(fn, defaultValue, i); +} + +export function forEach(fn: (t: T, index: number) => void): ReduceFunction { + return (i: IterableLike) => op.forEach(fn, i); +} + +export function max(selector: (t: T) => U): ReduceFunction>; +export function max(selector?: (t: T) => T): ReduceFunction> { + return (i: IterableLike) => op.max(selector, i); +} + +export function min(selector: (t: T) => U): ReduceFunction>; +export function min(selector?: (t: T) => T): ReduceFunction> { + return (i: IterableLike) => op.min(selector, i); +} + +export function reduce(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initialValue: U): ReduceFunction; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: T): ReduceFunction; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe): ReduceFunction>; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe): ReduceFunction> { + return (i: IterableLike) => op.reduce(fnReduce, initialValue, i); } diff --git a/src/operators/operators.test.ts b/src/operators/operatorsBase.test.ts similarity index 95% rename from src/operators/operators.test.ts rename to src/operators/operatorsBase.test.ts index 20c8d10..f0f8fe9 100644 --- a/src/operators/operators.test.ts +++ b/src/operators/operatorsBase.test.ts @@ -1,5 +1,5 @@ -import * as op from './operators'; -import { toIterator } from './util'; +import * as op from './operatorsBase'; +import { toIterator } from '../util/util'; describe('Tests Operators', () => { diff --git a/src/operators/operatorsBase.ts b/src/operators/operatorsBase.ts new file mode 100644 index 0000000..9ea7c0b --- /dev/null +++ b/src/operators/operatorsBase.ts @@ -0,0 +1,223 @@ +import { Maybe, IterableLike } from '../util/types'; + +/** + * Operators used by Sequence + */ + +//// Filters +export function* filter(fnFilter: (t: T) => boolean, i: IterableLike): IterableIterator { + for (const v of i) { + if (fnFilter(v)) { + yield v; + } + } +} + +export function* skip(n: number, i: IterableLike): IterableIterator { + let a = 0; + for (const t of i) { + if (a >= n) { + yield t; + } + a += 1; + } +} + + +export function* take(n: number, i: IterableLike): IterableIterator { + let a = 0; + if (n) { + for (const t of i) { + if (a >= n) { + break; + } + yield t; + a += 1; + } + } +} + +//// Extenders +/** + * Concat two iterables together + */ +export function* concat(i: IterableLike, j: IterableLike): IterableIterator { + yield *i; + yield *j; +} + +export function* concatMap(fn: (t: T) => IterableLike, i: IterableLike): IterableIterator { + for (const t of i) { + yield *fn(t); + } +} + +//// Mappers +/** + * Combine two iterables together using fnMap function. + */ +export function* combine( + fnMap: (t: T, u?: U) => V, + i: IterableLike, + j: IterableLike +): IterableIterator { + const jit = j[Symbol.iterator](); + for (const r of i) { + const s = jit.next().value; + yield fnMap(r, s); + } +} + +/** + * apply a mapping function to an Iterable. + */ +export function map(fnMap: (t: T) => U): (i: IterableLike) => IterableIterator; +export function map(fnMap: (t: T) => U, i: IterableLike): IterableIterator; +export function map(fnMap: (t: T) => U, i?: IterableLike): IterableIterator | ((i: IterableLike) => IterableIterator) { + function* fn(fnMap: (t: T) => U, i: IterableLike): IterableIterator { + for (const v of i) { + yield fnMap(v); + } + } + + if (i !== undefined) { + return fn(fnMap, i); + } + + return function(i: IterableLike) { + return fn(fnMap, i); + }; +} + +export function scan(i: IterableLike, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T): IterableIterator; +export function scan(i: IterableLike, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue: T): IterableIterator; +export function scan(i: IterableLike, fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initValue: U): IterableIterator; +export function* scan(i: IterableLike, fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initValue?: T): IterableIterator { + let index = 0; + if (initValue === undefined) { + // We need to create a new iterable to prevent for...of from restarting an array. + index = 1; + const iter = i[Symbol.iterator](); + let r = iter.next(); + if (!r.done) yield r.value; + initValue = r.value; + i = makeIterable(iter); + } + let prevValue: T = initValue!; + for (const t of i) { + const nextValue = fnReduce(prevValue, t, index); + yield nextValue; + prevValue = nextValue; + index += 1; + } +} + +//// Reducers +export function all(fn: (t: T) => boolean, i: IterableLike): boolean { + for (const t of i) { + if (!fn(t)) { + return false; + } + } + return true; +} + +export function any(fn: (t: T) => boolean, i: IterableLike): boolean { + for (const t of i) { + if (fn(t)) { + return true; + } + } + return false; +} + +export function count(i: IterableLike): number { + return reduce(p => p + 1, 0, i); +} + +export function first(fn: Maybe<(t: T) => boolean>, defaultValue: Maybe, i: IterableLike): Maybe; +export function first(fn: (t: T) => boolean, defaultValue: T, i: IterableLike): T; +export function first(fn: Maybe<(t: T) => boolean>, defaultValue: Maybe, i: IterableLike): Maybe { + fn = fn || (() => true); + for (const t of i) { + if (fn(t)) { + return t; + } + } + return defaultValue; +} + +export function forEach(fn: (t: T, index: number) => void, i: IterableLike) { + let index = 0; + for (const t of i) { + fn(t, index); + index += 1; + } +} + +export function max(selector: undefined, i: IterableLike): Maybe; +export function max(selector: ((t: T) => U) | undefined, i: IterableLike): Maybe; +export function max(selector: ((t: T) => T) | undefined = (t => t), i: IterableLike): Maybe { + return reduce((p: T, c: T) => selector(c) > selector(p) ? c : p, undefined, i); +} + +export function min(selector: undefined, i: IterableLike): Maybe; +export function min(selector: ((t: T) => U) | undefined, i: IterableLike): Maybe; +export function min(selector: ((t: T) => T) | undefined = (t => t), i: IterableLike): Maybe { + return reduce((p: T, c: T) => selector(c) < selector(p) ? c : p, undefined, i); +} + +export function reduce(fnReduce: (prevValue: U, curValue: T, curIndex: number) => U, initialValue: U, i: IterableLike): U; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: T, i: IterableLike): T; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe, i: IterableLike): Maybe; +export function reduce(fnReduce: (prevValue: T, curValue: T, curIndex: number) => T, initialValue: Maybe, i: IterableLike): Maybe { + let index = 0; + if (initialValue === undefined) { + index = 1; + const r = i[Symbol.iterator]().next(); + initialValue = r.value; + } + let prevValue: T = initialValue!; + for (const t of i) { + const nextValue = fnReduce(prevValue, t, index); + prevValue = nextValue; + index += 1; + } + return prevValue; +} + +//// Utilities +/** + * Convert an Iterator into an IterableIterator + */ +export function makeIterable(i: Iterator | IterableIterator) { + function* iterate() { + for (let r = i.next(); ! r.done; r = i.next()) { + yield r.value; + } + } + return isIterable(i) ? i : iterate(); +} + +export function isIterable(i: Iterator | IterableLike): i is IterableLike { + return !!(i as IterableIterator)[Symbol.iterator]; +} + +/** + * Creates a scan function that can be used in a map function. + */ +export function scanMap(accFn: (acc: T, value: T) => T, init?: T): ((value: T) => T); +export function scanMap(accFn: (acc: U, value: T) => U, init: U): ((value: T) => U); +export function scanMap(accFn: (acc: T, value: T) => T, init?: T): ((value: T) => T) { + let acc = init; + let first = true; + return function(value: T): T { + if (first && acc === undefined) { + first = false; + acc = value; + return acc; + } + acc = accFn(acc as T, value); + return acc; + }; +} diff --git a/src/operators/types.ts b/src/operators/types.ts deleted file mode 100644 index 4ee934f..0000000 --- a/src/operators/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type Maybe = T | undefined; - -export interface IterableLike { - [Symbol.iterator](): Iterator | IterableIterator; -} diff --git a/src/util/types.ts b/src/util/types.ts new file mode 100644 index 0000000..7307bcd --- /dev/null +++ b/src/util/types.ts @@ -0,0 +1,49 @@ +export type Maybe = T | undefined; + +export interface IterableLike { + [Symbol.iterator](): Iterator | IterableIterator; +} + +export interface GenIterable extends IterableLike {} + +export interface Sequence extends IterableLike { + next(): IteratorResult; + + //// Filters + /** keep values where the fnFilter(t) returns true */ + filter(fnFilter: (t: T) => boolean): Sequence; + skip(n: number): Sequence; + take(n: number): Sequence; + + //// Extenders + concat(j: Iterable): Sequence; + concatMap(fn: (t: T) => Iterable): Sequence; + + //// Mappers + combine(fn: (t: T, u?: U) => V, j: Iterable): Sequence; + /** map values from type T to type U */ + map(fnMap: (t: T) => U): Sequence; + scan(fnReduce: (previousValue: T, currentValue: T, currentIndex: number) => T, initialValue?: T): Sequence; + scan(fnReduce: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): Sequence; + + //// Reducers + all(fnFilter: (t: T) => boolean): boolean; + any(fnFilter: (t: T) => boolean): boolean; + count(): number; + first(fnFilter?: (t: T) => boolean, defaultValue?: T): Maybe; + first(fnFilter: (t: T) => boolean, defaultValue: T): T; + forEach(fn: (t: T, index: number) => void): void; + max(fnSelector?: (t: T) => T): Maybe; + max(fnSelector: (t: T) => U): Maybe; + min(fnSelector?: (t: T) => T): Maybe; + min(fnSelector: (t: T) => U): Maybe; + /** reduce function see Array.reduce */ + reduce(fnReduce: (previousValue: T, currentValue: T, currentIndex: number) => T): Maybe; + reduce(fnReduce: (previousValue: U, currentValue: T, currentIndex: number) => U, initialValue: U): U; + reduceToSequence>(fnReduce: (previousValue: V, currentValue: T, currentIndex: number) => V, initialValue: V): Sequence; + reduceToSequence(fnReduce: (previousValue: GenIterable, currentValue: T, currentIndex: number) => GenIterable, initialValue: GenIterable): Sequence; + + //// Cast + toArray(): T[]; + toIterable(): IterableIterator; +} diff --git a/src/operators/util.ts b/src/util/util.ts similarity index 82% rename from src/operators/util.ts rename to src/util/util.ts index cd60553..c81fd2e 100644 --- a/src/operators/util.ts +++ b/src/util/util.ts @@ -11,3 +11,7 @@ export function toIterator(values: Iterable | IterableIterator) { }; return rangeIterator; } + +export function* toIterableIterator(i: Iterable) { + yield* i; +}