Home Reference Source Repository

src/eslinq.js

/*
 * ESLinq - LINQ for EcmaScript 2015
 *
 * An elegant way of working with ES6 iterables.
 *
 * Full source code and examples:
 * http://github.com/balazsbotond/eslinq
 */

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Botond Balazs <[email protected]>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 */

"use strict";

/**
 * Helper function that wraps the specified iterable instance in an
 * ESLinq Sequence which can be queried using ESLinq operators (like
 * 'select', 'where', etc.).
 *
 * @param {Iterable} iterable An iterable object (like an Array, a Set,
 *     or any object with a [Symbol.iterator] property).
 * @return {Sequence} An ESLinq Sequence which can be queried using
 *     ESLinq operators (like 'select', 'where', etc.).
 *
 * @example
 * const numbers = [1, 2, 3, 4, 5];
 * const squaresOfEvenNumbers =
 *     from(numbers)
 *         .where(n => n % 2 === 0)
 *         .select(n => n * n);
 *
 * for (let n of squaresOfEvenNumbers)
 *     console.log(n); // 4 16
 */
export default function from(iterable) {
    return new Sequence(iterable);
}

/**
 * An iterable sequence that can be queried using ESLinq operators (like
 * 'select', 'where', etc.).
 *
 * @implements {Iterable}
 */
export class Sequence {
    /**
     * Creates a new `Sequence` instance wrapping the `iterable` specified.
     *
     * @param {Iterable} The `Iterable` to wrap.
     */
    constructor(iterable) {
        this.iterable = iterable;
        this[Symbol.iterator] = iterable[Symbol.iterator];
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Projection and restriction methods                                   //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Applies the specified transformation to all elements of the
     * `Sequence`, returning a new `Sequence` of the transformed elements.
     *
     * @param {function(item: *, index: number): *} transform A function
     *     that is used to transform the elements of the sequence. The first
     *     argument, `item`, is the current sequence element, the second one,
     *     `index`, is the zero-based index of the current element.
     *
     * @return {Sequence} A new `Sequence` of the transformed elements.
     *
     * @throws {TypeError} if `transform` is not a function
     *
     * @example
     * // Simple case, only the `item` parameter of `matches` is used
     * const numbers = [1, 2, 3, 4, 5],
     *       even = from(numbers).select(n => n % 2 === 0);
     *
     * for (let n of even) {
     *     console.log(n); // 2 4
     * }
     *
     * // Here we also use the `index` parameter
     * const numbers = [1, 2, 3, 4, 5],
     *       numbersPlusIndices = from(numbers).select((n, i) => n + i);
     *
     * for (let n of numbersPlusIndices) {
     *     console.log(n); // 1 3 5 7 9
     * }
     */
    select(transform) {
        ensureIsFunction(transform, "`transform` should be a function");

        const generator = function* () {
            let index = 0;
            for (let item of this.iterable) {
                yield transform(item, index);
                index++;
            }
        };

        return this._wrap(generator);
    }

    /**
     * Applies the specified transformations to all elements of the
     * Sequence. The first transformation returns an iterable. The second
     * one, if present, is called for each element of the output of the
     * first, and returns an arbitrary value. The result is either a concatenation
     * of the iterables returned by the first transformation, or a sequence
     * containing the values returned by the second.
     *
     * @param {function(item: *, index: number): Iterable} getIterable A
     *     function that returns an iterable for each sequence element. The
     *     first argument, `item`, is the current sequence element, the
     *     second one, `index`, is the zero-based index of the current element.
     *
     * @param {function(item: *, innerItem: *): *} [transform] A function
     *     that is called for each element of the iterables returned by
     *     `getIterable`. The final sequence contains the output of this
     *     function. The first argument, `item`, is the current item of the original
     *     sequence, the second, `innerItem`, is the current element of the iterable
     *     returned by `getIterable`.
     *
     * @return {Sequence} A sequence of the values returned by the composition
     *     of the transformation functions.
     *
     * @throws {Error} If the object returned by `getIterable` is not iterable.
     *
     * @example
     * // Simple example, only the `getIterable` function is used
     * const taskLists = [
     *     {tasks: [1]}, {tasks: [2, 3]}, {tasks: [4]},
     *     {tasks: []}, {tasks: [5]}
     * ];
     *
     * const allTasks = from(taskLists).selectMany(t => t.tasks);
     *
     * for (let t of allTasks) {
     *     console.log(t); // 1 2 3 4 5
     * }
     *
     * // Here we use both transformation functions
     * const tasksByDate = [
     *     {
     *         date: "2015-09-20",
     *         tasks: [
     *             { id: 1, text: "buy groceries" },
     *             { id: 2, text: "do laundry" },
     *             { id: 3, text: "meet Janet" }
     *         ]
     *     },
     *     // ... more objects like above ...
     * ];
     *
     * const allTasks =
     *     from(tasksByDate)
     *         .selectMany(date => date.tasks, (date, task) => task.text);
     *
     * for (let task of allTasks) {
     *     console.log(task);
     * }
     *
     * // Output:
     * //     buy groceries
     * //     do laundry
     * //     meet Janet
     * //     ...
     */
    selectMany(getIterable, transform = (_, n) => n) {
        ensureIsFunction(getIterable, "`getIterable` should be a function");
        ensureIsFunction(transform, "`transform` should be a function");

        const generator = function* () {
            let index = 0;

            for (let item of this.iterable) {
                const innerIterable = getIterable(item, index);
                ensureIsIterable(innerIterable, "`getIterable` should return an iterable");

                for (let innerItem of innerIterable) {
                    yield transform(item, innerItem);
                }

                index++;
            }
        };

        return this._wrap(generator);
    }

    /**
     * Filters the Sequence by a condition specified.
     *
     * @param {function(item: *, index: number): boolean} matches A
     *     function that returns true if an element is to be included in
     *     the result. The first argument, `item`, is the current sequence
     *     element, the second one, `index`, is the zero-based index of the
     *     current element.
     *
     * @return {Sequence} A Sequence of the elements for which the
     *     'matches' function returned true.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @example
     * // Simple case, only the `item` parameter of `matches` is used
     * const numbers = [1, 2, 3, 4, 5],
     *       even = from(numbers).where(n => n % 2 === 0);
     *
     * for (let n of even) console.log(n); // 2 4
     *
     * // Here we also use the `index` parameter
     * const numbers = [2, 1, 3, 0, 4],
     *       itemEqualsIndex = (item, index) => item === index,
     *       numbersEqualToTheirIndices = from(numbers).where(itemEqualsIndex);
     *
     * for (let n of numbersEqualToTheirIndices) {
     *     console.log(n); // 1 4
     * }
     */
    where(matches) {
        ensureIsFunction(matches, "`matches` should be a function");

        const generator = function* () {
            let index = 0;
            for (let item of this.iterable) {
                if (matches(item, index)) {
                    yield item;
                }
                index++;
            }
        };

        return this._wrap(generator);
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Join methods                                                         //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Performs a SQL-like join of two iterables.
     *
     * The `getOuterKey` and `getInnerKey` functions are used to determine the
     * join key for an element of the outer and the inner sequence, respectively.
     * The resulting pairs of elements can be transformed using the optional
     * `transform` function. If `transform` is not specified, `join` returns a
     * sequence of two-element arrays where the element at index 0 is from the
     * first sequence and the one at index 1 is from the second.
     *
     * @param {Sequence} inner The inner sequence.
     * @param {function(i: *): *} getOuterKey Returns the join key to be used for
     *     the current element of the outer sequence.
     * @param {function(i: *): *} getInnerKey Returns the join key to be used for
     *     the current element of the inner sequence.
     * @param {function(outer: *, inner: *): *} [transform] A transformation to be
     *     applied to the resulting pairs of elements.
     *
     * @return {*} If `transform` is not specified, a sequence of two-element
     *     arrays where the element at index 0 is from the first sequence and the
     *     one at index 1 is from the second. If `transform` is specified, a
     *     sequence of the output of `transform` applied to the outer and inner
     *     elements.
     *
     * @throws {TypeError} if `inner` is not iterable. If either `getOuterKey` or
     *     `getInnerKey` is not a function. If `transform` is specified but it is
     *     not a function.
     *
     * @example
     * const users = [
     *     { id: 1, name: "Jane Smith" },
     *     { id: 2, name: "John Smith" },
     *     { id: 3, name: "Mary Brown" }
     * ];
     *
     * const emails = [
     *     { user_id: 1, address: "[email protected]" },
     *     { user_id: 1, address: "[email protected]" },
     *     { user_id: 3, address: "[email protected]" }
     * ];
     *
     * //
     * // First example: no `transform` function
     * //
     *
     * const usersWithEmails =
     *     from(users)
     *         .join(emails, user => user.id, email => email.user_id);
     *
     * for (let uwe of usersWithEmails) {
     *     console.log(uwe[0].name + ": " + uwe[1]);
     * }
     *
     * // Output:
     * //     Jane Smith: [email protected]
     * //     Jane Smith: [email protected]
     * //     Mary Brown: [email protected]
     *
     * //
     * // Second example: using a `transform` function
     * //
     *
     * const userNamesWithEmails =
     *     from(users)
     *         .join(emails,
     *             user => user.id,
     *             email => email.user_id,
     *             (user, email) => user.name + ": " + email.address);
     *
     * for (let uwe of userNamesWithEmails) {
     *     console.log(uwe);
     * }
     *
     * // Output:
     * //     Jane Smith: [email protected]
     * //     Jane Smith: [email protected]
     * //     Mary Brown: [email protected]
     */
    join(inner, getOuterKey, getInnerKey, transform = defaultJoinTransform) {
        ensureIsIterable(inner, "`inner` should be iterable");
        ensureIsFunction(getOuterKey, "`getOuterKey` should be a function");
        ensureIsFunction(getInnerKey, "`getInnerKey` should be a function");
        ensureIsFunction(transform, "`transform` should be a function");

        const generator = function* () {
            const lookup = new Sequence(inner).toLookup(getInnerKey);

            for (let outer of this.iterable) {
                const key = getOuterKey(outer);

                if (!lookup.has(key)) {
                    continue;
                }

                for (let inner of lookup.get(key)) {
                    yield transform(outer, inner);
                }
            }
        };

        return this._wrap(generator);
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Set methods                                                          //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Determines whether all elements satisfy a condition.
     *
     * @param {function(i: *): boolean} matches A function that
     *     determines whether an element of the Sequence satisfies
     *     a condition.
     *
     * @return {boolean} true if all elements satisfy the condition,
     *     otherwise, false.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @example
     * const numbers = [1, 2, 3],
     *       even = [2, 4, 6],
     *       isEven = n => n % 2 === 0;
     *
     * from(numbers).all(isEven); // false
     * from(even).all(isEven); // true
     */
    all(matches) {
        ensureIsFunction(matches, "`matches` should be a function");

        for (let i of this.iterable) {
            if (!matches(i)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Determines whether at least one element satisfies a condition.
     *
     * Without a condition, `!c.any()` can be used to quickly test whether
     * a sequence is empty. This is the preferred way of testing emptiness
     * vs. `c.count() === 0`, as it always runs in O(1) time.
     *
     * @param {function(i: *): boolean} matches A function that
     *     determines whether an element of the Sequence satisfies
     *     a condition.
     *
     * @return {boolean} true if at least one element satisfies the
     *     condition, otherwise, false.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @example
     * // Without a condition, `any` can be used to quickly test whether
     * // a sequence is empty.
     * const empty = [],
     *       nonEmpty = [1];
     *
     * console.log(from(empty).any()); // false
     * console.log(from(nonEmpty).any()); // true
     *
     * // With a condition
     * const numbers = [1, 2, 3],
     *       even = [2, 4, 6],
     *       isOdd = n => n % 2 !== 0;
     * from(numbers).any(isOdd); // true
     * from(even).any(isOdd); // false
     */
    any(matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        for (let i of this.iterable) {
            if (matches(i)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Determines whether at least one element is equal to the item specified
     * using the strict equality operator.
     *
     * @param {*} item The item to find.
     * @return {boolean} true if at least one element is equal to the item
     *     specified, otherwise, false.
     *
     * @example
     * const numbers = [1, 2, 3];
     * from(numbers).contains(n => n % 2 === 0); // true, 2 is even
     * from(numbers).contains(n => n > 3); // false, all elements less than 3
     */
    contains(item) {
        for (let i of this.iterable) if (i === item) return true;
        return false;
    }

    /**
     * Concatenates the iterable specified with the current one.
     *
     * @param {Iterable} other The iterable to concatenate after this instance.
     * @return {Sequence} The concatenation of the two iterables.
     *
     * @example
     * const numbers = [1, 2],
     *       letters = ["a", "b"],
     *       result  = from(numbers).concat(letters);
     *
     * for (let i of result) {
     *     console.log(i); // 1 2 a b
     * }
     */
    concat(other) {
        ensureIsIterable(other, "`other` should be iterable");

        const generator = function* () {
            for (let i of this.iterable) yield i;
            for (let j of other) yield j;
        };

        return this._wrap(generator);
    }

    /**
     * Returns the distinct elements in the sequence (removes duplication).
     *
     * @return {Sequence} A Sequence containing the distinct elements
     *     of the original one.
     *
     * @example
     * const numbers = [1, 1, 1, 2, 3, 4, 3],
     *       noDupl = from(numbers).distinct();
     *
     * for (let n of noDupl) console.log(n); // 1 2 3 4
     */
    distinct() {
        const generator = function* () {
            const seen = new Set();

            for (let i of this.iterable) {
                if (!seen.has(i)) {
                    seen.add(i);
                    yield i;
                }
            }
        };

        return this._wrap(generator);
    }

    /**
     * Returns all elements of the iterable except the ones in `other`.
     *
     * @param {Iterable} other The iterable to be subtracted.
     *
     * @return {Sequence} All elements of the iterable except the ones
     *     in `other`.
     *
     * @throws {TypeError}
     *
     * @example
     * const numbers = [1, 2, 3, 4, 5],
     *       exceptions = [3, 4],
     *       difference = from(numbers).except(exceptions);
     *
     * for (let n of difference) console.log(n); // 1 2 5
     */
    except(other) {
        ensureIsIterable(other, "`other` should be iterable");

        const generator = function* () {
            const seen = new Set(other);

            for (let i of this.iterable) {
                if (!seen.has(i)) {
                    seen.add(i);
                    yield i;
                }
            }
        };

        return this._wrap(generator);
    }

    /**
     * Returns the distinct elements both iterables (this and `other`) contain.
     *
     * @param {Iterable} other The iterable to intersect the current one with.
     *
     * @return {Sequence} The elements both iterables contain.
     *
     * @throws {TypeError} if `other` is not iterable.
     *
     * @example
     * const a = [1, 2, 3],
     *       b = [2, 3, 4, 3],
     *       i = from(a).intersect(b);
     *
     * for (let n of i) console.log(i); // 2 3
     */
    intersect(other) {
        ensureIsIterable(other, "`other` should be iterable");

        const generator = function* () {
            const seen = new Set(other);

            for (let i of this.iterable) {
                if (seen.has(i)) {
                    yield i;
                }
            }
        };

        return this._wrap(generator);
    }

    /**
     * Returns the distinct elements from both iterables (this and `other`).
     *
     * @param {Iterable} other The iterable to union the current one with.
     * @return {Sequence} The distinct elements from both iterables.
     *
     * @example
     * const a = [1, 2, 1, 3, 2],
     *       b = [3, 3, 4, 5, 4],
     *       u = from(a).union(b);
     * for (let i of u) console.log(i); // 1 2 3 4 5
     */
    union(other) {
        ensureIsIterable(other, "`other` should be iterable");

        const generator = function* () {
            const seen = new Set();
            for (let i of this.iterable) {
                if (!seen.has(i)) {
                    seen.add(i);
                    yield i;
                }
            }
            for (let i of other) {
                if (!seen.has(i)) {
                    seen.add(i);
                    yield i;
                }
            }
        };

        return this._wrap(generator);
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Ordering methods                                                     //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Returns a new sequence that contains the elements of the original ordered by
     * the return value of the `get` function. An optional `compare` function can
     * also be specified to implement custom comparison logic (by default, the
     * `orderBy` operator orders the sequence based on the result of the standard
     * ECMAScript comparison operators).
     */
     orderBy(get, compare = compareDefault) {
         let result = Array.from(this.iterable);
         result.sort((a, b) => compare(get(a), get(b)));
         return new Sequence(result);
     }

    /**
     * Returns a new sequence that contains the elements of the original in descending order,
     * ordered by the return value of the `get` function. An optional `compare` function can
     * also be specified to implement custom comparison logic (by default, the
     * `orderBy` operator orders the sequence based on the result of the standard
     * ECMAScript comparison operators).
     */
     orderByDescending(get, compare = compareDefault) {
         let result = Array.from(this.iterable);
         result.sort((a, b) => compare(get(b), get(a)));
         return new Sequence(result);
     }

     reverse() {
         const array = Array.from(this.iterable);
         return this._wrap(function* () {
             for (let i = array.length - 1; i >= 0; i--) yield array[i];
         });
     }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Grouping methods                                                     //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    groupBy(getKey) {
        const map = new Map();
        for (let i of this.iterable) {
            const key = getKey(i);
            if (map.has(key)) map.get(key).push(i);
            else map.set(getKey(i), [i]);
        }
        return new Sequence(map);
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Aggregate methods                                                    //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Applies an aggregation function over the sequence.
     *
     * @param {function(accumulator: *, current: *): *} processNext An aggregation
     *     function that gets the accumulated value and the current element of the
     *     sequence.
     *
     * @param {*} [seed] The initial value of the accumulator. If it is not
     *     specified, the first element of the sequence is used as the seed.
     *
     * @param {function(result: *): *} [transformResult] A transformation that is
     *     applied to the end result of the aggregation.
     *
     * @throws {TypeError} if `process` is not a function
     * @throws {TypeError} if `transformResult` is specified but is not a function
     * @throws {RangeError} if the sequence is empty and no seed has been specified
     *
     * @example
     * // Add all numbers in the array. The first one is used as the seed.
     * const numbers = [1, 2, 3],
     *       sum = from(numbers).aggregate((a, b) => a + b);
     *
     * console.log(sum); // 6
     *
     * // Concatenate all strings in the array. Use a seed value.
     * const animals = ["cat", "dog", "fish", "frog"],
     *       list = from(animals).aggregate((a, b) => a + ", " + b, "Animals: ");
     *
     * console.log(list); // Animals: cat, dog, fish, frog
     *
     * // Use a transformation on the result
     * const animals = ["cat", "dog", "fish", "frog"],
     *       concatNext = (a, b) => a + ", " + b,
     *       prefix = "Animals: ",
     *       makeParagraph = x => "<p>" + x + "</p>",
     *       paragraph = from(animals).aggregate(concatNext, prefix, makeParagraph);
     *
     * console.log(paragraph);
     *
     * // Output:
     * //     <p>Animals: cat, dog, fish, frog</p>
     */
    aggregate(processNext, seed, transformResult = identity) {
        ensureIsFunction(transformResult, "`transformResult` should be a function");
        ensureIsFunction(processNext, "`processNext` should be a function");

        const iterator = this.iterable[Symbol.iterator]();
        let result = iterator.next(),
            accumulator = seed;

        if (result.done) {
            if (seed !== undefined) return transformResult(seed);
            throw new RangeError("The sequence is empty and no seed has been specified");
        }

        if (seed === undefined) accumulator = result.value;
        else accumulator = processNext(seed, result.value);

        result = iterator.next();

        while(!result.done) {
            accumulator = processNext(accumulator, result.value);
            result = iterator.next();
        }

        return transformResult(accumulator);
    }

    /**
     * Calculates the average (arithmetic mean) of the elements of the sequence.
     *
     * **Evaluation:** eager
     *
     * @param {function(i: *): *} [getValue] A function to determine the value
     *     to include in the average for the current element.
     *
     * @return {number} The average of the elements.
     *
     * @throws {TypeError} if `getValue` is not a function. If the current
     *     element, or if `getValue` is used, the value returned by it, is not
     *     a number.
     *
     * @throws {RangeError} if the sequence is empty.
     *
     * @example
     * // Fist example: simple average
     * console.log(
     *     from([1, 2, 3]).average()
     * );
     *
     * // Output: 2
     *
     * // Second example: using the `getValue` function
     * const employees = [
     *     { id: 1, salary: 20000 },
     *     { id: 2, salary: 30000 },
     *     { id: 3, salary: 40000 }
     * ];
     *
     * console.log(
     *     from(employees).average(emp => emp.salary);
     * );
     *
     * // Output: 30000
     */
    average(getValue = identity) {
        ensureIsFunction(getValue, "`getValue` should be a function");

        let sum = 0,
            count = 0;

        for (let i of this.iterable) {
            const value = getValue(i);
            ensureIsNumber(value, "Only numbers can be averaged");
            sum += value;
            count++;
        }

        if (count === 0) {
            throw new RangeError("Sequence was empty");
        }

        return sum / count;
    }

    /**
     * Returns the sum of the elements of the sequence. If a `getValue`
     * function is specified, it is called for each element of the
     * sequence, and the return values are summed.
     *
     * @param {function(i: *): number} [getValue] A function that returns
     *     the term to be summed for the current sequence element.
     *
     * @return The sum of the (matching) elements.
     *
     * @throws {TypeError} if the value returned by `getValue` is not
     *     a number.
     *
     * @example
     * // Without `getValue`
     * console.log(
     *     from([1, 2, 3]).sum()
     * );
     *
     * // Output: 6
     *
     * // With `getValue`
     * const employees = [
     *     { name: "John Smith", salary: 15000 },
     *     { name: "Jane Smith", salary: 26000 },
     *     { name: "Jane Doe", salary: 15000 },
     * ];
     *
     * const totalSalary = from(employees).sum(emp => emp.salary);
     *
     * console.log(totalSalary); // 56000
     */
    sum(getValue = identity) {
        ensureIsFunction(getValue, "`getValue` should be a function");

        let sum = 0;

        for (let i of this.iterable) {
            const value = getValue(i);
            ensureIsNumber(value, "Only numbers can be summed");
            sum += value;
        }

        return sum;
    }

    /**
     * Returns the number of elements in the `Sequence`.
     *
     * If a `matches` function is specified, returns the number of matching
     * elements.
     *
     * **Note:** To quickly test whether a sequence is empty, use `!c.any()` instead
     * of `.count() === 0` as the former always runs in O(1) time.
     *
     * **Complexity:**
     *
     * - O(1) if the `Iterable` wrapped by this `Sequence` is an `Array`, `Map` or `Set`
     * - O(n) for other `Iterable`s
     *
     * *Note:* The complexity is O(1) only if the `Array`, `Map` or `Set` is wrapped
     * directly, like `from([1, 2]).count()`. Indirect wrapping will cause the entire
     * sequence to be iterated, as it is the case for
     * `from([1, 2]).where(n => n % 2 === 0).count()`.
     *
     * @param {function(item: *): boolean} matches A function that should
     *     return `true` for all elements to be included in the count and
     *     `false` for those to be excluded.
     *
     * @return {number} The number of matching elements in the `Sequence`.
     *
     * @throws {TypeError} if `matches` is specified, but it is not a function.
     *
     * @example
     * // No condition specified
     * const numbers = [1, 2, 3, 4],
     *       isEven = n => n % 2 === 0,
     *       count = from(numbers).count();
     *
     * console.log(count); // 4
     *
     * // Count matching elements
     * const numbers = [1, 2, 3, 4],
     *       isEven = n => n % 2 === 0,
     *       numberOfEvens = from(numbers).count(isEven);
     *
     * console.log(numberOfEvens); // 2
     */
    count(matches) {
        if (matches === undefined) {
            // If the wrapped iterable is an `Array`, `Map` or `Set`,
            // we can use these O(1) shortcuts to get the length.
            if (this.iterable instanceof Array) {
                return this.iterable.length;
            } else if (
                this.iterable instanceof Set ||
                this.iterable instanceof Map) {
                return this.iterable.size;
            }
            matches = identity;
        } else {
            ensureIsFunction(matches, "`matches` should be a function");
        }

        let count = 0;

        for (let i of this.iterable) {
            if (matches(i)) {
                count++;
            }
        }

        return count;
    }

    /**
     * Returns the minimum value in the sequence.
     *
     * If a `transform` function is specified, the transformation is invoked
     * for each element of the sequence and the minimum of the transformed
     * values is returned. By default, `min` uses the standard ECMAScript
     * `<` and `>` operators for comparison, but that behavior can be
     * customized by specifying a `compare` function.
     *
     * **Evaluation:** eager
     *
     * @param {function(i: *): *} [transform] A transformation to be applied
     *     to each element of the sequence. If specified, the minimum of the
     *     transformed values is returned.
     * @param {function(a: *, b: *): number} [compare] A function that
     *     returns a negative number if its first argument is less than the
     *     second, a positive number if its first argument is greater than
     *     the second, otherwise, 0.
     *
     * @return {*} The minimum value in the sequence.
     *
     * @throws {TypeError} if either `transform` or `compare` is not a
     *     function.
     * @throws {RangeError} if the sequence is empty.
     *
     * @example
     * // Called with no arguments
     * const numbers = [20, 35, -12, 0, 4, -7];
     * console.log(from(numbers).min()); // Output: -12
     *
     * // Using a transformation
     * const people = [
     *     { name: "Jennifer", age: 23 },
     *     { name: "John", age: 33 },
     *     { name: "Jack", age: 42 },
     *     { name: "Jill", age: 18 },
     *     { name: "Bob", age: 20 }
     * ];
     *
     * console.log(from(people).min(p => p.age));
     *
     * // Output:
     * //     18
     *
     * // Using a transformation and a custom comparer
     * const people = [
     *     { name: "Jennifer", age: 23 },
     *     { name: "John", age: 33 },
     *     { name: "Jack", age: 42 },
     *     { name: "Jill", age: 18 },
     *     { name: "Bob", age: 20 }
     * ];
     *
     * const compareLength = (a, b) => a.length - b.length;
     *
     * console.log(
     *     from(people).min(p => p.name, compareLength);
     * );
     *
     * // Output:
     * //     "Bob"
     */
    min(transform = identity, compare = compareDefault) {
        ensureIsFunction(transform, "`transform` should be a function");
        ensureIsFunction(compare, "`compare` should be a function");

        return this._findExtremum(
            (current, min) => compare(current, min) < 0,
            transform);
    }

    /**
     * Returns the maximum value in the sequence.
     *
     * If a `transform` function is specified, the transformation is invoked
     * for each element of the sequence and the maximum of the transformed
     * values is returned. By default, `max` uses the standard ECMAScript
     * `<` and `>` operators for comparison, but that behavior can be
     * customized by specifying a `compare` function.
     *
     * **Evaluation:** eager
     *
     * @param {function(i: *): *} [transform] A transformation to be applied
     *     to each element of the sequence. If specified, the maximum of the
     *     transformed values is returned.
     * @param {function(a: *, b: *): number} [compare] A function that
     *     returns a negative number if its first argument is less than the
     *     second, a positive number if its first argument is greater than
     *     the second, otherwise, 0.
     *
     * @return {*} The maximum value in the sequence.
     *
     * @throws {TypeError} if either `transform` or `compare` is not a
     *     function.
     * @throws {RangeError} if the sequence is empty.
     *
     * @example
     * // Called with no arguments
     * const numbers = [20, 35, -12, 0, 4, -7];
     * console.log(from(numbers).max()); // Output: 35
     *
     * // Using a transformation
     * const people = [
     *     { name: "Jennifer", age: 23 },
     *     { name: "John", age: 33 },
     *     { name: "Jack", age: 42 },
     *     { name: "Jill", age: 18 },
     *     { name: "Bob", age: 20 }
     * ];
     *
     * console.log(from(people).max(p => p.age));
     *
     * // Output:
     * //     42
     *
     * // Using a transformation and a custom comparer
     * const people = [
     *     { name: "Jennifer", age: 23 },
     *     { name: "John", age: 33 },
     *     { name: "Jack", age: 42 },
     *     { name: "Jill", age: 18 },
     *     { name: "Bob", age: 20 }
     * ];
     *
     * const compareLength = (a, b) => a.length - b.length;
     *
     * console.log(
     *     from(people).max(p => p.name, compareLength);
     * );
     *
     * // Output:
     * //     "Jennifer"
     */
    max(transform = identity, compare = compareDefault) {
        ensureIsFunction(transform, "`transform` should be a function");
        ensureIsFunction(compare, "`compare` should be a function");

        return this._findExtremum(
            (current, max) => compare(current, max) > 0,
            transform);
    }

    _findExtremum(isMoreExtreme, transform = identity) {
        const iterator = this.iterable[Symbol.iterator]();
        let current = iterator.next();

        if (current.done) {
            throw new RangeError("Sequence was empty");
        }

        let value = transform(current.value);
        let extremum = value;
        current = iterator.next();

        while (!current.done) {
            value = transform(current.value);
            if (isMoreExtreme(value, extremum)) {
                extremum = value;
            }
            current = iterator.next();
        }

        return extremum;
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Paging methods                                                       //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Returns the element at the specified zero-based index.
     *
     * **Evaluation:** eager
     *
     * **Complexity:** O(1) for `Array`s, O(n) for other iterables.
     *
     * @param {number} index The non-negative integer index of the element to
     *     return.
     *
     * @return {*} The element of the sequence at the specified index.
     *
     * @throws {RangeError} if `index` is negative. If `index` is too large.
     *
     * @example
     * const numbers = [1, 2, 3];
     *
     * console.log(from(numbers).elementAt(2)); // Output: 3
     */
    elementAt(index) {
        ensureIsNumber(index, "`index` should be a number");
        ensureIsNotNegative(index, "`index` should not be negative");

        const { element, exhausted } = this._elementAt(index);

        if (exhausted) {
            throw new RangeError("Index too large");
        }

        return element;
    }

    /**
     * Returns the element at the specified zero-based index, or a user-
     * specified default value.
     *
     * **Evaluation:** eager
     *
     * **Complexity:** O(1) for `Array`s, O(n) for other iterables.
     *
     * @param {number} index The non-negative integer index of the element to
     *     return.
     * @param {*} [defaultValue] The default value to return if the index is
     *     out of bounds. If not specified, `undefined` is used as the
     *     default.
     *
     * @return {*} The element of the sequence at the specified index, or the
     *     default value if the index is out of bounds of the sequence.
     *
     * @throws {RangeError} if `index` is negative. If `index` is too large.
     *
     * @example
     * const numbers = [1, 2, 3];
     *
     * console.log(from(numbers).elementAtOrDefault(2)); // Output: 3
     * console.log(from(numbers).elementAtOrDefault(3)); // Output: undefined
     * console.log(from(numbers).elementAtOrDefault(false)); // Output: false
     */
    elementAtOrDefault(index, defaultValue) {
        ensureIsNumber(index, "`index` should be a number");
        ensureIsNotNegative(index, "`index` should not be negative");

        const { element, exhausted } = this._elementAt(index);

        return exhausted ? defaultValue : element;
    }

    _elementAt(index) {
        const exhaustedResult = { element: undefined, exhausted: true },
            valueResult = v => { return { element: v, exhausted: false }; };

        // O(1) optimization for arrays
        if (this.iterable instanceof Array) {
            if (index >= this.iterable.length) {
                return exhaustedResult;
            } else {
                return valueResult(this.iterable[index]);
            }
        }

        // O(n) algorithm for other iterables
        let i = 0;

        for (let item of this.iterable) {
            if (i === index) {
                return valueResult(item);
            }
            i++;
        }

        return exhaustedResult;
    }

    /**
     * Returns the first element of the sequence. If a `matches` function
     * is specified, it returns the first matching element.
     *
     * **Evaluation:** eager
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     * `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The first (matching) element.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @throws {RangeError} if the sequence contains no elements or no
     *     matching element has been found.
     *
     * @example
     * // No condition specified, simply retrieve the first element of
     * // the sequence:
     * const numbers = [1, 2, 3, 4, 5],
     *       first = from(numbers).first();
     *
     * console.log(first); // 1
     *
     * // Getting the first element that matches a condition:
     * const numbers = [1, 2, 3, 4, 5],
     *       firstEven = from(numbers).first(n => n % 2 === 0);
     *
     * console.log(firstEven); // 2
     */
    first(matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        for (let i of this.iterable) {
            if (matches(i)) {
                return i;
            }
        }

        throw new RangeError("No matching element found");
    }

    /**
     * Returns the first element of a sequence or a user-specified default value.
     * If a `matches` function is specified, it returns the first matching
     * element or the default value.
     *
     * **Evaluation:** eager
     *
     * @param {*} [defaultValue=undefined] The default value to return
     *     if the sequence contains no (matching) elements.
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     *     `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The first (matching) element of the sequence or the default
     *     value.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @example
     * // Get first element of a non-empty sequence
     * const numbers = [1, 2, 3];
     * console.log(from(numbers).firstOrDefault()); // Output: 1
     *
     * // Try to get first element of an empty sequence. No default value specified.
     * console.log(from([]).firstOrDefault()); // Output: undefined
     *
     * // Try to get first element of an empty sequence. 0 is specified as a default.
     * console.log(from([]).firstOrDefault(0)); // Output: 0
     *
     * // Get first matching element of a sequence with matching elements
     * const numbers = [1, 2, 3, 4];
     * console.log(from(numbers).firstOrDefault(0, n => n % 2 === 0)); // Output: 2
     *
     * // Get first matching element of a sequence without a matching element
     * const numbers = [1, 3, 5];
     * console.log(from(numbers).firstOrDefault(0, n => n % 2 === 0)); // Output: 0
     */
    firstOrDefault(defaultValue, matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        for (let i of this.iterable) {
            if (matches(i)) {
                return i;
            }
        }

        return defaultValue;
    }

    /**
     * Returns the last element of the sequence. If a `matches` function
     * is specified, it returns the last matching element.
     *
     * **Evaluation:** eager
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     * `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The last (matching) element.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @throws {RangeError} if the sequence contains no elements or no
     *     matching element has been found.
     *
     * @example
     * // No condition specified, simply retrieve the last element of
     * // the sequence:
     * const numbers = [1, 2, 3, 4, 5],
     *       last = from(numbers).last();
     *
     * console.log(last); // 5
     *
     * // Getting the last element that matches a condition:
     * const numbers = [1, 2, 3, 4, 5],
     *       lastEven = from(numbers).last(n => n % 2 === 0);
     *
     * console.log(lastEven); // 4
     */
    last(matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        const { found, item } = this._last(matches);

        if (!found) {
            throw new RangeError("No matching element found");
        }
        return item;
    }

    /**
     * Returns the last element of a sequence or a user-specified default value.
     * If a `matches` function is specified, it returns the last matching
     * element or the default value.
     *
     * **Evaluation:** eager
     *
     * @param {*} [defaultValue=undefined] The default value to return
     *     if the sequence contains no (matching) elements.
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     *     `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The last (matching) element of the sequence or the default
     *     value.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @example
     * // Get last element of a non-empty sequence
     * const numbers = [1, 2, 3];
     * console.log(from(numbers).lastOrDefault()); // Output: 3
     *
     * // Try to get last element of an empty sequence. No default value specified.
     * console.log(from([]).lastOrDefault()); // Output: undefined
     *
     * // Try to get last element of an empty sequence. 0 is specified as a default.
     * console.log(from([]).lastOrDefault(0)); // Output: 0
     *
     * // Get last matching element of a sequence with matching elements
     * const numbers = [1, 2, 3, 4];
     * console.log(from(numbers).lastOrDefault(0, n => n % 2 === 0)); // Output: 4
     *
     * // Get last matching element of a sequence without a matching element
     * const numbers = [1, 3, 5, 7];
     * console.log(from(numbers).lastOrDefault(0, n => n % 2 === 0)); // Output: 0
     */
    lastOrDefault(defaultValue, matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        const { found, item } = this._last(matches);

        return found ? item : defaultValue;
    }

    _last(matches = constantTrue) {
        let found = false, item;

        for (let i of this.iterable) {
            if (matches(i)) {
                item = i;
                found = true;
            }
        }

        return { found, item };
    }

    /**
     * Returns the only element of the sequence. If a `matches` function
     * is specified, it returns the single matching element.
     *
     * **Evaluation:** eager
     *
     * **Note:** `singleOrDefault` can be used to express that a sequence should
     * contain at most one (matching) element, and the presence of multiple (matching)
     * elements is an error.
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     * `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The single (matching) element.
     *
     * @throws {TypeError} if `matches` is not a function
     *
     * @throws {RangeError} if the sequence contains no elements, if no
     *     matching element has been found, if the sequence contains more
     *     than one (matching) element.
     *
     * @example
     * // No condition specified, simply retrieve the only element of
     * // the sequence:
     * const numbers = [1],
     *       single = from(numbers).single();
     *
     * console.log(single); // 1
     *
     * // Getting the only element that matches a condition:
     * const numbers = [1, 2, 3],
     *       singleEven = from(numbers).single(n => n % 2 === 0);
     *
     * console.log(singleEven); // 2
     *
     * // `single` can be used to express that we expect only one matching
     * // element when the presence of more than one matching elements is
     * // a programming error:
     *
     * // this is expected to contain only one even number
     * const numbers = [1, 2, 3, 4, 5];
     *
     * // BOOM! this throws a RangeError because the sequence contains more
     * // than one matching element
     * const even = from(numbers).single(even);
     */
    single(matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        const { found, item } = this._single(matches);

        if (!found) {
            throw new RangeError("No matching element found");
        }
        return item;
    }

    /**
     * Returns the single element of a sequence or a user-specified default value.
     * If a `matches` function is specified, it returns the single matching
     * element or the default value.
     *
     * **Evaluation:** eager
     *
     * **Note:** `singleOrDefault` can be used to express that a sequence should
     * contain at most one (matching) element, and the presence of multiple (matching)
     * elements is an error.
     *
     * @param {*} [defaultValue=undefined] The default value to return
     *     if the sequence contains no (matching) elements.
     *
     * @param {function(i: *): boolean} [matches] A function that returns
     *     `true` if an element satisfies a condition, `false` otherwise.
     *
     * @return {*} The single (matching) element of the sequence or the default
     *     value.
     *
     * @throws {TypeError} if `matches` is not a function
     * @throws {RangeError} if the sequence contains more than one matching element
     *
     * @example
     * // Get the only element of a non-empty sequence
     * const numbers = [1];
     * console.log(from(numbers).singleOrDefault()); // Output: 1
     *
     * // Try to get the single element of an empty sequence. No default value specified.
     * console.log(from([]).singleOrDefault()); // Output: undefined
     *
     * // Try to get single element of an empty sequence. 0 is specified as a default.
     * console.log(from([]).singleOrDefault(0)); // Output: 0
     *
     * // Get single matching element of a sequence with matching elements
     * const numbers = [1, 2, 3];
     * console.log(from(numbers).singleOrDefault(0, n => n % 2 === 0)); // Output: 2
     *
     * // Get last matching element of a sequence without a matching element
     * const numbers = [1, 3, 5, 7];
     * console.log(from(numbers).singleOrDefault(0, n => n % 2 === 0)); // Output: 0
     *
     * // Get single matching element of a sequence with multiple matching elements
     * const numbers = [1, 2, 3, 4];
     *
     * // BOOM! This throws a `RangeError`:
     * console.log(from(numbers).singleOrDefault(0, n => n % 2 === 0));
     */
    singleOrDefault(defaultValue, matches = constantTrue) {
        ensureIsFunction(matches, "`matches` should be a function");

        const { found, item } = this._single(matches);

        if (!found) {
            return defaultValue;
        }

        return item;
    }

    _single(matches = constantTrue) {
        let found = false, item;

        for (let i of this.iterable) {
            if (matches(i)) {
                if (found) {
                    throw new RangeError("Sequence contains more than one matching element");
                }
                item = i;
                found = true;
            }
        }

        return { found, item };
    }

    /**
     * Returns a sequence containing a default value if the input sequence
     * is empty; otherwise returns the input sequence.
     *
     * **Evaluation:** lazy
     *
     * @param {*} [defaultValue] if the input sequence is empty, the result
     *     is a sequence containing this value.
     *
     * @return {Sequence} a sequence containing a default value if the input
     *     sequence is empty; otherwise the input sequence.
     *
     * @example
     * // If the input sequence is not empty, the result is the original
     * // sequence
     * const ex1 = from([1, 2]).defaultIfEmpty();
     *
     * for (let i of ex1) {
     *     console.log(i);
     * }
     *
     * // Output:
     * //     1
     * //     2
     *
     * // If the input sequence is empty, the result is a sequence containing
     * // one `undefined` value
     * const ex2 = from([]).defaultIfEmpty();
     *
     * for (let i of ex2) {
     *     console.log(i);
     * }
     *
     * // Output:
     * //     undefined
     *
     * // If the input sequence is empty and a default value is specified,
     * // the result is a sequence the only item of which is the default value
     * const ex3 = from([]).defaultIfEmpty(0);
     *
     * for (let i of ex2) {
     *     console.log(i);
     * }
     *
     * // Output:
     * //     0
     */
    defaultIfEmpty(defaultValue) {
        const generator = function* () {
            const iterator = this.iterable[Symbol.iterator]();
            let next = iterator.next();

            if (next.done) {
                yield defaultValue;
                return;
            }

            while (!next.done) {
                yield next.value;
                next = iterator.next();
            }
        };

        return this._wrap(generator);
    }

    skip(count) {
        const iterable = this.iterable;
        return this._wrap(function* () {
            let c = 0;
            for (let i of iterable) if (++c > count) yield i;
        });
    }

    skipWhile(matches) {
        const iterable = this.iterable;
        return this._wrap(function* () {
            let wasFalse = false;
            for (let i of iterable) {
                if (wasFalse || !matches(i)) {
                    wasFalse = true;
                    yield i;
                }
            }
        });
    }

    take(count) {
        const iterable = this.iterable;
        return this._wrap(function* () {
            let c = 0;
            for (let i of iterable) {
                if (++c <= count) yield i;
                else break;
            }
        });
    }

    takeWhile(matches) {
        const iterable = this.iterable;
        return this._wrap(function* () {
            for (let i of iterable) {
                if (matches(i)) yield i;
                else break;
            }
        });
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Factory methods                                                      //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    /**
     * Returns an empty `Sequence`.
     *
     * **Note:**
     * The `Sequence` instance returned by `empty` is cached, that is,
     * it always returns the same instance.
     *
     * @return {Sequence} A `Sequence` that has no elements.
     *
     * @example
     * console.log("start");
     *
     * for (let i of Sequence.empty()) {
     *     console.log(i);
     * }
     *
     * console.log("end");
     *
     * // Output:
     * //     start
     * //     end
     */
    static empty() {
        let self = Sequence;

        if (self._emptyInstance === undefined) {
            self._emptyInstance = self._wrap(function* () {});
        }

        return self._emptyInstance;
    }

    /**
     * Returns a `Sequence` that contains the specified element repeated
     * the specified number of times.
     *
     * @param {*} item The item to be repeated.
     * @param {number} count How many times to repeat the item.
     *
     * @return {Sequence} A `Sequence` that contains the specified element
     *     repeated the specified number of times.
     *
     * @throws {TypeError} if `count` is not a number
     * @throws {RangeError} if `count` is negative
     *
     * @example
     * for (let i of Sequence.repeat("a", 3)) {
     *     console.log(i);
     * }
     *
     * // Output:
     * //     a
     * //     a
     * //     a
     */
    static repeat(item, count) {
        ensureIsNumber(count, "`count` should be a number");
        ensureIsNotNegative(count, "`count` should not be negative");

        const generator = function* () {
            for (let i = 0; i < count; i++) {
                yield item;
            }
        };

        return Sequence._wrap(generator);
    }

    /**
     * Returns an increasing sequence of integers, of `count` items,
     * starting at `start`.
     *
     * @param {number} start The first element of the sequence.
     * @param {number} count The number of elements.
     *
     * @return {Sequence} An increasing sequence of integers, of
     *     `count` items, starting at `start`.
     *
     * @throws {TypeError} if either `start` or `count` is not a number
     * @throws {RangeError} if count is negative
     *
     * @example
     * for (let i of Sequence.range(2, 4)) {
     *     console.log(i);
     * }
     *
     * // Output:
     * //     2
     * //     3
     * //     4
     * //     5
     */
    static range(start, count) {
        ensureIsNumber(start, "`start` should be a number");
        ensureIsNumber(count, "`count` should be a number");
        ensureIsNotNegative(count, "`count` should not be negative");

        const generator = function* () {
            for (let i = 0; i < count; i++) {
                yield start + i;
            }
        };

        return Sequence._wrap(generator);
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Conversion methods                                                   //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    toArray() {
        return Array.from(this.iterable);
    }

    /**
     * Creates an eagerly evaluated lookup table of keys mapped to iterables
     * using the key selector and transformation functions specified.
     *
     * **Evaluation:** eager
     *
     * **Note:** The difference between `toLookup` and `groupBy` is that `toLookup`
     * uses eager evaluation.
     *
     * @param {function(item: *): *} getKey A function that, given an element,
     *     returns the value to use as its key in the lookup table.
     *
     * @param {function(item: *): *} [transform] A function that is called
     *     for each element of the sequence and generates the values in the
     *     lookup table.
     *
     * @throws {TypeError} if either `getKey` or `transform` is not a function
     *
     * @example
     * const books = [
     *     { title: "The Lord of the Rings", author: "J. R. R. Tolkien", category: "fantasy" },
     *     { title: "Solaris", author: "Stanislaw Lem", category: "sci-fi" },
     *     { title: "The Hobbit", author: "J. R. R. Tolkien", category: "fantasy" },
     *     { title: "Rendezvous with Rama", author: "A. C. Clarke", category: "sci-fi" },
     *     { title: "JavaScript Allongé", author: "Reginald Braithwaithe", category: "non-fiction" }
     * ];
     *
     * // Only `getKey` used:
     * let booksByCategory =
     *     from(books)
     *         .toLookup(b => b.category);
     *
     * for (let {category, books} of booksByCategory) {
     *     console.log(category);
     *     for (let book of books) {
     *         console.log("    " + book.author + " - " + book.title);
     *     }
     * }
     *
     * // Output:
     * //     fantasy
     * //         J. R. R. Tolkien - The Lord of the Rings
     * //         J. R. R. Tolkien - The Hobbit
     * //     sci-fi
     * //         Stanislaw Lem - Solaris
     * //         A. C. Clarke - Rendezvous with Rama
     * //     non-fiction
     * //         Reginald Braithwaithe - JavaScript Allongé
     *
     * // `getKey` and `transform` used:
     * let booksByCategory =
     *     from(books)
     *         .toLookup(b => b.category, b => b.title);
     *
     * for (let {category, titles} of booksByCategory) {
     *     console.log(category);
     *     for (let title of titles) {
     *         console.log("    " + title);
     *     }
     * }
     *
     * // Output:
     * //     fantasy
     * //         The Lord of the Rings
     * //         The Hobbit
     * //     sci-fi
     * //         Solaris
     * //         Rendezvous with Rama
     * //     non-fiction
     * //         JavaScript Allongé
     */
    toLookup(getKey, transform = identity) {
        ensureIsFunction(getKey, "`getKey` should be a function");
        if (transform !== undefined) {
            ensureIsFunction(transform, "`transform` should be a function");
        }

        const lookup = new Map();

        for (let i of this.iterable) {
            const key = getKey(i),
                value = transform(i);

            if (lookup.has(key)) {
                lookup.get(key).push(value);
            } else {
                lookup.set(key, [value]);
            }
        }

        return lookup;
    }

    //////////////////////////////////////////////////////////////////////////
    //                                                                      //
    // Private helpers                                                      //
    //                                                                      //
    //////////////////////////////////////////////////////////////////////////

    _wrap(generator) {
        return new Sequence({ [Symbol.iterator]: generator.bind(this) });
    }

    static _wrap(generator) {
        return new Sequence({ [Symbol.iterator]: generator });
    }

    _log() {
        /* eslint-disable no-console */
        for (let i of this.iterable) console.log(i);
        /* eslint-enable no-console */
    }
}

//////////////////////////////////////////////////////////////////////////
//                                                                      //
// Functions used as default arguments                                  //
//                                                                      //
//////////////////////////////////////////////////////////////////////////

function compareDefault(a, b) {
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
}

function defaultJoinTransform(outer, inner) {
    return [outer, inner];
}

function identity(n) { return n; }
function constantTrue() { return true; }

//////////////////////////////////////////////////////////////////////////
//                                                                      //
// Argument validation helpers                                          //
//                                                                      //
//////////////////////////////////////////////////////////////////////////

function ensureIsNumber(n, message) {
    if (typeof n !== "number") {
        throw new TypeError(message);
    }
}

function ensureIsNotNegative(n, message) {
    if (n < 0) throw new RangeError(message);
}

function ensureIsDefined(x, message) {
    if (x === undefined) throw new TypeError(message);
}

function ensureIsFunction(f, message) {
    if (typeof f !== "function") throw new TypeError(message);
}

function ensureIsIterable(i, message) {
    ensureIsDefined(i, message);
    const iter = i[Symbol.iterator];
    ensureIsDefined(iter, message);
    ensureIsFunction(iter, message);
}