diff --git a/src/linq/iterables/cartesian.ts b/src/linq/iterables/cartesian.ts new file mode 100644 index 0000000..759548f --- /dev/null +++ b/src/linq/iterables/cartesian.ts @@ -0,0 +1,177 @@ +import { elementsSymbol, ElementsWrapper } from '../element-wrapper' + +export class Cartesian implements ElementsWrapper { + private cacheFirst = new Map> | IteratorResult>() + private cacheSecond = new Map> | IteratorResult>() + + constructor(private readonly first: Iterable | AsyncIterable, + private readonly second: Iterable | AsyncIterable, + private readonly preserveOrder: boolean = true) { + } + + *[elementsSymbol](): IterableIterator | AsyncIterable> { + yield this.first + yield this.second + } + + *[Symbol.iterator](): IterableIterator<[TFirst, TSecond]> { + if (typeof this.first[Symbol.iterator] !== 'function') { + throw Error('Expected @@iterator') + } + + if (this.preserveOrder) { + for (const outer of this.first as Iterable) { + for (const inner of this.second as Iterable) { + yield [outer, inner] + } + } + + return + } + + const itFirst = this.first[Symbol.iterator]() as Iterator + const itSecond = this.second[Symbol.iterator]() as Iterator + + let firstIndex = 0 + let secondIndex = 0 + let row = 0 + let currentRow = 0 + let currentCol = 0 + let firstLen = 0 + let secondLen = 0 + while (true) { + let first + if (this.cacheFirst.has(firstIndex)) { + first = this.cacheFirst.get(firstIndex) + } else { + const result = itFirst.next() + first = result + this.cacheFirst.set(firstIndex, result) + } + + let second + if (this.cacheSecond.has(secondIndex)) { + second = this.cacheSecond.get(secondIndex) + } else { + const result = itSecond.next() + second = result + this.cacheSecond.set(secondIndex, result) + } + + if (currentRow === 0) { + currentCol = 0 + currentRow = ++row + } else { + currentRow-- + currentCol++ + } + + firstIndex = currentRow + secondIndex = currentCol + + + if (first.done && second.done) { + break + } + + if (first.done) { + firstIndex = currentRow % firstLen + continue + } else { + firstLen++ + } + + if (second.done) { + secondIndex = currentCol % secondLen + continue + } else { + secondLen++ + } + + yield [first.value, second.value] + } + } + + async *[Symbol.asyncIterator](): AsyncIterableIterator<[TFirst, TSecond]> { + if (typeof this.first[Symbol.iterator] !== 'function' && typeof this.first[Symbol.asyncIterator] !== 'function') { + throw Error('Expected @@iterator or @@asyncIterator') + } + + if (this.preserveOrder) { + for await (const outer of this.first) { + for await (const inner of this.second) { + yield [outer, inner] + } + } + + return + } + + const itFirst = (typeof this.first[Symbol.asyncIterator] === 'function' && this.first[Symbol.asyncIterator]() || typeof this.first[Symbol.iterator] === 'function' && this.first[Symbol.iterator]()) as AsyncIterator + const itSecond = (typeof this.second[Symbol.asyncIterator] === 'function' && this.second[Symbol.asyncIterator]() || typeof this.second[Symbol.iterator] === 'function' && this.second[Symbol.iterator]()) as AsyncIterator + + let firstIndex = 0 + let secondIndex = 0 + let row = 0 + let currentRow = 0 + let currentCol = 0 + let firstLen = 0 + let secondLen = 0 + while (true) { + let firstResult: Promise> + if (this.cacheFirst.has(firstIndex)) { + firstResult = this.cacheFirst.get(firstIndex) as Promise> + } else { + const result = itFirst.next() + this.cacheFirst.set(firstIndex, result) + firstResult = result + } + + let secondResult: Promise> + if (this.cacheSecond.has(secondIndex)) { + secondResult = this.cacheSecond.get(secondIndex) as Promise> + } else { + const result = itSecond.next() + this.cacheSecond.set(secondIndex, result) + secondResult = result + } + + if (currentRow === 0) { + currentCol = 0 + currentRow = ++row + } else { + currentRow-- + currentCol++ + } + + firstIndex = currentRow + secondIndex = currentCol + + const [first, second] = await Promise.all([firstResult, secondResult]) + + if (first.done && second.done) { + break + } + + if (first.done) { + firstIndex = currentRow % firstLen + continue + } else { + firstLen++ + } + + if (second.done) { + secondIndex = currentCol % secondLen + continue + } else { + secondLen++ + } + + yield [first.value, second.value] + } + } + + toString(): string { + return `${Cartesian.name})` + } +} diff --git a/src/linq/linqable.ts b/src/linq/linqable.ts index 8305589..82cfc21 100644 --- a/src/linq/linqable.ts +++ b/src/linq/linqable.ts @@ -20,13 +20,14 @@ import { Zip, Tap, Repeat, - Grouping + Grouping, } from './iterables' import { elementsSymbol, ElementsWrapper, isWrapper } from './element-wrapper' import { AsyncSource, id, SyncSource } from '.' import { LinqMap, EqualityComparer, LinqSet } from './collections' import { GeneratorFunc } from './iterables/generatorFunc' import { Memoized } from './iterables/memoized' +import { Cartesian } from './iterables/cartesian' import { Scan } from './iterables/scan' export type ToMapArgs = { @@ -751,7 +752,7 @@ export class Linqable implements Iterable, ElementsWrapper void): Linqable { return new Linqable(new Tap(this.elements, action)) @@ -770,6 +771,17 @@ export class Linqable implements Iterable, ElementsWrapper} other - The other sequence. + * @param {boolean} preserveOrder - A flag indigating whether to traverse the sequences in order. + * @returns {Linqable<[TSource, TOther]>} A linqable of tuples representing elements of the cartesian product of the two sequences. + */ + cartesian(other: Iterable, preserveOrder = true): Linqable<[TSource, TOther]> { + return new Linqable(new Cartesian(this.elements, extractSync(other), preserveOrder)) + } + toString(): string { return Linqable.name } diff --git a/src/linq/linqableAsync.ts b/src/linq/linqableAsync.ts index 66fdd82..8e297be 100644 --- a/src/linq/linqableAsync.ts +++ b/src/linq/linqableAsync.ts @@ -2,6 +2,7 @@ import { AsyncSource, id, SyncSource } from '.' import { EqualityComparer, LinqMap, LinqSet } from './collections' import { elementsSymbol, ElementsWrapper, isWrapper } from './element-wrapper' import { Concat, Distinct, DistinctBy, Except, GroupBy, Grouping, Intersect, Join, Ordered, Repeat, Reverse, Select, SelectMany, Skip, SkipWhile, Take, TakeWhile, Tap, Union, Where, Windowed, Zip } from './iterables' +import { Cartesian } from './iterables/cartesian' import { GeneratorFunc } from './iterables/generatorFunc' import { Memoized } from './iterables/memoized' import { Scan } from './iterables/scan' @@ -749,6 +750,11 @@ export class AsyncLinqable implements AsyncIterable, ElementsW return new AsyncLinqable(new Memoized(this.elements)) } + cartesian(other: Iterable | AsyncIterable): AsyncLinqable<[TSource, TOther]> { + return new AsyncLinqable(new Cartesian(this.elements, extractAsync(other))) + } + + toString(): string { return AsyncLinqable.name } diff --git a/test/cartesian.spec.ts b/test/cartesian.spec.ts new file mode 100644 index 0000000..9b4c88d --- /dev/null +++ b/test/cartesian.spec.ts @@ -0,0 +1,50 @@ +import { expect } from 'chai' +import { linq } from '../src/linq' + +describe('cartesian', () => { + describe('sync', () => { + it('should return an empty seqeunce when one of the sequences is empty', () => { + expect(linq([]).cartesian([1, 2, 3, 4]).toArray()).to.be.empty + expect(linq([1, 2, 3, 4]).cartesian([]).toArray()).to.be.empty + expect(linq([]).cartesian([]).toArray()).to.be.empty + }) + + describe('preserveOrder = true', () => { + it('should return the cartesian product of two finite sequences preserving the order', () => { + const first = [1, 2, 3, 4] + const second = ['a', 'b', 'c'] + + const expected = [] + for (const outer of first) { + for (const inner of second) { + expected.push([outer, inner]) + } + } + + expect(linq(first).cartesian(second).toArray()).to.have.deep.ordered.members(expected) + }) + + it('should return a cartesion product of the first element in the first sequence with elements of the seqeunce when the second is infinite', () => { + const first = [1, 2, 3, 4] + function* second() { + let i = 0 + while (true) { + yield i++ + } + } + + const expected = [] + let i = 1 + for (const inner of second()) { + expected.push([1, inner]) + if (i === 100) { + break + } + i++ + } + + expect(linq(first).cartesian(second()).take(100).toArray()).to.have.deep.ordered.members(expected) + }) + }) + }) +}) \ No newline at end of file