From 1371d102e75f477aca73ca6c87413dcda0cdaf56 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 22:33:45 +0100 Subject: [PATCH 1/6] refactor: consistent naming Signed-off-by: vmladenov --- index.d.ts | 36 +++++++++---------- src/creation-async.ts | 8 ++--- src/creation.ts | 6 ++-- src/fluent-async.ts | 64 +++++++++++++++++----------------- src/iterables/select.ts | 2 +- src/iterables/set-iterators.ts | 2 +- src/iterables/skip-while.ts | 2 +- src/iterables/skip.ts | 2 +- src/iterables/take-while.ts | 2 +- src/iterables/take.ts | 2 +- src/utils.ts | 2 +- 11 files changed, 64 insertions(+), 64 deletions(-) diff --git a/index.d.ts b/index.d.ts index e221d5a..0c6df94 100644 --- a/index.d.ts +++ b/index.d.ts @@ -437,52 +437,52 @@ declare module 'fluent-iter' { key: TKey; } - export interface FluentIterableAsync extends AsyncIterable { + export interface FluentAsyncIterable extends AsyncIterable { /** * Filters the iterable using predicate function typed overload * @param predicate */ - where(predicate: (item: TValue) => item is TSubValue): FluentIterableAsync; + where(predicate: (item: TValue) => item is TSubValue): FluentAsyncIterable; /** * Filters the iterable using predicate function * @param predicate */ - where(predicate: (item: TValue) => boolean): FluentIterableAsync; + where(predicate: (item: TValue) => boolean): FluentAsyncIterable; /** * Maps the iterable items * @param map map function */ - select(map: (item: TValue) => TOutput): FluentIterableAsync; + select(map: (item: TValue) => TOutput): FluentAsyncIterable; /** * Take first N items from iterable */ - take(count: number): FluentIterableAsync; + take(count: number): FluentAsyncIterable; /** * Return items while condition return true * @param condition */ - takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync; + takeWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable; /** * Skip first N items from iterable * @param count */ - skip(count: number): FluentIterableAsync; + skip(count: number): FluentAsyncIterable; /** * Skip items while condition return true, get the rest * @param condition */ - skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync; + skipWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable; /** * Return distinct items. Can specify optional item comparer * @param keySelector function to get key for comparison. */ - distinct(keySelector?: (item: TValue) => TKey): FluentIterableAsync; + distinct(keySelector?: (item: TValue) => TKey): FluentAsyncIterable; /** * Group items @@ -490,10 +490,10 @@ declare module 'fluent-iter' { */ groupBy(keySelector: (item: TValue, index: number) => TKey): [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] - ? FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> - : FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync; + ? FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + : FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; /** * Return a promise to an array. @@ -518,8 +518,8 @@ declare module 'fluent-iter' { toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; } - export interface FluentIterableAsyncPromise extends FluentIterableAsync> { - groupByStatus(): FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; + export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; toStatusMap(): Promise>; } @@ -558,9 +558,9 @@ declare module 'fluent-iter' { export function fromObject(value: TValue): FluentIterable<{ key: string, value: TValue[TKey] }>; export function fromObject(value: TValue, resultCreator: (key: TKey, value: TValue[TKey]) => TResult): FluentIterable; - export function fromEvent(target: TTarget, event: TEvent): FluentIterableAsync; - export function fromTimer(interval: number, delay?: number): FluentIterableAsync; - export function fromPromises(...promises: Promise[]): FluentIterableAsyncPromise; + export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable; + export function fromTimer(interval: number, delay?: number): FluentAsyncIterable; + export function fromPromises(...promises: Promise[]): FluentAsyncIterablePromise; export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult; export function isRejected(result: PromiseResult): result is RejectedPromiseResult; } \ No newline at end of file diff --git a/src/creation-async.ts b/src/creation-async.ts index 0bfa366..9fe914a 100644 --- a/src/creation-async.ts +++ b/src/creation-async.ts @@ -1,19 +1,19 @@ -import type { FluentIterableAsync, FluentIterableAsyncPromise } from 'fluent-iter'; +import type { FluentAsyncIterable, FluentAsyncIterablePromise } from 'fluent-iter'; import FluentAsync, {FluentAsyncPromise} from "./fluent-async.js"; import fromEventAsync from "./generators/from-event.ts"; import fromTimerAsync from "./generators/from-timer.js"; import {fromPromisesIterable} from "./generators/promises.ts"; -export function fromEvent(target: TTarget, event: TEvent): FluentIterableAsync { +export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable { return new FluentAsync(fromEventAsync(target, event)); } -export function fromTimer(interval: number, delay?: number): FluentIterableAsync { +export function fromTimer(interval: number, delay?: number): FluentAsyncIterable { return new FluentAsync(fromTimerAsync(interval, delay)); } export function fromPromises( ...promises: Promise[] -): FluentIterableAsyncPromise { +): FluentAsyncIterablePromise { return new FluentAsyncPromise(fromPromisesIterable(...promises)); } diff --git a/src/creation.ts b/src/creation.ts index 141aae8..0abd1ac 100644 --- a/src/creation.ts +++ b/src/creation.ts @@ -1,4 +1,4 @@ -import type { FluentIterable, FluentIterableAsync } from 'fluent-iter'; +import type { FluentIterable, FluentAsyncIterable } from 'fluent-iter'; import Fluent from "./fluent.ts"; import arrayLikeIterator from "./generators/array-like.ts"; import objectIterator from "./generators/object.ts"; @@ -10,7 +10,7 @@ export function fromIterable(iterable: Iterable): FluentIterable return new Fluent(iterable); } -export function fromAsyncIterable(iterable: AsyncIterable): FluentIterableAsync { +export function fromAsyncIterable(iterable: AsyncIterable): FluentAsyncIterable { return new FluentAsync(iterable); } @@ -36,7 +36,7 @@ export function repeat(value: TValue, times: number): FluentIterable(iterable: AsyncIterable): FluentIterableAsync; +export function from(iterable: AsyncIterable): FluentAsyncIterable; export function from(iterable: Iterable | ArrayLike): FluentIterable; export function from(value: TValue): FluentIterable<{ key: string, value: TValue[TKey] }>; export function from(source: Iterable | ArrayLike | TValue | AsyncIterable) { diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 6869c3a..d7e48ff 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -1,7 +1,7 @@ import type { FluentIterable, - FluentIterableAsync, - FluentIterableAsyncPromise, + FluentAsyncIterable, + FluentAsyncIterablePromise, FulfilledPromiseResult, IGrouping, PromiseMap, @@ -10,54 +10,54 @@ import type { } from 'fluent-iter'; import type {Mapper, Predicate} from "./interfaces.js"; import {whereAsyncIterator} from "./iterables/where.js"; -import {selectIteratorAsync} from "./iterables/select.js"; -import takeIteratorAsync from "./iterables/take.js"; +import {selectAsyncIterator} from "./iterables/select.js"; +import takeAsyncIterator from "./iterables/take.js"; import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.js"; import {groupByAsyncIterator} from "./iterables/group.ts"; -import {takeWhileIteratorAsync} from "./iterables/take-while.ts"; -import {skipIteratorAsync} from "./iterables/skip.ts"; -import {skipWhileIteratorAsync} from "./iterables/skip-while.ts"; -import {distinctIteratorAsync} from "./iterables/set-iterators.ts"; +import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; +import {skipAsyncIterator} from "./iterables/skip.ts"; +import {skipWhileAsyncIterator} from "./iterables/skip-while.ts"; +import {distinctAsyncIterator} from "./iterables/set-iterators.ts"; -export default class FluentAsync implements FluentIterableAsync { +export default class FluentAsync implements FluentAsyncIterable { readonly #source: AsyncIterable; constructor(source: AsyncIterable) { this.#source = source; } - where(predicate: (item: TValue) => item is TSubValue): FluentIterableAsync; - where(predicate: Predicate): FluentIterableAsync; - where(predicate: Predicate): FluentIterableAsync | FluentIterableAsync { + where(predicate: (item: TValue) => item is TSubValue): FluentAsyncIterable; + where(predicate: Predicate): FluentAsyncIterable; + where(predicate: Predicate): FluentAsyncIterable | FluentAsyncIterable { return new FluentAsync(whereAsyncIterator(this, predicate)); } - select(map: Mapper): FluentIterableAsync { - return new FluentAsync(selectIteratorAsync(this, map)); + select(map: Mapper): FluentAsyncIterable { + return new FluentAsync(selectAsyncIterator(this, map)); } - take(count: number): FluentIterableAsync { - return new FluentAsync(takeIteratorAsync(this, count)); + take(count: number): FluentAsyncIterable { + return new FluentAsync(takeAsyncIterator(this, count)); } - takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync { - return new FluentAsync(takeWhileIteratorAsync(this, condition)); + takeWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable { + return new FluentAsync(takeWhileAsyncIterator(this, condition)); } - skip(count: number): FluentIterableAsync { - return new FluentAsync(skipIteratorAsync(this, count)); + skip(count: number): FluentAsyncIterable { + return new FluentAsync(skipAsyncIterator(this, count)); } - skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterableAsync { - return new FluentAsync(skipWhileIteratorAsync(this, condition)); + skipWhile(condition: (item: TValue, index: number) => boolean): FluentAsyncIterable { + return new FluentAsync(skipWhileAsyncIterator(this, condition)); } - distinct(keySelector?: (item: TValue) => TKey): FluentIterableAsync { - return new FluentAsync(distinctIteratorAsync(this, keySelector)); + distinct(keySelector?: (item: TValue) => TKey): FluentAsyncIterable { + return new FluentAsync(distinctAsyncIterator(this, keySelector)); } groupBy(keySelector: (item: TValue, index: number) => TKey): [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] ? - FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> - : FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterableAsync>; - groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync; + FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + : FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; + groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector?: (item: TValue, index: number) => TElement, - resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentIterableAsync | IGrouping | TResult> { + resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable | IGrouping | TResult> { return new FluentAsync(groupByAsyncIterator(this, keySelector, elementSelector, resultCreator)); } toArray(): Promise; @@ -80,9 +80,9 @@ export default class FluentAsync implements FluentIterableAsync } } -export class FluentAsyncPromise extends FluentAsync> implements FluentIterableAsyncPromise { - groupByStatus(): FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { - return super.groupBy(x => x.status) as FluentIterableAsync< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; +export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { + return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; } toStatusMap(): Promise> { diff --git a/src/iterables/select.ts b/src/iterables/select.ts index bfab9b8..28e5823 100644 --- a/src/iterables/select.ts +++ b/src/iterables/select.ts @@ -14,7 +14,7 @@ function* selectGenerator(input: Iterable, map: Mapper< } } -export function selectIteratorAsync(input: AsyncIterable, map: Mapper): AsyncIterable { +export function selectAsyncIterator(input: AsyncIterable, map: Mapper): AsyncIterable { return { [Symbol.asyncIterator]: async function* () { for await (const item of input) { diff --git a/src/iterables/set-iterators.ts b/src/iterables/set-iterators.ts index 17057c0..aa1832f 100644 --- a/src/iterables/set-iterators.ts +++ b/src/iterables/set-iterators.ts @@ -16,7 +16,7 @@ export function distinctIterator(source: Iterable, }; } -export function distinctIteratorAsync(source: AsyncIterable, keySelector?: (item: TValue) => TKey): AsyncIterable { +export function distinctAsyncIterator(source: AsyncIterable, keySelector?: (item: TValue) => TKey): AsyncIterable { const keySelectorFunc = keySelector ?? defaultKeySelector; return { [Symbol.asyncIterator]: async function* (){ diff --git a/src/iterables/skip-while.ts b/src/iterables/skip-while.ts index 9f52624..6ca6c70 100644 --- a/src/iterables/skip-while.ts +++ b/src/iterables/skip-while.ts @@ -20,7 +20,7 @@ function* skipWhileGenerator(input: Iterable, condition: (item: } } -export function skipWhileIteratorAsync(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { +export function skipWhileAsyncIterator(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let flag = false; diff --git a/src/iterables/skip.ts b/src/iterables/skip.ts index 3fa5df7..85dd631 100644 --- a/src/iterables/skip.ts +++ b/src/iterables/skip.ts @@ -18,7 +18,7 @@ function* skipGenerator(input: Iterable, count: number): Generat } } -export function skipIteratorAsync(input: AsyncIterable, count: number): AsyncIterable { +export function skipAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let skipped = 0; diff --git a/src/iterables/take-while.ts b/src/iterables/take-while.ts index 55b6778..cc850db 100644 --- a/src/iterables/take-while.ts +++ b/src/iterables/take-while.ts @@ -18,7 +18,7 @@ function* takeWhileGenerator(input: Iterable, condition: (item: } } -export function takeWhileIteratorAsync(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { +export function takeWhileAsyncIterator(input: AsyncIterable, condition: (item: TValue, index: number) => boolean): AsyncIterable { return { [Symbol.asyncIterator]: async function* (){ let index = 0; diff --git a/src/iterables/take.ts b/src/iterables/take.ts index 6c393ca..a889041 100644 --- a/src/iterables/take.ts +++ b/src/iterables/take.ts @@ -17,7 +17,7 @@ export function takeIterator(input: Iterable, count: number): It } } -export default function takeIteratorAsync(input: AsyncIterable, count: number): AsyncIterable { +export default function takeAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { return { [Symbol.asyncIterator]: async function* () { let fetched = 0; diff --git a/src/utils.ts b/src/utils.ts index c0e353c..4a4fc3e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import {Comparer} from "./interfaces.ts"; import {from, fromIterable} from "./creation.js"; -import { FluentIterable, FluentIterableAsync} from "fluent-iter"; +import { FluentIterable, FluentAsyncIterable} from "fluent-iter"; /** * Helper function to be use to access Symbol.iterator of iterable From f4212cc8606213a74943487d56a0e0777d28fbc7 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 22:58:45 +0100 Subject: [PATCH 2/6] feat: add page async operator Signed-off-by: vmladenov --- index.d.ts | 6 +++ src/fluent-async.ts | 4 ++ src/fluent.ts | 103 ++++++++++++++++++++++++++++++++--------- src/iterables/page.ts | 20 +++++++- test/unit/page.spec.ts | 31 ++++++++++++- 5 files changed, 139 insertions(+), 25 deletions(-) diff --git a/index.d.ts b/index.d.ts index 0c6df94..1cd1023 100644 --- a/index.d.ts +++ b/index.d.ts @@ -495,6 +495,12 @@ declare module 'fluent-iter' { groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; + /** + * Create a paging + * @param pageSize + */ + page(pageSize: number): FluentAsyncIterable; + /** * Return a promise to an array. */ diff --git a/src/fluent-async.ts b/src/fluent-async.ts index d7e48ff..3f65d94 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -18,6 +18,7 @@ import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; import {skipAsyncIterator} from "./iterables/skip.ts"; import {skipWhileAsyncIterator} from "./iterables/skip-while.ts"; import {distinctAsyncIterator} from "./iterables/set-iterators.ts"; +import {pageAsyncIterator} from "./iterables/page.ts"; export default class FluentAsync implements FluentAsyncIterable { readonly #source: AsyncIterable; @@ -60,6 +61,9 @@ export default class FluentAsync implements FluentAsyncIterable resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable | IGrouping | TResult> { return new FluentAsync(groupByAsyncIterator(this, keySelector, elementSelector, resultCreator)); } + page(pageSize: number): FluentAsyncIterable { + return new FluentAsync(pageAsyncIterator(this, pageSize)); + } toArray(): Promise; toArray(map: Mapper): Promise; toArray(map?: Mapper): Promise<(TValue|TResult)[]> { diff --git a/src/fluent.ts b/src/fluent.ts index 5919515..6e2d969 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -1,11 +1,11 @@ -import { whereIterator } from "./iterables/where.ts"; -import { selectIterator } from "./iterables/select.ts"; -import { selectManyIterator, flatIterator, flatMapIterator } from "./iterables/select-many.ts"; -import { takeIterator } from "./iterables/take.ts"; -import { skipIterator } from "./iterables/skip.ts"; -import { toArrayCollector } from "./finalizers/to-array.ts"; -import { takeWhileIterator } from "./iterables/take-while.ts"; -import { skipWhileIterator } from "./iterables/skip-while.ts"; +import {whereIterator} from "./iterables/where.ts"; +import {selectIterator} from "./iterables/select.ts"; +import {selectManyIterator, flatIterator, flatMapIterator} from "./iterables/select-many.ts"; +import {takeIterator} from "./iterables/take.ts"; +import {skipIterator} from "./iterables/skip.ts"; +import {toArrayCollector} from "./finalizers/to-array.ts"; +import {takeWhileIterator} from "./iterables/take-while.ts"; +import {skipWhileIterator} from "./iterables/skip-while.ts"; import takeLastIterator from "./iterables/take-last.ts"; import skipLastIterator from "./iterables/skip-last.ts"; import {allAndEveryCollector, allCollector} from "./finalizers/all.ts"; @@ -20,7 +20,7 @@ import {single, singleOrDefault} from "./finalizers/single.ts"; import {last, lastIndex, lastOrDefault, lastOrThrow} from "./finalizers/last.ts"; import {first, firstIndex, firstOrDefault, firstOrThrow} from "./finalizers/first.ts"; import reverseIterator from "./iterables/reverse.ts"; -import pageIterator from "./iterables/page.ts"; +import {pageIterator} from "./iterables/page.ts"; import { diffIterator, distinctIterator, @@ -36,7 +36,7 @@ import groupJoinIterator from "./iterables/group-join.ts"; import zipIterable from "./iterables/zip.js"; import type {Action, Comparer, Mapper, Predicate} from "./interfaces.ts"; -import type {FlatFluentIterable, FluentIterable, IGrouping } from 'fluent-iter'; +import type {FlatFluentIterable, FluentIterable, IGrouping} from 'fluent-iter'; export default class Fluent implements FluentIterable { readonly #source: Iterable; @@ -50,50 +50,67 @@ export default class Fluent implements FluentIterable { where(predicate: Predicate): FluentIterable | FluentIterable { return new Fluent(whereIterator(this, predicate)); } + select(map: Mapper): FluentIterable { return new Fluent(selectIterator(this, map)); } + flat(depth: number = 1): FlatFluentIterable { return new Fluent(flatIterator(this, depth)) as any; } + flatMap(mapper: (value: TValue) => TResult | ReadonlyArray): FluentIterable { return new Fluent(flatMapIterator(this, mapper)); } + selectMany(innerSelector: (item: TValue) => TInner[], resultCreator?: (outer: TValue, inner: TInner) => TResult): FluentIterable { if (typeof resultCreator !== 'undefined') { return new Fluent(selectManyIterator(this, innerSelector, resultCreator)); } return new Fluent(selectManyIterator(this, innerSelector)); } + take(count: number): FluentIterable { return new Fluent(takeIterator(this, count)); } + takeWhile(condition: (item: TValue, index: number) => boolean): FluentIterable { return new Fluent(takeWhileIterator(this, condition)); } + takeLast(count: number): FluentIterable { return new Fluent(takeLastIterator(this, count)); } + skip(count: number): FluentIterable { return new Fluent(skipIterator(this, count)); } + skipWhile(condition: (item: TValue, index: number) => boolean): FluentIterable { return new Fluent(skipWhileIterator(this, condition)); } + skipLast(count: number): FluentIterable { return new Fluent(skipLastIterator(this, count)); } + distinct(keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(distinctIterator(this, keySelector)); } - ofType(type: 'string'|'number'|'boolean'|'undefined'|'function'|'object'|'symbol'|((item: TValue) => item is TOutput)): FluentIterable { + + ofType(type: 'string' | 'number' | 'boolean' | 'undefined' | 'function' | 'object' | 'symbol' | ((item: TValue) => item is TOutput)): FluentIterable { const filter = typeof type === 'function' ? type : (item: TValue) => typeof item === type; return new Fluent(whereIterator(this, filter) as Iterable); } - ofClass(type: { new (...args: any[]): TOutput, prototype: TOutput }): FluentIterable { + + ofClass(type: { + new(...args: any[]): TOutput, + prototype: TOutput + }): FluentIterable { const filter = (item: TValue) => item instanceof type; return new Fluent(whereIterator(this, filter) as Iterable); } + groupBy(keySelector: (item: TValue, index: number) => TKey): FluentIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentIterable; @@ -102,18 +119,22 @@ export default class Fluent implements FluentIterable { resultCreator?: (key: TKey, items: FluentIterable) => TResult): FluentIterable | IGrouping | TResult> { return new Fluent(groupByIterator(this, keySelector, elementSelector, resultCreator)); } + orderBy(keySelector: (item: TValue) => TKey, comparer?: Comparer): FluentIterable { return new Fluent(sortAscendingIterator(this, keySelector, comparer)); } + orderByDescending(keySelector: (item: TValue) => TKey, comparer?: Comparer): FluentIterable { return new Fluent(sortDescendingIterator(this, keySelector, comparer)); } + groupJoin(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, joinIterableKeySelector: (item: TInner, index: number) => TKey, resultCreator: (outer: TValue, inner: FluentIterable & TInner[]) => TResult): FluentIterable { return new Fluent(groupJoinIterator(this, joinIterable, sourceKeySelector, joinIterableKeySelector, resultCreator as any)); } + join(separator: string): string; join(joinIterable: Iterable, sourceKeySelector: (item: TValue) => TKey, @@ -128,96 +149,125 @@ export default class Fluent implements FluentIterable { } return new Fluent(joinIterator(this, firstArgument, sourceKeySelector!, joinIterableKeySelector!, resultCreator!)); } + concat(secondIterable: Iterable): FluentIterable { return new Fluent(concatIterator(this, secondIterable)); } - union(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + union(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(unionIterator(this, secondIterable, keySelector)); } - intersect(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + intersect(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(intersectIterator(this, secondIterable, keySelector)); } - difference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + difference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(diffIterator(this, secondIterable, keySelector)); } - symmetricDifference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { + + symmetricDifference(secondIterable: Iterable, keySelector?: (item: TValue) => TKey): FluentIterable { return new Fluent(symmetricDiffIterator(this, secondIterable, keySelector)); } + page(pageSize: number): FluentIterable { return new Fluent(pageIterator(this, pageSize)); } + reverse(): FluentIterable { return new Fluent(reverseIterator(this)); } + toArray(): TValue[]; toArray(map: Mapper): TResult[]; toArray(map?: Mapper): TValue[] | TResult[] { return toArrayCollector(this, map); } + zip(second: Iterable): FluentIterable<[TValue, TOuter]> { return new Fluent(zipIterable(this, second)); } + toMap(keySelector: (item: TValue) => TKey): Map; toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Map; - toMap(keySelector: (item: TValue) => TKey, elementSelector?: (item: TValue) => TElement): Map { + toMap(keySelector: (item: TValue) => TKey, elementSelector?: (item: TValue) => TElement): Map { return new Map(this.select(item => [keySelector(item), elementSelector ? elementSelector(item) : item])); } + toSet(): Set { return new Set(this); } + first(predicate?: Predicate): TValue | undefined { return first(this, predicate); } + firstOrDefault(def: TValue, predicate?: Predicate): TValue { return firstOrDefault(this, def, predicate); } + firstOrThrow(predicate?: Predicate): TValue | never { return firstOrThrow(this, predicate); } + firstIndex(predicate: Predicate): number { return firstIndex(this, predicate); } + last(predicate?: Predicate): TValue | undefined { return last(this, predicate); } + lastOrDefault(def: TValue, predicate?: Predicate): TValue { return lastOrDefault(this, def, predicate); } + lastOrThrow(predicate?: Predicate): TValue | never { return lastOrThrow(this, predicate); } + lastIndex(predicate: (item: TValue) => boolean): number { return lastIndex(this, predicate); } + single(predicate?: Predicate): TValue | never { return single(this, predicate); } + singleOrDefault(def: TValue, predicate?: Predicate): TValue | never { return singleOrDefault(this, def, predicate); } + all(predicate: (item: TValue) => boolean): boolean { return allCollector(this, predicate); } + allAndEvery(predicate: (item: TValue) => boolean): boolean { return allAndEveryCollector(this, predicate); } + any(predicate?: ((item: TValue) => boolean) | undefined): boolean { return anyCollector(this, predicate); } + count(predicate?: ((item: TValue) => boolean) | undefined): number { return countCollector(this, predicate); } + aggregate(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; - aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; - aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial?: TResult): TResult | TValue | never { + aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial?: TResult): TResult | TValue | never { return aggregateCollector(this, accumulator, initial); } + sum(): TValue { return aggregateCollector(this, (a, i) => (a as any) + (i as any) as any); } + product(): TValue { return aggregateCollector(this, (a, i) => (a as any) * (i as any) as any); } + min(comparer?: Comparer): TValue | never { const compare = comparer ?? defaultSortComparer; return aggregateCollector(this, (a, b) => { @@ -225,6 +275,7 @@ export default class Fluent implements FluentIterable { return comp < 0 ? a : (comp > 0 ? b : a); }); } + max(comparer?: Comparer): TValue | never { const compare = typeof comparer === 'undefined' ? defaultSortComparer : comparer; return aggregateCollector(this, (a, b) => { @@ -232,22 +283,27 @@ export default class Fluent implements FluentIterable { return comp < 0 ? b : (comp > 0 ? a : b); }); } + elementAt(index: number): TValue | undefined { return elementAtCollector(this, index); } + forEach(action: Action): void { return forEachCollector(this, action); } + isEqual(iterable: Iterable): boolean; - isEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; - isEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { + isEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; + isEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { return sequenceEqual(this, second, comparer); } + isElementsEqual(iterable: Iterable): boolean; - isElementsEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; - isElementsEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { + isElementsEqual(second: Iterable, comparer: (a: TValue, b: TSecond) => boolean): boolean; + isElementsEqual(second: Iterable, comparer?: (first: TValue, second: TSecond) => boolean): boolean { return isElementsEqual(this, second, comparer); } + [Symbol.iterator](): Iterator { return this.#source[Symbol.iterator](); } @@ -255,6 +311,7 @@ export default class Fluent implements FluentIterable { export class Grouping extends Fluent implements IGrouping { readonly #key: TKey; + constructor(key: TKey, items: Iterable) { super(items); this.#key = key; diff --git a/src/iterables/page.ts b/src/iterables/page.ts index 50f1c3f..868ca85 100644 --- a/src/iterables/page.ts +++ b/src/iterables/page.ts @@ -1,6 +1,6 @@ import {createIterable} from "../utils.ts"; -export default function pageIterator(source: Iterable, pageSize: number): Iterable { +export function pageIterator(source: Iterable, pageSize: number): Iterable { return createIterable(() => pageGenerator(source, pageSize)); } @@ -17,3 +17,21 @@ function* pageGenerator(source: Iterable, pageSize: number): Gen yield page; } } + +export function pageAsyncIterator(source: AsyncIterable, pageSize: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + let page: TValue[] = []; + for await (const item of source) { + page.push(item); + if (page.length === pageSize) { + yield page; + page = []; + } + } + if (page.length > 0) { + yield page; + } + } + } +} diff --git a/test/unit/page.spec.ts b/test/unit/page.spec.ts index a87ba6a..9aa05c5 100644 --- a/test/unit/page.spec.ts +++ b/test/unit/page.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from "vitest"; -import { from, range } from "../../src/index.ts"; +import {from, fromTimer, range} from "../../src/index.ts"; +import {emptyAsyncIterable} from "../test-utils.ts"; describe('page iteration tests', () => { [ @@ -34,3 +35,31 @@ describe('page iteration tests', () => { expect(res1.length).toBe(2); }); }); + +describe('page async tests', () => { + it('should page async iterable', async () => { + let i = 0; + for await (const page of fromTimer(1).take(10).page(4)) { + switch (i) { + case 0: { + expect(page).toStrictEqual([0, 1, 2, 3]); + break; + } + case 1: { + expect(page).toStrictEqual([4, 5, 6, 7]); + break; + } + case 2: { + expect(page).toStrictEqual([8, 9]) + } + } + i++; + } + expect(i).toBe(3); + }); + + it('should return empty array when no items', async () => { + const res = await from(emptyAsyncIterable).page(4).toArray(); + expect(res).toStrictEqual([]); + }) +}); \ No newline at end of file From c6bcc03ea4932b7ca5db23e1183991dbda8238ee Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sat, 1 Nov 2025 23:33:58 +0100 Subject: [PATCH 3/6] feat: use builtin types Signed-off-by: vmladenov --- index.d.ts | 34 ++++++++++------------------------ src/fluent-async.ts | 17 +++++++---------- src/generators/promises.ts | 12 ++++++------ 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/index.d.ts b/index.d.ts index 1cd1023..e374222 100644 --- a/index.d.ts +++ b/index.d.ts @@ -489,8 +489,8 @@ declare module 'fluent-iter' { * @param keySelector group key selector */ groupBy(keySelector: (item: TValue, index: number) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] - ? FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseSettledResult] + ? FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> : FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; @@ -512,7 +512,7 @@ declare module 'fluent-iter' { * @param keySelector - key selector - keys should be unique, otherwise last keys will override first. */ toMap(keySelector: (item: TValue) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>] + [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>] ? Promise> : Promise>; @@ -524,28 +524,14 @@ declare module 'fluent-iter' { toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; } - export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { - groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; + export interface FluentAsyncIterablePromise extends FluentAsyncIterable> { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>>; toStatusMap(): Promise>; } - type PromiseResult = FulfilledPromiseResult | RejectedPromiseResult; - - interface FulfilledPromiseResult { - index: number; - status: 'fulfilled'; - value: T; - } - - interface RejectedPromiseResult { - index: number; - status: 'rejected'; - reason: any; - } - - interface PromiseMap extends Map<'fulfilled'|'rejected', FluentIterable>> { - get(key: 'fulfilled'): FluentIterable> | undefined; - get(key: 'rejected'): FluentIterable | undefined; + interface PromiseMap extends Map<'fulfilled'|'rejected', FluentIterable>> { + get(key: 'fulfilled'): FluentIterable> | undefined; + get(key: 'rejected'): FluentIterable | undefined; } type FlatFluentIterable = Value extends ReadonlyArray @@ -567,6 +553,6 @@ declare module 'fluent-iter' { export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable; export function fromTimer(interval: number, delay?: number): FluentAsyncIterable; export function fromPromises(...promises: Promise[]): FluentAsyncIterablePromise; - export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult; - export function isRejected(result: PromiseResult): result is RejectedPromiseResult; + export function isFulfilled(result: PromiseSettledResult): result is PromiseFulfilledResult; + export function isRejected(result: PromiseSettledResult): result is PromiseRejectedResult; } \ No newline at end of file diff --git a/src/fluent-async.ts b/src/fluent-async.ts index 3f65d94..c88dd79 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -2,11 +2,8 @@ import type { FluentIterable, FluentAsyncIterable, FluentAsyncIterablePromise, - FulfilledPromiseResult, IGrouping, - PromiseMap, - PromiseResult, - RejectedPromiseResult + PromiseMap } from 'fluent-iter'; import type {Mapper, Predicate} from "./interfaces.js"; import {whereAsyncIterator} from "./iterables/where.js"; @@ -51,8 +48,8 @@ export default class FluentAsync implements FluentAsyncIterable return new FluentAsync(distinctAsyncIterator(this, keySelector)); } groupBy(keySelector: (item: TValue, index: number) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseResult] ? - FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> + [TKey, TValue] extends ['fulfilled' | 'rejected', PromiseSettledResult] ? + FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> : FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement): FluentAsyncIterable>; groupBy(keySelector: (item: TValue, index: number) => TKey, elementSelector: (item: TValue, index: number) => TElement, resultCreator: (key: TKey, items: FluentIterable) => TResult): FluentAsyncIterable; @@ -71,7 +68,7 @@ export default class FluentAsync implements FluentAsyncIterable } toMap(keySelector: (item: TValue) => TKey): - [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>] + [TKey, TValue] extends ['fulfilled' | 'rejected', IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>] ? Promise> : Promise>; toMap(keySelector: (item: TValue) => TKey, elementSelector: (item: TValue) => TElement): Promise>; @@ -84,9 +81,9 @@ export default class FluentAsync implements FluentAsyncIterable } } -export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { - groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>> { - return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', FulfilledPromiseResult> | IGrouping<'rejected', RejectedPromiseResult>>; +export class FluentAsyncPromise extends FluentAsync> implements FluentAsyncIterablePromise { + groupByStatus(): FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>> { + return super.groupBy(x => x.status) as FluentAsyncIterable< IGrouping<'fulfilled', PromiseFulfilledResult> | IGrouping<'rejected', PromiseRejectedResult>>; } toStatusMap(): Promise> { diff --git a/src/generators/promises.ts b/src/generators/promises.ts index db94cb4..6c347d0 100644 --- a/src/generators/promises.ts +++ b/src/generators/promises.ts @@ -1,16 +1,16 @@ -import {FulfilledPromiseResult, PromiseResult, RejectedPromiseResult } from "fluent-iter"; import {createAsyncIterable} from "../utils.ts"; export function fromPromisesIterable( ...promises: Promise[] -): AsyncIterable> { +): AsyncIterable> { return createAsyncIterable(() => fromPromisesGenerator(...promises)); } +type IndexedPromiseSettledResult = PromiseSettledResult & { index: number }; async function* fromPromisesGenerator( ...promises: Promise[] -): AsyncGenerator> { - const pending = new Map>>( +): AsyncGenerator> { + const pending = new Map>>( promises.map((p, index) => [ index, p.then( @@ -27,10 +27,10 @@ async function* fromPromisesGenerator( } } -export function isFulfilled(result: PromiseResult): result is FulfilledPromiseResult { +export function isFulfilled(result: PromiseSettledResult): result is PromiseFulfilledResult { return result.status === 'fulfilled'; } -export function isRejected(result: PromiseResult): result is RejectedPromiseResult { +export function isRejected(result: PromiseSettledResult): result is PromiseRejectedResult { return result.status === 'rejected'; } From f5154f8144e8aeb207d76541368427c549a4f1d4 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:21:23 +0100 Subject: [PATCH 4/6] feat: improve subjects --- src/creation-async.ts | 4 +-- src/fluent-async.ts | 10 +++---- src/fluent.ts | 6 ++--- src/iterables/group.ts | 2 +- src/iterables/skip-last.ts | 55 +++++++++++++++----------------------- src/iterables/take-last.ts | 50 ++++++++++++++++------------------ src/iterables/zip.ts | 21 ++++++++++++++- src/utils.ts | 15 ++++++++++- test/unit/to-array.spec.ts | 2 +- test/unit/union.spec.ts | 2 +- 10 files changed, 91 insertions(+), 76 deletions(-) diff --git a/src/creation-async.ts b/src/creation-async.ts index 9fe914a..c5c5265 100644 --- a/src/creation-async.ts +++ b/src/creation-async.ts @@ -1,7 +1,7 @@ import type { FluentAsyncIterable, FluentAsyncIterablePromise } from 'fluent-iter'; -import FluentAsync, {FluentAsyncPromise} from "./fluent-async.js"; +import FluentAsync, {FluentAsyncPromise} from "./fluent-async.ts"; import fromEventAsync from "./generators/from-event.ts"; -import fromTimerAsync from "./generators/from-timer.js"; +import fromTimerAsync from "./generators/from-timer.ts"; import {fromPromisesIterable} from "./generators/promises.ts"; export function fromEvent(target: TTarget, event: TEvent): FluentAsyncIterable { diff --git a/src/fluent-async.ts b/src/fluent-async.ts index c88dd79..17a1cbb 100644 --- a/src/fluent-async.ts +++ b/src/fluent-async.ts @@ -5,11 +5,11 @@ import type { IGrouping, PromiseMap } from 'fluent-iter'; -import type {Mapper, Predicate} from "./interfaces.js"; -import {whereAsyncIterator} from "./iterables/where.js"; -import {selectAsyncIterator} from "./iterables/select.js"; -import takeAsyncIterator from "./iterables/take.js"; -import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.js"; +import type {Mapper, Predicate} from "./interfaces.ts"; +import {whereAsyncIterator} from "./iterables/where.ts"; +import {selectAsyncIterator} from "./iterables/select.ts"; +import takeAsyncIterator from "./iterables/take.ts"; +import {toArrayAsyncCollector, toMapAsyncCollector} from "./finalizers/to-array.ts"; import {groupByAsyncIterator} from "./iterables/group.ts"; import {takeWhileAsyncIterator} from "./iterables/take-while.ts"; import {skipAsyncIterator} from "./iterables/skip.ts"; diff --git a/src/fluent.ts b/src/fluent.ts index 6e2d969..bf692c7 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -6,8 +6,8 @@ import {skipIterator} from "./iterables/skip.ts"; import {toArrayCollector} from "./finalizers/to-array.ts"; import {takeWhileIterator} from "./iterables/take-while.ts"; import {skipWhileIterator} from "./iterables/skip-while.ts"; -import takeLastIterator from "./iterables/take-last.ts"; -import skipLastIterator from "./iterables/skip-last.ts"; +import {takeLastIterator} from "./iterables/take-last.ts"; +import {skipLastIterator} from "./iterables/skip-last.ts"; import {allAndEveryCollector, allCollector} from "./finalizers/all.ts"; import anyCollector from "./finalizers/any.ts"; import countCollector from "./finalizers/count.ts"; @@ -33,7 +33,7 @@ import {sortAscendingIterator, sortDescendingIterator} from "./iterables/order.t import {groupByIterator} from "./iterables/group.ts"; import joinIterator from "./iterables/join.ts"; import groupJoinIterator from "./iterables/group-join.ts"; -import zipIterable from "./iterables/zip.js"; +import {zipIterable} from "./iterables/zip.ts"; import type {Action, Comparer, Mapper, Predicate} from "./interfaces.ts"; import type {FlatFluentIterable, FluentIterable, IGrouping} from 'fluent-iter'; diff --git a/src/iterables/group.ts b/src/iterables/group.ts index 88455c5..9de3413 100644 --- a/src/iterables/group.ts +++ b/src/iterables/group.ts @@ -1,6 +1,6 @@ import type {FluentIterable, IGrouping} from "fluent-iter"; import {createAsyncIterable, createIterable, group, groupAsync} from "../utils.ts"; -import {Grouping} from "../fluent.js"; +import {Grouping} from "../fluent.ts"; export function groupByIterator( source: Iterable, diff --git a/src/iterables/skip-last.ts b/src/iterables/skip-last.ts index 7b3620c..f3fc3e4 100644 --- a/src/iterables/skip-last.ts +++ b/src/iterables/skip-last.ts @@ -1,43 +1,30 @@ /** * Return skip last N elements from sequence */ -import {doneValue, getIterator, iteratorResultCreator} from "../utils.ts"; - -export default function skipLastIterator(input: Iterable, count: number): Iterable { - return new SkipLastIterable(input, count); -} - -export class SkipLastIterable implements Iterable { - readonly #source: Iterable; - readonly #count: number; - /** - * - * @param {Iterable} source - * @param {number} count - */ - constructor(source: Iterable, count: number) { - this.#source = source; - this.#count = count <= 0 ? 0 : count; +export function skipLastIterator(input: Iterable, count: number): Iterable { + return { + [Symbol.iterator]: function* () { + const keep: TValue[] = []; + for (const item of input) { + keep.push(item); + if (keep.length > count) { + yield keep.shift() as TValue; + } + } + } } +} - [Symbol.iterator]() { - const iterator = getIterator(this.#source); - const count = this.#count; - const keep: TValue[] = []; - let next: IteratorResult = iteratorResultCreator(void 0 as TValue); - return { - next() { - while (!next.done && keep.length <= count) { - next = iterator.next(); - if (!next.done) { - keep.push(next.value); - } - } - if (next.done) { - return doneValue(); +export function skipLastAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + const keep: TValue[] = []; + for await (const item of input) { + keep.push(item); + if (keep.length > count) { + yield keep.shift() as TValue; } - return iteratorResultCreator(keep.shift() as TValue); } - }; + } } } diff --git a/src/iterables/take-last.ts b/src/iterables/take-last.ts index 2723608..71898f0 100644 --- a/src/iterables/take-last.ts +++ b/src/iterables/take-last.ts @@ -1,36 +1,32 @@ /** * Take last N elements */ -import {createIterable} from "../utils.ts"; - -export default function takeLastIterator(input: Iterable, count: number): Iterable { - return createIterable(() => takeLastGenerator(input, count)); -} - -function* takeLastGenerator(input: Iterable, count: number): Generator { - const queue = new LimitedQueue(count); - for (const item of input) { - queue.push(item); +export function takeLastIterator(input: Iterable, count: number): Iterable { + return { + [Symbol.iterator]: function* () { + const keep: TValue[] = []; + for (const item of input) { + keep.push(item); + if (keep.length > count) { + keep.shift(); + } + } + yield* keep; + } } - yield* queue; } -class LimitedQueue implements Iterable { - readonly #container: TItem[]; - readonly #limit: number; - constructor(limit: number) { - this.#container = []; - this.#limit = limit; - } - - push(item: TItem) { - this.#container.push(item); - if (this.#container.length > this.#limit) { - this.#container.shift(); +export function takeLastAsyncIterator(input: AsyncIterable, count: number): AsyncIterable { + return { + [Symbol.asyncIterator]: async function* () { + const keep: TValue[] = []; + for await (const item of input) { + keep.push(item); + if (keep.length > count) { + keep.shift(); + } + } + yield* keep; } } - - [Symbol.iterator]() { - return this.#container[Symbol.iterator](); - } } diff --git a/src/iterables/zip.ts b/src/iterables/zip.ts index fd28044..3f06489 100644 --- a/src/iterables/zip.ts +++ b/src/iterables/zip.ts @@ -1,5 +1,7 @@ // combines two iterables -export default function zipIterable(first: Iterable, second: Iterable): Iterable<[TThis, TOuter]> { +import {isFulfilled} from "../generators/promises.ts"; + +export function zipIterable(first: Iterable, second: Iterable): Iterable<[TThis, TOuter]> { return { [Symbol.iterator]: function* () { const firstIterator = first[Symbol.iterator](); @@ -15,3 +17,20 @@ export default function zipIterable(first: Iterable, secon } } } + +export function zipAsyncIterable(first: AsyncIterable, second: AsyncIterable): AsyncIterable<[TThis, TOuter]> { + return { + [Symbol.asyncIterator]: async function* () { + const firstIterator = first[Symbol.asyncIterator](); + const secondIterator = second[Symbol.asyncIterator](); + while (true) { + const [first, second] = await Promise.allSettled([firstIterator.next(), secondIterator.next()]); + if (isFulfilled(first) && isFulfilled(second) && !first.value.done && !second.value.done) { + yield [first.value.value, second.value.value]; + } else { + break; + } + } + } + } +} diff --git a/src/utils.ts b/src/utils.ts index 4a4fc3e..67765cd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ import {Comparer} from "./interfaces.ts"; -import {from, fromIterable} from "./creation.js"; +import {from, fromIterable} from "./creation.ts"; import { FluentIterable, FluentAsyncIterable} from "fluent-iter"; /** @@ -159,6 +159,19 @@ export function emptyIterator(): Iterator { }; } + +function extracted(keySelector: (item: TValue, index: number) => TKey, item: TValue, i: number, elementSelector: (item: TValue, index: number) => TElement, map: Map) { + const key = keySelector(item, i); + if ((key !== null && typeof key === 'object') || typeof key === "function") { + throw new TypeError('groupBy method does not support keys to be objects or functions'); + } + const element = elementSelector(item, i); + const value = map.get(key) || []; + value.push(element); + map.set(key, value); + i++; +} + export function group( iterable: Iterable, keySelector: (item: TValue, index: number) => TKey, diff --git a/test/unit/to-array.spec.ts b/test/unit/to-array.spec.ts index 5a43afe..e546e8c 100644 --- a/test/unit/to-array.spec.ts +++ b/test/unit/to-array.spec.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest"; -import {fromTimer} from "../../src/index.js"; +import {fromTimer} from "../../src/index.ts"; describe('to array', () => { it('should get an array from async iterable', async () => { diff --git a/test/unit/union.spec.ts b/test/unit/union.spec.ts index a1a6817..be94ff8 100644 --- a/test/unit/union.spec.ts +++ b/test/unit/union.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { from, range } from "../../src/index.ts"; -import {Person} from "./models.js"; +import {Person} from "./models.ts"; describe('union tests', () => { [ From 7aeb9a42e60656c86b12915141404508d9fec3eb Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:50:36 +0100 Subject: [PATCH 5/6] feat: Add alias functions for better compatible with Array Alias functions added: - filter - map - find - findIndex - findLast - findLastIndex - every - some - reduce - at --- index.d.ts | 89 ++++++++++++++++++++++++++++++++++++++ src/finalizers/to-array.ts | 7 +-- src/fluent.ts | 19 ++++++++ test/unit/where.spec.ts | 12 ++++- 4 files changed, 123 insertions(+), 4 deletions(-) diff --git a/index.d.ts b/index.d.ts index e374222..e128ed6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,18 +6,39 @@ declare module 'fluent-iter' { */ where(predicate: (item: TValue) => item is TSubValue): FluentIterable; + /** + * Filters the iterable using predicate function typed overload. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => item is TSubValue): FluentIterable; + /** * Filters the iterable using predicate function * @param predicate */ where(predicate: (item: TValue) => boolean): FluentIterable; + /** + * Filters the iterable using predicate function. Alias to where. + * @see where + * @param predicate + */ + filter(predicate: (item: TValue) => boolean): FluentIterable; + /** * Maps the iterable items * @param map map function */ select(map: (item: TValue) => TOutput): FluentIterable; + /** + * Maps the iterable items. Alias to select. + * @see select + * @param map map function + */ + map(map: (item: TValue) => TOutput): FluentIterable; + /** * Flat Iterable of collections up to N levels, default is 1 level. * @param depth @@ -274,6 +295,13 @@ declare module 'fluent-iter' { */ first(predicate?: (item: TValue) => boolean): TValue | undefined; + /** + * Get first item of iterable satisfiying the predicate. Alias for first. + * @see first + * @param predicate predicate for the item + */ + find(predicate: (item: TValue) => boolean): TValue | undefined; + /** * Get first item of iterable, if does not contain any return default * @param def @@ -293,12 +321,27 @@ declare module 'fluent-iter' { */ firstIndex(predicate: (item: TValue) => boolean): number; + /** + * Get index of first found item in sequence. Alias for firstIndex. + * @see firstIndex + * @param predicate predicate for the item + * @return index of item, when not found -1 + */ + findIndex(predicate: (item: TValue) => boolean): number; + /** * Get last item of iterable * @param predicate optional predicate for the item */ last(predicate?: (item: TValue) => boolean): TValue | undefined; + /** + * Get last item of iterable satisfiying the predicate. Alias for last + * @see last + * @param predicate optional predicate for the item + */ + findLast(predicate: (item: TValue) => boolean): TValue | undefined; + /** * Get last item of iterable, if does not contain any return default * @param def @@ -319,6 +362,14 @@ declare module 'fluent-iter' { */ lastIndex(predicate: (item: TValue) => boolean): number; + /** + * Get index of last found item in sequence satisfiying the predicate. Alias for lastIndex. + * @see lastIndex + * @param predicate predicate for the item + * @return index of item, when not found -1 + */ + findLastIndex(predicate: (item: TValue) => boolean): number; + /** * Checks if iterable has only one item and returns it. * @throws TypeError when no or multiple elements @@ -338,6 +389,13 @@ declare module 'fluent-iter' { */ all(predicate: (item: TValue) => boolean): boolean; + /** + * Returns if all items satisfy the predicate. It returns true if no items. Alias for all. + * @see all + * @param predicate + */ + every(predicate: (item: TValue) => boolean): boolean; + /** * Returns if all items satisfy the predicate. It returns false if no items. * @param predicate @@ -350,6 +408,13 @@ declare module 'fluent-iter' { */ any(predicate?: (item: TValue) => boolean): boolean; + /** + * Returns if any items satisfy the predicate. Alias for any. + * @see any + * @param predicate + */ + some(predicate?: (item: TValue) => boolean): boolean; + /** * Return count of items. * @param predicate if predicate is supplied then return the count of items that return true. @@ -363,6 +428,14 @@ declare module 'fluent-iter' { */ aggregate(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; + /** + * Produce single value form sequence values. The initial value is first element. Alias for aggregate. + * @see aggregate + * @param accumulator function which produces the result. + * @throws TypeError when no elements + */ + reduce(accumulator: (result: TValue, item: TValue, index: number) => TValue): TValue | never; + /** * Produce single value form sequence values. The initial value is the second argument. * @param accumulator function which produces the result. @@ -370,6 +443,14 @@ declare module 'fluent-iter' { */ aggregate(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + /** + * Produce single value form sequence values. The initial value is the second argument. Alias for aggregate. + * @see aggregate + * @param accumulator function which produces the result. + * @param initial initial value + */ + reduce(accumulator: (result: TResult, item: TValue, index: number) => TResult, initial: TResult): TResult; + /** * Produce a sum of sequence values * @throws {TypeError} if not items in sequence @@ -409,6 +490,14 @@ declare module 'fluent-iter' { */ elementAt(index: number): TValue | undefined; + /** + * Return element at specific index. Alias for elementAt. + * @see elementAt + * @param index index of requested element. + * @return undefined when no index out of range. + */ + at(index: number): TValue | undefined; + /** * do action over every item in the sequence * @param action diff --git a/src/finalizers/to-array.ts b/src/finalizers/to-array.ts index bf43394..47f2742 100644 --- a/src/finalizers/to-array.ts +++ b/src/finalizers/to-array.ts @@ -9,12 +9,13 @@ export function toArrayCollector(source: Iterable, map?: Mapper): } export async function toArrayAsyncCollector(source: AsyncIterable, map?: Mapper): Promise<(T|R)[]> { - const result: (T|R)[] = []; + if (!map) { + return Array.fromAsync(source); + } + const result: R[] = []; for await (const item of source) { if (map) { result.push(map(item)); - } else { - result.push(item); } } return result; diff --git a/src/fluent.ts b/src/fluent.ts index bf692c7..de218dd 100644 --- a/src/fluent.ts +++ b/src/fluent.ts @@ -50,11 +50,14 @@ export default class Fluent implements FluentIterable { where(predicate: Predicate): FluentIterable | FluentIterable { return new Fluent(whereIterator(this, predicate)); } + filter = this.where; select(map: Mapper): FluentIterable { return new Fluent(selectIterator(this, map)); } + map = this.select; + flat(depth: number = 1): FlatFluentIterable { return new Fluent(flatIterator(this, depth)) as any; } @@ -202,6 +205,8 @@ export default class Fluent implements FluentIterable { return first(this, predicate); } + find = this.first; + firstOrDefault(def: TValue, predicate?: Predicate): TValue { return firstOrDefault(this, def, predicate); } @@ -214,10 +219,14 @@ export default class Fluent implements FluentIterable { return firstIndex(this, predicate); } + findIndex = this.firstIndex; + last(predicate?: Predicate): TValue | undefined { return last(this, predicate); } + findLast = this.last; + lastOrDefault(def: TValue, predicate?: Predicate): TValue { return lastOrDefault(this, def, predicate); } @@ -230,6 +239,8 @@ export default class Fluent implements FluentIterable { return lastIndex(this, predicate); } + findLastIndex = this.lastIndex; + single(predicate?: Predicate): TValue | never { return single(this, predicate); } @@ -242,6 +253,8 @@ export default class Fluent implements FluentIterable { return allCollector(this, predicate); } + every = this.all; + allAndEvery(predicate: (item: TValue) => boolean): boolean { return allAndEveryCollector(this, predicate); } @@ -250,6 +263,8 @@ export default class Fluent implements FluentIterable { return anyCollector(this, predicate); } + some = this.any; + count(predicate?: ((item: TValue) => boolean) | undefined): number { return countCollector(this, predicate); } @@ -260,6 +275,8 @@ export default class Fluent implements FluentIterable { return aggregateCollector(this, accumulator, initial); } + reduce = this.aggregate; + sum(): TValue { return aggregateCollector(this, (a, i) => (a as any) + (i as any) as any); } @@ -288,6 +305,8 @@ export default class Fluent implements FluentIterable { return elementAtCollector(this, index); } + at = this.elementAt; + forEach(action: Action): void { return forEachCollector(this, action); } diff --git a/test/unit/where.spec.ts b/test/unit/where.spec.ts index a918fec..c91add3 100644 --- a/test/unit/where.spec.ts +++ b/test/unit/where.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { from, fromIterable } from "../../src/index.ts"; +import {from, fromIterable, range} from "../../src/index.ts"; describe("where tests", () => { [[1, 2, 3, 4, 5, 6, 7], new Set([1, 2, 3, 4, 5, 6, 7])].forEach( @@ -47,4 +47,14 @@ describe("where tests", () => { .toArray(); expect(res.length).toBe(2); }); + + it('should filter sequence', () => { + const res = from(range(0, 10)).filter(x => !(x & 1)).toArray(); + expect(res).toStrictEqual([0, 2, 4, 6, 8]); + }); + + it('should filter: typecheck sequence', () => { + const res = from([1, 2, '3', '4']).filter(x => typeof x === 'string').toArray(); + expect(res).toStrictEqual(['3', '4']); + }); }); From 4241f6520e97498b6a4a10ec0add70f593100ba0 Mon Sep 17 00:00:00 2001 From: vmladenov Date: Sun, 2 Nov 2025 11:57:18 +0100 Subject: [PATCH 6/6] docs: update README.md with new and updated functions --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7f0deb9..6bad7bb 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,15 @@ Operators: Transformers - where : filter iterable by predicate +- filter : alias for where - select : transform iterable using mapping function +- map : alias for select - selectMany : flatten an iterable and can transform it to another iterable +- flat: flatten an iterable up to N levels, default is 1 level +- flatMap: flatten an iterable one level and maps inner elements - ofType : produces typed iterable, used to filter by type - ofClass: filters iterable for elements of a given class +- groupBy: group items by a key - orderBy : Order iterable ascending by a key - orderByDescending : Order iterable descending by a key - groupJoin: Do a group join (left join) between current and external iterable. For each item of current sequence get array of items from external sequence. @@ -73,7 +78,6 @@ Combinations: - union: Produce a union of two iterables where the result is distinct values from both. - intersect: Return an intersection of two iterables where the result is distinct values. - difference: Return elements from the first iterable that are not in the second. Only distinct values are returned. -- except: Return elements from the first iterable that are not in the second. (TODO) - symmetricDifference: Return a symmetric difference ( distinct values except intersection ) of two iterables where the result is distinct values. Aggregators: @@ -81,28 +85,36 @@ Aggregators: - toMap(): Create a map object from sequence - toSet(): Creates a set from current sequence - first : first element in iterable or first element that satisfies predicate +- find: alias for first - firstOrDefault: like first but with default value -- firstOrThrows: like first but throws if no element is found +- firstOrThrow: like first but throws if no element is found - firstIndex: like first but returns index of element +- findIndex: alias for firstIndex - last: last element in iterable or last element that satisfies predicate +- findLast: alias for last - lastOrDefault: like last but with default value -- lastOrThrows: like last but throws if no element is found +- lastOrThrow: like last but throws if no element is found - lastIndex: like last but returns index of element +- findLastIndex: alias for lastIndex - single: single element in iterable or single element that satisfies predicate. Throws if more than one element is found. - singleOrDefault: like single but with default value when nothing found. Throws if more than one element is found. - all: check if all elements in iterable satisfy predicate +- every: alias for all - allAndEvery: like all but requires to have at least one element - any: check if at least one element in iterable satisfies predicate +- some: alias for any - count: count elements in iterable or count of elements that satisfy predicate - aggregate: Produce single value form sequence values. The initial value is the second argument. +- reduce: alias for aggregate - sum: Sum all elements in iterable - product: Multiply all elements in iterable - min: Get minimum value in iterable - max: Get maximum value in iterable - join: join all items from iterable with string concatenation - elementAt: get element at index +- at: alias for elementAt Misc: - forEach: iterate over iterable and execute a function for each element - isEqual: check if two iterables are equal. Same elements and order. -- isElementsEqual: check if two iterables are equal. Same elements but order can be different. +- isElementsEqual: check if two iterables are equal. Same elements but order can be different. \ No newline at end of file