import { Block, Comparator, Dictionary, PairPredicate, Positional, Predicate, Sequence, SequenceIterator, Transform, Falsy } from "../types" import { identity, negate } from "../../functions/static/function" import { is } from "../../functions/static/equality" import { isGreaterThan, isLessThan, ErrorMessage } from "../../shared" import { OmniSequence, OutDestination } from "./omni-sequence" import { range } from "../../functions/static/sequence" /** * @hidden */ export type PipeConstructor = { new (sequence: Sequence): OmniSequence } let Pipe = null as any as PipeConstructor /** * @hidden */ export function injectPipeConstructor(constructor: PipeConstructor) { Pipe = constructor } /** * An omni-sequence that is iterable more than once and supports random access. */ export class Span implements OmniSequence { constructor(positional: Positional) constructor(positional: Positional, start: number) constructor(positional: Positional, start: number, end: number) constructor(positional: Positional, start?: number, end?: number) { this._array = positional this._start = start === undefined ? 0 : start this._end = end === undefined ? positional.length : end } /** * The underlying array or string of the span. */ private _array: Positional /** * The underlying array or string of the span. */ get array(): Positional { return this._array } /** * The start index of the span. */ private _start: number /** * The start index of the span. */ get start(): number { return this._start } /** * The non-inclusive end index of the span. */ private _end: number /** * The non-inclusive end index of the span. */ get end(): number { return this._end } [Symbol.iterator](): Iterator { return this._iterate(false) } toString(): string { return "Span::{" + this.start + ":" + this.end + "}" } all(predicate: Predicate): boolean { const {array, start, end} = this for (let i = start; i < end; i++) { if (!predicate(array[i])) { return false } } return true } as(this: Span): 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) const {array, start, end} = this if (index < start || index >= end) { throw new Error(ErrorMessage.indexOutOfBoundsAt) } return array[index] } cleave(index: number): [T[], T[]] { index = this._sanitizeIndex(index) const {array, start, end} = this if (index < start || index > end) { throw new Error(ErrorMessage.indexOutOfBoundsCleave) } const before = array.slice(start, index) const after = array.slice(index, end) return [before, after] as [T[], T[]] } count(): number count(predicate: Predicate): number count(predicate?: Predicate): number { if (predicate === undefined) { return this.end - this.start } let count = 0 const {array, start, end} = this for (let i = start; i < end; i++) { if (predicate(array[i])) { count++ } } return count } cut(index: number): OmniSequence cut(index: number, count: number): OmniSequence cut(index: number, count: number = 1): OmniSequence { index = this._sanitizeIndex(index) count = this._sanitizeCount(count) const {array, start, end} = this if (index < start || index >= end) { 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[] { const {array, start, end} = this if (Array.isArray(array)) { return array.slice(start, end) as T[] } return Array.from(array.slice(start, end) as any) as T[] } drop(): OmniSequence drop(count: number): OmniSequence drop(predicate: Predicate): OmniSequence drop(argument?: number | Predicate): OmniSequence { if (argument === undefined) { argument = 1 } else if (typeof argument === "function") { const predicate = argument const index = this._indexNot(predicate) if (index < 0) { return new Span(this.array, this.end, this.end) } return this._after(index) } const count = this._sanitizeCount(argument) return this._after(Math.min(this.start + count, this.end)) } dropLast(): OmniSequence dropLast(count: number): OmniSequence dropLast(predicate: Predicate): OmniSequence dropLast(argument?: number | Predicate): OmniSequence { if (argument === undefined) { argument = 1 } else if (typeof argument === "function") { const predicate = argument const index = this._indexNot(predicate, true) if (index < 0) { return new Span(this.array, this.start, this.start) } return this._before(index + 1) } const count = this._sanitizeCount(argument) return this._before(Math.max(this.end - count, this.start)) } each(action: (element: T, index: number, end: Block) => void): OmniSequence { const {array, start, end} = this let exited = false const exit = () => { exited = true } for (let i = start; i < end; i++) { action(array[i], i, exit) if (exited) { break } } return this } endsWith(suffix: Positional): boolean endsWith(suffix: Positional, equals: PairPredicate): boolean endsWith(suffix: Positional, equals: PairPredicate = is): boolean { if (suffix.length > this.count()) { return false } const array = this.array const start = this.end - suffix.length for (let i = 0; i < suffix.length; i++) { if (!equals(suffix[i], array[start + i])) { return false } } return true } entries(): OmniSequence<[number, T]> { return new Pipe(generateEntries(this)) } entriesReversed(): OmniSequence<[number, T]> { return new Pipe(generateEntriesReversed(this)) } 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 { const {array, start, end} = this for (let i = start; i < end; i++) { if (predicate(array[i])) { return array[i] } } 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 { const {array, start, end} = this for (let i = end - 1; i >= start; i--) { if (predicate(array[i])) { return array[i] } } if (instead === undefined) { return null } return instead } first(): T first(instead: O): T | O first(instead?: O): T | O { const array = this.array if (array.length === 0) { if (instead === undefined) { throw new Error(ErrorMessage.noFirstElement) } return instead } return array[0] } flat(this: Span>): OmniSequence { return new Pipe(generateFlat(this)) } fold(initial: R, reducer: (accumulator: R, current: T) => R): R { const {array, start, end} = this let value = initial for (let i = start; i < end; i++) { value = reducer(value, array[i]) } return value } index(predicate: Predicate): number { return this._index(predicate, false) - this.start } indices(): OmniSequence indices(predicate: Predicate): OmniSequence indices(predicate?: Predicate): OmniSequence { if (predicate === undefined) { return range(this.count()) } return new Pipe(generateIndices(this, predicate)) } indicesReversed(): OmniSequence indicesReversed(predicate: Predicate): OmniSequence indicesReversed(predicate?: Predicate): OmniSequence { if (predicate === undefined) { return range(this.start, this.end, -1) } return new Pipe(generateIndicesReversed(this, predicate)) } inject(index: number, elements: Sequence): OmniSequence { index = this._sanitizeIndex(index) const {start, end} = this if (index < start || index > end) { throw new Error(ErrorMessage.indexOutOfBoundsInject) } return new Pipe(generateInject(this, index, elements)) } 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) const {start, end} = this if (index < start || index > end) { throw new Error(ErrorMessage.indexOutOfBoundsInsert) } return new Pipe(generateInject(this, index, elements)) } invert(this: Span<[K, V]>): OmniSequence<[V, K]> { return new Pipe(generateInvert(this)) } join(): string join(separator: string): string join(separator = ""): string { return this.yank().toArray().join(separator) } last(): T last(instead: O): T | O last(instead?: O): T | O { if (this.count() > 0) { return this.array[this.end - 1] } if (instead === undefined) { throw new Error(ErrorMessage.noLastElement) } return instead } lastIndex(): number lastIndex(predicate: Predicate): number lastIndex(predicate?: Predicate): number { if (predicate === undefined) { return this.count() - 1 } return this._index(predicate, true) - this.start } map(this: Span, transform: Transform): OmniSequence<[OK, OV]> map(this: Span, transform: Transform): OmniSequence map(this: Span, transform: Transform): OmniSequence { return new Pipe(generateMap(this, transform)) } max(this: Span): number max(transform: Transform): T max(transform?: Transform): T | number { return findExtreme(this, transform || identity, -Infinity, isGreaterThan, ErrorMessage.noMaxElement) } mean(this: Span): number { const {array, start, end} = this let sum = 0 if (this.count() === 0) { return 0 } for (let i = start; i < end; i++) { sum += array[i] } return sum / array.length } min(this: Span): 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 {array, start, end} = this const positive: T[] = [] const negative: T[] = [] for (let i = start; i < end; i++) { const element = array[i] if (predicate(element)) { positive.push(element) } else { negative.push(element) } } return [positive, negative] } permute(): OmniSequence { return new Pipe(generatePermute(this)) } prepend(...elements: T[]): OmniSequence { return new Pipe(generatePrepend(this, elements)) } rank(transform: Transform): OmniSequence { return this.sort((a: T, b: T) => transform(a) - transform(b)) } remove(predicate: Predicate): OmniSequence remove(predicate: Predicate, count: number): OmniSequence remove(predicate: Predicate, count: number = Infinity): OmniSequence { return new Pipe(generateRemove(this, predicate, this._sanitizeCount(count))) } removeLast(predicate: Predicate, count: number): OmniSequence { return new Pipe(generateRemoveLast(this, predicate, this._sanitizeCount(count))) } repeat(): OmniSequence repeat(count: number): OmniSequence repeat(count?: number): OmniSequence { return new Pipe(generateRepeat(this, count ? this._sanitizeCount(count) : count)) } 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 new Pipe(generateReplaceLast(this, predicate, replacement, this._sanitizeCount(count))) } reversed(): OmniSequence { return new Pipe(this._iterate(true)) } set(index: number, element: T): OmniSequence { index = this._sanitizeIndex(index) const {start, end} = this if (index < start || index >= end) { throw new Error(ErrorMessage.indexOutOfBoundsSet) } const copy = this.yank() const inner = copy.array as T[] inner[index] = element return copy } slice(start: number, end: number): OmniSequence { start = this._sanitizeIndex(start) end = this._sanitizeIndex(end) if (start < this.start || start > this.end || end < start) { throw new Error(ErrorMessage.invalidSliceBounds) } return this._slice(start, end) } solid(): Span { return this } some(): boolean some(predicate: Predicate): boolean some(predicate?: Predicate): boolean { if (predicate === undefined) { return this.end > this.start } return this.index(predicate) >= 0 } sort(this: Span): OmniSequence sort(this: Span, descending: boolean): OmniSequence sort(this: Span): OmniSequence sort(this: Span, descending: boolean): OmniSequence sort(comparator: Comparator): OmniSequence sort(this: Span, second: Comparator | boolean = false): OmniSequence { const {array, start, end} = this let segment = array.slice(start, end) if (typeof segment === "string") { segment = Array.from(segment) } if (segment.length === 0) { return new Span(segment) } let comparator: Comparator if (typeof second === "function") { comparator = second } else { const descending = second if (typeof segment[0] === "string") { comparator = ((a: string, b: string) => a.localeCompare(b)) as any } else { comparator = ((a: number, b: number) => a - b) as any } if (descending) { comparator = negate(comparator) } } return new Span(segment.sort(comparator 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: Span): number { const {array, start, end} = this let sum = 0 for (let i = start; i < end; i++) { sum += array[i] } return sum } swap(first: number, second: number): OmniSequence { first = this._sanitizeIndex(first) second = this._sanitizeIndex(second) const {start, end} = this if (first < start || first >= end) { throw new Error(ErrorMessage.indexOutOfBoundsSwap) } if (second < start || second >= end) { throw new Error(ErrorMessage.indexOutOfBoundsSwap) } const copy = this.yank() const array = copy.array as T[] const temporary = array[first] array[first] = array[second] array[second] = temporary return copy } take(): OmniSequence take(count: number): OmniSequence take(predicate: Predicate): OmniSequence take(argument?: number | Predicate): OmniSequence { if (argument === undefined) { return this._copy() } if (typeof argument === "function") { const predicate = argument const index = this._indexNot(predicate) if (index < 0) { return this._copy() } return this._before(index) } const count = argument === undefined ? this.count() : this._sanitizeCount(argument) return this._before(Math.min(count + this.start, this.end)) } takeLast(count: number): OmniSequence takeLast(predicate: Predicate): OmniSequence takeLast(argument: number | Predicate): OmniSequence { if (typeof argument === "function") { const predicate = argument const index = this._indexNot(predicate, true) return this._after(index + 1) } const count = argument === undefined ? this.count() : this._sanitizeCount(argument) return this._after(Math.max(0, this.end - count)) } toArray(): T[] toArray(destination: T[]): T[] toArray(destination: T[], clear: boolean): T[] toArray(destination?: T[], clear: boolean = false): T[] { if (destination === undefined) { return this.done() } const {array, start, end} = this if (clear) { destination.length = 0 } for (let i = start; i < end; i++) { destination.push(array[i]) } return destination } toMap(this: Span<[K, V]>): Map toMap(this: Span<[K, V]>, destination: Map): Map toMap(this: Span<[K, V]>, destination: Map, clear: boolean): Map toMap(this: Span<[K, V]>, destination?: Map, clear: boolean = false): Map { const {array, start, end} = this destination = destination || new Map() if (clear) { destination.clear() } for (let i = start; i < end; i++) { const [key, value] = array[i] destination.set(key, value) } return destination } toObject(this: Span<[string, V]>): Dictionary toObject(this: Span<[string, V]>, destination: Dictionary): Dictionary toObject(this: Span<[string, V]>, destination: Dictionary, clear: boolean): Dictionary toObject(this: Span<[string, V]>, destination?: Dictionary, clear: boolean = false): Dictionary { const {array, start, end} = this if (destination) { if (clear) { for (const key in destination) { if (destination.hasOwnProperty(key)) { delete destination[key] } } } } else { destination = {} } for (let i = start; i < end; i++) { const [key, value] = array[i] destination[key] = value } return destination } toSet(): Set toSet(destination: Set): Set toSet(destination: Set, clear: boolean): Set toSet(destination?: Set, clear: boolean = false): Set { const {array, start, end} = this destination = destination || new Set() if (clear) { destination.clear() } for (let i = start; i < end; i++) { destination.add(array[i]) } 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(this.toArray()) as Span } zip(elements: Sequence): OmniSequence<[T, V]> { return new Pipe(generateZip(this, elements)) } private _index(predicate: Predicate, reverse = false): number { const {array, start, end} = this if (reverse) { for (let i = end - 1; i >= start; i--) { if (predicate(array[i])) { return i } } } else { for (let i = start; i < end; i++) { if (predicate(array[i])) { return i } } } return -1 } private _indexNot(predicate: Predicate, reverse = false): number { const {array, start, end} = this if (reverse) { for (let i = end - 1; i >= start; i--) { if (!predicate(array[i])) { return i } } } else { for (let i = start; i < end; i++) { if (!predicate(array[i])) { return i } } } return -1 } private * _iterate(reverse: boolean): SequenceIterator { const {array, start, end} = this if (reverse) { for (let i = end - 1; i >= start; i--) { yield array[i] } } else { for (let i = start; i < end; i++) { yield array[i] } } } private _after(start: number) { return this._slice(start, this.end) } private _before(end: number) { return this._slice(this.start, end) } private _copy() { return this._slice(this.start, this.end) } private _empty() { return this._slice(this.start, this.start) } private _sanitizeIndex(index: number) { return this._start + Math.floor(index) } private _sanitizeCount(count: number) { return Math.max(Math.floor(count), 0) } private _seat(positional: Positional) { this._array = positional this._start = 0 this._end = positional.length } private _slice(start: number, end: number) { return new Span(this.array, start, end) } } function* generateAppend(span: Span, elements: Sequence): SequenceIterator { yield* span yield* elements } function* generateCut(span: Span, index: number, count: number): SequenceIterator { const {array, start, end} = span const first = index + start const last = Math.min(first + Math.max(count, 0), end) - 1 for (let i = start; i < first; i++) { yield array[i] } for (let i = last + 1; i < end; i++) { yield array[i] } } function* generateChunk(span: Span, count: number): SequenceIterator { const {array, start, end} = span let current: T[] = [] for (let i = start; i < end; i++) { current.push(array[i]) if (current.length === count) { yield current current = [] } } if (current.length !== 0) { yield current } } function* generateDifference(span: Span, sequence: Sequence, transform: Transform): SequenceIterator { sequence = Array.isArray(sequence) ? sequence : Array.from(sequence) const {array, start, end} = span const keys = new Set() const otherKeys = new Set() for (const element of sequence) { otherKeys.add(transform(element)) } for (let i = start; i < end; i++) { const element = array[i] const key = transform(element) if (keys.has(key)) { continue } keys.add(key) if (otherKeys.has(key)) { continue } yield element } otherKeys.clear() for (const element of sequence) { const key = transform(element) if (otherKeys.has(key)) { continue } otherKeys.add(key) if (keys.has(key)) { continue } yield element } } function* generateEntries(span: Span): SequenceIterator<[number, T]> { const {array, start, end} = span for (let i = start; i < end; i++) { yield [i, array[i]] } } function* generateEntriesReversed(span: Span): SequenceIterator<[number, T]> { const {array, start, end} = span for (let i = end - 1; i >= start; i--) { yield [i, array[i]] } } function* generateFilter(span: Span, predicate: Predicate): SequenceIterator { const {array, start, end} = span for (let i = start; i < end; i++) { const element = array[i] if (predicate(element)) { yield element } } } function* generateFlat(span: Span>): SequenceIterator { const {array, start, end} = span for (let i = start; i < end; i++) { yield* array[i] } } function* generateIndices(span: Span, predicate: Predicate): SequenceIterator { const {array, start, end} = span for (let i = start; i < end; i++) { const element = array[i] if (predicate(element)) { yield i } } } function* generateIndicesReversed(span: Span, predicate: Predicate): SequenceIterator { const {array, start, end} = span for (let i = end - 1; i >= start; i--) { const element = array[i] if (predicate(element)) { yield i } } } function* generateInject(span: Span, index: number, elements: Sequence): SequenceIterator { const {array, start, end} = span for (let i = start; i < index; i++) { yield array[i] } yield* elements for (let i = index; i < end; i++) { yield array[i] } } function* generateIntersect(span: Span, sequence: Sequence, transform: Transform): SequenceIterator { const {array, start, end} = span const keys = new Set() const other = new Set() for (const element of sequence) { other.add(transform(element)) } for (let i = start; i < end; i++) { const element = array[i] const key = transform(element) if (keys.has(key)) { continue } keys.add(key) if (other.has(key)) { yield element } } } function* generateInvert(span: Span<[K, V]>): SequenceIterator<[V, K]> { const {array, start, end} = span for (let i = start; i < end; i++) { const pair = array[i] yield [pair[1], pair[0]] } } function* generateMap(span: Span, transform: Transform): SequenceIterator { const {array, start, end} = span for (let i = start; i < end; i++) { yield transform(array[i]) } } function* generatePrepend(span: Span, elements: Sequence): SequenceIterator { yield* elements yield* span } function* generatePermute(span: Span): SequenceIterator { const length = span.count() const counts = new Array(length).fill(0) const permutation = span.toArray() let i = 0 yield span.toArray() while (i < length) { if (counts[i] >= i) { counts[i++] = 0 continue } const j = i % 2 && counts[i] const temporary = permutation[i] permutation[i] = permutation[j] permutation[j] = temporary yield permutation.slice() ++counts[i] i = 1 } } function* generateRemove(span: Span, predicate: Predicate, count: number): SequenceIterator { const {array, start, end} = span let removed = 0 let i = start if (count > 0) { while (i < end) { const element = array[i++] if (predicate(element)) { if (++removed >= count) { break } } else { yield element } } } while (i < end) { yield array[i++] } } function* generateRemoveLast(span: Span, predicate: Predicate, count: number): SequenceIterator { const {array, start, end} = span const back = [] let removed = 0 let i for (i = end - 1; i >= start && removed < count; i--) { const element = array[i] if (predicate(element)) { removed++ } else { back.push(element) } } for (let j = start; j <= i; j++) { yield array[j] } for (let j = back.length - 1; j >= 0; j--) { yield back[j] } } function* generateRepeat(span: Span, count: number | undefined): SequenceIterator { if (count === undefined) { while (true) { yield* span } } count = Math.max(count, 0) for (let i = 0; i < count; i++) { yield* span } } function* generateReplace(span: Span, predicate: Predicate, replacement: R, count: number): SequenceIterator { const {array, start, end} = span let i let replaced = 0 for (i = start; i < end && replaced < count; i++) { const element = array[i] if (predicate(element)) { yield replacement replaced++ } else { yield element } } while (i < end) { yield array[i++] } } function* generateReplaceLast(span: Span, predicate: Predicate, replacement: R, count: number): SequenceIterator { const {array, start, end} = span const back = [] let replaced = 0 let i for (i = end - 1; i >= start && replaced < count; i--) { const element = array[i] if (predicate(element)) { back.push(replacement) replaced++ } else { back.push(element) } } for (let j = start; j <= i; j++) { yield array[j] } for (let j = back.length - 1; j >= 0; j--) { yield back[j] } } function* generateUnion(span: Span, sequence: Sequence, transform: Transform): SequenceIterator { const {array, start, end} = span const seen = new Set() for (let i = start; i < end; i++) { const element = array[i] 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(span: Span, transform: Transform): SequenceIterator { const {array, start, end} = span const seen = new Set() for (let i = start; i < end; i++) { const element = array[i] const key = transform(element) if (seen.has(key)) { continue } yield element seen.add(key) } } function* generateZip(span: Span, values: Sequence): SequenceIterator<[T, V]> { const {array, start, end} = span let i = start for (const value of values) { if (i >= end) { break } yield [array[i++], value] } } function findExtreme(span: Span, transform: Transform, opposite: number, comparator: PairPredicate, errorMessage: string): T { if (span.none()) { throw new Error(errorMessage) } const {array, start, end} = span let extreme = opposite let result: T let found = false for (let i = start; i < end; i++) { const element = array[i] const value = transform(element) if (comparator(value, extreme)) { extreme = value result = element found = true } } return result!! }