UNPKG

lazy.js

Version:

Like Underscore, but lazier

1,601 lines (1,456 loc) 204 kB
/* * @name Lazy.js * Licensed under the MIT License (see LICENSE.txt) * * @fileOverview * Lazy.js is a lazy evaluation library for JavaScript. * * This has been done before. For examples see: * * - [wu.js](http://fitzgen.github.io/wu.js/) * - [Linq.js](http://linqjs.codeplex.com/) * - [from.js](https://github.com/suckgamoni/fromjs/) * - [IxJS](http://rx.codeplex.com/) * - [sloth.js](http://rfw.name/sloth.js/) * * However, at least at present, Lazy.js is faster (on average) than any of * those libraries. It is also more complete, with nearly all of the * functionality of [Underscore](http://underscorejs.org/) and * [Lo-Dash](http://lodash.com/). * * Finding your way around the code * -------------------------------- * * At the heart of Lazy.js is the {@link Sequence} object. You create an initial * sequence using {@link Lazy}, which can accept an array, object, or string. * You can then "chain" together methods from this sequence, creating a new * sequence with each call. * * Here's an example: * * var data = getReallyBigArray(); * * var statistics = Lazy(data) * .map(transform) * .filter(validate) * .reduce(aggregate); * * {@link Sequence} is the foundation of other, more specific sequence types. * * An {@link ArrayLikeSequence} provides indexed access to its elements. * * An {@link ObjectLikeSequence} consists of key/value pairs. * * A {@link StringLikeSequence} is like a string (duh): actually, it is an * {@link ArrayLikeSequence} whose elements happen to be characters. * * An {@link AsyncSequence} is special: it iterates over its elements * asynchronously (so calling `each` generally begins an asynchronous loop and * returns immediately). * * For more information * -------------------- * * I wrote a blog post that explains a little bit more about Lazy.js, which you * can read [here](http://philosopherdeveloper.com/posts/introducing-lazy-js.html). * * You can also [create an issue on GitHub](https://github.com/dtao/lazy.js/issues) * if you have any issues with the library. I work through them eventually. * * [@dtao](https://github.com/dtao) */ (function(root, factory) { if (typeof define === 'function' && define.amd) { define(factory); } else if (typeof exports === 'object') { module.exports = factory(); } else { root.Lazy = factory(); } })(this, function(context) { /** * Wraps an object and returns a {@link Sequence}. For `null` or `undefined`, * simply returns an empty sequence (see {@link Lazy.strict} for a stricter * implementation). * * - For **arrays**, Lazy will create a sequence comprising the elements in * the array (an {@link ArrayLikeSequence}). * - For **objects**, Lazy will create a sequence of key/value pairs * (an {@link ObjectLikeSequence}). * - For **strings**, Lazy will create a sequence of characters (a * {@link StringLikeSequence}). * * @public * @param {Array|Object|string} source An array, object, or string to wrap. * @returns {Sequence} The wrapped lazy object. * * @exampleHelpers * // Utility functions to provide to all examples * function increment(x) { return x + 1; } * function isEven(x) { return x % 2 === 0; } * function isPositive(x) { return x > 0; } * function isNegative(x) { return x < 0; } * * // HACK! * // autodoc tests for private methods don't pull in all variables defined * // within the current scope :( * var isArray = Array.isArray; * * @examples * Lazy([1, 2, 4]) // instanceof Lazy.ArrayLikeSequence * Lazy({ foo: "bar" }) // instanceof Lazy.ObjectLikeSequence * Lazy("hello, world!") // instanceof Lazy.StringLikeSequence * Lazy() // sequence: [] * Lazy(null) // sequence: [] */ function Lazy(source) { if (isArray(source)) { return new ArrayWrapper(source); } else if (typeof source === "string") { return new StringWrapper(source); } else if (source instanceof Sequence) { return source; } if (Lazy.extensions) { var extensions = Lazy.extensions, length = extensions.length, result; while (!result && length--) { result = extensions[length](source); } if (result) { return result; } } return new ObjectWrapper(source); } Lazy.VERSION = '0.5.1'; /*** Utility methods of questionable value ***/ Lazy.noop = function noop() {}; Lazy.identity = function identity(x) { return x; }; Lazy.equality = function equality(x, y) { return x === y; }; /** * Provides a stricter version of {@link Lazy} which throws an error when * attempting to wrap `null`, `undefined`, or numeric or boolean values as a * sequence. * * @public * @returns {Function} A stricter version of the {@link Lazy} helper function. * * @examples * var Strict = Lazy.strict(); * * Strict() // throws * Strict(null) // throws * Strict(true) // throws * Strict(5) // throws * Strict([1, 2, 3]) // instanceof Lazy.ArrayLikeSequence * Strict({ foo: "bar" }) // instanceof Lazy.ObjectLikeSequence * Strict("hello, world!") // instanceof Lazy.StringLikeSequence * * // Let's also ensure the static functions are still there. * Strict.range(3) // sequence: [0, 1, 2] * Strict.generate(Date.now) // instanceof Lazy.GeneratedSequence */ Lazy.strict = function strict() { function StrictLazy(source) { if (source == null) { throw new Error("You cannot wrap null or undefined using Lazy."); } if (typeof source === "number" || typeof source === "boolean") { throw new Error("You cannot wrap primitive values using Lazy."); } return Lazy(source); }; Lazy(Lazy).each(function(property, name) { StrictLazy[name] = property; }); return StrictLazy; }; /** * The `Sequence` object provides a unified API encapsulating the notion of * zero or more consecutive elements in a collection, stream, etc. * * Lazy evaluation * --------------- * * Generally speaking, creating a sequence should not be an expensive operation, * and should not iterate over an underlying source or trigger any side effects. * This means that chaining together methods that return sequences incurs only * the cost of creating the `Sequence` objects themselves and not the cost of * iterating an underlying data source multiple times. * * The following code, for example, creates 4 sequences and does nothing with * `source`: * * var seq = Lazy(source) // 1st sequence * .map(func) // 2nd * .filter(pred) // 3rd * .reverse(); // 4th * * Lazy's convention is to hold off on iterating or otherwise *doing* anything * (aside from creating `Sequence` objects) until you call `each`: * * seq.each(function(x) { console.log(x); }); * * Defining custom sequences * ------------------------- * * Defining your own type of sequence is relatively simple: * * 1. Pass a *method name* and an object containing *function overrides* to * {@link Sequence.define}. If the object includes a function called `init`, * this function will be called upon initialization. * 2. The object should include at least either a `getIterator` method or an * `each` method. The former supports both asynchronous and synchronous * iteration, but is slightly more cumbersome to implement. The latter * supports synchronous iteration and can be automatically implemented in * terms of the former. You can also implement both if you want, e.g. to * optimize performance. For more info, see {@link Iterator} and * {@link AsyncSequence}. * * As a trivial example, the following code defines a new method, `sample`, * which randomly may or may not include each element from its parent. * * Lazy.Sequence.define("sample", { * each: function(fn) { * return this.parent.each(function(e) { * // 50/50 chance of including this element. * if (Math.random() > 0.5) { * return fn(e); * } * }); * } * }); * * (Of course, the above could also easily have been implemented using * {@link #filter} instead of creating a custom sequence. But I *did* say this * was a trivial example, to be fair.) * * Now it will be possible to create this type of sequence from any parent * sequence by calling the method name you specified. In other words, you can * now do this: * * Lazy(arr).sample(); * Lazy(arr).map(func).sample(); * Lazy(arr).map(func).filter(pred).sample(); * * Etc., etc. * * @public * @constructor */ function Sequence() {} /** * Create a new constructor function for a type inheriting from `Sequence`. * * @public * @param {string|Array.<string>} methodName The name(s) of the method(s) to be * used for constructing the new sequence. The method will be attached to * the `Sequence` prototype so that it can be chained with any other * sequence methods, like {@link #map}, {@link #filter}, etc. * @param {Object} overrides An object containing function overrides for this * new sequence type. **Must** include either `getIterator` or `each` (or * both). *May* include an `init` method as well. For these overrides, * `this` will be the new sequence, and `this.parent` will be the base * sequence from which the new sequence was constructed. * @returns {Function} A constructor for a new type inheriting from `Sequence`. * * @examples * // This sequence type logs every element to the specified logger as it * // iterates over it. * Lazy.Sequence.define("verbose", { * init: function(logger) { * this.logger = logger; * }, * * each: function(fn) { * var logger = this.logger; * return this.parent.each(function(e, i) { * logger(e); * return fn(e, i); * }); * } * }); * * Lazy([1, 2, 3]).verbose(logger).each(Lazy.noop) // calls logger 3 times */ Sequence.define = function define(methodName, overrides) { if (!overrides || (!overrides.getIterator && !overrides.each)) { throw new Error("A custom sequence must implement *at least* getIterator or each!"); } return defineSequenceType(Sequence, methodName, overrides); }; /** * Gets the number of elements in the sequence. In some cases, this may * require eagerly evaluating the sequence. * * @public * @returns {number} The number of elements in the sequence. * * @examples * Lazy([1, 2, 3]).size(); // => 3 * Lazy([1, 2]).map(Lazy.identity).size(); // => 2 * Lazy([1, 2, 3]).reject(isEven).size(); // => 2 * Lazy([1, 2, 3]).take(1).size(); // => 1 * Lazy({ foo: 1, bar: 2 }).size(); // => 2 * Lazy('hello').size(); // => 5 */ Sequence.prototype.size = function size() { return this.getIndex().length(); }; /** * Creates an {@link Iterator} object with two methods, `moveNext` -- returning * true or false -- and `current` -- returning the current value. * * This method is used when asynchronously iterating over sequences. Any type * inheriting from `Sequence` must implement this method or it can't support * asynchronous iteration. * * Note that **this method is not intended to be used directly by application * code.** Rather, it is intended as a means for implementors to potentially * define custom sequence types that support either synchronous or * asynchronous iteration. * * @public * @returns {Iterator} An iterator object. * * @examples * var iterator = Lazy([1, 2]).getIterator(); * * iterator.moveNext(); // => true * iterator.current(); // => 1 * iterator.moveNext(); // => true * iterator.current(); // => 2 * iterator.moveNext(); // => false */ Sequence.prototype.getIterator = function getIterator() { return new Iterator(this); }; /** * Gets the root sequence underlying the current chain of sequences. */ Sequence.prototype.root = function root() { return this.parent.root(); }; /** * Whether or not the current sequence is an asynchronous one. This is more * accurate than checking `instanceof {@link AsyncSequence}` because, for * example, `Lazy([1, 2, 3]).async().map(Lazy.identity)` returns a sequence * that iterates asynchronously even though it's not an instance of * `AsyncSequence`. * * @returns {boolean} Whether or not the current sequence is an asynchronous one. */ Sequence.prototype.isAsync = function isAsync() { return this.parent ? this.parent.isAsync() : false; }; /** * Evaluates the sequence and produces the appropriate value (an array in most * cases, an object for {@link ObjectLikeSequence}s or a string for * {@link StringLikeSequence}s). * * @returns {Array|string|Object} The value resulting from fully evaluating * the sequence. */ Sequence.prototype.value = function value() { return this.toArray(); }; /** * Applies the current transformation chain to a given source, returning the * resulting value. * * @examples * var sequence = Lazy([]) * .map(function(x) { return x * -1; }) * .filter(function(x) { return x % 2 === 0; }); * * sequence.apply([1, 2, 3, 4]); // => [-2, -4] */ Sequence.prototype.apply = function apply(source) { var root = this.root(), previousSource = root.source, result; try { root.source = source; result = this.value(); } finally { root.source = previousSource; } return result; }; /** * The Iterator object provides an API for iterating over a sequence. * * The purpose of the `Iterator` type is mainly to offer an agnostic way of * iterating over a sequence -- either synchronous (i.e. with a `while` loop) * or asynchronously (with recursive calls to either `setTimeout` or --- if * available --- `setImmediate`). It is not intended to be used directly by * application code. * * @public * @constructor * @param {Sequence} sequence The sequence to iterate over. */ function Iterator(sequence) { this.sequence = sequence; this.index = -1; } /** * Gets the current item this iterator is pointing to. * * @public * @returns {*} The current item. */ Iterator.prototype.current = function current() { return this.cachedIndex && this.cachedIndex.get(this.index); }; /** * Moves the iterator to the next item in a sequence, if possible. * * @public * @returns {boolean} True if the iterator is able to move to a new item, or else * false. */ Iterator.prototype.moveNext = function moveNext() { var cachedIndex = this.cachedIndex; if (!cachedIndex) { cachedIndex = this.cachedIndex = this.sequence.getIndex(); } if (this.index >= cachedIndex.length() - 1) { return false; } ++this.index; return true; }; /** * Creates an array snapshot of a sequence. * * Note that for indefinite sequences, this method may raise an exception or * (worse) cause the environment to hang. * * @public * @returns {Array} An array containing the current contents of the sequence. * * @examples * Lazy([1, 2, 3]).toArray() // => [1, 2, 3] */ Sequence.prototype.toArray = function toArray() { return this.reduce(function(arr, element) { arr.push(element); return arr; }, []); }; /** * Compare this to another sequence for equality. * * @public * @param {Sequence} other The other sequence to compare this one to. * @param {Function=} equalityFn An optional equality function, which should * take two arguments and return true or false to indicate whether those * values are considered equal. * @returns {boolean} Whether the two sequences contain the same values in * the same order. * * @examples * Lazy([1, 2]).equals(Lazy([1, 2])) // => true * Lazy([1, 2]).equals(Lazy([2, 1])) // => false * Lazy([1]).equals(Lazy([1, 2])) // => false * Lazy([1, 2]).equals(Lazy([1])) // => false * Lazy([]).equals(Lazy([])) // => true * Lazy(['foo']).equals(Lazy(['foo'])) // => true * Lazy(['1']).equals(Lazy([1])) // => false * Lazy([false]).equals(Lazy([0])) // => false * Lazy([1, 2]).equals([1, 2]) // => false * Lazy([1, 2]).equals('[1, 2]') // => false */ Sequence.prototype.equals = function equals(other, equalityFn) { if (!(other instanceof Sequence)) { return false; } var it = this.getIterator(), oit = other.getIterator(), eq = equalityFn || Lazy.equality; while (it.moveNext()) { if (!oit.moveNext()) { return false; } if (!eq(it.current(), oit.current())) { return false; } } return !oit.moveNext(); }; /** * Provides an indexed view into the sequence. * * For sequences that are already indexed, this will simply return the * sequence. For non-indexed sequences, this will eagerly evaluate the * sequence. * * @returns {ArrayLikeSequence} A sequence containing the current contents of * the sequence. * * @examples * Lazy([1, 2, 3]).filter(isEven) // instanceof Lazy.Sequence * Lazy([1, 2, 3]).filter(isEven).getIndex() // instanceof Lazy.ArrayLikeSequence */ Sequence.prototype.getIndex = function getIndex() { return new ArrayWrapper(this.toArray()); }; /** * Returns the element at the specified index. Note that, for sequences that * are not {@link ArrayLikeSequence}s, this may require partially evaluating * the sequence, iterating to reach the result. (In other words for such * sequences this method is not O(1).) * * @public * @param {number} i The index to access. * @returns {*} The element. * */ Sequence.prototype.get = function get(i) { var element; this.each(function(e, index) { if (index === i) { element = e; return false; } }); return element; }; /** * Provides an indexed, memoized view into the sequence. This will cache the * result whenever the sequence is first iterated, so that subsequent * iterations will access the same element objects. * * @public * @returns {ArrayLikeSequence} An indexed, memoized sequence containing this * sequence's elements, cached after the first iteration. * * @example * function createObject() { return new Object(); } * * var plain = Lazy.generate(createObject, 10), * memoized = Lazy.generate(createObject, 10).memoize(); * * plain.toArray()[0] === plain.toArray()[0]; // => false * memoized.toArray()[0] === memoized.toArray()[0]; // => true */ Sequence.prototype.memoize = function memoize() { return new MemoizedSequence(this); }; /** * @constructor */ function MemoizedSequence(parent) { this.parent = parent; this.memo = []; this.iterator = undefined; this.complete = false; } // MemoizedSequence needs to have its prototype set up after ArrayLikeSequence /** * Creates an object from a sequence of key/value pairs. * * @public * @returns {Object} An object with keys and values corresponding to the pairs * of elements in the sequence. * * @examples * var details = [ * ["first", "Dan"], * ["last", "Tao"], * ["age", 29] * ]; * * Lazy(details).toObject() // => { first: "Dan", last: "Tao", age: 29 } */ Sequence.prototype.toObject = function toObject() { return this.reduce(function(object, pair) { object[pair[0]] = pair[1]; return object; }, {}); }; /** * Iterates over this sequence and executes a function for every element. * * @public * @aka forEach * @param {Function} fn The function to call on each element in the sequence. * Return false from the function to end the iteration. * @returns {boolean} `true` if the iteration evaluated the entire sequence, * or `false` if iteration was ended early. * * @examples * Lazy([1, 2, 3, 4]).each(fn) // calls fn 4 times */ Sequence.prototype.each = function each(fn) { var iterator = this.getIterator(), i = -1; while (iterator.moveNext()) { if (fn(iterator.current(), ++i) === false) { return false; } } return true; }; Sequence.prototype.forEach = function forEach(fn) { return this.each(fn); }; /** * Creates a new sequence whose values are calculated by passing this sequence's * elements through some mapping function. * * @public * @aka collect * @param {Function} mapFn The mapping function used to project this sequence's * elements onto a new sequence. This function takes up to two arguments: * the element, and the current index. * @returns {Sequence} The new sequence. * * @examples * function addIndexToValue(e, i) { return e + i; } * * Lazy([]).map(increment) // sequence: [] * Lazy([1, 2, 3]).map(increment) // sequence: [2, 3, 4] * Lazy([1, 2, 3]).map(addIndexToValue) // sequence: [1, 3, 5] * * @benchmarks * function increment(x) { return x + 1; } * * var smArr = Lazy.range(10).toArray(), * lgArr = Lazy.range(100).toArray(); * * Lazy(smArr).map(increment).each(Lazy.noop) // lazy - 10 elements * Lazy(lgArr).map(increment).each(Lazy.noop) // lazy - 100 elements * _.each(_.map(smArr, increment), _.noop) // lodash - 10 elements * _.each(_.map(lgArr, increment), _.noop) // lodash - 100 elements */ Sequence.prototype.map = function map(mapFn) { return new MappedSequence(this, createCallback(mapFn)); }; Sequence.prototype.collect = function collect(mapFn) { return this.map(mapFn); }; /** * @constructor */ function MappedSequence(parent, mapFn) { this.parent = parent; this.mapFn = mapFn; } MappedSequence.prototype = Object.create(Sequence.prototype); MappedSequence.prototype.getIterator = function getIterator() { return new MappingIterator(this.parent, this.mapFn); }; MappedSequence.prototype.each = function each(fn) { var mapFn = this.mapFn; return this.parent.each(function(e, i) { return fn(mapFn(e, i), i); }); }; /** * @constructor */ function MappingIterator(sequence, mapFn) { this.iterator = sequence.getIterator(); this.mapFn = mapFn; this.index = -1; } MappingIterator.prototype.current = function current() { return this.mapFn(this.iterator.current(), this.index); }; MappingIterator.prototype.moveNext = function moveNext() { if (this.iterator.moveNext()) { ++this.index; return true; } return false; }; /** * Creates a new sequence whose values are calculated by accessing the specified * property from each element in this sequence. * * @public * @param {string} propertyName The name of the property to access for every * element in this sequence. * @returns {Sequence} The new sequence. * * @examples * var people = [ * { first: "Dan", last: "Tao" }, * { first: "Bob", last: "Smith" } * ]; * * Lazy(people).pluck("last") // sequence: ["Tao", "Smith"] */ Sequence.prototype.pluck = function pluck(property) { return this.map(property); }; /** * Creates a new sequence whose values are calculated by invoking the specified * function on each element in this sequence. * * @public * @param {string} methodName The name of the method to invoke for every element * in this sequence. * @returns {Sequence} The new sequence. * * @examples * function Person(first, last) { * this.fullName = function fullName() { * return first + " " + last; * }; * } * * var people = [ * new Person("Dan", "Tao"), * new Person("Bob", "Smith") * ]; * * Lazy(people).invoke("fullName") // sequence: ["Dan Tao", "Bob Smith"] */ Sequence.prototype.invoke = function invoke(methodName) { return this.map(function(e) { return e[methodName](); }); }; /** * Creates a new sequence whose values are the elements of this sequence which * satisfy the specified predicate. * * @public * @aka select * @param {Function} filterFn The predicate to call on each element in this * sequence, which returns true if the element should be included. * @returns {Sequence} The new sequence. * * @examples * var numbers = [1, 2, 3, 4, 5, 6]; * * Lazy(numbers).filter(isEven) // sequence: [2, 4, 6] * * @benchmarks * function isEven(x) { return x % 2 === 0; } * * var smArr = Lazy.range(10).toArray(), * lgArr = Lazy.range(100).toArray(); * * Lazy(smArr).filter(isEven).each(Lazy.noop) // lazy - 10 elements * Lazy(lgArr).filter(isEven).each(Lazy.noop) // lazy - 100 elements * _.each(_.filter(smArr, isEven), _.noop) // lodash - 10 elements * _.each(_.filter(lgArr, isEven), _.noop) // lodash - 100 elements */ Sequence.prototype.filter = function filter(filterFn) { return new FilteredSequence(this, createCallback(filterFn)); }; Sequence.prototype.select = function select(filterFn) { return this.filter(filterFn); }; /** * @constructor */ function FilteredSequence(parent, filterFn) { this.parent = parent; this.filterFn = filterFn; } FilteredSequence.prototype = Object.create(Sequence.prototype); FilteredSequence.prototype.getIterator = function getIterator() { return new FilteringIterator(this.parent, this.filterFn); }; FilteredSequence.prototype.each = function each(fn) { var filterFn = this.filterFn, j = 0; return this.parent.each(function(e, i) { if (filterFn(e, i)) { return fn(e, j++); } }); }; FilteredSequence.prototype.reverse = function reverse() { return this.parent.reverse().filter(this.filterFn); }; /** * @constructor */ function FilteringIterator(sequence, filterFn) { this.iterator = sequence.getIterator(); this.filterFn = filterFn; this.index = 0; } FilteringIterator.prototype.current = function current() { return this.value; }; FilteringIterator.prototype.moveNext = function moveNext() { var iterator = this.iterator, filterFn = this.filterFn, value; while (iterator.moveNext()) { value = iterator.current(); if (filterFn(value, this.index++)) { this.value = value; return true; } } this.value = undefined; return false; }; /** * Creates a new sequence whose values exclude the elements of this sequence * identified by the specified predicate. * * @public * @param {Function} rejectFn The predicate to call on each element in this * sequence, which returns true if the element should be omitted. * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3, 4, 5]).reject(isEven) // sequence: [1, 3, 5] * Lazy([{ foo: 1 }, { bar: 2 }]).reject('foo') // sequence: [{ bar: 2 }] * Lazy([{ foo: 1 }, { foo: 2 }]).reject({ foo: 2 }) // sequence: [{ foo: 1 }] */ Sequence.prototype.reject = function reject(rejectFn) { rejectFn = createCallback(rejectFn); return this.filter(function(e) { return !rejectFn(e); }); }; /** * Creates a new sequence whose values have the specified type, as determined * by the `typeof` operator. * * @public * @param {string} type The type of elements to include from the underlying * sequence, i.e. where `typeof [element] === [type]`. * @returns {Sequence} The new sequence, comprising elements of the specified * type. * * @examples * Lazy([1, 2, 'foo', 'bar']).ofType('number') // sequence: [1, 2] * Lazy([1, 2, 'foo', 'bar']).ofType('string') // sequence: ['foo', 'bar'] * Lazy([1, 2, 'foo', 'bar']).ofType('boolean') // sequence: [] */ Sequence.prototype.ofType = function ofType(type) { return this.filter(function(e) { return typeof e === type; }); }; /** * Creates a new sequence whose values are the elements of this sequence with * property names and values matching those of the specified object. * * @public * @param {Object} properties The properties that should be found on every * element that is to be included in this sequence. * @returns {Sequence} The new sequence. * * @examples * var people = [ * { first: "Dan", last: "Tao" }, * { first: "Bob", last: "Smith" } * ]; * * Lazy(people).where({ first: "Dan" }) // sequence: [{ first: "Dan", last: "Tao" }] * * @benchmarks * var animals = ["dog", "cat", "mouse", "horse", "pig", "snake"]; * * Lazy(animals).where({ length: 3 }).each(Lazy.noop) // lazy * _.each(_.where(animals, { length: 3 }), _.noop) // lodash */ Sequence.prototype.where = function where(properties) { return this.filter(properties); }; /** * Creates a new sequence with the same elements as this one, but to be iterated * in the opposite order. * * Note that in some (but not all) cases, the only way to create such a sequence * may require iterating the entire underlying source when `each` is called. * * @public * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3]).reverse() // sequence: [3, 2, 1] * Lazy([]).reverse() // sequence: [] */ Sequence.prototype.reverse = function reverse() { return new ReversedSequence(this); }; /** * @constructor */ function ReversedSequence(parent) { this.parent = parent; } ReversedSequence.prototype = Object.create(Sequence.prototype); ReversedSequence.prototype.getIterator = function getIterator() { return new ReversedIterator(this.parent); }; /** * @constuctor */ function ReversedIterator(sequence) { this.sequence = sequence; } ReversedIterator.prototype.current = function current() { return this.getIndex().get(this.index); }; ReversedIterator.prototype.moveNext = function moveNext() { var index = this.getIndex(), length = index.length(); if (typeof this.index === "undefined") { this.index = length; } return (--this.index >= 0); }; ReversedIterator.prototype.getIndex = function getIndex() { if (!this.cachedIndex) { this.cachedIndex = this.sequence.getIndex(); } return this.cachedIndex; }; /** * Creates a new sequence with all of the elements of this one, plus those of * the given array(s). * * @public * @param {...*} var_args One or more values (or arrays of values) to use for * additional items after this sequence. * @returns {Sequence} The new sequence. * * @examples * var left = [1, 2, 3]; * var right = [4, 5, 6]; * * Lazy(left).concat(right) // sequence: [1, 2, 3, 4, 5, 6] * Lazy(left).concat(Lazy(right)) // sequence: [1, 2, 3, 4, 5, 6] * Lazy(left).concat(right, [7, 8]) // sequence: [1, 2, 3, 4, 5, 6, 7, 8] * Lazy(left).concat([4, [5, 6]]) // sequence: [1, 2, 3, 4, [5, 6]] * Lazy(left).concat(Lazy([4, [5, 6]])) // sequence: [1, 2, 3, 4, [5, 6]] */ Sequence.prototype.concat = function concat(var_args) { return new ConcatenatedSequence(this, arraySlice.call(arguments, 0)); }; /** * @constructor */ function ConcatenatedSequence(parent, arrays) { this.parent = parent; this.arrays = arrays; } ConcatenatedSequence.prototype = Object.create(Sequence.prototype); ConcatenatedSequence.prototype.each = function each(fn) { var done = false, i = 0; this.parent.each(function(e) { if (fn(e, i++) === false) { done = true; return false; } }); if (!done) { Lazy(this.arrays).flatten(true).each(function(e) { if (fn(e, i++) === false) { return false; } }); } }; /** * Creates a new sequence comprising the first N elements from this sequence, OR * (if N is `undefined`) simply returns the first element of this sequence. * * @public * @aka head, take * @param {number=} count The number of elements to take from this sequence. If * this value exceeds the length of the sequence, the resulting sequence * will be essentially the same as this one. * @returns {*} The new sequence (or the first element from this sequence if * no count was given). * * @examples * function powerOfTwo(exp) { * return Math.pow(2, exp); * } * * Lazy.generate(powerOfTwo).first() // => 1 * Lazy.generate(powerOfTwo).first(5) // sequence: [1, 2, 4, 8, 16] * Lazy.generate(powerOfTwo).skip(2).first() // => 4 * Lazy.generate(powerOfTwo).skip(2).first(2) // sequence: [4, 8] */ Sequence.prototype.first = function first(count) { if (typeof count === "undefined") { return getFirst(this); } return new TakeSequence(this, count); }; Sequence.prototype.head = Sequence.prototype.take = function (count) { return this.first(count); }; /** * @constructor */ function TakeSequence(parent, count) { this.parent = parent; this.count = count; } TakeSequence.prototype = Object.create(Sequence.prototype); TakeSequence.prototype.getIterator = function getIterator() { return new TakeIterator(this.parent, this.count); }; TakeSequence.prototype.each = function each(fn) { var count = this.count, i = 0; var result; var handle = this.parent.each(function(e) { if (i < count) { result = fn(e, i++); } if (i >= count) { return false; } return result; }); if (handle instanceof AsyncHandle) { return handle; } return i === count && result !== false; }; /** * @constructor */ function TakeIterator(sequence, count) { this.iterator = sequence.getIterator(); this.count = count; } TakeIterator.prototype.current = function current() { return this.iterator.current(); }; TakeIterator.prototype.moveNext = function moveNext() { return ((--this.count >= 0) && this.iterator.moveNext()); }; /** * Creates a new sequence comprising the elements from the head of this sequence * that satisfy some predicate. Once an element is encountered that doesn't * satisfy the predicate, iteration will stop. * * @public * @param {Function} predicate * @returns {Sequence} The new sequence * * @examples * function lessThan(x) { * return function(y) { * return y < x; * }; * } * * Lazy([1, 2, 3, 4]).takeWhile(lessThan(3)) // sequence: [1, 2] * Lazy([1, 2, 3, 4]).takeWhile(lessThan(0)) // sequence: [] */ Sequence.prototype.takeWhile = function takeWhile(predicate) { return new TakeWhileSequence(this, predicate); }; /** * @constructor */ function TakeWhileSequence(parent, predicate) { this.parent = parent; this.predicate = predicate; } TakeWhileSequence.prototype = Object.create(Sequence.prototype); TakeWhileSequence.prototype.each = function each(fn) { var predicate = this.predicate, finished = false, j = 0; var result = this.parent.each(function(e, i) { if (!predicate(e, i)) { finished = true; return false; } return fn(e, j++); }); if (result instanceof AsyncHandle) { return result; } return finished; }; /** * Creates a new sequence comprising all but the last N elements of this * sequence. * * @public * @param {number=} count The number of items to omit from the end of the * sequence (defaults to 1). * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3, 4]).initial() // sequence: [1, 2, 3] * Lazy([1, 2, 3, 4]).initial(2) // sequence: [1, 2] * Lazy([1, 2, 3]).filter(Lazy.identity).initial() // sequence: [1, 2] */ Sequence.prototype.initial = function initial(count) { return new InitialSequence(this, count); }; function InitialSequence(parent, count) { this.parent = parent; this.count = typeof count === "number" ? count : 1; } InitialSequence.prototype = Object.create(Sequence.prototype); InitialSequence.prototype.each = function each(fn) { var index = this.parent.getIndex(); return index.take(index.length() - this.count).each(fn); }; /** * Creates a new sequence comprising the last N elements of this sequence, OR * (if N is `undefined`) simply returns the last element of this sequence. * * @public * @param {number=} count The number of items to take from the end of the * sequence. * @returns {*} The new sequence (or the last element from this sequence * if no count was given). * * @examples * Lazy([1, 2, 3]).last() // => 3 * Lazy([1, 2, 3]).last(2) // sequence: [2, 3] * Lazy([1, 2, 3]).filter(isEven).last(2) // sequence: [2] */ Sequence.prototype.last = function last(count) { if (typeof count === "undefined") { return this.reverse().first(); } return this.reverse().take(count).reverse(); }; /** * Returns the first element in this sequence with property names and values * matching those of the specified object. * * @public * @param {Object} properties The properties that should be found on some * element in this sequence. * @returns {*} The found element, or `undefined` if none exists in this * sequence. * * @examples * var words = ["foo", "bar"]; * * Lazy(words).findWhere({ 0: "f" }); // => "foo" * Lazy(words).findWhere({ 0: "z" }); // => undefined */ Sequence.prototype.findWhere = function findWhere(properties) { return this.where(properties).first(); }; /** * Creates a new sequence comprising all but the first N elements of this * sequence. * * @public * @aka skip, tail, rest * @param {number=} count The number of items to omit from the beginning of the * sequence (defaults to 1). * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3, 4]).rest() // sequence: [2, 3, 4] * Lazy([1, 2, 3, 4]).rest(0) // sequence: [1, 2, 3, 4] * Lazy([1, 2, 3, 4]).rest(2) // sequence: [3, 4] * Lazy([1, 2, 3, 4]).rest(5) // sequence: [] */ Sequence.prototype.rest = function rest(count) { return new DropSequence(this, count); }; Sequence.prototype.skip = Sequence.prototype.tail = Sequence.prototype.drop = function drop(count) { return this.rest(count); }; /** * @constructor */ function DropSequence(parent, count) { this.parent = parent; this.count = typeof count === "number" ? count : 1; } DropSequence.prototype = Object.create(Sequence.prototype); DropSequence.prototype.each = function each(fn) { var count = this.count, dropped = 0, i = 0; return this.parent.each(function(e) { if (dropped++ < count) { return; } return fn(e, i++); }); }; /** * Creates a new sequence comprising the elements from this sequence *after* * those that satisfy some predicate. The sequence starts with the first * element that does not match the predicate. * * @public * @aka skipWhile * @param {Function} predicate * @returns {Sequence} The new sequence */ Sequence.prototype.dropWhile = function dropWhile(predicate) { return new DropWhileSequence(this, predicate); }; Sequence.prototype.skipWhile = function skipWhile(predicate) { return this.dropWhile(predicate); }; /** * @constructor */ function DropWhileSequence(parent, predicate) { this.parent = parent; this.predicate = predicate; } DropWhileSequence.prototype = Object.create(Sequence.prototype); DropWhileSequence.prototype.each = function each(fn) { var predicate = this.predicate, done = false; return this.parent.each(function(e) { if (!done) { if (predicate(e)) { return; } done = true; } return fn(e); }); }; /** * Creates a new sequence with the same elements as this one, but ordered * using the specified comparison function. * * This has essentially the same behavior as calling * [`Array#sort`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), * but obviously instead of modifying the collection it returns a new * {@link Sequence} object. * * @public * @param {Function=} sortFn The function used to compare elements in the * sequence. The function will be passed two elements and should return: * - 1 if the first is greater * - -1 if the second is greater * - 0 if the two values are the same * @param {boolean} descending Whether or not the resulting sequence should be * in descending order (defaults to `false`). * @returns {Sequence} The new sequence. * * @examples * Lazy([5, 10, 1]).sort() // sequence: [1, 5, 10] * Lazy(['foo', 'bar']).sort() // sequence: ['bar', 'foo'] * Lazy(['b', 'c', 'a']).sort(null, true) // sequence: ['c', 'b', 'a'] * Lazy([5, 10, 1]).sort(null, true) // sequence: [10, 5, 1] * * // Sorting w/ custom comparison function * Lazy(['a', 'ab', 'aa', 'ba', 'b', 'abc']).sort(function compare(x, y) { * if (x.length && (x.length !== y.length)) { return compare(x.length, y.length); } * if (x === y) { return 0; } * return x > y ? 1 : -1; * }); * // => sequence: ['a', 'b', 'aa', 'ab', 'ba', 'abc'] */ Sequence.prototype.sort = function sort(sortFn, descending) { sortFn || (sortFn = compare); if (descending) { sortFn = reverseArguments(sortFn); } return new SortedSequence(this, sortFn); }; /** * Creates a new sequence with the same elements as this one, but ordered by * the results of the given function. * * You can pass: * * - a *string*, to sort by the named property * - a function, to sort by the result of calling the function on each element * * @public * @param {Function} sortFn The function to call on the elements in this * sequence, in order to sort them. * @param {boolean} descending Whether or not the resulting sequence should be * in descending order (defaults to `false`). * @returns {Sequence} The new sequence. * * @examples * function population(country) { * return country.pop; * } * * function area(country) { * return country.sqkm; * } * * var countries = [ * { name: "USA", pop: 320000000, sqkm: 9600000 }, * { name: "Brazil", pop: 194000000, sqkm: 8500000 }, * { name: "Nigeria", pop: 174000000, sqkm: 924000 }, * { name: "China", pop: 1350000000, sqkm: 9700000 }, * { name: "Russia", pop: 143000000, sqkm: 17000000 }, * { name: "Australia", pop: 23000000, sqkm: 7700000 } * ]; * * Lazy(countries).sortBy(population).last(3).pluck('name') // sequence: ["Brazil", "USA", "China"] * Lazy(countries).sortBy(area).last(3).pluck('name') // sequence: ["USA", "China", "Russia"] * Lazy(countries).sortBy(area, true).first(3).pluck('name') // sequence: ["Russia", "China", "USA"] * * @benchmarks * var randoms = Lazy.generate(Math.random).take(100).toArray(); * * Lazy(randoms).sortBy(Lazy.identity).each(Lazy.noop) // lazy * _.each(_.sortBy(randoms, Lazy.identity), _.noop) // lodash */ Sequence.prototype.sortBy = function sortBy(sortFn, descending) { sortFn = createComparator(sortFn); if (descending) { sortFn = reverseArguments(sortFn); } return new SortedSequence(this, sortFn); }; /** * @constructor */ function SortedSequence(parent, sortFn) { this.parent = parent; this.sortFn = sortFn; } SortedSequence.prototype = Object.create(Sequence.prototype); SortedSequence.prototype.each = function each(fn) { var sortFn = this.sortFn, result = this.parent.toArray(); result.sort(sortFn); return forEach(result, fn); }; /** * @examples * var items = [{ a: 4 }, { a: 3 }, { a: 5 }]; * * Lazy(items).sortBy('a').reverse(); * // => sequence: [{ a: 5 }, { a: 4 }, { a: 3 }] * * Lazy(items).sortBy('a').reverse().reverse(); * // => sequence: [{ a: 3 }, { a: 4 }, { a: 5 }] */ SortedSequence.prototype.reverse = function reverse() { return new SortedSequence(this.parent, reverseArguments(this.sortFn)); }; /** * Creates a new {@link ObjectLikeSequence} comprising the elements in this * one, grouped together according to some key. The value associated with each * key in the resulting object-like sequence is an array containing all of * the elements in this sequence with that key. * * @public * @param {Function|string} keyFn The function to call on the elements in this * sequence to obtain a key by which to group them, or a string representing * a parameter to read from all the elements in this sequence. * @param {Function|string} valFn (Optional) The function to call on the elements * in this sequence to assign to the value for each instance to appear in the * group, or a string representing a parameter to read from all the elements * in this sequence. * @returns {ObjectLikeSequence} The new sequence. * * @examples * function oddOrEven(x) { * return x % 2 === 0 ? 'even' : 'odd'; * } * function square(x) { * return x*x; * } * * var numbers = [1, 2, 3, 4, 5]; * * Lazy(numbers).groupBy(oddOrEven) // sequence: { odd: [1, 3, 5], even: [2, 4] } * Lazy(numbers).groupBy(oddOrEven).get("odd") // => [1, 3, 5] * Lazy(numbers).groupBy(oddOrEven).get("foo") // => undefined * Lazy(numbers).groupBy(oddOrEven, square).get("even") // => [4, 16] * * Lazy([ * { name: 'toString' }, * { name: 'toString' } * ]).groupBy('name'); * // => sequence: { * 'toString': [ * { name: 'toString' }, * { name: 'toString' } * ] * } */ Sequence.prototype.groupBy = function groupBy(keyFn, valFn) { return new GroupedSequence(this, keyFn, valFn); }; /** * @constructor */ function GroupedSequence(parent, keyFn, valFn) { this.parent = parent; this.keyFn = keyFn; this.valFn = valFn; } // GroupedSequence must have its prototype set after ObjectLikeSequence has // been fully initialized. /** * Creates a new {@link ObjectLikeSequence} comprising the elements in this * one, indexed according to some key. * * @public * @param {Function|string} keyFn The function to call on the elements in this * sequence to obtain a key by which to index them, or a string * representing a property to read from all the elements in this sequence. * @param {Function|string} valFn (Optional) The function to call on the elements * in this sequence to assign to the value of the indexed object, or a string * representing a parameter to read from all the elements in this sequence. * @returns {Sequence} The new sequence. * * @examples * var people = [ * { name: 'Bob', age: 25 }, * { name: 'Fred', age: 34 } * ]; * * var bob = people[0], * fred = people[1]; * * Lazy(people).indexBy('name') // sequence: { 'Bob': bob, 'Fred': fred } * Lazy(people).indexBy('name', 'age') // sequence: { 'Bob': 25, 'Fred': 34 } */ Sequence.prototype.indexBy = function(keyFn, valFn) { return new IndexedSequence(this, keyFn, valFn); }; /** * @constructor */ function IndexedSequence(parent, keyFn, valFn) { this.parent = parent; this.keyFn = keyFn; this.valFn = valFn; } // IndexedSequence must have its prototype set after ObjectLikeSequence has // been fully initialized. /** * Creates a new {@link ObjectLikeSequence} containing the unique keys of all * the elements in this sequence, each paired with the number of elements * in this sequence having that key. * * @public * @param {Function|string} keyFn The function to call on the elements in this * sequence to obtain a key by which to count them, or a string representing * a parameter to read from all the elements in this sequence. * @returns {Sequence} The new sequence. * * @examples * function oddOrEven(x) { * return x % 2 === 0 ? 'even' : 'odd'; * } * * var numbers = [1, 2, 3, 4, 5]; * * Lazy(numbers).countBy(oddOrEven) // sequence: { odd: 3, even: 2 } * Lazy(numbers).countBy(oddOrEven).get("odd") // => 3 * Lazy(numbers).countBy(oddOrEven).get("foo") // => undefined */ Sequence.prototype.countBy = function countBy(keyFn) { return new CountedSequence(this, keyFn); }; /** * @constructor */ function CountedSequence(parent, keyFn) { this.parent = parent; this.keyFn = keyFn; } // CountedSequence, like GroupedSequence, must have its prototype set after // ObjectLikeSequence has been fully initiali