diff --git a/src/Enumerable.ts b/src/Enumerable.ts index fa12617..eb9fc53 100644 --- a/src/Enumerable.ts +++ b/src/Enumerable.ts @@ -1,47 +1,71 @@ -import { from } from './functions/from'; -import { aggregate } from './functions/aggregate'; -import { all } from './functions/all'; -import { any } from './functions/any'; -import { append } from './functions/append'; -import { atLeast } from './functions/atLeast'; -import { atMost } from './functions/atMost'; -import { concat } from './functions/concat'; -import { contains } from './functions/contains'; -import { count } from './functions/count'; -import { defaultIfEmpty } from './functions/defaultIfEmpty'; -import { distinct, distinctBy } from './functions/distinct'; -import { elementAt, elementAtOrDefault } from './functions/elementAt'; -import { empty } from './functions/empty'; -import { endsWith } from './functions/endsWith'; -import { except, exceptBy } from './functions/except'; -import { first, firstOrDefault } from './functions/first'; -import { forEach } from './functions/forEach'; -import { groupBy } from './functions/groupBy'; -import { intersect, intersectBy } from './functions/intersect'; -import { last, lastOrDefault } from './functions/last'; -import { max, maxBy } from './functions/max'; -import { ofType } from './functions/ofType'; -import { orderBy } from './functions/orderBy'; -import { prepend } from './functions/prepend'; -import { quantile } from './functions/quantile'; -import { range } from './functions/range'; -import { repeat } from './functions/repeat'; -import { reverse } from './functions/reverse'; -import { select, selectMany } from './functions/select'; -import { sequenceEqual } from './functions/sequenceEqual'; -import { shuffle } from './functions/shuffle'; -import { skip, skipLast, skipWhile } from './functions/skip'; -import { startsWith } from './functions/startsWith'; -import { sum } from './functions/sum'; -import { take, takeEvery, takeLast, takeWhile } from './functions/take'; -import { thenBy } from './functions/thenBy'; -import { toMap } from './functions/toMap'; -import { toObject } from './functions/toObject'; -import { toSet } from './functions/toSet'; -import { union, unionBy } from './functions/union'; -import { where } from './functions/where'; -import { zip } from './functions/zip'; -import { Comparer, EqualityComparer } from './types'; +import { + from, + empty, + range, + repeat, + aggregate, + all, + any, + append, + asEnumerable, + atLeast, + atMost, + average, + chunk, + concat, + EqualityComparer, + contains, + count, + defaultIfEmpty, + distinct, + distinctBy, + elementAt, + elementAtOrDefault, + endsWith, + except, + exceptBy, + first, + firstOrDefault, + forEach, + groupBy, + intersect, + intersectBy, + last, + lastOrDefault, + max, + maxBy, + min, + minBy, + ofType, + Comparer, + orderBy, + pipe, + prepend, + quantile, + reverse, + select, + selectMany, + sequenceEqual, + shuffle, + skip, + skipLast, + skipWhile, + startsWith, + sum, + take, + takeEvery, + takeLast, + takeWhile, + toArray, + toMap, + toObject, + toSet, + union, + unionBy, + where, + zip, + thenBy +} from '.'; export class Enumerable<TSource> implements Iterable<TSource> { private readonly srcGenerator: () => Generator<TSource>; @@ -87,7 +111,7 @@ export class Enumerable<TSource> implements Iterable<TSource> { return this.srcGenerator(); } - public aggregate<T>(aggregator: (prev: T, curr: T, index: number) => T): T; + public aggregate(aggregator: (prev: TSource, curr: TSource, index: number) => TSource): TSource; public aggregate<TAccumulate>( aggregator: (prev: TAccumulate, curr: TSource, index: number) => TAccumulate, seed: TAccumulate @@ -112,7 +136,7 @@ export class Enumerable<TSource> implements Iterable<TSource> { } public asEnumerable(): Enumerable<TSource> { - return new Enumerable(this.srcGenerator); + return asEnumerable(this); } public atLeast(count: number, predicate?: (item: TSource, index: number) => boolean): boolean { @@ -124,13 +148,11 @@ export class Enumerable<TSource> implements Iterable<TSource> { } public average(selector?: (item: TSource) => number): number { - return this.sum(selector) / this.count(); + return average(this, selector); } public chunk(chunkSize: number): Enumerable<Enumerable<TSource>> { - return this.select((x, i) => ({ index: i, value: x })) - .groupBy(x => Math.floor(x.index / chunkSize)) - .select(x => x.select(v => v.value)); + return chunk(this, chunkSize); } public concat(second: Iterable<TSource>): Enumerable<TSource> { @@ -256,15 +278,11 @@ export class Enumerable<TSource> implements Iterable<TSource> { public min(): TSource; public min<TResult>(selector: (item: TSource) => TResult): TResult; public min<TResult>(selector?: (item: TSource) => TResult): TSource | TResult { - if (!selector) { - return this.aggregate((prev, curr) => (prev < curr ? prev : curr)); - } - - return this.select(selector).aggregate((prev, curr) => (prev < curr ? prev : curr)); + return min(this, selector); } public minBy<TKey>(keySelector: (item: TSource) => TKey): TSource { - return this.aggregate((prev, curr) => (keySelector(prev) <= keySelector(curr) ? prev : curr)); + return minBy(this, keySelector); } public ofType<TResult>(type: new (...params: unknown[]) => TResult): Enumerable<TResult> { @@ -282,6 +300,10 @@ export class Enumerable<TSource> implements Iterable<TSource> { return orderBy(this, false, selector, comparer); } + public pipe(action: (item: TSource, index: number) => void): Enumerable<TSource> { + return pipe(this, action); + } + public prepend(item: TSource): Enumerable<TSource> { return prepend(this, item); } @@ -362,7 +384,7 @@ export class Enumerable<TSource> implements Iterable<TSource> { } public toArray(): TSource[] { - return [...this]; + return toArray(this); } public toLookup(): unknown { diff --git a/src/__tests__/ofType.ts b/src/__tests__/ofType.ts new file mode 100644 index 0000000..6157a29 --- /dev/null +++ b/src/__tests__/ofType.ts @@ -0,0 +1,32 @@ +import { Enumerable, ofType, TypeOfMember } from '..'; + +describe('ofType', () => { + it.each([[1, 2, 3], new Set([1, 2, 3]), '123', new Map()])('should return an Enumerable', src => { + const result = ofType<unknown, string>(src, 'string'); + + expect(result).toBeInstanceOf(Enumerable); + }); + + it.each([ + [[1, 'b', 'c', false], 'string', ['b', 'c']], + [[1, 'b', 'c', false, 2, true], 'number', [1, 2]], + [[1, 'b', 'c', false, 2, true], 'boolean', [false, true]] + ])('should only return the specified type', (src, type, expected) => { + const result = ofType(src, type as TypeOfMember).toArray(); + + expect(result).toEqual(expected); + }); + + it('should only return instances of passed class', () => { + class Test {} + + const a = new Test(); + const b = new Test(); + + const items = [1, 3, 'sadf', a, false, null, undefined, b, NaN]; + + const result = ofType(items, Test).toArray(); + + expect(result).toEqual([a, b]); + }); +}); diff --git a/src/__tests__/pipe.ts b/src/__tests__/pipe.ts new file mode 100644 index 0000000..4b7e3e4 --- /dev/null +++ b/src/__tests__/pipe.ts @@ -0,0 +1,35 @@ +import { Enumerable, pipe } from '..'; + +describe('pipe', () => { + it.each([[1, 2, 3], new Set([1, 2, 3]), '123', new Map()])('should return an Enumerable', src => { + const result = pipe<unknown>(src, () => {}); + + expect(result).toBeInstanceOf(Enumerable); + }); + + it('should call action for each item', () => { + const action = jest.fn(); + + const items = [1, 2, 3]; + + const _ = pipe(items, action).toArray(); + + expect(action).toHaveBeenCalledTimes(items.length); + }); + + it('should have deferred execution', () => { + const action = jest.fn(); + + const items = [1, 2, 3]; + + const result = pipe(items, action); + + expect(action).toHaveBeenCalledTimes(0); + + items.push(4); + + const _ = result.toArray(); + + expect(action).toHaveBeenCalledTimes(items.length); + }); +}); diff --git a/src/functions/aggregate.ts b/src/functions/aggregate.ts index 1b88f3a..ea1597f 100644 --- a/src/functions/aggregate.ts +++ b/src/functions/aggregate.ts @@ -1,3 +1,17 @@ +export function aggregate<TSource>( + src: Iterable<TSource>, + aggregator: (prev: TSource, curr: TSource, index: number) => TSource +): TSource; +export function aggregate<TSource, TAccumulate>( + src: Iterable<TSource>, + aggregator: (prev: TAccumulate, curr: TSource, index: number) => TAccumulate, + seed: TAccumulate +): TAccumulate; +export function aggregate<TSource, TAccumulate>( + src: Iterable<TSource>, + aggregator: (prev: TAccumulate | TSource, curr: TSource, index: number) => TAccumulate | TSource, + seed?: TAccumulate | TSource +): TAccumulate | TSource; export function aggregate<TSource, TAccumulate>( src: Iterable<TSource>, aggregator: (prev: TAccumulate | TSource, curr: TSource, index: number) => TAccumulate | TSource, diff --git a/src/functions/asEnumerable.ts b/src/functions/asEnumerable.ts new file mode 100644 index 0000000..b2d7a6e --- /dev/null +++ b/src/functions/asEnumerable.ts @@ -0,0 +1,5 @@ +import { Enumerable } from '../Enumerable'; + +export function asEnumerable<TSource>(src: Iterable<TSource>): Enumerable<TSource> { + return new Enumerable(src); +} diff --git a/src/functions/average.ts b/src/functions/average.ts new file mode 100644 index 0000000..b309f77 --- /dev/null +++ b/src/functions/average.ts @@ -0,0 +1,6 @@ +import { count } from './count'; +import { sum } from './sum'; + +export function average<TSource>(src: Iterable<TSource>, selector?: (item: TSource) => number): number { + return sum(src, selector) / count(src); +} diff --git a/src/functions/chunk.ts b/src/functions/chunk.ts new file mode 100644 index 0000000..e3bf2f5 --- /dev/null +++ b/src/functions/chunk.ts @@ -0,0 +1,8 @@ +import { Enumerable } from '../Enumerable'; +import { select } from './select'; + +export function chunk<TSource>(src: Iterable<TSource>, chunkSize: number): Enumerable<Enumerable<TSource>> { + return select(src, (x, i) => ({ index: i, value: x })) + .groupBy(x => Math.floor(x.index / chunkSize)) + .select(x => x.select(v => v.value)); +} diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000..d75d644 --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1,49 @@ +export * from './aggregate'; +export * from './all'; +export * from './any'; +export * from './append'; +export * from './asEnumerable'; +export * from './atLeast'; +export * from './atMost'; +export * from './average'; +export * from './chunk'; +export * from './concat'; +export * from './contains'; +export * from './count'; +export * from './defaultIfEmpty'; +export * from './distinct'; +export * from './elementAt'; +export * from './empty'; +export * from './endsWith'; +export * from './except'; +export * from './first'; +export * from './forEach'; +export * from './from'; +export * from './groupBy'; +export * from './intersect'; +export * from './last'; +export * from './max'; +export * from './min'; +export * from './ofType'; +export * from './orderBy'; +export * from './pipe'; +export * from './prepend'; +export * from './quantile'; +export * from './range'; +export * from './repeat'; +export * from './reverse'; +export * from './select'; +export * from './sequenceEqual'; +export * from './shuffle'; +export * from './skip'; +export * from './startsWith'; +export * from './sum'; +export * from './take'; +export * from './thenBy'; +export * from './toArray'; +export * from './toMap'; +export * from './toObject'; +export * from './toSet'; +export * from './union'; +export * from './where'; +export * from './zip'; diff --git a/src/functions/min.ts b/src/functions/min.ts new file mode 100644 index 0000000..92704e4 --- /dev/null +++ b/src/functions/min.ts @@ -0,0 +1,20 @@ +import { aggregate } from './aggregate'; +import { select } from './select'; + +export function min<TSource>(src: Iterable<TSource>): TSource; +export function min<TSource, TResult>(src: Iterable<TSource>, selector: (item: TSource) => TResult): TResult; +export function min<TSource, TResult>(src: Iterable<TSource>, selector?: (item: TSource) => TResult): TSource | TResult; +export function min<TSource, TResult>( + src: Iterable<TSource>, + selector?: (item: TSource) => TResult +): TSource | TResult { + if (!selector) { + return aggregate(src, (prev, curr) => (prev < curr ? prev : curr)); + } + + return select(src, selector).aggregate((prev, curr) => (prev < curr ? prev : curr)); +} + +export function minBy<TSource, TKey>(src: Iterable<TSource>, keySelector: (item: TSource) => TKey): TSource { + return aggregate(src, (prev, curr) => (keySelector(prev) <= keySelector(curr) ? prev : curr)); +} diff --git a/src/functions/ofType.ts b/src/functions/ofType.ts index c0e5a2e..cb32aaa 100644 --- a/src/functions/ofType.ts +++ b/src/functions/ofType.ts @@ -1,13 +1,29 @@ +import { TypeOfMember } from '../types'; import { Enumerable } from '../Enumerable'; export function ofType<TSource, TResult>( src: Iterable<TSource>, type: new (...params: unknown[]) => TResult -): Enumerable<TResult> { - function* generator(): Generator<TResult> { +): Enumerable<TResult>; +export function ofType<TSource, TResult>(src: Iterable<TSource>, type: TypeOfMember): Enumerable<TResult>; +// export function ofType<TSource, TResult>( +// src: Iterable<TSource>, +// type: (new (...params: unknown[]) => TResult) | TypeOfMember +// ): Enumerable<TResult | TSource>; +export function ofType<TSource, TResult>( + src: Iterable<TSource>, + type: (new (...params: unknown[]) => TResult) | TypeOfMember +): Enumerable<TResult | TSource> { + function* generator(): Generator<TResult | TSource> { for (const item of src) { - if (item instanceof type) { - yield item; + if (typeof type === 'string') { + if (typeof item === type) { + yield item; + } + } else { + if (item instanceof type) { + yield item; + } } } } diff --git a/src/functions/pipe.ts b/src/functions/pipe.ts new file mode 100644 index 0000000..edb2a64 --- /dev/null +++ b/src/functions/pipe.ts @@ -0,0 +1,19 @@ +import { Enumerable } from '../Enumerable'; + +export function pipe<TSource>( + src: Iterable<TSource>, + action: (item: TSource, index: number) => void +): Enumerable<TSource> { + function* generator(): Generator<TSource> { + let i = 0; + + for (const item of src) { + action(item, i); + yield item; + + i++; + } + } + + return new Enumerable(generator); +} diff --git a/src/functions/sum.ts b/src/functions/sum.ts index 5aa8fb3..5740a9f 100644 --- a/src/functions/sum.ts +++ b/src/functions/sum.ts @@ -11,5 +11,5 @@ export function sum<TSource>(src: Iterable<TSource>, selector?: (item: TSource) }); } - return aggregate(src, (prev, curr) => (prev as number) + selector(curr), 0) as number; + return aggregate(src, (prev, curr) => prev + selector(curr), 0); } diff --git a/src/functions/toArray.ts b/src/functions/toArray.ts new file mode 100644 index 0000000..dcd9896 --- /dev/null +++ b/src/functions/toArray.ts @@ -0,0 +1,3 @@ +export function toArray<TSource>(src: Iterable<TSource>): TSource[] { + return [...src]; +} diff --git a/src/functions/toMap.ts b/src/functions/toMap.ts index 703e7bb..ed6eb35 100644 --- a/src/functions/toMap.ts +++ b/src/functions/toMap.ts @@ -1,3 +1,14 @@ +export function toMap<TSource, TKey>(src: Iterable<TSource>, keySelector: (item: TSource) => TKey): Map<TKey, TSource>; +export function toMap<TSource, TKey, TValue>( + src: Iterable<TSource>, + keySelector: (item: TSource) => TKey, + valueSelector: (item: TSource) => TValue +): Map<TKey, TValue>; +export function toMap<TSource, TKey, TValue>( + src: Iterable<TSource>, + keySelector: (item: TSource) => TKey, + valueSelector?: (item: TSource) => TValue +): Map<TKey, TSource | TValue>; export function toMap<TSource, TKey, TValue>( src: Iterable<TSource>, keySelector: (item: TSource) => TKey, diff --git a/src/functions/toObject.ts b/src/functions/toObject.ts index d43ecb4..2ced33b 100644 --- a/src/functions/toObject.ts +++ b/src/functions/toObject.ts @@ -1,3 +1,17 @@ +export function toObject<TSource>( + src: Iterable<TSource>, + keySelector: (item: TSource) => string +): Record<string, TSource>; +export function toObject<TSource, TValue>( + src: Iterable<TSource>, + keySelector: (item: TSource) => string, + valueSelector: (item: TSource) => TValue +): Record<string, TValue>; +export function toObject<TSource, TValue>( + src: Iterable<TSource>, + keySelector: (item: TSource) => string, + valueSelector?: (item: TSource) => TValue +): Record<string, TSource | TValue>; export function toObject<TSource, TValue>( src: Iterable<TSource>, keySelector: (item: TSource) => string, diff --git a/src/functions/zip.ts b/src/functions/zip.ts index 0551844..417895c 100644 --- a/src/functions/zip.ts +++ b/src/functions/zip.ts @@ -1,5 +1,19 @@ import { Enumerable } from '../Enumerable'; +export function zip<TSource, TSecond>( + src: Iterable<TSource>, + second: Iterable<TSecond> +): Enumerable<[TSource, TSecond]>; +export function zip<TSource, TSecond, TResult>( + src: Iterable<TSource>, + second: Iterable<TSecond>, + resultSelector: (first: TSource, second: TSecond) => TResult +): Enumerable<TResult>; +export function zip<TSource, TSecond, TResult>( + src: Iterable<TSource>, + second: Iterable<TSecond>, + resultSelector?: (first: TSource, second: TSecond) => TResult +): Enumerable<[TSource, TSecond] | TResult>; export function zip<TSource, TSecond, TResult>( src: Iterable<TSource>, second: Iterable<TSecond>, diff --git a/src/index.ts b/src/index.ts index 96dc20f..0d3caa4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ export * from './types'; -export * from './functions/from'; export * from './Enumerable'; +export * from './functions'; diff --git a/src/types.ts b/src/types.ts index 66d8da9..91503e8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ export type EqualityComparer<T> = (a: T, b: T) => boolean; export type Comparer<T> = (a: T, b: T) => number; + +export type TypeOfMember = 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'object' | 'symbol' | 'undefined';