diff --git a/src/collectors.ts b/src/collectors.ts new file mode 100644 index 0000000..20bdffa --- /dev/null +++ b/src/collectors.ts @@ -0,0 +1,22 @@ +import { IteratorCollector } from "./types"; + +/** + * Collector which converts this iterator to an array, using `Array.from`. + */ +export const toArray = (): IteratorCollector => ( + input +) => Array.from(input); + +/** + * Collector which converts this iterator into a single value, much like `reduce` + * @param initialValue + * @param reducer + */ +export const reduce = ( + reducer: (previousValue: TResult, value: TType) => TResult, + initialValue: TResult +): IteratorCollector => (input) => { + let result = initialValue; + for (const val of input) result = reducer(result, val); + return result; +}; diff --git a/src/index.test.ts b/src/index.test.ts deleted file mode 100644 index 377366f..0000000 --- a/src/index.test.ts +++ /dev/null @@ -1,3 +0,0 @@ -describe("todo", () => { - test.todo("hello world!"); -}); diff --git a/src/index.ts b/src/index.ts index 286e173..7de25b3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,8 @@ -"hello world!"; +export * from "./types"; +export * from "./query"; + +export * from "./operators"; +export * as operators from "./operators"; + +export * from "./collectors"; +export * as collectors from "./collectors"; diff --git a/src/operators.ts b/src/operators.ts new file mode 100644 index 0000000..fbb5d0c --- /dev/null +++ b/src/operators.ts @@ -0,0 +1,25 @@ +import { IteratorOperator, IteratorSingleOperator } from "./types"; + +// === OPERATORS +/** + * Yields values from the the previous iterator after they have been passed over the given mapping function + * @param mapper The mapping function + */ +export const map = ( + mapper: (value: TInput) => TOutput +): IteratorOperator => + function* (input) { + for (const val of input) yield mapper(val); + }; + +// === SINGLE OPERATORS +/** + * Yields values from the previous iterator only if they satisfy the filter function + * @param filter The filter function + */ +export const filter = ( + filter: (value: TType) => boolean +): IteratorSingleOperator => + function* (input) { + for (const val of input) if (filter(val)) yield val; + }; diff --git a/src/query.test.ts b/src/query.test.ts new file mode 100644 index 0000000..7d5b11f --- /dev/null +++ b/src/query.test.ts @@ -0,0 +1,24 @@ +import { reduce, toArray } from "./collectors"; +import { filter, map } from "./operators"; +import { queryOf } from "./query"; + +const strings = ["foo", "bar", "baz", "foobar"]; + +describe("query", () => { + it("pipe operators onto iterator", () => { + const arr = queryOf(strings) + .pipe(filter((v) => v.length === 3)) + .pipe(map((v) => v.length)) + .collect(toArray()); + + expect(arr).toEqual(expect.arrayContaining([3, 3, 3])); + }); + + it("should collect the iterator", () => { + const sl = queryOf(strings) + .pipe(filter((v) => v.length === 3)) + .collect(reduce((p, v) => p + v.length, 0)); + + expect(sl).toBeCloseTo(9); + }); +}); diff --git a/src/query.ts b/src/query.ts new file mode 100644 index 0000000..caa23ca --- /dev/null +++ b/src/query.ts @@ -0,0 +1,51 @@ +import { QueryIterator, IteratorOperator, IteratorCollector } from "./types"; + +const queryProps: PropertyDescriptorMap = { + pipe: { + value( + this: IterableIterator, + operator: IteratorOperator + ) { + return query(operator(this)); + }, + }, + collect: { + value( + this: IterableIterator, + collector: IteratorCollector + ) { + return collector(this); + }, + }, +}; + +/** + * Creates a query iterator from a given iterable + * @param iterable The iterable + */ +export function queryOf( + iterable: Iterable +): QueryIterator { + return query(iterable[Symbol.iterator]()); +} + +/** + * Creates a query iterator from a given iterator + * @param iterator The iterator + */ +export function query(iterator: Iterator): QueryIterator { + // set prototype of the iterator + const qi = Object.setPrototypeOf( + iterator, + Object.defineProperties( + Object.getPrototypeOf(iterator) ?? {}, + queryProps + ) + ); + + // make the Iterator an IterableIterator if it isn't already + if (!(Symbol.iterator in qi)) + Object.defineProperty(qi, Symbol.iterator, () => iterator); + + return qi; +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..f0e3c73 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,29 @@ +/** + * A function which accepts the current iterator and returns a new one, pulling from the initial iterator as needed + */ +export type IteratorOperator = ( + iterator: IterableIterator +) => IterableIterator; +export type IteratorSingleOperator = IteratorOperator; + +/** + * A function which collects all values from an iterator into a result + */ +export type IteratorCollector = ( + iterator: IterableIterator +) => TResult; + +export interface QueryIterator extends IterableIterator { + /** + * Pipes this iterator over a given operator, returning the resulting iterator + * @param operator The operator to map the iterator values + */ + pipe(operator: IteratorOperator): QueryIterator; + // TODO multiple operators + + /** + * Collects all values from the iterator into a single result + * @param collector The collector function + */ + collect(collector: IteratorCollector): TResult; +}