itertools-ts
Version:
Extended itertools port for TypeScript and JavaScript. Provides a huge set of functions for working with iterable collections (including async ones)
575 lines (499 loc) • 13 kB
text/typescript
import { LengthError } from "./exceptions";
import { reduce, single, Stream } from "./index";
import { toAsyncIterator, toIterator } from "./transform";
import { ZipTuple } from "./types";
export enum MultipleIterationMode {
SHORTEST,
LONGEST,
STRICT_EQUAL,
}
/**
* Creates iterable instance to iterate several iterables simultaneously.
*
* @param mode shortest, longest or strict equal
* @param iterables
* @param noValueFiller
* @param iterables
*/
export function* createMultipleIterator<
T extends Array<Iterable<unknown> | Iterator<unknown>>,
F
>(
mode: MultipleIterationMode,
noValueFiller: F,
...iterables: T
): Iterable<ZipTuple<T, F>> {
if (iterables.length === 0) {
return;
}
const iterators = [];
for (const it of iterables) {
iterators.push(toIterator(it));
}
iterate: while (true) {
const statuses = single.map(iterators, (it: Iterator<unknown>) =>
it.next()
);
const values = [];
let allValid = true;
let anyValid = false;
for (const status of statuses) {
let value;
if (status.done) {
allValid = false;
value = noValueFiller;
} else {
anyValid = true;
value = status.value;
}
values.push(value);
}
if (!allValid && anyValid) {
switch (mode) {
case MultipleIterationMode.SHORTEST:
break iterate;
case MultipleIterationMode.STRICT_EQUAL:
throw new LengthError("Iterators must have equal lengths");
}
}
if (!anyValid) {
break;
}
yield values as ZipTuple<T, F>;
}
}
/**
* Creates async iterable instance to iterate several iterables simultaneously.
*
* @param mode shortest, longest or strict equal
* @param iterables
* @param noValueFiller
* @param iterables
*/
export async function* createAsyncMultipleIterator<
T extends Array<
| AsyncIterable<unknown>
| AsyncIterator<unknown>
| Iterable<unknown>
| Iterator<unknown>
>,
F
>(
mode: MultipleIterationMode,
noValueFiller: F,
...iterables: T
): AsyncIterable<ZipTuple<T, F>> {
if (iterables.length === 0) {
return;
}
const iterators = [];
for (const it of iterables) {
iterators.push(toAsyncIterator(it));
}
iterate: while (true) {
const statuses = [];
for (const it of iterators) {
const status = await it.next();
statuses.push(status);
}
const values = [];
let allValid = true;
let anyValid = false;
for (const status of statuses) {
let value;
if (status.done) {
allValid = false;
value = noValueFiller;
} else {
anyValid = true;
value = status.value;
}
values.push(value);
}
if (!allValid && anyValid) {
switch (mode) {
case MultipleIterationMode.SHORTEST:
break iterate;
case MultipleIterationMode.STRICT_EQUAL:
throw new LengthError("Iterators must have equal lengths");
}
}
if (!anyValid) {
break;
}
yield values as ZipTuple<T, F>;
}
}
/**
* Internal class for counting unique values usage.
*/
export class UsageMap {
private addedMap: Map<unknown, Map<string, number>> = new Map();
private deletedMap: Map<unknown, number> = new Map();
/**
* Adds new usage of value by given owner.
*
* @param value
* @param owner
*/
public addUsage(value: unknown, owner: string): void {
if (!this.addedMap.has(value)) {
this.addedMap.set(value, new Map());
}
const valueMap = this.addedMap.get(value) as Map<string, number>;
if (!valueMap.has(owner)) {
valueMap.set(owner, 0);
}
valueMap.set(owner, (valueMap.get(owner) as number) + 1);
}
/**
* Removes usage of value.
*
* @param value
*/
public deleteUsage(value: unknown): void {
if (!this.deletedMap.has(value)) {
this.deletedMap.set(value, 1);
} else {
this.deletedMap.set(value, (this.deletedMap.get(value) as number) + 1);
}
}
/**
* Returns owners count of given value.
*
* @param value
*/
public getOwnersCount(value: unknown): number {
const deletesCount = this.deletedMap.get(value) ?? 0;
return Stream.of(this.addedMap.get(value) ?? new Map())
.map((datum) => (datum as [string, number])[1])
.filter((count) => (count as number) > deletesCount)
.toValue(
(carry: number | undefined) => (carry as number) + 1,
0
) as number;
}
/**
* Returns usages count of given value.
*
* @param value
* @param maxOwnersCount
*/
public getUsagesCount(value: unknown, maxOwnersCount = 1): number {
const deletesCount = this.deletedMap.get(value) ?? 0;
let owners = Stream.of(this.addedMap.get(value) ?? new Map())
.map((pair) => (pair as [string, number])[1])
.map((value) => (value as number) - deletesCount)
.filter((value) => (value as number) > 0)
.toArray();
while (owners.length > maxOwnersCount) {
const minValue = reduce.toMin(owners) as number;
owners = Stream.of(owners)
.map((value) => (value as number) - minValue)
.filter((value) => (value as number) > 0)
.toArray();
}
return reduce.toSum(owners as Array<number>) as number;
}
}
/**
* No value filler monad.
*/
export class NoValueMonad {}
/**
* Internal tool for duplicating another iterators using cache.
*/
export class TeeIterator<T> {
private iterator: Iterator<T>;
private related: Array<RelatedIterable<T>> = [];
private positions: Array<number> = [];
private cache: Map<number, T> = new Map();
private lastCacheIndex = 0;
private isValid = true;
/**
* TeeIterator constructor
*
* @param iterator
* @param relatedCount
*/
constructor(iterator: Iterator<T>, relatedCount: number) {
this.iterator = iterator;
for (let i = 0; i < relatedCount; ++i) {
this.related.push(new RelatedIterable<T>(this, i));
this.positions.push(0);
}
this.cacheNextValue();
}
/**
* Returns current value of related iterable.
*
* @param relatedIterable
*/
public current(relatedIterable: RelatedIterable<T>): T {
const index = this.getPosition(relatedIterable);
return this.cache.get(index) as T;
}
/**
* Moves related iterable to the next element.
*
* @param relatedIterable
*/
public next(relatedIterable: RelatedIterable<T>): void {
const [relPos, minPos, maxPos] = [
this.getPosition(relatedIterable),
Math.min(...this.positions),
Math.max(...this.positions),
];
if (relPos === maxPos) {
this.cacheNextValue();
}
this.positions[relatedIterable.getId()]++;
if (minPos < Math.min(...this.positions)) {
this.cache.delete(minPos);
}
}
/**
* Returns true if related iterable is not done.
*
* @param relatedIterable
*/
public valid(relatedIterable: RelatedIterable<T>): boolean {
return (
this.getPosition(relatedIterable) < this.lastCacheIndex || this.isValid
);
}
/**
* Returns related iterables list.
*/
public getRelatedIterables(): Array<RelatedIterable<T>> {
return this.related;
}
/**
* Gets and caches the next element of parent iterator.
*
* @private
*/
private cacheNextValue(): void {
const status = this.iterator.next();
if (!status.done) {
this.cache.set(this.lastCacheIndex++, status.value);
}
this.isValid = !status.done;
}
/**
* Returns current position index of related iterable.
*
* @param related
*/
private getPosition(related: RelatedIterable<T>): number {
return this.positions[related.getId()];
}
}
/**
* Duplicated iterable.
*/
export class RelatedIterable<T> implements IterableIterator<T> {
private parent: TeeIterator<T>;
private readonly id: number;
/**
* RelatedIterable constructor.
*
* @param parentIterable
* @param id
*/
constructor(parentIterable: TeeIterator<T>, id: number) {
this.parent = parentIterable;
this.id = id;
}
/**
* Id getter.
*/
public getId(): number {
return this.id;
}
/**
* Returns true if the iterator is valid.
*/
public valid(): boolean {
return this.parent.valid(this);
}
/**
* Moves the iterator to the next element.
*/
public next(): IteratorResult<T> {
const result = { value: this.current(), done: !this.valid() };
if (!result.done) {
this.parent.next(this);
}
return result as IteratorResult<T>;
}
/**
* Returns current value of the iterator.
*/
public current(): T | undefined {
return this.parent.valid(this) ? this.parent.current(this) : undefined;
}
/**
* Aggregated iterator.
*/
*[Symbol.iterator](): IterableIterator<T> {
while (this.parent.valid(this)) {
yield this.parent.current(this);
this.parent.next(this);
}
}
}
/**
* Internal tool for duplicating another async iterators using cache.
*/
export class AsyncTeeIterator<T> {
private iterator: AsyncIterator<T>;
private related: Array<AsyncRelatedIterable<T>> = [];
private positions: Array<number> = [];
private cache: Map<number, T> = new Map();
private lastCacheIndex = 0;
private isValid = true;
private isFirstIteration = true;
/**
* AsyncTeeIterator constructor
*
* @param iterator
* @param relatedCount
*/
constructor(iterator: AsyncIterator<T>, relatedCount: number) {
this.iterator = iterator;
for (let i = 0; i < relatedCount; ++i) {
this.related.push(new AsyncRelatedIterable<T>(this, i));
this.positions.push(0);
}
}
/**
* Returns current value of related iterable.
*
* @param relatedIterable
*/
public async current(relatedIterable: AsyncRelatedIterable<T>): Promise<T> {
if (this.isFirstIteration) {
await this.cacheNextValue();
}
const index = this.getPosition(relatedIterable);
return this.cache.get(index) as T;
}
/**
* Moves related iterable to the next element.
*
* @param relatedIterable
*/
public async next(relatedIterable: AsyncRelatedIterable<T>): Promise<void> {
const [relPos, minPos, maxPos] = [
this.getPosition(relatedIterable),
Math.min(...this.positions),
Math.max(...this.positions),
];
if (relPos === maxPos) {
await this.cacheNextValue();
}
this.positions[relatedIterable.getId()]++;
if (minPos < Math.min(...this.positions)) {
this.cache.delete(minPos);
}
}
/**
* Returns true if related iterable is not done.
*
* @param relatedIterable
*/
public async valid(
relatedIterable: AsyncRelatedIterable<T>
): Promise<boolean> {
if (this.isFirstIteration) {
await this.cacheNextValue();
}
return (
this.getPosition(relatedIterable) < this.lastCacheIndex || this.isValid
);
}
/**
* Returns related iterables list.
*/
public getRelatedIterables(): Array<AsyncRelatedIterable<T>> {
return this.related;
}
/**
* Gets and caches the next element of parent iterator.
*
* @private
*/
private async cacheNextValue(): Promise<void> {
const status = await this.iterator.next();
if (!status.done) {
this.cache.set(this.lastCacheIndex++, status.value);
}
this.isFirstIteration = false;
this.isValid = !status.done;
}
/**
* Returns current position index of related iterable.
*
* @param related
*/
private getPosition(related: AsyncRelatedIterable<T>): number {
return this.positions[related.getId()];
}
}
/**
* Duplicated async iterable.
*/
export class AsyncRelatedIterable<T> implements AsyncIterableIterator<T> {
private parent: AsyncTeeIterator<T>;
private readonly id: number;
/**
* AsyncRelatedIterable constructor.
*
* @param parentIterable
* @param id
*/
constructor(parentIterable: AsyncTeeIterator<T>, id: number) {
this.parent = parentIterable;
this.id = id;
}
/**
* Id getter.
*/
public getId(): number {
return this.id;
}
/**
* Returns true if the iterator is valid.
*/
public async valid(): Promise<boolean> {
return await this.parent.valid(this);
}
/**
* Moves the iterator to the next element.
*/
public async next(): Promise<IteratorResult<T>> {
const result = { value: await this.current(), done: !(await this.valid()) };
if (!result.done) {
await this.parent.next(this);
}
return result as IteratorResult<T>;
}
/**
* Returns current value of the iterator.
*/
public async current(): Promise<T | undefined> {
return (await this.parent.valid(this))
? await this.parent.current(this)
: undefined;
}
/**
* Aggregated iterator.
*/
async *[Symbol.asyncIterator](): AsyncIterableIterator<T> {
while (await this.parent.valid(this)) {
yield await this.parent.current(this);
await this.parent.next(this);
}
}
}