Darkhogg/polyethylene

View on GitHub
src/lib/sync/poly-iterable.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {
  ChunkingPredicate,
  Comparator,
  IndexedMapping,
  IndexedPredicate,
  IndexedReducer,
  IndexedRunnable,
  IndexedTypePredicate,
} from '../types.js'

import {
  appendGen,
  chunkGen,
  chunkWhileGen,
  dropGen,
  dropLastGen,
  dropWhileGen,
  filterGen,
  flattenGen,
  groupByGen,
  mapGen,
  prependGen,
  reverseGen,
  sliceGen,
  sortGen,
  takeGen,
  takeLastGen,
  takeWhileGen,
  tapGen,
  uniqueGen,
} from './generators.js'

import {comparator, asserts, identity, isNotNullish} from '../utils.js'
import PolyAsyncIterable from '../async/poly-iterable.js'

/**
 * A `SyncIterable<T>` with a suite of methods for transforming the iteration into other iterations or to get a single
 * result from it.
 *
 * The methods of this class are intended to resemble those of `Array`, with added utilities where appropriate and made
 * for any kind of iterable.
 *
 * @public
 */
export default class PolySyncIterable<T> implements Iterable<T> {
  #iterable: Iterable<T>

  /** @internal */
  constructor (iterable: Iterable<T>) {
    this.#iterable = iterable
  }


  /**
   * Allows this class to work as a regular `Iterable<T>`
   *
   * @returns an iterable that will yield the same elements as the iterable used to create this instance
   */
  * [Symbol.iterator] (): Generator<T, void, undefined> {
    yield * this.#iterable
  }

  /**
   * Return an async version of this same iteration.
   *
   * @returns A {@link PolyAsyncIterable} that yields the same elements as `this`
   */
  async (): PolyAsyncIterable<T> {
    const syncIterable = this.#iterable
    const asyncIterable = {
      async * [Symbol.asyncIterator] (): AsyncIterator<T> {
        yield * syncIterable
      },
    }
    return new PolyAsyncIterable<T>(asyncIterable)
  }

  /**
   * Return a new iteration that will iterate over `this`, then over `other`.
   *
   * @remarks
   * The resulting iteration is of the combined generic type of `this` and `other`, allowing this method to merge the
   * types of two distinct iterables.
   *
   * @typeParam U - Type of the elements to be appended
   * @param other - Iterable to be appended
   * @returns a new {@link PolySyncIterable} that yields the elements of `this` and then the elements of `other`
   */
  append<U> (other: Iterable<U>): PolySyncIterable<T | U> {
    asserts.isSyncIterable(other)
    return new PolySyncIterable(appendGen(this.#iterable, other))
  }

  /**
   * Return a new iteration that will iterate over `this`, then over `other`.
   *
   * @remarks
   * This method is an alias for {@link PolySyncIterable.append}.
   *
   * @typeParam U - Type of the elements to be appended
   * @param other - Iterable to be appended
   * @returns a new {@link PolySyncIterable} that yields the elements of `this` and then the elements of `other`
   */
  concat<U> (other: Iterable<U>): PolySyncIterable<T | U> {
    return this.append(other)
  }

  /**
   * Return a new iteration that will iterate over `other`, then over `this`.
   *
   * @remarks
   * The resulting iteration is of the combined generic type of `this` and `other`, allowing this method to merge the
   * types of two distinct iterables.
   *
   * @typeParam U - Type of the elements to be prepended
   * @param other - Iterable to be prepended
   * @returns a new {@link PolySyncIterable} that yields the elements of `other` and then the elements of `this`
   */
  prepend<U> (other: Iterable<U>): PolySyncIterable<T | U> {
    asserts.isSyncIterable(other)
    return new PolySyncIterable(prependGen(this.#iterable, other))
  }


  /**
   * Return a new iteration that skips the first `num` elements.  If there were less than `num` elements in the
   * iteration, no elements are yielded.
   *
   * @param num - The number of elements to skip
   * @returns a new {@link PolySyncIterable} that yields the same the elements of `this`, except for the first
   * `num` elements
   */
  drop (num: number = 0): PolySyncIterable<T> {
    asserts.isNonNegativeInteger(num)
    return new PolySyncIterable(dropGen(this.#iterable, num))
  }

  /**
   * Return a new iteration that iterates only over the first `num` elements.  If there were less than than `num`
   * elements in the iteration, all elements are yielded with no additions.
   *
   * @param num - The number of elements to yield
   * @returns a new {@link PolySyncIterable} that yields the first `num` elements elements of `this`
   */
  take (num: number = 0): PolySyncIterable<T> {
    asserts.isNonNegativeInteger(num)
    return new PolySyncIterable(takeGen(this.#iterable, num))
  }

  /**
   * Return a new iteration that skips the last `num` elements.  If there were less than `num` elements in the
   * iteration, no elements are yielded.
   *
   * @remarks
   * The returned iteration keeps a buffer of `num` elements internally in order to skip those if the iteration ends,
   * and so elements effectively get delayed by `num` elements.
   *
   * @param num - The number of elements to skip
   * @returns a new {@link PolySyncIterable} that yields the same the elements of `this`, except for the last
   * `num` elements
   */
  dropLast (num: number = 0): PolySyncIterable<T> {
    asserts.isNonNegativeInteger(num)
    return new PolySyncIterable(dropLastGen(this.#iterable, num))
  }

  /**
   * Return a new iteration that iterates only over the last `num` elements.  If there were less than than `num`
   * elements in the iteration, all elements are yielded with no additions.
   *
   * @remarks
   * The returned iteration keeps a buffer of `num` elements internally in order to know which elements to keep.
   * and so elements effectively get delayed until the iteration ends.
   *
   * @param num - The number of elements to yield
   * @returns a new {@link PolySyncIterable} that yields the last `num` elements elements of `this`
   */
  takeLast (num: number = 0): PolySyncIterable<T> {
    asserts.isNonNegativeInteger(num)
    return new PolySyncIterable(takeLastGen(this.#iterable, num))
  }

  /**
   * Return a new iteration that skips the first few elements for which `func(element)` returns `true`.
   *
   * @param func - The function to call on the elements
   * @returns a new {@link PolySyncIterable} that yields the same the elements of `this`, excepts the first few for
   * which`func(element)` returns `true`
   */
  dropWhile (func: IndexedPredicate<T>): PolySyncIterable<T> {
    asserts.isFunction(func)
    return new PolySyncIterable(dropWhileGen(this.#iterable, func))
  }

  /**
   * Return a new iteration that yields the first few elements for which `func(element)` returns `true`.
   *
   * @param func - The function to call on the elements
   * @returns a new {@link PolySyncIterable} that yields the same the elements of `this` as long as `func(element)`
   * returns `true`
   */
  takeWhile (func: IndexedPredicate<T>): PolySyncIterable<T> {
    asserts.isFunction(func)
    return new PolySyncIterable(takeWhileGen(this.#iterable, func))
  }

  /**
   * Return a new iteration that starts from the `start`th element (included)
   * and ends at the `end`th element (excluded) of `this`.
   *
   * @remarks
   * Both `start` and `end` allow for negative values, in which case they refer to the nth-to-last element,
   * with n being the absolute value of the argument.  `end` might also be `undefined`, in which case the iteration is
   * not shortened on the end side, yielding up to the end, including the last element.
   * This mimics the behaviour of {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/slice Array.slice}.
   *
   * This function will likely need a buffer, effectively delaying the yielding of elements for a while.
   *
   * @param start - The index of the first element returned
   * @param end - The index of the first element *not* returned, inclusive
   * @returns a new {@link PolySyncIterable} that yields the elements going that starts from the `start`th element
   * (included) and ends at the `end`th element (excluded) of `this`
   */
  slice (start: number, end?: number): PolySyncIterable<T> {
    asserts.isInteger(start, 'start')
    asserts.isInteger(end ?? 0, 'end')
    return new PolySyncIterable(sliceGen(this.#iterable, start, end))
  }

  /**
   * Return an iteration of the elements of `this` for which `func(element)` returns `true`.
   *
   * @remarks
   * Because the `func` argument is a type predicate, the result iteration will have the type asserted by `func`.
   *
   * @typeParam U - The type asserted by `func`, if any
   * @param func - The function to be called on all elements
   * @returns A new {@link PolySyncIterable} with only elements for which `func(element)` returned true and correctly
   * narrowed to the type asserted by `func`
   *
   * {@label FILTER_TYPEPRED}
   */
  filter<U extends T> (func: IndexedTypePredicate<T, U>): PolySyncIterable<U>

  /**
   * Return an iteration of the elements of `this` for which `func(element)` returns `true`.
   *
   * @param func - The function to be called on all elements
   * @returns A new {@link PolySyncIterable} with only elements for which `func(element)` returned
   */
  filter (func: IndexedPredicate<T>): PolySyncIterable<T>

  filter<U extends T> (func: IndexedPredicate<T> | IndexedTypePredicate<T, U>): PolySyncIterable<T | U> {
    asserts.isFunction(func)
    return new PolySyncIterable(filterGen(this.#iterable, func))
  }


  /**
   * Return an iteration of all the elements as `this` that aren't `null` or `undefined`.
   *
   * @remarks
   * This function is a shortcut to calling {@link PolySyncIterable.filter.(:FILTER_TYPEPRED)} with a type predicate
   * function that correctly filters out `null` and `undefined` values from the iteration.  Note that other falsy values
   * will remain in the iteration, and that the return value is correctly typed to exclude `null` and `undefined`.
   *
   * @returns A new {@link PolySyncIterable} that yields the same elements as `this`
   * except for `null` or `undefined` values
   */
  filterNotNullish (): PolySyncIterable<NonNullable<T>> {
    return this.filter(isNotNullish)
  }


  /**
   * Return an iteration of the result of calling `func(element)` for every element in `this`.
   *
   * @typeParam U - The return type of `func` and the generic type of the resulting iterable
   * @param func - A function that takes an element of `this` and returns something else
   * @returns A new {@link PolySyncIterable} that yields the results of calling `func(element)`
   * for every element of `this`
   */
  map<U> (func: IndexedMapping<T, U>): PolySyncIterable<U> {
    asserts.isFunction(func)
    return new PolySyncIterable(mapGen(this.#iterable, func))
  }

  /**
   * Return an iteration of the same elements as `this` after calling `func(element)` for all elements.
   *
   * @typeParam U - The return type of `func`
   * @param func - A function called for all elements
   * @returns A new {@link PolySyncIterable} that yields the same elements as `this`
   */
  tap (func: IndexedRunnable<T>): PolySyncIterable<T> {
    asserts.isFunction(func)
    return new PolySyncIterable(tapGen(this.#iterable, func))
  }

  /**
   * Return an iteration of the yielded elements of the sub-iterables.
   *
   * @typeParam U - The type of the sub-iterable elements
   * @returns A new {@link PolySyncIterable} that will yield the elements of all sub-iterables
   */
  flatten<U> (this: PolySyncIterable<Iterable<U>>): PolySyncIterable<U> {
    return new PolySyncIterable(flattenGen(this.#iterable))
  }

  /**
   * Return an iteration of the yielded elements of the sub-iterables.
   *
   * @remarks
   * This method is an alias of {@link PolySyncIterable.flatten}.
   *
   * @typeParam U - The type of the sub-iterable elements
   * @returns A new {@link PolySyncIterable} that will yield the elements of all sub-iterables
   */
  flat<U> (this: PolySyncIterable<Iterable<U>>): PolySyncIterable<U> {
    return this.flatten()
  }

  /**
   * Return an iteration of elements of the sub-iterables that result from calling `func(element)`
   * for every element in `this`.
   *
   * @remarks
   * This method is equivalent to calling {@link PolySyncIterable.map map(func)}
   * and then {@link PolySyncIterable.flatten flatten()}
   *
   * @typeParam U - The type of the sub-iterables returned by `func`
   * @param func - A function that takes an element of `this` and returns an iterable
   * @returns A new {@link PolySyncIterable} that yields the elements of the subiterables that results from
   * calling `func(element)` for every element of `this`
   */
  flatMap<U> (func: IndexedMapping<T, Iterable<U>>): PolySyncIterable<U> {
    return this.map(func).flatten()
  }

  /**
   * Return an iteration of arrays of size `num` (except possibly the last) containing
   * groupings of elements of `this` iteration.
   *
   * @remarks
   * All chunks except possibly the last one will have exactly `num` elements.  The last chunk will have less elements
   * if the number of elements in the iteration is not divisible by `num`.  No chunk will ever be returned empty or
   * have more than `num` elements.
   *
   * @param num - Size of the chunks
   * @returns A new {@link PolySyncIterable} that yields arrays of size `num` (except possibly the last) containing
   * groupings of elements of `this`
   */
  chunk (num: number = 1): PolySyncIterable<Array<T>> {
    asserts.isPositiveInteger(num)
    return new PolySyncIterable(chunkGen(this.#iterable, num))
  }

  /**
   * Return an iteration of arrays with elements of this separated based on the result of calling `func(elements)`.
   *
   * @remarks
   * The chunking process works by keeping an open _current chunk_ and calling `func` to decide whether the next
   * element of the iteration will be part of the _current chunk_ or if it will be part of a new chunk.
   *
   * To do this, `func` will be called for every element of the iteration except the first, which will automatically
   * become part of the first chunk.  If `func` returns `true`, the element will be part of the current chunk, and
   * if it returns `false`, the _current chunk_ is closed and the element becomes the first element if the new
   * _current chunk_.
   * The arguments passed to `func` will be, in order:
   *   - `elem` - The element being currently processed
   *   - `lastElem` - The last element that was added to the current chunk
   *   - `firstElem` - The first element of the current chunk (might be the same as `lastElem`)
   *
   * All elements will be part of a chunk, and no chunk will ever be empty.
   *
   * @param func - A function that decides if an element is part of the current chunk or initiates a new one
   * @returns A new {@link PolySyncIterable} that yields arrays with the elements of `this` as separated by `func`
   */
  chunkWhile (func: ChunkingPredicate<T>): PolySyncIterable<Array<T>> {
    asserts.isFunction(func)
    return new PolySyncIterable(chunkWhileGen(this.#iterable, func))
  }

  /**
   * Return an iteration of group pairs, where the first element is a _group key_ and the second is an iterable of all
   * the elements for which `func(element)` returned the key.
   *
   * @remarks
   * This method is intended to be combined with {@link PolySyncIterable.toObject toObject}
   * or {@link PolySyncIterable.toMap toMap}, thus behaving like
   * {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/groupBy Array.groupBy} and
   * {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/groupByToMap Array.groupByToMap}
   * respectively, but without losing the ablity to further process the iteration, such as by mapping, filtering, etc.
   *
   * @typeParam K - Type of the keys used to group elements
   * @param func - A function that returns the grouping key of each element
   * @returns A new {@link PolySyncIterable} of group pairs with the key and the group
   */
  groupBy<K> (func: IndexedMapping<T, K>): PolySyncIterable<[K, Array<T>]> {
    asserts.isFunction(func)
    return new PolySyncIterable(groupByGen(this.#iterable, func))
  }

  /**
   * Return an iteration of unique elements, where two elements are considered equal if the result of `func(element)` is
   * the same for both elements.
   *
   * @remarks
   * Note that the first element seen with a specific key is always the one yielded, and every other element afterwards
   * is ignored.
   *
   * If no key-mapping function is given, the elements theselves are used as keys.
   * This is likely _not_ what you want in most situations unless elements are primitive types.
   *
   * @param func - A function that returns a _key_ used for uniqueness checks.
   * If not passed, an identitity function is used.
   * @returns A new {@link PolySyncIterable} only elements for which `func(element)` returns a value that hasn't
   * been seen before
   */
  unique (func: IndexedMapping<T, unknown> = identity): PolySyncIterable<T> {
    asserts.isFunction(func)
    return new PolySyncIterable(uniqueGen(this.#iterable, func))
  }

  /**
   * Return an iteration of the elements of `this` in reverse order.
   *
   * @remarks
   * This method will buffer _all_ elements of the iteration, and yield them all at once at the end
   *
   * @returns A new {@link PolySyncIterable} that yields the elements of `this` in reverse order
   */
  reverse (): PolySyncIterable<T> {
    return new PolySyncIterable(reverseGen(this.#iterable))
  }

  /**
   * Return an iteration of the elements of `this` sorted according to `func`
   *
   * @remarks
   * The sort function `func` is used to call
   * {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort Array.sort}
   * on an array of all the elements.
   * However, the default comparator function will sort elements according to the `<` and `>` operators defined on
   * their own type, of always sorting lexicagraphically like
   * {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort Array.sort}
   * does.
   *
   * This method will buffer _all_ elements of the iteration, and yield them all at once at the end
   *
   * @param func - A comparator function
   * @returns A new {@link PolySyncIterable} that yields the elements of `this` sorted according to `func`
   */
  sort (func: Comparator<T> = comparator): PolySyncIterable<T> {
    asserts.isFunction(func)
    return new PolySyncIterable(sortGen(this.#iterable, func))
  }

  /**
   * Return an array of all elements of this iteration in the same order that were yielded.
   *
   * @returns An array that contains the same elements as this iteration, in the same order
   */
  toArray (): Array<T> {
    return Array.from(this.#iterable)
  }

  /**
   * Splits this iteration into two arrays, one with elements for which `func(element)` returns `true` (the _truthy
   * elements_) and one for which it returns `false` (the _falsy elements_).
   *
   * @remarks
   * The array of _truthy elements_ has its element type narrowed to the type asserted by `func`.
   *
   * @typeParam U - The type asserted by `func`
   * @param func - A function that will be called for all elements to split them into the result arrays
   * @returns A tuple with the array of values for which `func` returned `true` as the first element, and the array of
   * values for which `func` returned `false` as the second element.
   */
  toPartitionArrays<U extends T> (func: IndexedTypePredicate<T, U>): [Array<U>, Array<Exclude<T, U>>]

  /**
   * Splits this iteration into two arrays, one with elements for which `func(element)` returns `true` (the _truthy
   * elements_) and one for which it returns `false` (the _falsy elements_).
   *
   * @param func - A function that will be called for all elements to split them into the result arrays
   * @returns A tuple with the array of values for which `func` returned `true` as the first element, and the array of
   * values for which `func` returned `false` as the second element.
   */
  toPartitionArrays (func: IndexedPredicate<T>): [Array<T>, Array<T>]

  toPartitionArrays<U extends T> (
    func: IndexedPredicate<T> | IndexedTypePredicate<T, U>,
  ): [Array<U | T>, Array<T | Exclude<T, U>>] {
    const trues: Array<U | T> = []
    const falses: Array<T | Exclude<T, U>> = []

    let idx = 0
    for (const elem of this.#iterable) {
      if (func(elem, idx++)) {
        trues.push(elem)
      } else {
        falses.push(elem)
      }
    }

    return [trues, falses]
  }

  /**
   * Return an object made from the entries of `this`.
   * This method is roughly equivalent to calling `Object.fromEntires(iter.toArray())`.
   *
   * @remarks
   * This method is only available for iterations of pairs where the first component is a valid object key type.
   *
   * @returns An object composed of the entries yielded by this iterable.
   */
  toObject<K extends PropertyKey, V> (this: PolySyncIterable<readonly [K, V]>): Record<K, V> {
    const object = {} as Record<K, V>

    for (const [key, value] of this.#iterable) {
      object[key] = value
    }

    return object
  }

  /**
   * Return a `Map` made from the entries of `this`.
   * This method is roughly equivalent to calling `new Map(iter.toArray())`.
   *
   * @remarks
   * This method is only available for iterations of pairs where the first component is a valid object key type.
   *
   * @returns A `Map` composed of the entries yielded by this iterable.
   */
  toMap<K, V> (this: PolySyncIterable<readonly [K, V]>): Map<K, V> {
    const map = new Map<K, V>()

    for (const [key, value] of this.#iterable) {
      map.set(key, value)
    }

    return map
  }

  /**
   * Returns the first element for which `func(element)` returns `true`, or `undefined` if it never does.
   *
   * @remarks
   * `func` will be called on elements of this iteration until it returns `true`, and then not called again.
   *
   * The return type of this function is narrowed to the type asserted by `func`.
   *
   * @typeParam U - The type asserted by `func`
   * @param func - A type predicate called for elements of `this`
   * @returns The first element of the iteration for which `func` returned `true`
   */
  find<U extends T> (func: IndexedTypePredicate<T, U>): U | undefined

  /**
   * Returns the first element for which `func(element)` returns `true`, or `undefined` if it never does.
   *
   * @remarks
   * `func` will be called on elements of this iteration until it returns `true`, and then not called again.
   *
   * @param func - A boolean returning function called for elements of `this`
   * @returns The first element of the iteration for which `func` returned `true`
   */
  find (func: IndexedPredicate<T>): T | undefined

  find<U extends T> (func: IndexedPredicate<T> | IndexedTypePredicate<T, U>): T | U | undefined {
    asserts.isFunction(func)
    let idx = 0
    for (const elem of this.#iterable) {
      if (func(elem, idx++)) {
        return elem
      }
    }
    return undefined
  }

  /**
   * Returns whether an element is present in this iteration.
   *
   * @remarks
   * If the element is found in the iteration, no more elements are iterated.
   *
   * @param obj - The element to search in the iteration
   * @returns Whether `obj` is present in this iteration or not
   */
  includes (obj: T): boolean {
    for (const elem of this.#iterable) {
      if (Object.is(obj, elem) || obj === elem) {
        return true
      }
    }

    return false
  }

  /**
   * Returns `true` if calling `func(element)` returns `true` for at least one element, and `false` otherwise
   *
   * @remarks
   * If a call to `func(element)` returns `true`, no more elements are iterated.
   *
   * @param func - A function to be called on the elements of the iteration
   * @returns Whether calling `func` returned `true` on at least one element.
   */
  some (func: IndexedPredicate<T>): boolean {
    asserts.isFunction(func)

    let idx = 0
    for (const item of this.#iterable) {
      if (func(item, idx++)) {
        return true
      }
    }

    return false
  }

  /**
   * Returns `true` if calling `func(element)` returns `true` for every element, and `false` otherwise
   *
   * @remarks
   * If a call to `func(element)` returns `false`, no more elements are iterated.
   *
   * @param func - A function to be called on the elements of the iteration
   * @returns Whether calling `func` returned `true` for all elements.
   */
  every (func: IndexedPredicate<T>): boolean {
    asserts.isFunction(func)

    let idx = 0
    for (const item of this.#iterable) {
      if (!func(item, idx++)) {
        return false
      }
    }

    return true
  }

  /**
   * Returns the result of calling the passed `reducer` for all elements of the iteration and the result of the
   * previous call to `reducer`, starting by passing `init` or, if not present, the first element of the iteration.
   *
   * @remarks
   * If the `init` argument is not present, at least one element must be present in the iteration, else an error will
   * be thrown
   *
   * `reducer` will be called with the accumulated result, the next element of the iteration, and the index of the
   * iteration.  The resolved return value will be the value passed to the next call as the first argument, or the
   * value returned if no more elements remain.
   *
   * @param reducer - A function to call for all elements with the result of a previous call
   * @param init - First element to be passed to the `reducer` function
   * @returns The result to continually call `reducer` with all elements and the previous result
   */
  reduce (reducer: IndexedReducer<T, T>, init?: T): T

  /**
   * Returns the result of calling the passed `reducer` for all elements of the iteration and the result of the
   * previous call to `reducer`, starting by passing `init`.
   *
   * @remarks
   * `reducer` will be called with the accumulated result, the next element of the iteration, and the index of the
   * iteration.  The resolved return value will be the value passed to the next call as the first argument, or the
   * value returned if no more elements remain.
   *
   * @param reducer - A function to call for all elements with the result of a previous call
   * @param init - First element to be passed to the `reducer` function
   * @returns The result to continually call `reducer` with all elements and the previous result
   */
  reduce<U> (reducer: IndexedReducer<T, U>, init: U): U

  reduce<U> (reducer: IndexedReducer<T, U>, init: T extends U ? (U | undefined) : U): U {
    asserts.isFunction(reducer)

    let accumulated: U | undefined = init
    let isFirst = (accumulated === undefined)
    let idx = 0

    for (const elem of this.#iterable) {
      accumulated = isFirst ? (elem as unknown as U) : reducer(accumulated as U, elem, idx++)
      isFirst = false
    }

    return accumulated as U
  }

  /**
   * Call a function for each element of `this` iteration.
   *
   * @param func - A function to be called for every element of the iteration
   */
  forEach (func: IndexedRunnable<T>): void {
    asserts.isFunction(func)

    let idx = 0
    for (const elem of this.#iterable) {
      func(elem, idx++)
    }
  }

  /**
   * Return the result of joining the elements of `this` with the given `glue`, or `','` if no glue is given.
   *
   * @remarks
   * `null` or `undefined` elements are treated as empty strings.
   *
   * @param glue - The string to use for joining the elements
   * @returns A string concatenating all elements of `this` using the given `glue`
   */
  join (glue: string = ','): string {
    let str = ''
    let first = true

    for (const elem of this.#iterable) {
      str += (first ? '' : glue) + (elem == null ? '' : elem)
      first = false
    }

    return str
  }

  /**
   * Perform this iteration doing nothing.
   */
  complete (): void {
    /* eslint-disable-next-line no-unused-vars */
    for (const elem of this.#iterable) {
      /* do nothing, just iterate */
    }
  }
}