import { Block, Comparator, Dictionary, PairPredicate, Positional, Predicate, Sequence, SequenceIterator, Transform, Falsy } from "../types" import { identity } from "../../functions/static/function" import { is } from "../../functions/static/equality" import { OmniSequence, OutDestination, OutSource } from "./omni-sequence" import { range } from "../../functions/static/sequence" import { Span } from "./span" import { ErrorMessage, isGreaterThan, isLessThan } from "../../shared"; /** * An omni-sequence that is only guaranteed be iterable once and and does not support random access. */ export class Pipe implements OmniSequence { /** * The underlying iterable of the pipe. */ private _sequence: Sequence /** * Creates a new pipe over any sequence. */ constructor(sequence: Sequence) { this._sequence = sequence } [Symbol.iterator](): Iterator { return this._sequence[Symbol.iterator]() } toString(): string { return "Pipe::{?}" } all(predicate: Predicate): boolean { for (const element of this) { if (!predicate(element)) { return false } } return true } as(this: Pipe): OmniSequence { return this as any } concat(elements: Sequence): OmniSequence { return new Pipe(generateAppend(this, elements)) } append(...elements: T[]): OmniSequence { return new Pipe(generateAppend(this, elements)) } associate(transform: Transform): OmniSequence<[K, T]> { return this.map((element) => [transform(element), element] as [K, T]) } at(index: number): T { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsAt) } let i = 0 for (const element of this) { if (i++ === index) { return element } } throw new Error(ErrorMessage.indexOutOfBoundsAt) } cleave(index: number): [T[], T[]] { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsCleave) } const before: T[] = [] const after: T[] = [] let i = 0 for (const element of this) { if (i++ < index) { before.push(element) } else { after.push(element) } } if (i < index) { throw new Error(ErrorMessage.indexOutOfBoundsCleave) } return [before, after] as [T[], T[]] } count(): number count(predicate: Predicate): number count(predicate?: Predicate): number { let count = 0 if (predicate === undefined) { for (const element of this) { count++ } return count } for (const element of this) { if (predicate(element)) { count++ } } return count } cut(index: number): OmniSequence cut(index: number, count: number): OmniSequence cut(index: number, count: number = 1): OmniSequence { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsCut) } return new Pipe(generateCut(this, index, count)) } chunk(count: number): OmniSequence { count = this._sanitizeCount(count) if (count < 1) { throw new Error(ErrorMessage.invalidChunkSize) } return new Pipe(generateChunk(this, count)) } difference(sequence: Sequence): OmniSequence difference(sequence: Sequence, transform: Transform): OmniSequence difference(sequence: Sequence, transform: Transform = identity): OmniSequence { return new Pipe(generateDifference(this, sequence, transform as Transform)) } done(): T[] { return Array.from(this) } drop(): OmniSequence drop(count: number): OmniSequence drop(predicate: Predicate): OmniSequence drop(argument?: number | Predicate): OmniSequence { if (argument === undefined) { argument = 1 } return new Pipe(generateDrop(this, typeof argument === "number" ? this._sanitizeCount(argument) : argument)) } dropLast(): OmniSequence dropLast(count: number): OmniSequence dropLast(predicate: Predicate): OmniSequence dropLast(argument?: number | Predicate): OmniSequence { return this.solid().dropLast(argument as any) } each(action: (element: T, index: number, end: Block) => void): OmniSequence { let exited = false const exit = () => { exited = true } let i = 0 for (const element of this) { action(element, i++, exit) if (exited) { break } } return this } endsWith(suffix: Positional): boolean endsWith(suffix: Positional, equals: PairPredicate): boolean endsWith(suffix: Positional, equals: PairPredicate = is): boolean { return this.solid().endsWith(suffix, equals) } entries(): OmniSequence<[number, T]> { return new Pipe(generateEntries(this)) } entriesReversed(): OmniSequence<[number, T]> { return this.solid().entriesReversed() } filter(): OmniSequence filter(predicate: Predicate): OmniSequence filter(predicate: Predicate = identity): OmniSequence { return new Pipe(generateFilter(this, predicate)) } find(predicate: Predicate): T | null find(predicate: Predicate, instead: O): T | O find(predicate: Predicate, instead?: O): T | O | null { for (const element of this) { if (predicate(element)) { return element } } if (instead === undefined) { return null } return instead } findLast(predicate: Predicate): T | null findLast(predicate: Predicate, instead: O): T | O findLast(predicate: Predicate, instead?: O): T | O | null { let last: T | undefined = undefined for (const element of this) { if (predicate(element)) { last = element } } if (last !== undefined) { return last } if (instead === undefined) { return null } return instead } first(): T first(instead: O): T | O first(instead?: O): T | O { for (const element of this) { return element } if (instead === undefined) { throw new Error(ErrorMessage.noFirstElement) } return instead } flat(this: Pipe>): OmniSequence { return new Pipe(generateFlat(this)) } fold(initial: R, reducer: (accumulator: R, current: T) => R): R { let value = initial for (const element of this) { value = reducer(value, element) } return value } index(predicate: Predicate): number { return this.indices(predicate).first(-1) } indices(): OmniSequence indices(predicate: Predicate): OmniSequence indices(predicate?: Predicate): OmniSequence { return new Pipe(generateIndices(this, predicate)) } indicesReversed(): OmniSequence indicesReversed(predicate: Predicate): OmniSequence indicesReversed(predicate?: Predicate): OmniSequence { if (predicate === undefined) { return range(0, this.count(), -1) } return this.solid().indicesReversed(predicate) } inject(index: number, elements: Sequence): OmniSequence { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsInject) } return new Pipe(generateInject(this, index, elements, ErrorMessage.indexOutOfBoundsInject)) } intersect(sequence: Sequence): OmniSequence intersect(sequence: Sequence, transform: Transform): OmniSequence intersect(sequence: Sequence, transform: Transform = identity): OmniSequence { return new Pipe(generateIntersect(this, sequence, transform)) } insert(index: number, ...elements: T[]): OmniSequence { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsInsert) } return new Pipe(generateInject(this, index, elements, ErrorMessage.indexOutOfBoundsInsert)) } invert(this: Pipe<[K, V]>): OmniSequence<[V, K]> { return new Pipe(generateInvert(this)) } join(): string join(separator: string): string join(separator = ""): string { return Array.from(this).join(separator) } last(): T last(instead: O): T | O last(instead?: O): T | O { const iterator = this[Symbol.iterator]() let current = iterator.next() if (current.done) { if (instead === undefined) { throw new Error(ErrorMessage.noLastElement) } return instead } let last: T while (true) { if (current.done) { break } last = current.value current = iterator.next() } return last!! } lastIndex(): number lastIndex(predicate: Predicate): number lastIndex(predicate?: Predicate): number { if (predicate === undefined) { return this.count() - 1 } return this.indices(predicate).last(-1) } map(this: Pipe, transform: Transform): OmniSequence<[OK, OV]> map(this: Pipe, transform: Transform): OmniSequence map(this: Pipe, transform: Transform): OmniSequence { return new Pipe(generateMap(this, transform)) } max(this: Pipe): number max(transform: Transform): T max(transform?: Transform): T | number { return findExtreme(this, transform || identity, -Infinity, isGreaterThan, ErrorMessage.noMaxElement) } mean(this: Pipe): number { let sum = 0 let count = 0 for (const value of this) { sum += value count++ } if (count === 0) { return 0 } return sum / count } min(this: Pipe): number min(transform: Transform): T min(transform?: Transform): T | number { return findExtreme(this, transform || identity, Infinity, isLessThan, ErrorMessage.noMinElement) } none(): boolean none(predicate: Predicate): boolean none(predicate?: Predicate): boolean { return !this.some(predicate as Predicate) } partition(predicate: Predicate): [T[], T[]] { const positive: T[] = [] const negative: T[] = [] for (const element of this) { if (predicate(element)) { positive.push(element) } else { negative.push(element) } } return [positive, negative] } permute(): OmniSequence { return this.solid().permute() } prepend(...elements: T[]): OmniSequence { return new Pipe(generatePrepend(this, elements)) } rank(transform: Transform): OmniSequence { return this.solid().rank(transform) } remove(predicate: Predicate): OmniSequence remove(predicate: Predicate, count: number): OmniSequence remove(predicate: Predicate, count = Infinity): OmniSequence { return new Pipe(generateRemove(this, predicate, Math.floor(count))) } removeLast(predicate: Predicate, count: number): OmniSequence { return this.solid().removeLast(predicate, count) } repeat(): OmniSequence repeat(count: number): OmniSequence repeat(count?: number): OmniSequence { return this.solid().repeat(count as number) } replace(predicate: Predicate, replacement: R): OmniSequence replace(predicate: Predicate, replacement: R, count: number): OmniSequence replace(predicate: Predicate, replacement: R, count = Infinity): OmniSequence { return new Pipe(generateReplace(this, predicate, replacement, this._sanitizeCount(count))) } replaceLast(predicate: Predicate, replacement: R, count: number): OmniSequence { return this.solid().replaceLast(predicate, replacement, this._sanitizeCount(count)) } reversed(): OmniSequence { return this.solid().reversed() } set(index: number, element: T): OmniSequence { index = this._sanitizeIndex(index) if (index < 0) { throw new Error(ErrorMessage.indexOutOfBoundsSet) } return new Pipe(generateSet(this, index, element)) } slice(start: number, end: number): OmniSequence { start = this._sanitizeIndex(start) end = this._sanitizeIndex(end) if (start < 0 || end < start) { throw new Error(ErrorMessage.invalidSliceBounds) } return new Pipe(generateSlice(this, start, end)) } solid(): Span { return this.yank() } some(): boolean some(predicate: Predicate): boolean some(predicate?: Predicate): boolean { if (predicate === undefined) { for (const element of this) { return true } return false } return this.index(predicate) >= 0 } sort(this: Pipe): OmniSequence sort(this: Pipe, descending: boolean): OmniSequence sort(this: Pipe): OmniSequence sort(this: Pipe, descending: boolean): OmniSequence sort(comparator: Comparator): OmniSequence sort(this: Pipe, argument?: Comparator | boolean): OmniSequence { return this.solid().sort(argument as any) } startsWith(prefix: Sequence): boolean startsWith(prefix: Sequence, equals: PairPredicate): boolean startsWith(prefix: Sequence, equals: PairPredicate = is): boolean { const iterator = this[Symbol.iterator]() const prefixIterator = prefix[Symbol.iterator]() while (true) { const current = iterator.next() const prefixResult = prefixIterator.next() if (prefixResult.done) { break } if (current.done) { return false } if (!equals(current.value, prefixResult.value)) { return false } } return true } sum(this: Pipe): number { let sum = 0 for (const value of this) { sum += value } return sum } swap(first: number, second: number): OmniSequence { first = this._sanitizeIndex(first) second = this._sanitizeIndex(second) if (first < 0 || second < 0) { throw new Error(ErrorMessage.indexOutOfBoundsSwap) } const iterator = first < second ? generateSwap(this, first, second) : generateSwap(this, second, first) return new Pipe(iterator) } take(): OmniSequence take(count: number): OmniSequence take(predicate: Predicate): OmniSequence take(argument?: number | Predicate): OmniSequence { if (typeof argument === "number") { argument = this._sanitizeCount(argument) } return new Pipe(generateTake(this, argument === undefined ? Infinity : argument)) } takeLast(count: number): OmniSequence takeLast(predicate: Predicate): OmniSequence takeLast(argument: number | Predicate): OmniSequence { return this.solid().takeLast(argument as any) } toArray(): T[] toArray(destination: T[]): T[] toArray(destination: T[], clear: boolean): T[] toArray(destination?: T[], clear: boolean = false): T[] { if (destination) { if (clear) { destination.length = 0 } destination.push(...this) return destination } return this.done() } toMap(this: Pipe<[K, V]>): Map toMap(this: Pipe<[K, V]>, destination: Map): Map toMap(this: Pipe<[K, V]>, destination: Map, clear: boolean): Map toMap(this: Pipe<[K, V]>, destination?: Map, clear: boolean = false): Map { destination = destination || new Map() if (clear) { destination.clear() } for (const [key, value] of this) { destination.set(key, value) } return destination } toObject(this: Pipe<[string, V]>): Dictionary toObject(this: Pipe<[string, V]>, destination: Dictionary): Dictionary toObject(this: Pipe<[string, V]>, destination: Dictionary, clear: boolean): Dictionary toObject(this: Pipe<[string, V]>, destination?: Dictionary, clear: boolean = false): Dictionary { if (destination) { if (clear) { for (const key in destination) { if (destination.hasOwnProperty(key)) { delete destination[key] } } } } else { destination = {} } for (const [key, value] of this) { destination[key] = value } return destination } toSet(): Set toSet(destination: Set): Set toSet(destination: Set, clear: boolean): Set toSet(destination?: Set, clear: boolean = false): Set { destination = destination || new Set() if (clear) { destination.clear() } for (const element of this) { destination.add(element) } return destination } union(sequence: Sequence): OmniSequence union(sequence: Sequence, transform: Transform): OmniSequence union(sequence: Sequence, transform: Transform = identity): OmniSequence { return new Pipe(generateUnion(this, sequence, transform)) } unique(): OmniSequence unique(transform: Transform): OmniSequence unique(transform: Transform = identity): OmniSequence { return new Pipe(generateUnique(this, transform)) } use(transform: Transform, R>): R { return transform(this) } yank(): Span { return new Span(Array.from(this)) as Span } zip(elements: Sequence): OmniSequence<[T, V]> { return new Pipe(generateZip(this, elements)) } private _sanitizeIndex(index: number) { return Math.floor(index) } private _sanitizeCount(count: number) { return Math.max(Math.floor(count), 0) } private _seat(sequence: Sequence) { this._sequence = sequence } } function* generateAppend(pipe: Pipe, elements: Sequence): SequenceIterator { yield* pipe yield* elements } function* generateRemove(pipe: Pipe, predicate: Predicate, count: number): SequenceIterator { let removed = 0 for (const element of pipe) { if (removed >= count) { yield element } else if (predicate(element)) { removed++ } else { yield element } } } function* generateChunk(pipe: Pipe, count: number): SequenceIterator { let current: T[] = [] for (const element of pipe) { current.push(element) if (current.length === count) { yield current current = [] } } if (current.length !== 0) { yield current } } function* generateCut(pipe: Pipe, index: number, count: number): SequenceIterator { let i = 0 const end = index + count for (const element of pipe) { if (i < index || i >= end) { yield element } i++ } if (i < index || i === 0) { throw new Error(ErrorMessage.indexOutOfBoundsCut) } } function* generateDifference(pipe: Pipe, sequence: Sequence, transform: Transform): SequenceIterator { sequence = Array.isArray(sequence) ? sequence : Array.from(sequence) const keys = new Set() const other = new Set() for (const element of sequence) { other.add(transform(element)) } for (const element of pipe) { const key = transform(element) if (keys.has(key)) { continue } keys.add(key) if (other.has(key)) { continue } yield element } other.clear() for (const element of sequence) { const key = transform(element) if (other.has(key)) { continue } other.add(key) if (keys.has(key)) { continue } yield element } } function* generateDrop(pipe: Pipe, argument: number | Predicate): SequenceIterator { if (typeof argument === "function") { const predicate = argument let done = false for (const element of pipe) { if (done) { yield element } else if (!predicate(element)) { yield element done = true } } } else { const count = argument let dropped = 0 for (const element of pipe) { if (dropped >= count) { yield element } else { dropped++ } } } } function* generateEntries(pipe: Pipe,): SequenceIterator<[number, T]> { let i = 0 for (const element of pipe) { yield [i++, element] } } function* generateFilter(pipe: Pipe, predicate: Predicate): SequenceIterator { for (const element of pipe) { if (predicate(element)) { yield element } } } function* generateFlat(pipe: Pipe>): SequenceIterator { for (const element of pipe) { yield* element } } function* generateIndices(pipe: Pipe, predicate?: Predicate): SequenceIterator { if (predicate === undefined) { let i = 0 for (const element of pipe) { yield i++ } } else { let i = 0 for (const element of pipe) { if (predicate(element)) { yield i } i++ } } } function* generateInject(pipe: Pipe, index: number, elements: Sequence, errorMessage: string): SequenceIterator { let i = 0 for (const element of pipe) { if (i++ === index) { yield* elements } yield element } if (i === index) { yield* elements } else if (i < index) { throw new Error(errorMessage) } } function* generateIntersect(pipe: Pipe, sequence: Sequence, transform: Transform): SequenceIterator { const keys = new Set() const other = new Set() for (const element of sequence) { other.add(transform(element)) } for (const element of pipe) { const key = transform(element) if (keys.has(key)) { continue } keys.add(key) if (other.has(key)) { yield element } } } function* generateInvert(pipe: Pipe<[K, V]>): SequenceIterator<[V, K]> { for (const [key, value] of pipe) { yield [value, key] } } function* generateMap(pipe: Pipe, transform: Transform): SequenceIterator { for (const element of pipe) { yield transform(element) } } function* generatePositions(pipe: Pipe, predicate: Predicate): SequenceIterator { let i = 0 for (const element of pipe) { if (predicate(element)) { yield i } i++ } } function* generatePrepend(pipe: Pipe, elements: Sequence): SequenceIterator { yield* elements yield* pipe } function* generateReplace(pipe: Pipe, predicate: Predicate, replacement: R, count: number): SequenceIterator { let replaced = 0 for (const element of pipe) { if (replaced >= count) { yield element } else if (predicate(element)) { yield replacement replaced++ } else { yield element } } } function* generateSet(pipe: Pipe, index: number, element: T): SequenceIterator { let i = 0 let invalid = true for (const other of pipe) { if (i++ === index) { yield element invalid = false } else { yield other } } if (invalid) { throw new Error(ErrorMessage.indexOutOfBoundsSet) } } function* generateSlice(pipe: Pipe, start: number, end: number): SequenceIterator { let i = 0 for (const element of pipe) { if (i >= start && i < end) { yield element } i++ } if (i < start) { throw new Error(ErrorMessage.invalidSliceBounds) } } function* generateSwap(pipe: Pipe, first: number, second: number): SequenceIterator { const iterator = pipe[Symbol.iterator]() const elements: T[] = [] for (let i = 0; i <= second; i++) { const current = iterator.next() if (current.done) { throw new Error(ErrorMessage.indexOutOfBoundsSwap) } elements.push(current.value) } const temporary = elements[first] elements[first] = elements[second] elements[second] = temporary for (let i = 0; i < elements.length; i++) { yield elements[i] } while (true) { const current = iterator.next() if (current.done) { break } yield current.value } } function* generateTake(pipe: Pipe, argument: number | Predicate): SequenceIterator { if (typeof argument === "function") { const predicate = argument for (const element of pipe) { if (predicate(element)) { yield element } else { break } } } else { const count = argument let taken = 0 for (const element of pipe) { if (taken++ >= count) { break } yield element } } } function* generateUnion(pipe: Pipe, sequence: Sequence, transform: Transform): SequenceIterator { const seen = new Set() for (const element of pipe) { const key = transform(element) if (seen.has(key)) { continue } yield element seen.add(key) } for (const element of sequence) { const key = transform(element) if (seen.has(key)) { continue } yield element seen.add(key) } } function* generateUnique(pipe: Pipe, transform: Transform): SequenceIterator { const indices: number[] = [] const seen = new Set() for (const element of pipe) { const key = transform(element) if (seen.has(key)) { continue } yield element seen.add(key) } } function* generateZip(pipe: Pipe, values: Sequence): SequenceIterator<[T, V]> { const iterator = values[Symbol.iterator]() for (const key of pipe) { const result = iterator.next() if (result.done) { break } yield [key, result.value] } } function findExtreme(pipe: Pipe, transform: Transform, start: number, comparator: PairPredicate, errorMessage: string): T { let extreme = start let result: T let found = false for (const element of pipe) { const value = transform(element) if (comparator(value, extreme)) { extreme = value result = element found = true } } if (found) { return result!! } throw new Error(errorMessage) }