Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cartesian product implementation #14

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions src/linq/iterables/cartesian.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { elementsSymbol, ElementsWrapper } from '../element-wrapper'

export class Cartesian<TFirst, TSecond> implements ElementsWrapper<TFirst | TSecond> {
private cacheFirst = new Map<number, Promise<IteratorResult<TFirst>> | IteratorResult<TFirst>>()
private cacheSecond = new Map<number, Promise<IteratorResult<TSecond>> | IteratorResult<TSecond>>()

constructor(private readonly first: Iterable<TFirst> | AsyncIterable<TFirst>,
private readonly second: Iterable<TSecond> | AsyncIterable<TSecond>,
private readonly preserveOrder: boolean = true) {
}

*[elementsSymbol](): IterableIterator<Iterable<TFirst | TSecond> | AsyncIterable<TFirst | TSecond>> {
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<TFirst>) {
for (const inner of this.second as Iterable<TSecond>) {
yield [outer, inner]
}
}

return
}

const itFirst = this.first[Symbol.iterator]() as Iterator<TFirst>
const itSecond = this.second[Symbol.iterator]() as Iterator<TSecond>

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<TFirst>
const itSecond = (typeof this.second[Symbol.asyncIterator] === 'function' && this.second[Symbol.asyncIterator]() || typeof this.second[Symbol.iterator] === 'function' && this.second[Symbol.iterator]()) as AsyncIterator<TSecond>

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<IteratorResult<TFirst>>
if (this.cacheFirst.has(firstIndex)) {
firstResult = this.cacheFirst.get(firstIndex) as Promise<IteratorResult<TFirst>>
} else {
const result = itFirst.next()
this.cacheFirst.set(firstIndex, result)
firstResult = result
}

let secondResult: Promise<IteratorResult<TSecond>>
if (this.cacheSecond.has(secondIndex)) {
secondResult = this.cacheSecond.get(secondIndex) as Promise<IteratorResult<TSecond>>
} 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})`
}
}
16 changes: 14 additions & 2 deletions src/linq/linqable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TSource, TKey, TValue> = {
Expand Down Expand Up @@ -751,7 +752,7 @@ export class Linqable<TSource> implements Iterable<TSource>, ElementsWrapper<TSo

/**
* Executes an action on each element of the sequence and yields the element.
* @param action - The action to execute on each element.
* @param action {FunctionF} - The action to execute on each element.
*/
tap(action: (element: TSource) => void): Linqable<TSource> {
return new Linqable(new Tap(this.elements, action))
Expand All @@ -770,6 +771,17 @@ export class Linqable<TSource> implements Iterable<TSource>, ElementsWrapper<TSo
return new Linqable(new Memoized(this.elements))
}

/**
* Cartesion product of the linable with another sequence.
* For infinite sequences false should be passed for preserverOrder.
* @param {ITerable<TOther>} 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<TOther>(other: Iterable<TOther>, preserveOrder = true): Linqable<[TSource, TOther]> {
return new Linqable(new Cartesian(this.elements, extractSync(other), preserveOrder))
}

toString(): string {
return Linqable.name
}
Expand Down
6 changes: 6 additions & 0 deletions src/linq/linqableAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -749,6 +750,11 @@ export class AsyncLinqable<TSource> implements AsyncIterable<TSource>, ElementsW
return new AsyncLinqable(new Memoized(this.elements))
}

cartesian<TOther>(other: Iterable<TOther> | AsyncIterable<TOther>): AsyncLinqable<[TSource, TOther]> {
return new AsyncLinqable(new Cartesian(this.elements, extractAsync(other)))
}


toString(): string {
return AsyncLinqable.name
}
Expand Down
50 changes: 50 additions & 0 deletions test/cartesian.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})