%PDF- %PDF-
Direktori : /proc/self/root/home/infra/fusioninventory/lib/lazy.js-0.5.1/ |
Current File : //proc/self/root/home/infra/fusioninventory/lib/lazy.js-0.5.1/lazy.js |
/* * @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 initialized. /** * Creates a new sequence with every unique element from this one appearing * exactly once (i.e., with duplicates removed). * * @public * @aka unique * @param {Function=} keyFn An optional function to produce the key for each * object. This key is then tested for uniqueness as opposed to the * object reference. * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 2, 3, 3, 3]).uniq() // sequence: [1, 2, 3] * Lazy([{ name: 'mike' }, * { name: 'sarah' }, * { name: 'mike' } * ]).uniq('name') * // sequence: [{ name: 'mike' }, { name: 'sarah' }] * * @benchmarks * function randomOf(array) { * return function() { * return array[Math.floor(Math.random() * array.length)]; * }; * } * * var mostUnique = Lazy.generate(randomOf(_.range(100)), 100).toArray(), * someUnique = Lazy.generate(randomOf(_.range(50)), 100).toArray(), * mostDupes = Lazy.generate(randomOf(_.range(5)), 100).toArray(); * * Lazy(mostUnique).uniq().each(Lazy.noop) // lazy - mostly unique elements * Lazy(someUnique).uniq().each(Lazy.noop) // lazy - some unique elements * Lazy(mostDupes).uniq().each(Lazy.noop) // lazy - mostly duplicate elements * _.each(_.uniq(mostUnique), _.noop) // lodash - mostly unique elements * _.each(_.uniq(someUnique), _.noop) // lodash - some unique elements * _.each(_.uniq(mostDupes), _.noop) // lodash - mostly duplicate elements */ Sequence.prototype.uniq = function uniq(keyFn) { return new UniqueSequence(this, keyFn); }; Sequence.prototype.unique = function unique(keyFn) { return this.uniq(keyFn); }; /** * @constructor */ function UniqueSequence(parent, keyFn) { this.parent = parent; this.keyFn = keyFn; } UniqueSequence.prototype = Object.create(Sequence.prototype); UniqueSequence.prototype.each = function each(fn) { var cache = new Set(), keyFn = this.keyFn, i = 0; if (keyFn) { keyFn = createCallback(keyFn); return this.parent.each(function(e) { if (cache.add(keyFn(e))) { return fn(e, i++); } }); } else { return this.parent.each(function(e) { if (cache.add(e)) { return fn(e, i++); } }); } }; /** * Creates a new sequence by combining the elements from this sequence with * corresponding elements from the specified array(s). * * @public * @param {...Array} var_args One or more arrays of elements to combine with * those of this sequence. * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2]).zip([3, 4]) // sequence: [[1, 3], [2, 4]] * Lazy([]).zip([0]) // sequence: [[undefined, 0]] * Lazy([0]).zip([]) // sequence: [[0, undefined]] * Lazy([]).zip([1, 2], [3, 4]) // sequence: [[undefined, 1, 3], [undefined, 2, 4]] * Lazy([]).zip([1], [2, 3]) // sequence: [[undefined, 1, 2], [undefined, undefined, 3]] * Lazy([1, 2]).zip([3], [4]) // sequence: [[1, 3, 4], [2, undefined, undefined]] * * @benchmarks * var smArrL = Lazy.range(10).toArray(), * smArrR = Lazy.range(10, 20).toArray(), * lgArrL = Lazy.range(100).toArray(), * lgArrR = Lazy.range(100, 200).toArray(); * * Lazy(smArrL).zip(smArrR).each(Lazy.noop) // lazy - zipping 10-element arrays * Lazy(lgArrL).zip(lgArrR).each(Lazy.noop) // lazy - zipping 100-element arrays * _.each(_.zip(smArrL, smArrR), _.noop) // lodash - zipping 10-element arrays * _.each(_.zip(lgArrL, lgArrR), _.noop) // lodash - zipping 100-element arrays */ Sequence.prototype.zip = function zip(var_args) { if (arguments.length === 1) { return new SimpleZippedSequence(this, (/** @type {Array} */ var_args)); } else { return new ZippedSequence(this, arraySlice.call(arguments, 0)); } }; /** * @constructor */ function ZippedSequence(parent, arrays) { this.parent = parent; this.arrays = arrays; } ZippedSequence.prototype = Object.create(Sequence.prototype); ZippedSequence.prototype.each = function each(fn) { var arrays = this.arrays, i = 0; var iteratedLeft = this.parent.each(function(e) { var group = [e]; for (var j = 0; j < arrays.length; ++j) { group.push(arrays[j][i]); } return fn(group, i++); }); if (!iteratedLeft) { return false; } var group, keepGoing = true; while (keepGoing) { keepGoing = false; group = [undefined]; for (var j = 0; j < arrays.length; ++j) { group.push(arrays[j][i]); // Check if *any* of the arrays have more elements to iterate. if (arrays[j].length > i) { keepGoing = true; } } if (keepGoing && (fn(group, i++) === false)) { return false; } } return true; }; /** * Creates a new sequence with the same elements as this one, in a randomized * order. * * @public * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3, 4, 5]).shuffle().value() // =~ [1, 2, 3, 4, 5] * Lazy([]).shuffle().value() // => [] * Lazy([1]).shuffle().each(Lazy.noop) // => true * Lazy([]).shuffle().each(Lazy.noop) // => true */ Sequence.prototype.shuffle = function shuffle() { return new ShuffledSequence(this); }; /** * @constructor */ function ShuffledSequence(parent) { this.parent = parent; } ShuffledSequence.prototype = Object.create(Sequence.prototype); ShuffledSequence.prototype.each = function each(fn) { var shuffled = this.parent.toArray(), floor = Math.floor, random = Math.random, j = 0; for (var i = shuffled.length - 1; i > 0; --i) { swap(shuffled, i, floor(random() * (i + 1))); if (fn(shuffled[i], j++) === false) { return false; } } if (shuffled.length) { fn(shuffled[0], j); } return true; }; /** * Creates a new sequence with every element from this sequence, and with arrays * exploded so that a sequence of arrays (of arrays) becomes a flat sequence of * values. * * @public * @param {boolean} shallow Option to flatten only one level deep (default is * recursive). * @returns {Sequence} The new sequence. * * @examples * Lazy([1, [2, 3], [4, [5]]]).flatten() // sequence: [1, 2, 3, 4, 5] * Lazy([1, [2, 3], [4, [5]]]).flatten(true) // sequence: [1, 2, 3, 4, [5]] * Lazy([1, Lazy([2, 3])]).flatten() // sequence: [1, 2, 3] */ Sequence.prototype.flatten = function flatten(shallow) { return new FlattenedSequence(this, shallow); }; /** * @constructor */ function FlattenedSequence(parent, shallow) { this.parent = parent; this.each = shallow ? this.eachShallow : this.eachRecursive; } FlattenedSequence.prototype = Object.create(Sequence.prototype); FlattenedSequence.prototype.eachShallow = function(fn) { var index = 0; return this.parent.each(function(e) { if (isArray(e)) { return forEach(e, function(val) { return fn(val, index++); }); } if (e instanceof Sequence) { return e.each(function(val) { return fn(val, index++); }); } return fn(e, index++); }); }; FlattenedSequence.prototype.eachRecursive = function each(fn) { var index = 0; return this.parent.each(function recurseVisitor(e) { if (isArray(e)) { return forEach(e, recurseVisitor); } if (e instanceof Sequence) { return e.each(recurseVisitor); } return fn(e, index++); }); }; /** * Creates a new sequence with the same elements as this one, except for all * falsy values (`false`, `0`, `""`, `null`, and `undefined`). * * @public * @returns {Sequence} The new sequence. * * @examples * Lazy(["foo", null, "bar", undefined]).compact() // sequence: ["foo", "bar"] */ Sequence.prototype.compact = function compact() { return this.filter(function(e) { return !!e; }); }; /** * Creates a new sequence with all the elements of this sequence that are not * also among the specified arguments. * * @public * @aka difference * @param {...*} var_args The values, or array(s) of values, to be excluded from the * resulting sequence. * @returns {Sequence} The new sequence. * * @examples * Lazy([1, 2, 3, 4, 5]).without(2, 3) // sequence: [1, 4, 5] * Lazy([1, 2, 3, 4, 5]).without([4, 5]) // sequence: [1, 2, 3] */ Sequence.prototype.without = function without(var_args) { return new WithoutSequence(this, arraySlice.call(arguments, 0)); }; Sequence.prototype.difference = function difference(var_args) { return this.without.apply(this, arguments); }; /** * @constructor */ function WithoutSequence(parent, values) { this.parent = parent; this.values = values; } WithoutSequence.prototype = Object.create(Sequence.prototype); WithoutSequence.prototype.each = function each(fn) { var set = createSet(this.values), i = 0; return this.parent.each(function(e) { if (!set.contains(e)) { return fn(e, i++); } }); }; /** * Creates a new sequence with all the unique elements either in this sequence * or among the specified arguments. * * @public * @param {...*} var_args The values, or array(s) of values, to be additionally * included in the resulting sequence. * @returns {Sequence} The new sequence. * * @examples * Lazy(["foo", "bar"]).union([]) // sequence: ["foo", "bar"] * Lazy(["foo", "bar"]).union(["bar", "baz"]) // sequence: ["foo", "bar", "baz"] */ Sequence.prototype.union = function union(var_args) { return this.concat(var_args).uniq(); }; /** * Creates a new sequence with all the elements of this sequence that also * appear among the specified arguments. * * @public * @param {...*} var_args The values, or array(s) of values, in which elements * from this sequence must also be included to end up in the resulting sequence. * @returns {Sequence} The new sequence. * * @examples * Lazy(["foo", "bar"]).intersection([]) // sequence: [] * Lazy(["foo", "bar"]).intersection(["bar", "baz"]) // sequence: ["bar"] * Lazy(["a", "a"]).intersection(["a"]) // sequence: ["a"] * Lazy(["a"]).intersection(["a", "a"]) // sequence: ["a"] * Lazy(["a", "a"]).intersection(["a", "a"]) // sequence: ["a"] * Lazy(["a", "a"]).intersection(["a"], ["a"]) // sequence: ["a"] */ Sequence.prototype.intersection = function intersection(var_args) { if (arguments.length === 1 && isArray(arguments[0])) { return new SimpleIntersectionSequence(this, (/** @type {Array} */ var_args)); } else { return new IntersectionSequence(this, arraySlice.call(arguments, 0)); } }; /** * @constructor */ function IntersectionSequence(parent, arrays) { this.parent = parent; this.arrays = arrays; } IntersectionSequence.prototype = Object.create(Sequence.prototype); IntersectionSequence.prototype.each = function each(fn) { var sets = Lazy(this.arrays).map(function(values) { return new UniqueMemoizer(Lazy(values).getIterator()); }); var setIterator = new UniqueMemoizer(sets.getIterator()), i = 0; return this.parent.uniq().each(function(e) { var includedInAll = true; setIterator.each(function(set) { if (!set.contains(e)) { includedInAll = false; return false; } }); if (includedInAll) { return fn(e, i++); } }); }; /** * @constructor */ function Memoizer(memo, iterator) { this.iterator = iterator; this.memo = memo; this.currentIndex = 0; this.currentValue = undefined; } Memoizer.prototype.current = function current() { return this.currentValue; }; Memoizer.prototype.moveNext = function moveNext() { var iterator = this.iterator, memo = this.memo, current; if (this.currentIndex < memo.length) { this.currentValue = memo[this.currentIndex++]; return true; } if (iterator.moveNext()) { this.currentValue = memo[this.currentIndex++] = iterator.current(); return true; } return false; }; /** * @constructor */ function UniqueMemoizer(iterator) { this.iterator = iterator; this.set = new Set(); this.memo = []; this.currentValue = undefined; } UniqueMemoizer.prototype.current = function current() { return this.currentValue; }; UniqueMemoizer.prototype.moveNext = function moveNext() { var iterator = this.iterator, set = this.set, memo = this.memo, current; while (iterator.moveNext()) { current = iterator.current(); if (set.add(current)) { memo.push(current); this.currentValue = current; return true; } } return false; }; UniqueMemoizer.prototype.each = function each(fn) { var memo = this.memo, length = memo.length, i = -1; while (++i < length) { if (fn(memo[i], i) === false) { return false; } } while (this.moveNext()) { if (fn(this.currentValue, i++) === false) { break; } } }; UniqueMemoizer.prototype.contains = function contains(e) { if (this.set.contains(e)) { return true; } while (this.moveNext()) { if (this.currentValue === e) { return true; } } return false; }; /** * Checks whether every element in this sequence satisfies a given predicate. * * @public * @aka all * @param {Function} predicate A function to call on (potentially) every element * in this sequence. * @returns {boolean} True if `predicate` returns true for every element in the * sequence (or the sequence is empty). False if `predicate` returns false * for at least one element. * * @examples * var numbers = [1, 2, 3, 4, 5]; * * var objects = [{ foo: true }, { foo: false, bar: true }]; * * Lazy(numbers).every(isEven) // => false * Lazy(numbers).every(isPositive) // => true * Lazy(objects).all('foo') // => false * Lazy(objects).all('bar') // => false */ Sequence.prototype.every = function every(predicate) { predicate = createCallback(predicate); return this.each(function(e, i) { return !!predicate(e, i); }); }; Sequence.prototype.all = function all(predicate) { return this.every(predicate); }; /** * Checks whether at least one element in this sequence satisfies a given * predicate (or, if no predicate is specified, whether the sequence contains at * least one element). * * @public * @aka any * @param {Function=} predicate A function to call on (potentially) every element * in this sequence. * @returns {boolean} True if `predicate` returns true for at least one element * in the sequence. False if `predicate` returns false for every element (or * the sequence is empty). * * @examples * var numbers = [1, 2, 3, 4, 5]; * * Lazy(numbers).some() // => true * Lazy(numbers).some(isEven) // => true * Lazy(numbers).some(isNegative) // => false * Lazy([]).some() // => false */ Sequence.prototype.some = function some(predicate) { predicate = createCallback(predicate, true); var success = false; this.each(function(e) { if (predicate(e)) { success = true; return false; } }); return success; }; Sequence.prototype.any = function any(predicate) { return this.some(predicate); }; /** * Checks whether NO elements in this sequence satisfy the given predicate * (the opposite of {@link Sequence#all}, basically). * * @public * @param {Function=} predicate A function to call on (potentially) every element * in this sequence. * @returns {boolean} True if `predicate` does not return true for any element * in the sequence. False if `predicate` returns true for at least one * element. * * @examples * var numbers = [1, 2, 3, 4, 5]; * * Lazy(numbers).none() // => false * Lazy(numbers).none(isEven) // => false * Lazy(numbers).none(isNegative) // => true * Lazy([]).none(isEven) // => true * Lazy([]).none(isNegative) // => true * Lazy([]).none() // => true */ Sequence.prototype.none = function none(predicate) { return !this.any(predicate); }; /** * Checks whether the sequence has no elements. * * @public * @returns {boolean} True if the sequence is empty, false if it contains at * least one element. * * @examples * Lazy([]).isEmpty() // => true * Lazy([1, 2, 3]).isEmpty() // => false */ Sequence.prototype.isEmpty = function isEmpty() { return !this.any(); }; /** * Performs (at worst) a linear search from the head of this sequence, * returning the first index at which the specified value is found. * * @public * @param {*} value The element to search for in the sequence. * @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 {number} The index within this sequence where the given value is * located, or -1 if the sequence doesn't contain the value. * * @examples * function reciprocal(x) { return 1 / x; } * * Lazy(["foo", "bar", "baz"]).indexOf("bar") // => 1 * Lazy([1, 2, 3]).indexOf(4) // => -1 * Lazy([1, 2, 3]).map(reciprocal).indexOf(0.5) // => 1 */ Sequence.prototype.indexOf = function indexOf(value, equalityFn) { var eq = equalityFn || Lazy.equality, foundIndex = -1; this.each(function(e, i) { if (eq(e, value)) { foundIndex = i; return false; } }); return foundIndex; }; /** * Performs (at worst) a linear search from the tail of this sequence, * returning the last index at which the specified value is found. * * @public * @param {*} value The element to search for in the sequence. * @returns {number} The last index within this sequence where the given value * is located, or -1 if the sequence doesn't contain the value. * * @examples * Lazy(["a", "b", "c", "b", "a"]).lastIndexOf("b") // => 3 * Lazy([1, 2, 3]).lastIndexOf(0) // => -1 * Lazy([2, 2, 1, 2, 4]).filter(isEven).lastIndexOf(2) // 2 */ Sequence.prototype.lastIndexOf = function lastIndexOf(value, equalityFn) { var reversed = this.getIndex().reverse(), index = reversed.indexOf(value, equalityFn); if (index !== -1) { index = reversed.length() - index - 1; } return index; }; /** * Performs a binary search of this sequence, returning the lowest index where * the given value is either found, or where it belongs (if it is not already * in the sequence). * * This method assumes the sequence is in sorted order and will fail otherwise. * * @public * @param {*} value The element to search for in the sequence. * @returns {number} An index within this sequence where the given value is * located, or where it belongs in sorted order. * * @examples * Lazy([1, 3, 6, 9]).sortedIndex(3) // => 1 * Lazy([1, 3, 6, 9]).sortedIndex(7) // => 3 * Lazy([5, 10, 15, 20]).filter(isEven).sortedIndex(10) // => 0 * Lazy([5, 10, 15, 20]).filter(isEven).sortedIndex(12) // => 1 */ Sequence.prototype.sortedIndex = function sortedIndex(value) { var indexed = this.getIndex(), lower = 0, upper = indexed.length(), i; while (lower < upper) { i = (lower + upper) >>> 1; if (compare(indexed.get(i), value) === -1) { lower = i + 1; } else { upper = i; } } return lower; }; /** * Checks whether the given value is in this sequence. * * @public * @param {*} value The element to search for in the sequence. * @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} True if the sequence contains the value, false if not. * * @examples * var numbers = [5, 10, 15, 20]; * * Lazy(numbers).contains(15) // => true * Lazy(numbers).contains(13) // => false */ Sequence.prototype.contains = function contains(value, equalityFn) { return this.indexOf(value, equalityFn) !== -1; }; /** * Aggregates a sequence into a single value according to some accumulator * function. * * For an asynchronous sequence, instead of immediately returning a result * (which it can't, obviously), this method returns an {@link AsyncHandle} * whose `onComplete` method can be called to supply a callback to handle the * final result once iteration has completed. * * @public * @aka inject, foldl * @param {Function} aggregator The function through which to pass every element * in the sequence. For every element, the function will be passed the total * aggregated result thus far and the element itself, and should return a * new aggregated result. * @param {*=} memo The starting value to use for the aggregated result * (defaults to the first element in the sequence). * @returns {*} The result of the aggregation, or, for asynchronous sequences, * an {@link AsyncHandle} whose `onComplete` method accepts a callback to * handle the final result. * * @examples * function multiply(x, y) { return x * y; } * * var numbers = [1, 2, 3, 4]; * * Lazy(numbers).reduce(multiply) // => 24 * Lazy(numbers).reduce(multiply, 5) // => 120 */ Sequence.prototype.reduce = function reduce(aggregator, memo) { if (arguments.length < 2) { return this.tail().reduce(aggregator, this.head()); } var eachResult = this.each(function(e, i) { memo = aggregator(memo, e, i); }); // TODO: Think of a way more efficient solution to this problem. if (eachResult instanceof AsyncHandle) { return eachResult.then(function() { return memo; }); } return memo; }; Sequence.prototype.inject = Sequence.prototype.foldl = function foldl(aggregator, memo) { return this.reduce(aggregator, memo); }; /** * Aggregates a sequence, from the tail, into a single value according to some * accumulator function. * * @public * @aka foldr * @param {Function} aggregator The function through which to pass every element * in the sequence. For every element, the function will be passed the total * aggregated result thus far and the element itself, and should return a * new aggregated result. * @param {*} memo The starting value to use for the aggregated result. * @returns {*} The result of the aggregation. * * @examples * function append(s1, s2) { * return s1 + s2; * } * * function isVowel(str) { * return "aeiou".indexOf(str) !== -1; * } * * Lazy("abcde").reduceRight(append) // => "edcba" * Lazy("abcde").filter(isVowel).reduceRight(append) // => "ea" */ Sequence.prototype.reduceRight = function reduceRight(aggregator, memo) { if (arguments.length < 2) { return this.initial(1).reduceRight(aggregator, this.last()); } // This bothers me... but frankly, calling reverse().reduce() is potentially // going to eagerly evaluate the sequence anyway; so it's really not an issue. var indexed = this.getIndex(), i = indexed.length() - 1; return indexed.reverse().reduce(function(m, e) { return aggregator(m, e, i--); }, memo); }; Sequence.prototype.foldr = function foldr(aggregator, memo) { return this.reduceRight(aggregator, memo); }; /** * Groups this sequence into consecutive (overlapping) segments of a specified * length. If the underlying sequence has fewer elements than the specified * length, then this sequence will be empty. * * @public * @param {number} length The length of each consecutive segment. * @returns {Sequence} The resulting sequence of consecutive segments. * * @examples * function sum(vals) { return Lazy(vals).sum(); } * var pairs = Lazy([1, 2, 3, 4]).consecutive(2); * * // Make sure consecutive sequences are reusable. * pairs.map(sum) // => sequence: [3, 5, 7] * pairs.map(sum) // => sequence: [3, 5, 7] * * Lazy([]).consecutive(2) // => sequence: [] * Lazy([1]).consecutive(2) // => sequence: [] * Lazy([1, 2]).consecutive(2) // => sequence: [[1, 2]] * Lazy([1, 2, 3]).consecutive(2) // => sequence: [[1, 2], [2, 3]] * Lazy([1, 2, 3]).consecutive(0) // => sequence: [[]] * Lazy([1, 2, 3]).consecutive(1) // => sequence: [[1], [2], [3]] */ Sequence.prototype.consecutive = function consecutive(count) { return new ConsecutiveSequence(this, count); }; function ConsecutiveSequence(parent, count) { this.parent = parent; this.count = count; } ConsecutiveSequence.prototype = Object.create(Sequence.prototype); ConsecutiveSequence.prototype.each = function each(fn) { var count = this.count, queue = new Queue(count); var segments = this.parent.map(function(element) { if (queue.add(element).count === count) { return queue.toArray(); } }); return segments.compact().each(fn); }; /** * Breaks this sequence into chunks (arrays) of a specified length. * * @public * @param {number} size The size of each chunk. * @returns {Sequence} The resulting sequence of chunks. * * @examples * Lazy([]).chunk(2) // sequence: [] * Lazy([1, 2, 3]).chunk(2) // sequence: [[1, 2], [3]] * Lazy([1, 2, 3]).chunk(1) // sequence: [[1], [2], [3]] * Lazy([1, 2, 3]).chunk(4) // sequence: [[1, 2, 3]] * Lazy([1, 2, 3]).chunk(0) // throws */ Sequence.prototype.chunk = function chunk(size) { if (size < 1) { throw new Error("You must specify a positive chunk size."); } return new ChunkedSequence(this, size); }; /** * @constructor */ function ChunkedSequence(parent, size) { this.parent = parent; this.chunkSize = size; } ChunkedSequence.prototype = Object.create(Sequence.prototype); ChunkedSequence.prototype.getIterator = function getIterator() { return new ChunkedIterator(this.parent, this.chunkSize); }; /** * @constructor */ function ChunkedIterator(sequence, size) { this.iterator = sequence.getIterator(); this.size = size; } ChunkedIterator.prototype.current = function current() { return this.currentChunk; }; ChunkedIterator.prototype.moveNext = function moveNext() { var iterator = this.iterator, chunkSize = this.size, chunk = []; while (chunk.length < chunkSize && iterator.moveNext()) { chunk.push(iterator.current()); } if (chunk.length === 0) { return false; } this.currentChunk = chunk; return true; }; /** * Passes each element in the sequence to the specified callback during * iteration. This is like {@link Sequence#each}, except that it can be * inserted anywhere in the middle of a chain of methods to "intercept" the * values in the sequence at that point. * * @public * @param {Function} callback A function to call on every element in the * sequence during iteration. The return value of this function does not * matter. * @returns {Sequence} A sequence comprising the same elements as this one. * * @examples * Lazy([1, 2, 3]).tap(fn).each(Lazy.noop); // calls fn 3 times */ Sequence.prototype.tap = function tap(callback) { return new TappedSequence(this, callback); }; /** * @constructor */ function TappedSequence(parent, callback) { this.parent = parent; this.callback = callback; } TappedSequence.prototype = Object.create(Sequence.prototype); TappedSequence.prototype.each = function each(fn) { var callback = this.callback; return this.parent.each(function(e, i) { callback(e, i); return fn(e, i); }); }; /** * Seaches for the first element in the sequence satisfying a given predicate. * * @public * @aka detect * @param {Function} predicate A function to call on (potentially) every element * in the sequence. * @returns {*} The first element in the sequence for which `predicate` returns * `true`, or `undefined` if no such element is found. * * @examples * function divisibleBy3(x) { * return x % 3 === 0; * } * * var numbers = [5, 6, 7, 8, 9, 10]; * * Lazy(numbers).find(divisibleBy3) // => 6 * Lazy(numbers).find(isNegative) // => undefined */ Sequence.prototype.find = function find(predicate) { return this.filter(predicate).first(); }; Sequence.prototype.detect = function detect(predicate) { return this.find(predicate); }; /** * Gets the minimum value in the sequence. * * @public * @param {Function=} valueFn The function by which the value for comparison is * calculated for each element in the sequence. * @returns {*} The element with the lowest value in the sequence, or * undefined` if the sequence is empty. * * @examples * function negate(x) { return x * -1; } * * Lazy([]).min() // => undefined * Lazy([1]).min() // => 1 * Lazy([1, 2]).min() // => 1 * Lazy([2, 1]).min() // => 1 * Lazy([6, 18, 2, 49, 34]).min() // => 2 * Lazy([6, 18, 2, 49, 34]).min(negate) // => 49 * Lazy(['b', 'a', 'c']).min() // => 'a' */ Sequence.prototype.min = function min(valueFn) { if (typeof valueFn !== "undefined") { return this.minBy(valueFn); } return this.reduce(function(prev, current, i) { if (typeof prev === "undefined") { return current; } return current < prev ? current : prev; }); }; Sequence.prototype.minBy = function minBy(valueFn) { valueFn = createCallback(valueFn); return this.reduce(function(x, y) { return valueFn(y) < valueFn(x) ? y : x; }); }; /** * Gets the maximum value in the sequence. * * @public * @param {Function=} valueFn The function by which the value for comparison is * calculated for each element in the sequence. * @returns {*} The element with the highest value in the sequence, or * undefined if the sequence is empty. * * @examples * function reverseDigits(x) { * return Number(String(x).split('').reverse().join('')); * } * * Lazy([]).max() // => undefined * Lazy([1]).max() // => 1 * Lazy([1, 2]).max() // => 2 * Lazy([2, 1]).max() // => 2 * Lazy([6, 18, 2, 48, 29]).max() // => 48 * Lazy([6, 18, 2, 48, 29]).max(reverseDigits) // => 29 * Lazy(['b', 'c', 'a']).max() // => 'c' */ Sequence.prototype.max = function max(valueFn) { if (typeof valueFn !== "undefined") { return this.maxBy(valueFn); } return this.reduce(function(prev, current, i) { if (typeof prev === "undefined") { return current; } return current > prev ? current : prev; }); }; Sequence.prototype.maxBy = function maxBy(valueFn) { valueFn = createCallback(valueFn); return this.reduce(function(x, y) { return valueFn(y) > valueFn(x) ? y : x; }); }; /** * Gets the sum of the numeric values in the sequence. * * @public * @param {Function=} valueFn The function used to select the numeric values * that will be summed up. * @returns {*} The sum. * * @examples * Lazy([]).sum() // => 0 * Lazy([1, 2, 3, 4]).sum() // => 10 * Lazy([1.2, 3.4]).sum(Math.floor) // => 4 * Lazy(['foo', 'bar']).sum('length') // => 6 */ Sequence.prototype.sum = function sum(valueFn) { if (typeof valueFn !== "undefined") { return this.sumBy(valueFn); } return this.reduce(function(x, y) { return x + y; }, 0); }; Sequence.prototype.sumBy = function sumBy(valueFn) { valueFn = createCallback(valueFn); return this.reduce(function(x, y) { return x + valueFn(y); }, 0); }; /** * Creates a string from joining together all of the elements in this sequence, * separated by the given delimiter. * * @public * @aka toString * @param {string=} delimiter The separator to insert between every element from * this sequence in the resulting string (defaults to `","`). * @returns {string} The delimited string. * * @examples * function toParam(v, k) { * return k + '=' + v; * } * * Lazy([6, 29, 1984]).join("/") // => "6/29/1984" * Lazy(["a", "b", "c"]).join() // => "a,b,c" * Lazy(["a", "b", "c"]).join("") // => "abc" * Lazy([1, 2, 3]).join() // => "1,2,3" * Lazy([1, 2, 3]).join("") // => "123" * Lazy(["", "", ""]).join(",") // => ",," * Lazy([1, 2]).join(0) // => "102" * Lazy(["cons", "d"]).join(true) // => "construed" * Lazy({foo: 1, bar: 2}).values().join() // "1,2" * Lazy({foo: 1, bar: 2}).keys().join() // "foo,bar" * Lazy({foo: 1, bar: 2}).map(toParam).join('&') // 'foo=1&bar=2' */ Sequence.prototype.join = function join(delimiter) { delimiter = typeof delimiter === "undefined" ? "," : String(delimiter); var i = -1; return this.reduce(function(str, e) { if (++i > 0) { str += delimiter; } return str + e; }, ""); }; Sequence.prototype.toString = function toString(delimiter) { return this.join(delimiter); }; /** * Creates a sequence, with the same elements as this one, that will be iterated * over asynchronously when calling `each`. * * @public * @param {number=} interval The approximate period, in milliseconds, that * should elapse between each element in the resulting sequence. Omitting * this argument will result in the fastest possible asynchronous iteration. * @returns {AsyncSequence} The new asynchronous sequence. * * @examples * Lazy([1, 2, 3]).async(100).each(fn) // calls fn 3 times asynchronously */ Sequence.prototype.async = function async(interval) { return new AsyncSequence(this, interval); }; /** * @constructor */ function SimpleIntersectionSequence(parent, array) { this.parent = parent; this.array = array; this.each = getEachForIntersection(array); } SimpleIntersectionSequence.prototype = Object.create(Sequence.prototype); SimpleIntersectionSequence.prototype.eachMemoizerCache = function eachMemoizerCache(fn) { var iterator = new UniqueMemoizer(Lazy(this.array).getIterator()), i = 0; return this.parent.uniq().each(function(e) { if (iterator.contains(e)) { return fn(e, i++); } }); }; SimpleIntersectionSequence.prototype.eachArrayCache = function eachArrayCache(fn) { var array = this.array, find = arrayContains, i = 0; return this.parent.uniq().each(function(e) { if (find(array, e)) { return fn(e, i++); } }); }; function getEachForIntersection(source) { if (source.length < 40) { return SimpleIntersectionSequence.prototype.eachArrayCache; } else { return SimpleIntersectionSequence.prototype.eachMemoizerCache; } } /** * An optimized version of {@link ZippedSequence}, when zipping a sequence with * only one array. * * @param {Sequence} parent The underlying sequence. * @param {Array} array The array with which to zip the sequence. * @constructor */ function SimpleZippedSequence(parent, array) { this.parent = parent; this.array = array; } SimpleZippedSequence.prototype = Object.create(Sequence.prototype); SimpleZippedSequence.prototype.each = function each(fn) { var array = this.array, i = -1; var iteratedLeft = this.parent.each(function(e) { ++i; return fn([e, array[i]], i); }); if (!iteratedLeft) { return false; } while (++i < array.length) { if (fn([undefined, array[i]], i) === false) { return false; } } return true; }; /** * An `ArrayLikeSequence` is a {@link Sequence} that provides random access to * its elements. This extends the API for iterating with the additional methods * {@link #get} and {@link #length}, allowing a sequence to act as a "view" into * a collection or other indexed data source. * * The initial sequence created by wrapping an array with `Lazy(array)` is an * `ArrayLikeSequence`. * * All methods of `ArrayLikeSequence` that conceptually should return * something like a array (with indexed access) return another * `ArrayLikeSequence`, for example: * * - {@link Sequence#map} * - {@link ArrayLikeSequence#slice} * - {@link Sequence#take} and {@link Sequence#drop} * - {@link Sequence#reverse} * * The above is not an exhaustive list. There are also certain other cases * where it might be possible to return an `ArrayLikeSequence` (e.g., calling * {@link Sequence#concat} with a single array argument), but this is not * guaranteed by the API. * * Note that in many cases, it is not possible to provide indexed access * without first performing at least a partial iteration of the underlying * sequence. In these cases an `ArrayLikeSequence` will not be returned: * * - {@link Sequence#filter} * - {@link Sequence#uniq} * - {@link Sequence#union} * - {@link Sequence#intersect} * * etc. The above methods only return ordinary {@link Sequence} objects. * * Defining custom array-like sequences * ------------------------------------ * * Creating a custom `ArrayLikeSequence` is essentially the same as creating a * custom {@link Sequence}. You just have a couple more methods you need to * implement: `get` and (optionally) `length`. * * Here's an example. Let's define a sequence type called `OffsetSequence` that * offsets each of its parent's elements by a set distance, and circles back to * the beginning after reaching the end. **Remember**: the initialization * function you pass to {@link #define} should always accept a `parent` as its * first parameter. * * ArrayLikeSequence.define("offset", { * init: function(parent, offset) { * this.offset = offset; * }, * * get: function(i) { * return this.parent.get((i + this.offset) % this.parent.length()); * } * }); * * It's worth noting a couple of things here. * * First, Lazy's default implementation of `length` simply returns the parent's * length. In this case, since an `OffsetSequence` will always have the same * number of elements as its parent, that implementation is fine; so we don't * need to override it. * * Second, the default implementation of `each` uses `get` and `length` to * essentially create a `for` loop, which is fine here. If you want to implement * `each` your own way, you can do that; but in most cases (as here), you can * probably just stick with the default. * * So we're already done, after only implementing `get`! Pretty easy, huh? * * Now the `offset` method will be chainable from any `ArrayLikeSequence`. So * for example: * * Lazy([1, 2, 3]).map(mapFn).offset(3); * * ...will work, but: * * Lazy([1, 2, 3]).filter(mapFn).offset(3); * * ...will not (because `filter` does not return an `ArrayLikeSequence`). * * (Also, as with the example provided for defining custom {@link Sequence} * types, this example really could have been implemented using a function * already available as part of Lazy.js: in this case, {@link Sequence#map}.) * * @public * @constructor * * @examples * Lazy([1, 2, 3]) // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2, 3]).map(Lazy.identity) // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2, 3]).take(2) // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2, 3]).drop(2) // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2, 3]).reverse() // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2, 3]).slice(1, 2) // instanceof Lazy.ArrayLikeSequence */ function ArrayLikeSequence() {} ArrayLikeSequence.prototype = Object.create(Sequence.prototype); /** * Create a new constructor function for a type inheriting from * `ArrayLikeSequence`. * * @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 `ArrayLikeSequence` prototype so that it can be chained with any other * methods that return array-like sequences. * @param {Object} overrides An object containing function overrides for this * new sequence type. **Must** include `get`. *May* include `init`, * `length`, `getIterator`, and `each`. For each function, `this` will be * the new sequence and `this.parent` will be the source sequence. * @returns {Function} A constructor for a new type inheriting from * `ArrayLikeSequence`. * * @examples * Lazy.ArrayLikeSequence.define("offset", { * init: function(offset) { * this.offset = offset; * }, * * get: function(i) { * return this.parent.get((i + this.offset) % this.parent.length()); * } * }); * * Lazy([1, 2, 3]).offset(1) // sequence: [2, 3, 1] */ ArrayLikeSequence.define = function define(methodName, overrides) { if (!overrides || typeof overrides.get !== 'function') { throw new Error("A custom array-like sequence must implement *at least* get!"); } return defineSequenceType(ArrayLikeSequence, methodName, overrides); }; /** * Returns the element at the specified index. * * @public * @param {number} i The index to access. * @returns {*} The element. * * @examples * function increment(x) { return x + 1; } * * Lazy([1, 2, 3]).get(1) // => 2 * Lazy([1, 2, 3]).get(-1) // => undefined * Lazy([1, 2, 3]).map(increment).get(1) // => 3 */ ArrayLikeSequence.prototype.get = function get(i) { return this.parent.get(i); }; /** * Returns the length of the sequence. * * @public * @returns {number} The length. * * @examples * function increment(x) { return x + 1; } * * Lazy([]).length() // => 0 * Lazy([1, 2, 3]).length() // => 3 * Lazy([1, 2, 3]).map(increment).length() // => 3 */ ArrayLikeSequence.prototype.length = function length() { return this.parent.length(); }; /** * Returns the current sequence (since it is already indexed). */ ArrayLikeSequence.prototype.getIndex = function getIndex() { return this; }; /** * An optimized version of {@link Sequence#getIterator}. */ ArrayLikeSequence.prototype.getIterator = function getIterator() { return new IndexedIterator(this); }; /** * An optimized version of {@link Iterator} meant to work with already-indexed * sequences. * * @param {ArrayLikeSequence} sequence The sequence to iterate over. * @constructor */ function IndexedIterator(sequence) { this.sequence = sequence; this.index = -1; } IndexedIterator.prototype.current = function current() { return this.sequence.get(this.index); }; IndexedIterator.prototype.moveNext = function moveNext() { if (this.index >= this.sequence.length() - 1) { return false; } ++this.index; return true; }; /** * An optimized version of {@link Sequence#each}. */ ArrayLikeSequence.prototype.each = function each(fn) { var length = this.length(), i = -1; while (++i < length) { if (fn(this.get(i), i) === false) { return false; } } return true; }; /** * Returns a new sequence with the same elements as this one, plus the * specified element at the end. * * @public * @returns {ArrayLikeSequence} The new array-like sequence. * * @examples * Lazy([1, 2]).push(3) // sequence: [1, 2, 3] * Lazy([]).push(1) // sequence: [1] */ ArrayLikeSequence.prototype.push = function push(value) { return this.concat([value]); }; /** * Returns a new sequence with the same elements as this one, minus the last * element. * * @public * @returns {ArrayLikeSequence} The new array-like sequence. * * @examples * Lazy([1, 2, 3]).pop() // sequence: [1, 2] * Lazy([]).pop() // sequence: [] */ ArrayLikeSequence.prototype.pop = function pop() { return this.initial(); }; /** * Returns a new sequence with the same elements as this one, plus the * specified element at the beginning. * * @public * @returns {ArrayLikeSequence} The new array-like sequence. * * @examples * Lazy([1, 2]).unshift(3) // sequence: [3, 1, 2] * Lazy([]).unshift(1) // sequence: [1] */ ArrayLikeSequence.prototype.unshift = function unshift(value) { return Lazy([value]).concat(this); }; /** * Returns a new sequence with the same elements as this one, minus the first * element. * * @public * @returns {ArrayLikeSequence} The new array-like sequence. * * @examples * Lazy([1, 2, 3]).shift() // sequence: [2, 3] * Lazy([]).shift() // sequence: [] */ ArrayLikeSequence.prototype.shift = function shift() { return this.drop(); }; /** * Returns a new sequence comprising the portion of this sequence starting * from the specified starting index and continuing until the specified ending * index or to the end of the sequence. * * @public * @param {number} begin The index at which the new sequence should start. * @param {number=} end The index at which the new sequence should end. * @returns {ArrayLikeSequence} The new array-like sequence. * * @examples * Lazy([1, 2, 3, 4, 5]).slice(0) // sequence: [1, 2, 3, 4, 5] * Lazy([1, 2, 3, 4, 5]).slice(2) // sequence: [3, 4, 5] * Lazy([1, 2, 3, 4, 5]).slice(2, 4) // sequence: [3, 4] * Lazy([1, 2, 3, 4, 5]).slice(-1) // sequence: [5] * Lazy([1, 2, 3, 4, 5]).slice(1, -1) // sequence: [2, 3, 4] * Lazy([1, 2, 3, 4, 5]).slice(0, 10) // sequence: [1, 2, 3, 4, 5] */ ArrayLikeSequence.prototype.slice = function slice(begin, end) { var length = this.length(); if (begin < 0) { begin = length + begin; } var result = this.drop(begin); if (typeof end === "number") { if (end < 0) { end = length + end; } result = result.take(end - begin); } return result; }; /** * An optimized version of {@link Sequence#map}, which creates an * {@link ArrayLikeSequence} so that the result still provides random access. * * @public * * @examples * Lazy([1, 2, 3]).map(Lazy.identity) // instanceof Lazy.ArrayLikeSequence */ ArrayLikeSequence.prototype.map = function map(mapFn) { return new IndexedMappedSequence(this, createCallback(mapFn)); }; /** * @constructor */ function IndexedMappedSequence(parent, mapFn) { this.parent = parent; this.mapFn = mapFn; } IndexedMappedSequence.prototype = Object.create(ArrayLikeSequence.prototype); IndexedMappedSequence.prototype.get = function get(i) { if (i < 0 || i >= this.parent.length()) { return undefined; } return this.mapFn(this.parent.get(i), i); }; /** * An optimized version of {@link Sequence#filter}. */ ArrayLikeSequence.prototype.filter = function filter(filterFn) { return new IndexedFilteredSequence(this, createCallback(filterFn)); }; /** * @constructor */ function IndexedFilteredSequence(parent, filterFn) { this.parent = parent; this.filterFn = filterFn; } IndexedFilteredSequence.prototype = Object.create(FilteredSequence.prototype); IndexedFilteredSequence.prototype.each = function each(fn) { var parent = this.parent, filterFn = this.filterFn, length = this.parent.length(), i = -1, j = 0, e; while (++i < length) { e = parent.get(i); if (filterFn(e, i) && fn(e, j++) === false) { return false; } } return true; }; /** * An optimized version of {@link Sequence#reverse}, which creates an * {@link ArrayLikeSequence} so that the result still provides random access. * * @public * * @examples * Lazy([1, 2, 3]).reverse() // instanceof Lazy.ArrayLikeSequence */ ArrayLikeSequence.prototype.reverse = function reverse() { return new IndexedReversedSequence(this); }; /** * @constructor */ function IndexedReversedSequence(parent) { this.parent = parent; } IndexedReversedSequence.prototype = Object.create(ArrayLikeSequence.prototype); IndexedReversedSequence.prototype.get = function get(i) { return this.parent.get(this.length() - i - 1); }; /** * An optimized version of {@link Sequence#first}, which creates an * {@link ArrayLikeSequence} so that the result still provides random access. * * @public * * @examples * Lazy([1, 2, 3]).first(2) // instanceof Lazy.ArrayLikeSequence */ ArrayLikeSequence.prototype.first = function first(count) { if (typeof count === "undefined") { return this.get(0); } return new IndexedTakeSequence(this, count); }; /** * @constructor */ function IndexedTakeSequence(parent, count) { this.parent = parent; this.count = count; } IndexedTakeSequence.prototype = Object.create(ArrayLikeSequence.prototype); IndexedTakeSequence.prototype.length = function length() { var parentLength = this.parent.length(); return this.count <= parentLength ? this.count : parentLength; }; /** * An optimized version of {@link Sequence#rest}, which creates an * {@link ArrayLikeSequence} so that the result still provides random access. * * @public * * @examples * Lazy([1, 2, 3]).rest() // instanceof Lazy.ArrayLikeSequence */ ArrayLikeSequence.prototype.rest = function rest(count) { return new IndexedDropSequence(this, count); }; /** * @constructor */ function IndexedDropSequence(parent, count) { this.parent = parent; this.count = typeof count === "number" ? count : 1; } IndexedDropSequence.prototype = Object.create(ArrayLikeSequence.prototype); IndexedDropSequence.prototype.get = function get(i) { return this.parent.get(this.count + i); }; IndexedDropSequence.prototype.length = function length() { var parentLength = this.parent.length(); return this.count <= parentLength ? parentLength - this.count : 0; }; /** * An optimized version of {@link Sequence#concat} that returns another * {@link ArrayLikeSequence} *if* the argument is an array. * * @public * @param {...*} var_args * * @examples * Lazy([1, 2]).concat([3, 4]) // instanceof Lazy.ArrayLikeSequence * Lazy([1, 2]).concat([3, 4]) // sequence: [1, 2, 3, 4] */ ArrayLikeSequence.prototype.concat = function concat(var_args) { if (arguments.length === 1 && isArray(arguments[0])) { return new IndexedConcatenatedSequence(this, (/** @type {Array} */ var_args)); } else { return Sequence.prototype.concat.apply(this, arguments); } }; /** * @constructor */ function IndexedConcatenatedSequence(parent, other) { this.parent = parent; this.other = other; } IndexedConcatenatedSequence.prototype = Object.create(ArrayLikeSequence.prototype); IndexedConcatenatedSequence.prototype.get = function get(i) { var parentLength = this.parent.length(); if (i < parentLength) { return this.parent.get(i); } else { return this.other[i - parentLength]; } }; IndexedConcatenatedSequence.prototype.length = function length() { return this.parent.length() + this.other.length; }; /** * An optimized version of {@link Sequence#uniq}. */ ArrayLikeSequence.prototype.uniq = function uniq(keyFn) { return new IndexedUniqueSequence(this, createCallback(keyFn)); }; /** * @param {ArrayLikeSequence} parent * @constructor */ function IndexedUniqueSequence(parent, keyFn) { this.parent = parent; this.each = getEachForParent(parent); this.keyFn = keyFn; } IndexedUniqueSequence.prototype = Object.create(Sequence.prototype); IndexedUniqueSequence.prototype.eachArrayCache = function eachArrayCache(fn) { // Basically the same implementation as w/ the set, but using an array because // it's cheaper for smaller sequences. var parent = this.parent, keyFn = this.keyFn, length = parent.length(), cache = [], find = arrayContains, key, value, i = -1, j = 0; while (++i < length) { value = parent.get(i); key = keyFn(value); if (!find(cache, key)) { cache.push(key); if (fn(value, j++) === false) { return false; } } } }; IndexedUniqueSequence.prototype.eachSetCache = UniqueSequence.prototype.each; function getEachForParent(parent) { if (parent.length() < 100) { return IndexedUniqueSequence.prototype.eachArrayCache; } else { return UniqueSequence.prototype.each; } } // Now that we've fully initialized the ArrayLikeSequence prototype, we can // set the prototype for MemoizedSequence. MemoizedSequence.prototype = Object.create(Sequence.prototype); MemoizedSequence.prototype.getParentIterator = function getParentIterator() { // Since the premise of this sequence is that it only iterates over each // element of its parent a grand total of one (1) time, we should only ever // need to get the parent iterator once. if (!this.iterator) { this.iterator = this.parent.getIterator(); } return this.iterator; }; MemoizedSequence.prototype.getIterator = function getIterator() { return new Memoizer(this.memo, this.getParentIterator()); }; MemoizedSequence.prototype.iterateTo = function iterateTo(i) { var memo = this.memo, iterator = this.getParentIterator(); while (i >= memo.length) { if (!iterator.moveNext()) { this.complete = true; return false; } memo.push(iterator.current()); } return true; }; MemoizedSequence.prototype.get = function get(i) { var memo = this.memo; if (i < memo.length) { return memo[i]; } if (!this.iterateTo(i)) { return undefined; } return memo[i]; }; MemoizedSequence.prototype.length = function length() { if (!this.complete) { this.iterateTo(Infinity); } return this.memo.length; }; MemoizedSequence.prototype.slice = function slice(begin, end) { if (!this.complete) { this.iterateTo(end); } return Lazy(this.memo.slice(begin, end)); }; MemoizedSequence.prototype.toArray = function toArray() { if (!this.complete) { this.iterateTo(Infinity); } return this.memo.slice(0); }; /** * ArrayWrapper is the most basic {@link Sequence}. It directly wraps an array * and implements the same methods as {@link ArrayLikeSequence}, but more * efficiently. * * @constructor */ function ArrayWrapper(source) { this.source = source; } ArrayWrapper.prototype = Object.create(ArrayLikeSequence.prototype); ArrayWrapper.prototype.root = function root() { return this; }; ArrayWrapper.prototype.isAsync = function isAsync() { return false; }; /** * Returns the element at the specified index in the source array. * * @param {number} i The index to access. * @returns {*} The element. */ ArrayWrapper.prototype.get = function get(i) { return this.source[i]; }; /** * Returns the length of the source array. * * @returns {number} The length. */ ArrayWrapper.prototype.length = function length() { return this.source.length; }; /** * An optimized version of {@link Sequence#each}. */ ArrayWrapper.prototype.each = function each(fn) { return forEach(this.source, fn); }; /** * An optimized version of {@link Sequence#map}. */ ArrayWrapper.prototype.map = function map(mapFn) { return new MappedArrayWrapper(this, createCallback(mapFn)); }; /** * An optimized version of {@link Sequence#filter}. */ ArrayWrapper.prototype.filter = function filter(filterFn) { return new FilteredArrayWrapper(this, createCallback(filterFn)); }; /** * An optimized version of {@link Sequence#uniq}. */ ArrayWrapper.prototype.uniq = function uniq(keyFn) { return new UniqueArrayWrapper(this, keyFn); }; /** * An optimized version of {@link ArrayLikeSequence#concat}. * * @param {...*} var_args */ ArrayWrapper.prototype.concat = function concat(var_args) { if (arguments.length === 1 && isArray(arguments[0])) { return new ConcatArrayWrapper(this, (/** @type {Array} */ var_args)); } else { return ArrayLikeSequence.prototype.concat.apply(this, arguments); } }; /** * An optimized version of {@link Sequence#toArray}. */ ArrayWrapper.prototype.toArray = function toArray() { return this.source.slice(0); }; /** * @constructor */ function MappedArrayWrapper(parent, mapFn) { this.parent = parent; this.mapFn = mapFn; } MappedArrayWrapper.prototype = Object.create(ArrayLikeSequence.prototype); MappedArrayWrapper.prototype.get = function get(i) { var source = this.parent.source; if (i < 0 || i >= source.length) { return undefined; } return this.mapFn(source[i]); }; MappedArrayWrapper.prototype.length = function length() { return this.parent.source.length; }; MappedArrayWrapper.prototype.each = function each(fn) { var source = this.parent.source, length = source.length, mapFn = this.mapFn, i = -1; while (++i < length) { if (fn(mapFn(source[i], i), i) === false) { return false; } } return true; }; /** * @constructor */ function FilteredArrayWrapper(parent, filterFn) { this.parent = parent; this.filterFn = filterFn; } FilteredArrayWrapper.prototype = Object.create(FilteredSequence.prototype); FilteredArrayWrapper.prototype.each = function each(fn) { var source = this.parent.source, filterFn = this.filterFn, length = source.length, i = -1, j = 0, e; while (++i < length) { e = source[i]; if (filterFn(e, i) && fn(e, j++) === false) { return false; } } return true; }; /** * @constructor */ function UniqueArrayWrapper(parent, keyFn) { this.parent = parent; this.each = getEachForSource(parent.source); this.keyFn = keyFn; } UniqueArrayWrapper.prototype = Object.create(Sequence.prototype); UniqueArrayWrapper.prototype.eachNoCache = function eachNoCache(fn) { var source = this.parent.source, keyFn = this.keyFn, length = source.length, find = arrayContainsBefore, value, // Yes, this is hideous. // Trying to get performance first, will refactor next! i = -1, k = 0; while (++i < length) { value = source[i]; if (!find(source, value, i, keyFn) && fn(value, k++) === false) { return false; } } return true; }; UniqueArrayWrapper.prototype.eachArrayCache = function eachArrayCache(fn) { // Basically the same implementation as w/ the set, but using an array because // it's cheaper for smaller sequences. var source = this.parent.source, keyFn = this.keyFn, length = source.length, cache = [], find = arrayContains, key, value, i = -1, j = 0; if (keyFn) { keyFn = createCallback(keyFn); while (++i < length) { value = source[i]; key = keyFn(value); if (!find(cache, key)) { cache.push(key); if (fn(value, j++) === false) { return false; } } } } else { while (++i < length) { value = source[i]; if (!find(cache, value)) { cache.push(value); if (fn(value, j++) === false) { return false; } } } } return true; }; UniqueArrayWrapper.prototype.eachSetCache = UniqueSequence.prototype.each; /** * My latest findings here... * * So I hadn't really given the set-based approach enough credit. The main issue * was that my Set implementation was totally not optimized at all. After pretty * heavily optimizing it (just take a look; it's a monstrosity now!), it now * becomes the fastest option for much smaller values of N. */ function getEachForSource(source) { if (source.length < 40) { return UniqueArrayWrapper.prototype.eachNoCache; } else if (source.length < 100) { return UniqueArrayWrapper.prototype.eachArrayCache; } else { return UniqueArrayWrapper.prototype.eachSetCache; } } /** * @constructor */ function ConcatArrayWrapper(parent, other) { this.parent = parent; this.other = other; } ConcatArrayWrapper.prototype = Object.create(ArrayLikeSequence.prototype); ConcatArrayWrapper.prototype.get = function get(i) { var source = this.parent.source, sourceLength = source.length; if (i < sourceLength) { return source[i]; } else { return this.other[i - sourceLength]; } }; ConcatArrayWrapper.prototype.length = function length() { return this.parent.source.length + this.other.length; }; ConcatArrayWrapper.prototype.each = function each(fn) { var source = this.parent.source, sourceLength = source.length, other = this.other, otherLength = other.length, i = 0, j = -1; while (++j < sourceLength) { if (fn(source[j], i++) === false) { return false; } } j = -1; while (++j < otherLength) { if (fn(other[j], i++) === false) { return false; } } return true; }; /** * An `ObjectLikeSequence` object represents a sequence of key/value pairs. * * The initial sequence you get by wrapping an object with `Lazy(object)` is * an `ObjectLikeSequence`. * * All methods of `ObjectLikeSequence` that conceptually should return * something like an object return another `ObjectLikeSequence`. * * @public * @constructor * * @examples * var obj = { foo: 'bar' }; * * Lazy(obj).assign({ bar: 'baz' }) // instanceof Lazy.ObjectLikeSequence * Lazy(obj).defaults({ bar: 'baz' }) // instanceof Lazy.ObjectLikeSequence * Lazy(obj).invert() // instanceof Lazy.ObjectLikeSequence */ function ObjectLikeSequence() {} ObjectLikeSequence.prototype = Object.create(Sequence.prototype); /** * Create a new constructor function for a type inheriting from * `ObjectLikeSequence`. * * @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 `ObjectLikeSequence` prototype so that it can be chained with any other * methods that return object-like sequences. * @param {Object} overrides An object containing function overrides for this * new sequence type. **Must** include `each`. *May* include `init` and * `get` (for looking up an element by key). * @returns {Function} A constructor for a new type inheriting from * `ObjectLikeSequence`. * * @examples * function downcaseKey(value, key) { * return [key.toLowerCase(), value]; * } * * Lazy.ObjectLikeSequence.define("caseInsensitive", { * init: function() { * var downcased = this.parent * .map(downcaseKey) * .toObject(); * this.downcased = Lazy(downcased); * }, * * get: function(key) { * return this.downcased.get(key.toLowerCase()); * }, * * each: function(fn) { * return this.downcased.each(fn); * } * }); * * Lazy({ Foo: 'bar' }).caseInsensitive() // sequence: { foo: 'bar' } * Lazy({ FOO: 'bar' }).caseInsensitive().get('foo') // => 'bar' * Lazy({ FOO: 'bar' }).caseInsensitive().get('FOO') // => 'bar' */ ObjectLikeSequence.define = function define(methodName, overrides) { if (!overrides || typeof overrides.each !== 'function') { throw new Error("A custom object-like sequence must implement *at least* each!"); } return defineSequenceType(ObjectLikeSequence, methodName, overrides); }; ObjectLikeSequence.prototype.value = function value() { return this.toObject(); }; /** * Gets the element at the specified key in this sequence. * * @public * @param {string} key The key. * @returns {*} The element. * * @examples * Lazy({ foo: "bar" }).get("foo") // => "bar" * Lazy({ foo: "bar" }).extend({ foo: "baz" }).get("foo") // => "baz" * Lazy({ foo: "bar" }).defaults({ bar: "baz" }).get("bar") // => "baz" * Lazy({ foo: "bar" }).invert().get("bar") // => "foo" * Lazy({ foo: 1, bar: 2 }).pick(["foo"]).get("foo") // => 1 * Lazy({ foo: 1, bar: 2 }).pick(["foo"]).get("bar") // => undefined * Lazy({ foo: 1, bar: 2 }).omit(["foo"]).get("bar") // => 2 * Lazy({ foo: 1, bar: 2 }).omit(["foo"]).get("foo") // => undefined */ ObjectLikeSequence.prototype.get = function get(key) { var pair = this.pairs().find(function(pair) { return pair[0] === key; }); return pair ? pair[1] : undefined; }; /** * Returns a {@link Sequence} whose elements are the keys of this object-like * sequence. * * @public * @returns {Sequence} The sequence based on this sequence's keys. * * @examples * var obj = { hello: "hola", goodbye: "hasta luego" }; * * Lazy(obj).keys() // sequence: ["hello", "goodbye"] * Lazy(obj).keys().map(function(v, i) { return [v, i]; }) // sequence: [["hello", 0], ["goodbye", 1]] */ ObjectLikeSequence.prototype.keys = function keys() { return new KeySequence(this); }; function KeySequence(parent) { this.parent = parent; } KeySequence.prototype = Object.create(Sequence.prototype); KeySequence.prototype.each = function each(fn) { var i = -1; return this.parent.each(function(v, k) { return fn(k, ++i); }); }; /** * Returns a {@link Sequence} whose elements are the values of this object-like * sequence. * * @public * @returns {Sequence} The sequence based on this sequence's values. * * @examples * Lazy({ hello: "hola", goodbye: "hasta luego" }).values() // sequence: ["hola", "hasta luego"] */ ObjectLikeSequence.prototype.values = function values() { return new ValuesSequence(this); }; function ValuesSequence(parent) { this.parent = parent; } ValuesSequence.prototype = Object.create(Sequence.prototype); ValuesSequence.prototype.each = function each(fn) { var i = -1; return this.parent.each(function(v, k) { return fn(v, ++i); }); }; /** * Throws an exception. Asynchronous iteration over object-like sequences is * not supported. * * @public * @examples * Lazy({ foo: 'bar' }).async() // throws */ ObjectLikeSequence.prototype.async = function async() { throw new Error('An ObjectLikeSequence does not support asynchronous iteration.'); }; ObjectLikeSequence.prototype.filter = function filter(filterFn) { return new FilteredObjectLikeSequence(this, createCallback(filterFn)); }; function FilteredObjectLikeSequence(parent, filterFn) { this.parent = parent; this.filterFn = filterFn; } FilteredObjectLikeSequence.prototype = Object.create(ObjectLikeSequence.prototype); FilteredObjectLikeSequence.prototype.each = function each(fn) { var filterFn = this.filterFn; return this.parent.each(function(v, k) { if (filterFn(v, k)) { return fn(v, k); } }); }; /** * Returns this same sequence. (Reversing an object-like sequence doesn't make * any sense.) */ ObjectLikeSequence.prototype.reverse = function reverse() { return this; }; /** * Returns an {@link ObjectLikeSequence} whose elements are the combination of * this sequence and another object. In the case of a key appearing in both this * sequence and the given object, the other object's value will override the * one in this sequence. * * @public * @aka extend * @param {Object} other The other object to assign to this sequence. * @returns {ObjectLikeSequence} A new sequence comprising elements from this * sequence plus the contents of `other`. * * @examples * Lazy({ "uno": 1, "dos": 2 }).assign({ "tres": 3 }) // sequence: { uno: 1, dos: 2, tres: 3 } * Lazy({ foo: "bar" }).assign({ foo: "baz" }); // sequence: { foo: "baz" } * Lazy({ foo: 'foo' }).assign({ foo: false }).get('foo') // false */ ObjectLikeSequence.prototype.assign = function assign(other) { return new AssignSequence(this, other); }; ObjectLikeSequence.prototype.extend = function extend(other) { return this.assign(other); }; /** * @constructor */ function AssignSequence(parent, other) { this.parent = parent; this.other = other; } AssignSequence.prototype = Object.create(ObjectLikeSequence.prototype); AssignSequence.prototype.get = function get(key) { return key in this.other ? this.other[key] : this.parent.get(key); }; AssignSequence.prototype.each = function each(fn) { var merged = new Set(), done = false; Lazy(this.other).each(function(value, key) { if (fn(value, key) === false) { done = true; return false; } merged.add(key); }); if (!done) { return this.parent.each(function(value, key) { if (!merged.contains(key) && fn(value, key) === false) { return false; } }); } }; /** * Returns an {@link ObjectLikeSequence} whose elements are the combination of * this sequence and a 'default' object. In the case of a key appearing in both * this sequence and the given object, this sequence's value will override the * default object's. * * @public * @param {Object} defaults The 'default' object to use for missing keys in this * sequence. * @returns {ObjectLikeSequence} A new sequence comprising elements from this * sequence supplemented by the contents of `defaults`. * * @examples * Lazy({ name: "Dan" }).defaults({ name: "User", password: "passw0rd" }) // sequence: { name: "Dan", password: "passw0rd" } * Lazy({ foo: false }).defaults({ foo: 'foo' }).get('foo') // false * Lazy({ a: 1 }).defaults({ b: 2 }).defaults({ c: 3 }) // sequence: { a: 1, b: 2, c: 3 } * Lazy({ a: 1 }).defaults({ b: 2 }).defaults({ a: 3 }) // sequence: { a: 1, b: 2 } * Lazy({ a: 1, b: 2 }).defaults({ b: 5 }).defaults({ c: 3, d: 4 }) // sequence: { a: 1, b: 2, c: 3, d: 4 } */ ObjectLikeSequence.prototype.defaults = function defaults(defaults) { return new DefaultsSequence(this, defaults); }; /** * @constructor */ function DefaultsSequence(parent, defaults) { this.parent = parent; this.defaultValues = defaults; } DefaultsSequence.prototype = Object.create(ObjectLikeSequence.prototype); DefaultsSequence.prototype.get = function get(key) { var parentValue = this.parent.get(key); return parentValue !== undefined ? parentValue : this.defaultValues[key]; }; DefaultsSequence.prototype.each = function each(fn) { var merged = new Set(), done = false; this.parent.each(function(value, key) { if (fn(value, key) === false) { done = true; return false; } if (typeof value !== "undefined") { merged.add(key); } }); if (!done) { Lazy(this.defaultValues).each(function(value, key) { if (!merged.contains(key) && fn(value, key) === false) { return false; } }); } }; /** * Returns an {@link ObjectLikeSequence} whose values are this sequence's keys, * and whose keys are this sequence's values. * * @public * @returns {ObjectLikeSequence} A new sequence comprising the inverted keys and * values from this sequence. * * @examples * Lazy({ first: "Dan", last: "Tao" }).invert() // sequence: { Dan: "first", Tao: "last" } */ ObjectLikeSequence.prototype.invert = function invert() { return new InvertedSequence(this); }; /** * @constructor */ function InvertedSequence(parent) { this.parent = parent; } InvertedSequence.prototype = Object.create(ObjectLikeSequence.prototype); InvertedSequence.prototype.each = function each(fn) { this.parent.each(function(value, key) { return fn(key, value); }); }; /** * Produces an {@link ObjectLikeSequence} consisting of all the recursively * merged values from this and the given object(s) or sequence(s). * * Note that by default this method only merges "vanilla" objects (bags of * key/value pairs), not arrays or any other custom object types. To customize * how merging works, you can provide the mergeFn argument, e.g. to handling * merging arrays, strings, or other types of objects. * * @public * @param {...Object|ObjectLikeSequence} others The other object(s) or * sequence(s) whose values will be merged into this one. * @param {Function=} mergeFn An optional function used to customize merging * behavior. The function should take two values as parameters and return * whatever the "merged" form of those values is. If the function returns * undefined then the new value will simply replace the old one in the * final result. * @returns {ObjectLikeSequence} The new sequence consisting of merged values. * * @examples * // These examples are completely stolen from Lo-Dash's documentation: * // lodash.com/docs#merge * * var names = { * 'characters': [ * { 'name': 'barney' }, * { 'name': 'fred' } * ] * }; * * var ages = { * 'characters': [ * { 'age': 36 }, * { 'age': 40 } * ] * }; * * var food = { * 'fruits': ['apple'], * 'vegetables': ['beet'] * }; * * var otherFood = { * 'fruits': ['banana'], * 'vegetables': ['carrot'] * }; * * function mergeArrays(a, b) { * return Array.isArray(a) ? a.concat(b) : undefined; * } * * Lazy(names).merge(ages); // => sequence: { 'characters': [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] } * Lazy(food).merge(otherFood, mergeArrays); // => sequence: { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } * * // ----- Now for my own tests: ----- * * // merges objects * Lazy({ foo: 1 }).merge({ foo: 2 }); // => sequence: { foo: 2 } * Lazy({ foo: 1 }).merge({ bar: 2 }); // => sequence: { foo: 1, bar: 2 } * * // goes deep * Lazy({ foo: { bar: 1 } }).merge({ foo: { bar: 2 } }); // => sequence: { foo: { bar: 2 } } * Lazy({ foo: { bar: 1 } }).merge({ foo: { baz: 2 } }); // => sequence: { foo: { bar: 1, baz: 2 } } * Lazy({ foo: { bar: 1 } }).merge({ foo: { baz: 2 } }); // => sequence: { foo: { bar: 1, baz: 2 } } * * // gives precedence to later sources * Lazy({ foo: 1 }).merge({ bar: 2 }, { bar: 3 }); // => sequence: { foo: 1, bar: 3 } * * // undefined gets passed over * Lazy({ foo: 1 }).merge({ foo: undefined }); // => sequence: { foo: 1 } * * // null doesn't get passed over * Lazy({ foo: 1 }).merge({ foo: null }); // => sequence: { foo: null } * * // array contents get merged as well * Lazy({ foo: [{ bar: 1 }] }).merge({ foo: [{ baz: 2 }] }); // => sequence: { foo: [{ bar: 1, baz: 2}] } */ ObjectLikeSequence.prototype.merge = function merge(var_args) { var mergeFn = arguments.length > 1 && typeof arguments[arguments.length - 1] === "function" ? arrayPop.call(arguments) : null; return new MergedSequence(this, arraySlice.call(arguments, 0), mergeFn); }; /** * @constructor */ function MergedSequence(parent, others, mergeFn) { this.parent = parent; this.others = others; this.mergeFn = mergeFn; } MergedSequence.prototype = Object.create(ObjectLikeSequence.prototype); MergedSequence.prototype.each = function each(fn) { var others = this.others, mergeFn = this.mergeFn || mergeObjects, keys = {}; var iteratedFullSource = this.parent.each(function(value, key) { var merged = value; forEach(others, function(other) { if (key in other) { merged = mergeFn(merged, other[key]); } }); keys[key] = true; return fn(merged, key); }); if (iteratedFullSource === false) { return false; } var remaining = {}; forEach(others, function(other) { for (var k in other) { if (!keys[k]) { remaining[k] = mergeFn(remaining[k], other[k]); } } }); return Lazy(remaining).each(fn); }; /** * @private * @examples * mergeObjects({ foo: 1 }, { bar: 2 }); // => { foo: 1, bar: 2 } * mergeObjects({ foo: { bar: 1 } }, { foo: { baz: 2 } }); // => { foo: { bar: 1, baz: 2 } } * mergeObjects({ foo: { bar: 1 } }, { foo: undefined }); // => { foo: { bar: 1 } } * mergeObjects({ foo: { bar: 1 } }, { foo: null }); // => { foo: null } * mergeObjects({ array: [0, 1, 2] }, { array: [3, 4, 5] }).array; // instanceof Array * mergeObjects({ date: new Date() }, { date: new Date() }).date; // instanceof Date * mergeObjects([{ foo: 1 }], [{ bar: 2 }]); // => [{ foo: 1, bar: 2 }] */ function mergeObjects(a, b) { var merged, prop; if (typeof b === 'undefined') { return a; } // Check that we're dealing with two objects or two arrays. if (isVanillaObject(a) && isVanillaObject(b)) { merged = {}; } else if (isArray(a) && isArray(b)) { merged = []; } else { // Otherwise there's no merging to do -- just replace a w/ b. return b; } for (prop in a) { merged[prop] = mergeObjects(a[prop], b[prop]); } for (prop in b) { if (!merged[prop]) { merged[prop] = b[prop]; } } return merged; } /** * Checks whether an object is a "vanilla" object, i.e. {'foo': 'bar'} as * opposed to an array, date, etc. * * @private * @examples * isVanillaObject({foo: 'bar'}); // => true * isVanillaObject(new Date()); // => false * isVanillaObject([1, 2, 3]); // => false */ function isVanillaObject(object) { return object && object.constructor === Object; } /** * Creates a {@link Sequence} consisting of the keys from this sequence whose * values are functions. * * @public * @aka methods * @returns {Sequence} The new sequence. * * @examples * var dog = { * name: "Fido", * breed: "Golden Retriever", * bark: function() { console.log("Woof!"); }, * wagTail: function() { console.log("TODO: implement robotic dog interface"); } * }; * * Lazy(dog).functions() // sequence: ["bark", "wagTail"] */ ObjectLikeSequence.prototype.functions = function functions() { return this .filter(function(v, k) { return typeof(v) === "function"; }) .map(function(v, k) { return k; }); }; ObjectLikeSequence.prototype.methods = function methods() { return this.functions(); }; /** * Creates an {@link ObjectLikeSequence} consisting of the key/value pairs from * this sequence whose keys are included in the given array of property names. * * @public * @param {Array.<string>} properties An array of the properties to "pick" from this * sequence. * @returns {ObjectLikeSequence} The new sequence. * * @examples * var players = { * "who": "first", * "what": "second", * "i don't know": "third" * }; * * Lazy(players).pick(["who", "what"]) // sequence: { who: "first", what: "second" } */ ObjectLikeSequence.prototype.pick = function pick(properties) { return new PickSequence(this, properties); }; /** * @constructor */ function PickSequence(parent, properties) { this.parent = parent; this.properties = properties; } PickSequence.prototype = Object.create(ObjectLikeSequence.prototype); PickSequence.prototype.get = function get(key) { return arrayContains(this.properties, key) ? this.parent.get(key) : undefined; }; PickSequence.prototype.each = function each(fn) { var inArray = arrayContains, properties = this.properties; return this.parent.each(function(value, key) { if (inArray(properties, key)) { return fn(value, key); } }); }; /** * Creates an {@link ObjectLikeSequence} consisting of the key/value pairs from * this sequence excluding those with the specified keys. Non-string keys are * effectively ignored. * * @public * @param {Array} properties An array of the properties to *omit* from this * sequence. * @returns {ObjectLikeSequence} The new sequence. * * @examples * var players = { * "who": "first", * "what": "second", * "i don't know": "third" * }; * * Lazy(players).omit(["who", "what"]) // sequence: { "i don't know": "third" } * * // Example to show handling of non-string keys * Lazy({1: 2, true: false}).omit([1, true]) // sequence: { "1": 2, "true": false } */ ObjectLikeSequence.prototype.omit = function omit(properties) { return new OmitSequence(this, properties); }; /** * @constructor */ function OmitSequence(parent, properties) { this.parent = parent; this.properties = properties; } OmitSequence.prototype = Object.create(ObjectLikeSequence.prototype); OmitSequence.prototype.get = function get(key) { return arrayContains(this.properties, key) ? undefined : this.parent.get(key); }; OmitSequence.prototype.each = function each(fn) { var inArray = arrayContains, properties = this.properties; return this.parent.each(function(value, key) { if (!inArray(properties, key)) { return fn(value, key); } }); }; /** * Maps the key/value pairs in this sequence to arrays. * * @public * @aka toArray * @returns {Sequence} An sequence of `[key, value]` pairs. * * @examples * var colorCodes = { * red: "#f00", * green: "#0f0", * blue: "#00f" * }; * * Lazy(colorCodes).pairs() // sequence: [["red", "#f00"], ["green", "#0f0"], ["blue", "#00f"]] */ ObjectLikeSequence.prototype.pairs = function pairs() { return this.map(function(v, k) { return [k, v]; }); }; /** * Creates an array from the key/value pairs in this sequence. * * @public * @returns {Array} An array of `[key, value]` elements. * * @examples * var colorCodes = { * red: "#f00", * green: "#0f0", * blue: "#00f" * }; * * Lazy(colorCodes).toArray() // => [["red", "#f00"], ["green", "#0f0"], ["blue", "#00f"]] */ ObjectLikeSequence.prototype.toArray = function toArray() { return this.pairs().toArray(); }; /** * Creates an object with the key/value pairs from this sequence. * * @public * @returns {Object} An object with the same key/value pairs as this sequence. * * @examples * var colorCodes = { * red: "#f00", * green: "#0f0", * blue: "#00f" * }; * * Lazy(colorCodes).toObject() // => { red: "#f00", green: "#0f0", blue: "#00f" } */ ObjectLikeSequence.prototype.toObject = function toObject() { return this.reduce(function(object, value, key) { object[key] = value; return object; }, {}); }; // Now that we've fully initialized the ObjectLikeSequence prototype, we can // actually set the prototypes for GroupedSequence, IndexedSequence, and // CountedSequence. GroupedSequence.prototype = Object.create(ObjectLikeSequence.prototype); /** * @examples * var objects = [{a: 'x'}, {a: 'x'}]; * * Lazy(objects).groupBy('a') // sequence: {x: [{a: 'x'}, {a: 'x'}]} * Lazy(objects).groupBy('a').each(Lazy.noop) // true */ GroupedSequence.prototype.each = function each(fn) { var keyFn = createCallback(this.keyFn), valFn = createCallback(this.valFn), result; result = this.parent.reduce(function(grouped,e) { var key = keyFn(e), val = valFn(e); if (!isArray(grouped[key])) { grouped[key] = [val]; } else { grouped[key].push(val); } return grouped; },{}); return transform(function(grouped) { for (var key in grouped) { if (fn(grouped[key], key) === false) { return false; } } return true; }, result); }; IndexedSequence.prototype = Object.create(ObjectLikeSequence.prototype); IndexedSequence.prototype.each = function each(fn) { var keyFn = createCallback(this.keyFn), valFn = createCallback(this.valFn), indexed = {}; return this.parent.each(function(e) { var key = keyFn(e), val = valFn(e); if (!indexed[key]) { indexed[key] = val; return fn(val, key); } }); }; CountedSequence.prototype = Object.create(ObjectLikeSequence.prototype); CountedSequence.prototype.each = function each(fn) { var keyFn = createCallback(this.keyFn), counted = {}; this.parent.each(function(e) { var key = keyFn(e); if (!counted[key]) { counted[key] = 1; } else { counted[key] += 1; } }); for (var key in counted) { if (fn(counted[key], key) === false) { return false; } } return true; }; /** * Watches for all changes to a specified property (or properties) of an * object and produces a sequence whose elements have the properties * `{ property, value }` indicating which property changed and what it was * changed to. * * Note that this method **only works on directly wrapped objects**; it will * *not* work on any arbitrary {@link ObjectLikeSequence}. * * @public * @param {(string|Array)=} propertyNames A property name or array of property * names to watch. If this parameter is `undefined`, all of the object's * current (enumerable) properties will be watched. * @returns {Sequence} A sequence comprising `{ property, value }` objects * describing each change to the specified property/properties. * * @examples * var obj = {}, * changes = []; * * Lazy(obj).watch('foo').each(function(change) { * changes.push(change); * }); * * obj.foo = 1; * obj.bar = 2; * obj.foo = 3; * * obj.foo; // => 3 * changes; // => [{ property: 'foo', value: 1 }, { property: 'foo', value: 3 }] */ ObjectLikeSequence.prototype.watch = function watch(propertyNames) { throw new Error('You can only call #watch on a directly wrapped object.'); }; /** * @constructor */ function ObjectWrapper(source) { this.source = source; } ObjectWrapper.prototype = Object.create(ObjectLikeSequence.prototype); ObjectWrapper.prototype.root = function root() { return this; }; ObjectWrapper.prototype.isAsync = function isAsync() { return false; }; ObjectWrapper.prototype.get = function get(key) { return this.source[key]; }; ObjectWrapper.prototype.each = function each(fn) { var source = this.source, keys = source ? Object.keys(source) : [], length = keys.length, key, index; for (index = 0; index < length; ++index) { key = keys[index]; if (fn(source[key], key) === false) { return false; } } return true; }; /** * A `StringLikeSequence` represents a sequence of characters. * * The initial sequence you get by wrapping a string with `Lazy(string)` is a * `StringLikeSequence`. * * All methods of `StringLikeSequence` that conceptually should return * something like a string return another `StringLikeSequence`. * * @public * @constructor * * @examples * function upcase(str) { return str.toUpperCase(); } * * Lazy('foo') // instanceof Lazy.StringLikeSequence * Lazy('foo').toUpperCase() // instanceof Lazy.StringLikeSequence * Lazy('foo').reverse() // instanceof Lazy.StringLikeSequence * Lazy('foo').take(2) // instanceof Lazy.StringLikeSequence * Lazy('foo').drop(1) // instanceof Lazy.StringLikeSequence * Lazy('foo').substring(1) // instanceof Lazy.StringLikeSequence * * // Note that `map` does not create a `StringLikeSequence` because there's * // no guarantee the mapping function will return characters. In the event * // you do want to map a string onto a string-like sequence, use * // `mapString`: * Lazy('foo').map(Lazy.identity) // instanceof Lazy.ArrayLikeSequence * Lazy('foo').mapString(Lazy.identity) // instanceof Lazy.StringLikeSequence */ function StringLikeSequence() {} StringLikeSequence.prototype = Object.create(ArrayLikeSequence.prototype); /** * Create a new constructor function for a type inheriting from * `StringLikeSequence`. * * @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 `StringLikeSequence` prototype so that it can be chained with any other * methods that return string-like sequences. * @param {Object} overrides An object containing function overrides for this * new sequence type. Has the same requirements as * {@link ArrayLikeSequence.define}. * @returns {Function} A constructor for a new type inheriting from * `StringLikeSequence`. * * @examples * Lazy.StringLikeSequence.define("zomg", { * length: function() { * return this.parent.length() + "!!ZOMG!!!1".length; * }, * * get: function(i) { * if (i < this.parent.length()) { * return this.parent.get(i); * } * return "!!ZOMG!!!1".charAt(i - this.parent.length()); * } * }); * * Lazy('foo').zomg() // sequence: "foo!!ZOMG!!!1" */ StringLikeSequence.define = function define(methodName, overrides) { if (!overrides || typeof overrides.get !== 'function') { throw new Error("A custom string-like sequence must implement *at least* get!"); } return defineSequenceType(StringLikeSequence, methodName, overrides); }; StringLikeSequence.prototype.value = function value() { return this.toString(); }; /** * Returns an {@link IndexedIterator} that will step over each character in this * sequence one by one. * * @returns {IndexedIterator} The iterator. */ StringLikeSequence.prototype.getIterator = function getIterator() { return new CharIterator(this); }; /** * @constructor */ function CharIterator(source) { this.source = Lazy(source); this.index = -1; } CharIterator.prototype.current = function current() { return this.source.charAt(this.index); }; CharIterator.prototype.moveNext = function moveNext() { return (++this.index < this.source.length()); }; /** * Returns the character at the given index of this sequence, or the empty * string if the specified index lies outside the bounds of the sequence. * * @public * @param {number} i The index of this sequence. * @returns {string} The character at the specified index. * * @examples * Lazy("foo").charAt(0) // => "f" * Lazy("foo").charAt(-1) // => "" * Lazy("foo").charAt(10) // => "" */ StringLikeSequence.prototype.charAt = function charAt(i) { return this.get(i); }; /** * Returns the character code at the given index of this sequence, or `NaN` if * the index lies outside the bounds of the sequence. * * @public * @param {number} i The index of the character whose character code you want. * @returns {number} The character code. * * @examples * Lazy("abc").charCodeAt(0) // => 97 * Lazy("abc").charCodeAt(-1) // => NaN * Lazy("abc").charCodeAt(10) // => NaN */ StringLikeSequence.prototype.charCodeAt = function charCodeAt(i) { var char = this.charAt(i); if (!char) { return NaN; } return char.charCodeAt(0); }; /** * Returns a {@link StringLikeSequence} comprising the characters from *this* * sequence starting at `start` and ending at `stop` (exclusive), or---if * `stop` is `undefined`, including the rest of the sequence. * * @public * @param {number} start The index where this sequence should begin. * @param {number=} stop The index (exclusive) where this sequence should end. * @returns {StringLikeSequence} The new sequence. * * @examples * Lazy("foo").substring(1) // sequence: "oo" * Lazy("foo").substring(-1) // sequence: "foo" * Lazy("hello").substring(1, 3) // sequence: "el" * Lazy("hello").substring(1, 9) // sequence: "ello" * Lazy("foo").substring(0, 0) // sequence: "" * Lazy("foo").substring(3, 3) // sequence: "" */ StringLikeSequence.prototype.substring = function substring(start, stop) { return new StringSegment(this, start, stop); }; /** * @constructor */ function StringSegment(parent, start, stop) { this.parent = parent; this.start = Math.max(0, start); this.stop = stop; } StringSegment.prototype = Object.create(StringLikeSequence.prototype); StringSegment.prototype.get = function get(i) { return this.parent.get(i + this.start); }; StringSegment.prototype.length = function length() { return (typeof this.stop === "number" ? this.stop : this.parent.length()) - this.start; }; /** * An optimized version of {@link Sequence#first} that returns another * {@link StringLikeSequence} (or just the first character, if `count` is * undefined). * * @public * @examples * Lazy('foo').first() // => 'f' * Lazy('fo').first(2) // sequence: 'fo' * Lazy('foo').first(10) // sequence: 'foo' * Lazy('foo').toUpperCase().first() // => 'F' * Lazy('foo').toUpperCase().first(2) // sequence: 'FO' */ StringLikeSequence.prototype.first = function first(count) { if (typeof count === "undefined") { return this.charAt(0); } return this.substring(0, count); }; /** * An optimized version of {@link Sequence#last} that returns another * {@link StringLikeSequence} (or just the last character, if `count` is * undefined). * * @public * @examples * Lazy('foo').last() // => 'o' * Lazy('foo').last(2) // sequence: 'oo' * Lazy('foo').last(10) // sequence: 'foo' * Lazy('foo').toUpperCase().last() // => 'O' * Lazy('foo').toUpperCase().last(2) // sequence: 'OO' */ StringLikeSequence.prototype.last = function last(count) { if (typeof count === "undefined") { return this.charAt(this.length() - 1); } return this.substring(this.length() - count); }; StringLikeSequence.prototype.drop = function drop(count) { return this.substring(count); }; /** * Finds the index of the first occurrence of the given substring within this * sequence, starting from the specified index (or the beginning of the * sequence). * * @public * @param {string} substring The substring to search for. * @param {number=} startIndex The index from which to start the search. * @returns {number} The first index where the given substring is found, or * -1 if it isn't in the sequence. * * @examples * Lazy('canal').indexOf('a') // => 1 * Lazy('canal').indexOf('a', 2) // => 3 * Lazy('canal').indexOf('ana') // => 1 * Lazy('canal').indexOf('andy') // => -1 * Lazy('canal').indexOf('x') // => -1 */ StringLikeSequence.prototype.indexOf = function indexOf(substring, startIndex) { return this.toString().indexOf(substring, startIndex); }; /** * Finds the index of the last occurrence of the given substring within this * sequence, starting from the specified index (or the end of the sequence) * and working backwards. * * @public * @param {string} substring The substring to search for. * @param {number=} startIndex The index from which to start the search. * @returns {number} The last index where the given substring is found, or * -1 if it isn't in the sequence. * * @examples * Lazy('canal').lastIndexOf('a') // => 3 * Lazy('canal').lastIndexOf('a', 2) // => 1 * Lazy('canal').lastIndexOf('ana') // => 1 * Lazy('canal').lastIndexOf('andy') // => -1 * Lazy('canal').lastIndexOf('x') // => -1 */ StringLikeSequence.prototype.lastIndexOf = function lastIndexOf(substring, startIndex) { return this.toString().lastIndexOf(substring, startIndex); }; /** * Checks if this sequence contains a given substring. * * @public * @param {string} substring The substring to check for. * @returns {boolean} Whether or not this sequence contains `substring`. * * @examples * Lazy('hello').contains('ell') // => true * Lazy('hello').contains('') // => true * Lazy('hello').contains('abc') // => false */ StringLikeSequence.prototype.contains = function contains(substring) { return this.indexOf(substring) !== -1; }; /** * Checks if this sequence ends with a given suffix. * * @public * @param {string} suffix The suffix to check for. * @returns {boolean} Whether or not this sequence ends with `suffix`. * * @examples * Lazy('foo').endsWith('oo') // => true * Lazy('foo').endsWith('') // => true * Lazy('foo').endsWith('abc') // => false */ StringLikeSequence.prototype.endsWith = function endsWith(suffix) { return this.substring(this.length() - suffix.length).toString() === suffix; }; /** * Checks if this sequence starts with a given prefix. * * @public * @param {string} prefix The prefix to check for. * @returns {boolean} Whether or not this sequence starts with `prefix`. * * @examples * Lazy('foo').startsWith('fo') // => true * Lazy('foo').startsWith('') // => true * Lazy('foo').startsWith('abc') // => false */ StringLikeSequence.prototype.startsWith = function startsWith(prefix) { return this.substring(0, prefix.length).toString() === prefix; }; /** * Converts all of the characters in this string to uppercase. * * @public * @returns {StringLikeSequence} A new sequence with the same characters as * this sequence, all uppercase. * * @examples * function nextLetter(a) { * return String.fromCharCode(a.charCodeAt(0) + 1); * } * * Lazy('foo').toUpperCase() // sequence: 'FOO' * Lazy('foo').substring(1).toUpperCase() // sequence: 'OO' * Lazy('abc').mapString(nextLetter).toUpperCase() // sequence: 'BCD' */ StringLikeSequence.prototype.toUpperCase = function toUpperCase() { return this.mapString(function(char) { return char.toUpperCase(); }); }; /** * Converts all of the characters in this string to lowercase. * * @public * @returns {StringLikeSequence} A new sequence with the same characters as * this sequence, all lowercase. * * @examples * function nextLetter(a) { * return String.fromCharCode(a.charCodeAt(0) + 1); * } * * Lazy('FOO').toLowerCase() // sequence: 'foo' * Lazy('FOO').substring(1).toLowerCase() // sequence: 'oo' * Lazy('ABC').mapString(nextLetter).toLowerCase() // sequence: 'bcd' */ StringLikeSequence.prototype.toLowerCase = function toLowerCase() { return this.mapString(function(char) { return char.toLowerCase(); }); }; /** * Maps the characters of this sequence onto a new {@link StringLikeSequence}. * * @public * @param {Function} mapFn The function used to map characters from this * sequence onto the new sequence. * @returns {StringLikeSequence} The new sequence. * * @examples * function upcase(char) { return char.toUpperCase(); } * * Lazy("foo").mapString(upcase) // sequence: "FOO" * Lazy("foo").mapString(upcase).charAt(0) // => "F" * Lazy("foo").mapString(upcase).charCodeAt(0) // => 70 * Lazy("foo").mapString(upcase).substring(1) // sequence: "OO" */ StringLikeSequence.prototype.mapString = function mapString(mapFn) { return new MappedStringLikeSequence(this, mapFn); }; /** * @constructor */ function MappedStringLikeSequence(parent, mapFn) { this.parent = parent; this.mapFn = mapFn; } MappedStringLikeSequence.prototype = Object.create(StringLikeSequence.prototype); MappedStringLikeSequence.prototype.get = IndexedMappedSequence.prototype.get; MappedStringLikeSequence.prototype.length = IndexedMappedSequence.prototype.length; /** * Returns a copy of this sequence that reads back to front. * * @public * * @examples * Lazy("abcdefg").reverse() // sequence: "gfedcba" */ StringLikeSequence.prototype.reverse = function reverse() { return new ReversedStringLikeSequence(this); }; /** * @constructor */ function ReversedStringLikeSequence(parent) { this.parent = parent; } ReversedStringLikeSequence.prototype = Object.create(StringLikeSequence.prototype); ReversedStringLikeSequence.prototype.get = IndexedReversedSequence.prototype.get; ReversedStringLikeSequence.prototype.length = IndexedReversedSequence.prototype.length; StringLikeSequence.prototype.toString = function toString() { return this.join(""); }; /** * Creates a {@link Sequence} comprising all of the matches for the specified * pattern in the underlying string. * * @public * @param {RegExp} pattern The pattern to match. * @returns {Sequence} A sequence of all the matches. * * @examples * Lazy("abracadabra").match(/a[bcd]/) // sequence: ["ab", "ac", "ad", "ab"] * Lazy("fee fi fo fum").match(/\w+/) // sequence: ["fee", "fi", "fo", "fum"] * Lazy("hello").match(/xyz/) // sequence: [] */ StringLikeSequence.prototype.match = function match(pattern) { return new StringMatchSequence(this, pattern); }; /** * @constructor */ function StringMatchSequence(parent, pattern) { this.parent = parent; this.pattern = pattern; } StringMatchSequence.prototype = Object.create(Sequence.prototype); StringMatchSequence.prototype.getIterator = function getIterator() { return new StringMatchIterator(this.parent.toString(), this.pattern); }; /** * @constructor */ function StringMatchIterator(source, pattern) { this.source = source; this.pattern = cloneRegex(pattern); } StringMatchIterator.prototype.current = function current() { return this.match[0]; }; StringMatchIterator.prototype.moveNext = function moveNext() { return !!(this.match = this.pattern.exec(this.source)); }; /** * Creates a {@link Sequence} comprising all of the substrings of this string * separated by the given delimiter, which can be either a string or a regular * expression. * * @public * @param {string|RegExp} delimiter The delimiter to use for recognizing * substrings. * @returns {Sequence} A sequence of all the substrings separated by the given * delimiter. * * @examples * Lazy("foo").split("") // sequence: ["f", "o", "o"] * Lazy("yo dawg").split(" ") // sequence: ["yo", "dawg"] * Lazy("bah bah\tblack sheep").split(/\s+/) // sequence: ["bah", "bah", "black", "sheep"] */ StringLikeSequence.prototype.split = function split(delimiter) { return new SplitStringSequence(this, delimiter); }; /** * @constructor */ function SplitStringSequence(parent, pattern) { this.parent = parent; this.pattern = pattern; } SplitStringSequence.prototype = Object.create(Sequence.prototype); SplitStringSequence.prototype.getIterator = function getIterator() { var source = this.parent.toString(); if (this.pattern instanceof RegExp) { if (this.pattern.source === "" || this.pattern.source === "(?:)") { return new CharIterator(source); } else { return new SplitWithRegExpIterator(source, this.pattern); } } else if (this.pattern === "") { return new CharIterator(source); } else { return new SplitWithStringIterator(source, this.pattern); } }; /** * @constructor */ function SplitWithRegExpIterator(source, pattern) { this.source = source; this.pattern = cloneRegex(pattern); } SplitWithRegExpIterator.prototype.current = function current() { return this.source.substring(this.start, this.end); }; SplitWithRegExpIterator.prototype.moveNext = function moveNext() { if (!this.pattern) { return false; } var match = this.pattern.exec(this.source); if (match) { this.start = this.nextStart ? this.nextStart : 0; this.end = match.index; this.nextStart = match.index + match[0].length; return true; } else if (this.pattern) { this.start = this.nextStart; this.end = undefined; this.nextStart = undefined; this.pattern = undefined; return true; } return false; }; /** * @constructor */ function SplitWithStringIterator(source, delimiter) { this.source = source; this.delimiter = delimiter; } SplitWithStringIterator.prototype.current = function current() { return this.source.substring(this.leftIndex, this.rightIndex); }; SplitWithStringIterator.prototype.moveNext = function moveNext() { if (!this.finished) { this.leftIndex = typeof this.leftIndex !== "undefined" ? this.rightIndex + this.delimiter.length : 0; this.rightIndex = this.source.indexOf(this.delimiter, this.leftIndex); } if (this.rightIndex === -1) { this.finished = true; this.rightIndex = undefined; return true; } return !this.finished; }; /** * Wraps a string exposing {@link #match} and {@link #split} methods that return * {@link Sequence} objects instead of arrays, improving on the efficiency of * JavaScript's built-in `String#split` and `String.match` methods and * supporting asynchronous iteration. * * @param {string} source The string to wrap. * @constructor */ function StringWrapper(source) { this.source = source; } StringWrapper.prototype = Object.create(StringLikeSequence.prototype); StringWrapper.prototype.root = function root() { return this; }; StringWrapper.prototype.isAsync = function isAsync() { return false; }; StringWrapper.prototype.get = function get(i) { return this.source.charAt(i); }; StringWrapper.prototype.length = function length() { return this.source.length; }; StringWrapper.prototype.toString = function toString() { return this.source; }; /** * A `GeneratedSequence` does not wrap an in-memory collection but rather * determines its elements on-the-fly during iteration according to a generator * function. * * You create a `GeneratedSequence` by calling {@link Lazy.generate}. * * @public * @constructor * @param {function(number):*} generatorFn A function which accepts an index * and returns a value for the element at that position in the sequence. * @param {number=} length The length of the sequence. If this argument is * omitted, the sequence will go on forever. */ function GeneratedSequence(generatorFn, length) { this.get = generatorFn; this.fixedLength = length; } GeneratedSequence.prototype = Object.create(Sequence.prototype); GeneratedSequence.prototype.isAsync = function isAsync() { return false; }; /** * Returns the length of this sequence. * * @public * @returns {number} The length, or `undefined` if this is an indefinite * sequence. */ GeneratedSequence.prototype.length = function length() { return this.fixedLength; }; /** * Iterates over the sequence produced by invoking this sequence's generator * function up to its specified length, or, if length is `undefined`, * indefinitely (in which case the sequence will go on forever--you would need * to call, e.g., {@link Sequence#take} to limit iteration). * * @public * @param {Function} fn The function to call on each output from the generator * function. */ GeneratedSequence.prototype.each = function each(fn) { var generatorFn = this.get, length = this.fixedLength, i = 0; while (typeof length === "undefined" || i < length) { if (fn(generatorFn(i), i++) === false) { return false; } } return true; }; GeneratedSequence.prototype.getIterator = function getIterator() { return new GeneratedIterator(this); }; /** * Iterates over a generated sequence. (This allows generated sequences to be * iterated asynchronously.) * * @param {GeneratedSequence} sequence The generated sequence to iterate over. * @constructor */ function GeneratedIterator(sequence) { this.sequence = sequence; this.index = 0; this.currentValue = null; } GeneratedIterator.prototype.current = function current() { return this.currentValue; }; GeneratedIterator.prototype.moveNext = function moveNext() { var sequence = this.sequence; if (typeof sequence.fixedLength === "number" && this.index >= sequence.fixedLength) { return false; } this.currentValue = sequence.get(this.index++); return true; }; /** * An `AsyncSequence` iterates over its elements asynchronously when * {@link #each} is called. * * You get an `AsyncSequence` by calling {@link Sequence#async} on any * sequence. Note that some sequence types may not support asynchronous * iteration. * * Returning values * ---------------- * * Because of its asynchronous nature, an `AsyncSequence` cannot be used in the * same way as other sequences for functions that return values directly (e.g., * `reduce`, `max`, `any`, even `toArray`). * * Instead, these methods return an `AsyncHandle` whose `onComplete` method * accepts a callback that will be called with the final result once iteration * has finished. * * Defining custom asynchronous sequences * -------------------------------------- * * There are plenty of ways to define an asynchronous sequence. Here's one. * * 1. First, implement an {@link Iterator}. This is an object whose prototype * has the methods {@link Iterator#moveNext} (which returns a `boolean`) and * {@link current} (which returns the current value). * 2. Next, create a simple wrapper that inherits from `AsyncSequence`, whose * `getIterator` function returns an instance of the iterator type you just * defined. * * The default implementation for {@link #each} on an `AsyncSequence` is to * create an iterator and then asynchronously call {@link Iterator#moveNext} * (using `setImmediate`, if available, otherwise `setTimeout`) until the iterator * can't move ahead any more. * * @public * @constructor * @param {Sequence} parent A {@link Sequence} to wrap, to expose asynchronous * iteration. * @param {number=} interval How many milliseconds should elapse between each * element when iterating over this sequence. Note that this interval * applies even to the first value in the sequence; i.e., when calling * each(), this much time will elapse before the first element is * iterated. * * If this argument is omitted, asynchronous iteration will be executed * as fast as possible. */ function AsyncSequence(parent, interval) { if (parent instanceof AsyncSequence) { throw new Error("Sequence is already asynchronous!"); } this.parent = parent; this.interval = interval; this.onNextCallback = getOnNextCallback(interval); this.cancelCallback = getCancelCallback(interval); } AsyncSequence.prototype = Object.create(Sequence.prototype); AsyncSequence.prototype.isAsync = function isAsync() { return true; }; /** * Throws an exception. You cannot manually iterate over an asynchronous * sequence. * * @public * @example * Lazy([1, 2, 3]).async().getIterator() // throws */ AsyncSequence.prototype.getIterator = function getIterator() { throw new Error('An AsyncSequence does not support synchronous iteration.'); }; /** * An asynchronous version of {@link Sequence#each}. * * @public * @param {Function} fn The function to invoke asynchronously on each element in * the sequence one by one. * @returns {AsyncHandle} An {@link AsyncHandle} providing the ability to * cancel the asynchronous iteration (by calling `cancel()`) as well as * supply callback(s) for when an error is encountered (`onError`) or when * iteration is complete (`onComplete`). */ AsyncSequence.prototype.each = function each(fn) { var iterator = this.parent.getIterator(), onNextCallback = this.onNextCallback, cancelCallback = this.cancelCallback, i = 0; var handle = new AsyncHandle(function cancel() { if (cancellationId) { cancelCallback(cancellationId); } }); var cancellationId = onNextCallback(function iterate() { cancellationId = null; try { if (iterator.moveNext() && fn(iterator.current(), i++) !== false) { cancellationId = onNextCallback(iterate); } else { handle._resolve(); } } catch (e) { handle._reject(e); } }); return handle; }; /** * An `AsyncHandle` provides a [Promises/A+](http://promises-aplus.github.io/promises-spec/) * compliant interface for an {@link AsyncSequence} that is currently (or was) * iterating over its elements. * * In addition to behaving as a promise, an `AsyncHandle` provides the ability * to {@link AsyncHandle#cancel} iteration (if `cancelFn` is provided) * and also offers convenient {@link AsyncHandle#onComplete} and * {@link AsyncHandle#onError} methods to attach listeners for when iteration * is complete or an error is thrown during iteration. * * @public * @param {Function} cancelFn A function to cancel asynchronous iteration. * This is passed in to support different cancellation mechanisms for * different forms of asynchronous sequences (e.g., timeout-based * sequences, sequences based on I/O, etc.). * @constructor * * @example * // Create a sequence of 100,000 random numbers, in chunks of 100. * var sequence = Lazy.generate(Math.random) * .chunk(100) * .async() * .take(1000); * * // Reduce-style operations -- i.e., operations that return a *value* (as * // opposed to a *sequence*) -- return an AsyncHandle for async sequences. * var handle = sequence.toArray(); * * handle.onComplete(function(array) { * // Do something w/ 1,000-element array. * }); * * // Since an AsyncHandle is a promise, you can also use it to create * // subsequent promises using `then` (see the Promises/A+ spec for more * // info). * var flattened = handle.then(function(array) { * return Lazy(array).flatten(); * }); */ function AsyncHandle(cancelFn) { this.resolveListeners = []; this.rejectListeners = []; this.state = PENDING; this.cancelFn = cancelFn; } // Async handle states var PENDING = 1, RESOLVED = 2, REJECTED = 3; AsyncHandle.prototype.then = function then(onFulfilled, onRejected) { var promise = new AsyncHandle(this.cancelFn); this.resolveListeners.push(function(value) { try { if (typeof onFulfilled !== 'function') { resolve(promise, value); return; } resolve(promise, onFulfilled(value)); } catch (e) { promise._reject(e); } }); this.rejectListeners.push(function(reason) { try { if (typeof onRejected !== 'function') { promise._reject(reason); return; } resolve(promise, onRejected(reason)); } catch (e) { promise._reject(e); } }); if (this.state === RESOLVED) { this._resolve(this.value); } if (this.state === REJECTED) { this._reject(this.reason); } return promise; }; AsyncHandle.prototype._resolve = function _resolve(value) { if (this.state === REJECTED) { return; } if (this.state === PENDING) { this.state = RESOLVED; this.value = value; } consumeListeners(this.resolveListeners, this.value); }; AsyncHandle.prototype._reject = function _reject(reason) { if (this.state === RESOLVED) { return; } if (this.state === PENDING) { this.state = REJECTED; this.reason = reason; } consumeListeners(this.rejectListeners, this.reason); }; /** * Cancels asynchronous iteration. * * @public */ AsyncHandle.prototype.cancel = function cancel() { if (this.cancelFn) { this.cancelFn(); this.cancelFn = null; this._resolve(false); } }; /** * Updates the handle with a callback to execute when iteration is completed. * * @public * @param {Function} callback The function to call when the asynchronous * iteration is completed. * @return {AsyncHandle} A reference to the handle (for chaining). */ AsyncHandle.prototype.onComplete = function onComplete(callback) { this.resolveListeners.push(callback); return this; }; /** * Updates the handle with a callback to execute if/when any error is * encountered during asynchronous iteration. * * @public * @param {Function} callback The function to call, with any associated error * object, when an error occurs. * @return {AsyncHandle} A reference to the handle (for chaining). */ AsyncHandle.prototype.onError = function onError(callback) { this.rejectListeners.push(callback); return this; }; /** * Promise resolution procedure: * http://promises-aplus.github.io/promises-spec/#the_promise_resolution_procedure */ function resolve(promise, x) { if (promise === x) { promise._reject(new TypeError('Cannot resolve a promise to itself')); return; } if (x instanceof AsyncHandle) { x.then( function(value) { resolve(promise, value); }, function(reason) { promise._reject(reason); } ); return; } var then; try { then = (/function|object/).test(typeof x) && x != null && x.then; } catch (e) { promise._reject(e); return; } var thenableState = PENDING; if (typeof then === 'function') { try { then.call( x, function resolvePromise(value) { if (thenableState !== PENDING) { return; } thenableState = RESOLVED; resolve(promise, value); }, function rejectPromise(reason) { if (thenableState !== PENDING) { return; } thenableState = REJECTED; promise._reject(reason); } ); } catch (e) { if (thenableState !== PENDING) { return; } promise._reject(e); } return; } promise._resolve(x); } function consumeListeners(listeners, value, callback) { callback || (callback = getOnNextCallback()); callback(function() { if (listeners.length > 0) { listeners.shift()(value); consumeListeners(listeners, value, callback); } }); } function getOnNextCallback(interval) { if (typeof interval === "undefined") { if (typeof setImmediate === "function") { return setImmediate; } } interval = interval || 0; return function(fn) { return setTimeout(fn, interval); }; } function getCancelCallback(interval) { if (typeof interval === "undefined") { if (typeof clearImmediate === "function") { return clearImmediate; } } return clearTimeout; } /** * Transform a value, whether the value is retrieved asynchronously or directly. * * @private * @param {Function} fn The function that transforms the value. * @param {*} value The value to be transformed. This can be an {@link AsyncHandle} when the value * is retrieved asynchronously, otherwise it can be anything. * @returns {*} An {@link AsyncHandle} when `value` is also an {@link AsyncHandle}, otherwise * whatever `fn` resulted in. */ function transform(fn, value) { if (value instanceof AsyncHandle) { return value.then(function() { fn(value); }); } return fn(value); } /** * An async version of {@link Sequence#reverse}. */ AsyncSequence.prototype.reverse = function reverse() { return this.parent.reverse().async(); }; /** * A version of {@link Sequence#find} which returns an {@link AsyncHandle}. * * @public * @param {Function} predicate A function to call on (potentially) every element * in the sequence. * @returns {AsyncHandle} An {@link AsyncHandle} (promise) which resolves to * the found element, once it is detected, or else `undefined`. */ AsyncSequence.prototype.find = function find(predicate) { var found; var handle = this.each(function(e, i) { if (predicate(e, i)) { found = e; return false; } }); return handle.then(function() { return found; }); }; /** * A version of {@link Sequence#indexOf} which returns an {@link AsyncHandle}. * * @public * @param {*} value The element to search for in the sequence. * @returns {AsyncHandle} An {@link AsyncHandle} (promise) which resolves to * the found index, once it is detected, or -1. */ AsyncSequence.prototype.indexOf = function indexOf(value) { var foundIndex = -1; var handle = this.each(function(e, i) { if (e === value) { foundIndex = i; return false; } }); return handle.then(function() { return foundIndex; }); }; /** * A version of {@link Sequence#contains} which returns an {@link AsyncHandle}. * * @public * @param {*} value The element to search for in the sequence. * @returns {AsyncHandle} An {@link AsyncHandle} (promise) which resolves to * either `true` or `false` to indicate whether the element was found. */ AsyncSequence.prototype.contains = function contains(value) { var found = false; var handle = this.each(function(e) { if (e === value) { found = true; return false; } }); return handle.then(function() { return found; }); }; /** * Just return the same sequence for `AsyncSequence#async` (I see no harm in this). */ AsyncSequence.prototype.async = function async() { return this; }; /** * See {@link ObjectLikeSequence#watch} for docs. */ ObjectWrapper.prototype.watch = function watch(propertyNames) { return new WatchedPropertySequence(this.source, propertyNames); }; function WatchedPropertySequence(object, propertyNames) { this.listeners = []; if (!propertyNames) { propertyNames = Lazy(object).keys().toArray(); } else if (!isArray(propertyNames)) { propertyNames = [propertyNames]; } var listeners = this.listeners, index = 0; Lazy(propertyNames).each(function(propertyName) { var propertyValue = object[propertyName]; Object.defineProperty(object, propertyName, { get: function() { return propertyValue; }, set: function(value) { for (var i = listeners.length - 1; i >= 0; --i) { if (listeners[i]({ property: propertyName, value: value }, index) === false) { listeners.splice(i, 1); } } propertyValue = value; ++index; } }); }); } WatchedPropertySequence.prototype = Object.create(AsyncSequence.prototype); WatchedPropertySequence.prototype.each = function each(fn) { this.listeners.push(fn); }; /** * A StreamLikeSequence comprises a sequence of 'chunks' of data, which are * typically multiline strings. * * @constructor */ function StreamLikeSequence() {} StreamLikeSequence.prototype = Object.create(AsyncSequence.prototype); StreamLikeSequence.prototype.isAsync = function isAsync() { return true; }; StreamLikeSequence.prototype.split = function split(delimiter) { return new SplitStreamSequence(this, delimiter); }; /** * @constructor */ function SplitStreamSequence(parent, delimiter) { this.parent = parent; this.delimiter = delimiter; this.each = this.getEachForDelimiter(delimiter); } SplitStreamSequence.prototype = Object.create(Sequence.prototype); SplitStreamSequence.prototype.getEachForDelimiter = function getEachForDelimiter(delimiter) { if (delimiter instanceof RegExp) { return this.regexEach; } return this.stringEach; }; SplitStreamSequence.prototype.regexEach = function each(fn) { var delimiter = cloneRegex(this.delimiter), buffer = '', start = 0, end, index = 0; var handle = this.parent.each(function(chunk) { buffer += chunk; var match; while (match = delimiter.exec(buffer)) { end = match.index; if (fn(buffer.substring(start, end), index++) === false) { return false; } start = end + match[0].length; } buffer = buffer.substring(start); start = 0; }); handle.onComplete(function() { if (buffer.length > 0) { fn(buffer, index++); } }); return handle; }; SplitStreamSequence.prototype.stringEach = function each(fn) { var delimiter = this.delimiter, pieceIndex = 0, buffer = '', bufferIndex = 0; var handle = this.parent.each(function(chunk) { buffer += chunk; var delimiterIndex; while ((delimiterIndex = buffer.indexOf(delimiter)) >= 0) { var piece = buffer.substr(0,delimiterIndex); buffer = buffer.substr(delimiterIndex+delimiter.length); if (fn(piece,pieceIndex++) === false) { return false; } } return true; }); handle.onComplete(function() { fn(buffer, pieceIndex++); }); return handle; }; StreamLikeSequence.prototype.lines = function lines() { return this.split("\n"); }; StreamLikeSequence.prototype.match = function match(pattern) { return new MatchedStreamSequence(this, pattern); }; /** * @constructor */ function MatchedStreamSequence(parent, pattern) { this.parent = parent; this.pattern = cloneRegex(pattern); } MatchedStreamSequence.prototype = Object.create(AsyncSequence.prototype); MatchedStreamSequence.prototype.each = function each(fn) { var pattern = this.pattern, done = false, i = 0; return this.parent.each(function(chunk) { Lazy(chunk).match(pattern).each(function(match) { if (fn(match, i++) === false) { done = true; return false; } }); return !done; }); }; /** * Defines a wrapper for custom {@link StreamLikeSequence}s. This is useful * if you want a way to handle a stream of events as a sequence, but you can't * use Lazy's existing interface (i.e., you're wrapping an object from a * library with its own custom events). * * This method defines a *factory*: that is, it produces a function that can * be used to wrap objects and return a {@link Sequence}. Hopefully the * example will make this clear. * * @public * @param {Function} initializer An initialization function called on objects * created by this factory. `this` will be bound to the created object, * which is an instance of {@link StreamLikeSequence}. Use `emit` to * generate data for the sequence. * @returns {Function} A function that creates a new {@link StreamLikeSequence}, * initializes it using the specified function, and returns it. * * @example * var factory = Lazy.createWrapper(function(eventSource) { * var sequence = this; * * eventSource.handleEvent(function(data) { * sequence.emit(data); * }); * }); * * var eventEmitter = { * triggerEvent: function(data) { * eventEmitter.eventHandler(data); * }, * handleEvent: function(handler) { * eventEmitter.eventHandler = handler; * }, * eventHandler: function() {} * }; * * var events = []; * * factory(eventEmitter).each(function(e) { * events.push(e); * }); * * eventEmitter.triggerEvent('foo'); * eventEmitter.triggerEvent('bar'); * * events // => ['foo', 'bar'] */ Lazy.createWrapper = function createWrapper(initializer) { var ctor = function() { this.listeners = []; }; ctor.prototype = Object.create(StreamLikeSequence.prototype); ctor.prototype.each = function(listener) { this.listeners.push(listener); }; ctor.prototype.emit = function(data) { var listeners = this.listeners; for (var len = listeners.length, i = len - 1; i >= 0; --i) { if (listeners[i](data) === false) { listeners.splice(i, 1); } } }; return function() { var sequence = new ctor(); initializer.apply(sequence, arguments); return sequence; }; }; /** * Creates a {@link GeneratedSequence} using the specified generator function * and (optionally) length. * * @public * @param {function(number):*} generatorFn The function used to generate the * sequence. This function accepts an index as a parameter and should return * a value for that index in the resulting sequence. * @param {number=} length The length of the sequence, for sequences with a * definite length. * @returns {GeneratedSequence} The generated sequence. * * @examples * var randomNumbers = Lazy.generate(Math.random); * var countingNumbers = Lazy.generate(function(i) { return i + 1; }, 5); * * randomNumbers // instanceof Lazy.GeneratedSequence * randomNumbers.length() // => undefined * countingNumbers // sequence: [1, 2, 3, 4, 5] * countingNumbers.length() // => 5 */ Lazy.generate = function generate(generatorFn, length) { return new GeneratedSequence(generatorFn, length); }; /** * Creates a sequence from a given starting value, up to a specified stopping * value, incrementing by a given step. Invalid values for any of these * arguments (e.g., a step of 0) result in an empty sequence. * * @public * @returns {GeneratedSequence} The sequence defined by the given ranges. * * @examples * Lazy.range(3) // sequence: [0, 1, 2] * Lazy.range(1, 4) // sequence: [1, 2, 3] * Lazy.range(2, 10, 2) // sequence: [2, 4, 6, 8] * Lazy.range(5, 1, 2) // sequence: [] * Lazy.range(5, 15, -2) // sequence: [] * Lazy.range(3, 10, 3) // sequence: [3, 6, 9] * Lazy.range(5, 2) // sequence: [5, 4, 3] * Lazy.range(7, 2, -2) // sequence: [7, 5, 3] * Lazy.range(3, 5, 0) // sequence: [] */ Lazy.range = function range() { var start = arguments.length > 1 ? arguments[0] : 0, stop = arguments.length > 1 ? arguments[1] : arguments[0], step = arguments.length > 2 && arguments[2]; if (step === false) { step = stop > start ? 1 : -1; } if (step === 0) { return Lazy([]); } return Lazy.generate(function(i) { return start + (step * i); }) .take(Math.ceil((stop - start) / step)); }; /** * Creates a sequence consisting of the given value repeated a specified number * of times. * * @public * @param {*} value The value to repeat. * @param {number=} count The number of times the value should be repeated in * the sequence. If this argument is omitted, the value will repeat forever. * @returns {GeneratedSequence} The sequence containing the repeated value. * * @examples * Lazy.repeat("hi", 3) // sequence: ["hi", "hi", "hi"] * Lazy.repeat("young") // instanceof Lazy.GeneratedSequence * Lazy.repeat("young").length() // => undefined * Lazy.repeat("young").take(3) // sequence: ["young", "young", "young"] */ Lazy.repeat = function repeat(value, count) { return Lazy.generate(function() { return value; }, count); }; Lazy.Sequence = Sequence; Lazy.ArrayLikeSequence = ArrayLikeSequence; Lazy.ObjectLikeSequence = ObjectLikeSequence; Lazy.StringLikeSequence = StringLikeSequence; Lazy.StreamLikeSequence = StreamLikeSequence; Lazy.GeneratedSequence = GeneratedSequence; Lazy.AsyncSequence = AsyncSequence; Lazy.AsyncHandle = AsyncHandle; /*** Useful utility methods ***/ /** * Creates a shallow copy of an array or object. * * @examples * var array = [1, 2, 3], clonedArray, * object = { foo: 1, bar: 2 }, clonedObject; * * clonedArray = Lazy.clone(array); // => [1, 2, 3] * clonedArray.push(4); // clonedArray == [1, 2, 3, 4] * array; // => [1, 2, 3] * * clonedObject = Lazy.clone(object); // => { foo: 1, bar: 2 } * clonedObject.baz = 3; // clonedObject == { foo: 1, bar: 2, baz: 3 } * object; // => { foo: 1, bar: 2 } */ Lazy.clone = function clone(target) { return Lazy(target).value(); }; /** * Marks a method as deprecated, so calling it will issue a console warning. */ Lazy.deprecate = function deprecate(message, fn) { return function() { console.warn(message); return fn.apply(this, arguments); }; }; var isArray = Array.isArray || function(x) { return x instanceof Array; }, arrayPop = Array.prototype.pop, arraySlice = Array.prototype.slice; /** * If you know what function currying is, then you know what this does. * * @param {Function} fn The function to curry. * @returns {Function} The curried function. * * @examples * function abc(a, b, c) { return [a, b, c]; } * var curried = Lazy.curry(abc); * * curried(1)(2)(3) // => [1, 2, 3] * curried(1, 2)(3) // => [1, 2, 3] * curried(1)(2, 3) // => [1, 2, 3] * curried(1, 2, 3) // => [1, 2, 3] * Lazy([1, 2, 3]).map(curried(1, 2)) // sequence: [[1, 2, 1], [1, 2, 2], [1, 2, 3]] */ function curry(fn, arity) { arity || (arity = fn.length); function curried(args) { if (args.length < arity) { return function() { return curried(args.concat(arraySlice.call(arguments, 0))); }; } return fn.apply(null, args); } return curried([]); } /** * Same as Lazy.curry, but... you know... from the right. * * @param {Function} fn The function to curry from the right. * @returns {Function} The curried-from-the-right function. * * @examples * function abc(a, b, c) { return [a, b, c]; } * var curriedRight = Lazy.curryRight(abc); * * curriedRight(3)(2)(1) // => [1, 2, 3] * curriedRight(2, 3)(1) // => [1, 2, 3] * curriedRight(3)(1, 2) // => [1, 2, 3] * curriedRight(1, 2, 3) // => [1, 2, 3] * Lazy([1, 2, 3]).map(curriedRight(3)) // sequence: [[1, 0, 3], [2, 1, 3], [3, 2, 3]] */ function curryRight(fn, arity) { arity || (arity = fn.length); function curriedRight(args) { if (args.length < arity) { return function() { return curriedRight(arraySlice.call(arguments, 0).concat(args)); }; } return fn.apply(null, args); } return curriedRight([]); } Lazy.curry = curry; Lazy.curryRight = curryRight; /** * Creates a callback... you know, Lo-Dash style. * * - for functions, just returns the function * - for strings, returns a pluck-style callback * - for objects, returns a where-style callback * * @param {Function|string|Object} callback A function, string, or object to * convert to a callback. * @param {*} defaultReturn If the callback is undefined, a default return * value to use for the function. * @returns {Function} The callback function. * * @examples * Lazy.createCallback(function() {}) // instanceof Function * Lazy.createCallback('foo') // instanceof Function * Lazy.createCallback('foo')({ foo: 'bar'}) // => 'bar' * Lazy.createCallback({ foo: 'bar' })({ foo: 'bar' }) // => true * Lazy.createCallback({ foo: 'bar' })({ foo: 'baz' }) // => false */ function createCallback(callback, defaultValue) { switch (typeof callback) { case "function": return callback; case "string": return function(e) { return e[callback]; }; case "object": return function(e) { return Lazy(callback).all(function(value, key) { return e[key] === value; }); }; case "undefined": return defaultValue ? function() { return defaultValue; } : Lazy.identity; default: throw new Error("Don't know how to make a callback from a " + typeof callback + "!"); } } Lazy.createCallback = createCallback; /** * Takes a function that returns a value for one argument and produces a * function that compares two arguments. * * @param {Function|string|Object} callback A function, string, or object to * convert to a callback using `createCallback`. * @returns {Function} A function that accepts two values and returns 1 if * the first is greater, -1 if the second is greater, or 0 if they are * equivalent. * * @examples * Lazy.createComparator('a')({ a: 1 }, { a: 2 }); // => -1 * Lazy.createComparator('a')({ a: 6 }, { a: 2 }); // => 1 * Lazy.createComparator('a')({ a: 1 }, { a: 1 }); // => 0 * Lazy.createComparator()(3, 5); // => -1 * Lazy.createComparator()(7, 5); // => 1 * Lazy.createComparator()(3, 3); // => 0 */ function createComparator(callback) { if (!callback) { return compare; } callback = createCallback(callback); return function(x, y) { return compare(callback(x), callback(y)); }; } Lazy.createComparator = createComparator; /** * Takes a function and returns a function with the same logic but the * arguments reversed. Only applies to functions w/ arity=2 as this is private * and I can do what I want. * * @private * @param {Function} fn The function to "reverse" * @returns {Function} The "reversed" function * * @examples * reverseArguments(function(x, y) { return x + y; })('a', 'b'); // => 'ba' */ function reverseArguments(fn) { return function(x, y) { return fn(y, x); }; } /** * Creates a Set containing the specified values. * * @param {...Array} values One or more array(s) of values used to populate the * set. * @returns {Set} A new set containing the values passed in. */ function createSet(values) { var set = new Set(); Lazy(values || []).flatten().each(function(e) { set.add(e); }); return set; } /** * Compares two elements for sorting purposes. * * @private * @param {*} x The left element to compare. * @param {*} y The right element to compare. * @returns {number} 1 if x > y, -1 if x < y, or 0 if x and y are equal. * * @examples * compare(1, 2) // => -1 * compare(1, 1) // => 0 * compare(2, 1) // => 1 * compare('a', 'b') // => -1 */ function compare(x, y) { if (x === y) { return 0; } return x > y ? 1 : -1; } /** * Iterates over every element in an array. * * @param {Array} array The array. * @param {Function} fn The function to call on every element, which can return * false to stop the iteration early. * @returns {boolean} True if every element in the entire sequence was iterated, * otherwise false. */ function forEach(array, fn) { var i = -1, len = array.length; while (++i < len) { if (fn(array[i], i) === false) { return false; } } return true; } function getFirst(sequence) { var result; sequence.each(function(e) { result = e; return false; }); return result; } /** * Checks if an element exists in an array. * * @private * @param {Array} array * @param {*} element * @returns {boolean} Whether or not the element exists in the array. * * @examples * arrayContains([1, 2], 2) // => true * arrayContains([1, 2], 3) // => false * arrayContains([undefined], undefined) // => true * arrayContains([NaN], NaN) // => true */ function arrayContains(array, element) { var i = -1, length = array.length; // Special handling for NaN if (element !== element) { while (++i < length) { if (array[i] !== array[i]) { return true; } } return false; } while (++i < length) { if (array[i] === element) { return true; } } return false; } /** * Checks if an element exists in an array before a given index. * * @private * @param {Array} array * @param {*} element * @param {number} index * @param {Function} keyFn * @returns {boolean} * * @examples * arrayContainsBefore([1, 2, 3], 3, 2) // => false * arrayContainsBefore([1, 2, 3], 3, 3) // => true */ function arrayContainsBefore(array, element, index, keyFn) { var i = -1; if (keyFn) { keyFn = createCallback(keyFn); while (++i < index) { if (keyFn(array[i]) === keyFn(element)) { return true; } } } else { while (++i < index) { if (array[i] === element) { return true; } } } return false; } /** * Swaps the elements at two specified positions of an array. * * @private * @param {Array} array * @param {number} i * @param {number} j * * @examples * var array = [1, 2, 3, 4, 5]; * * swap(array, 2, 3) // array == [1, 2, 4, 3, 5] */ function swap(array, i, j) { var temp = array[i]; array[i] = array[j]; array[j] = temp; } /** * "Clones" a regular expression (but makes it always global). * * @private * @param {RegExp|string} pattern * @returns {RegExp} */ function cloneRegex(pattern) { return eval("" + pattern + (!pattern.global ? "g" : "")); }; /** * A collection of unique elements. * * @private * @constructor * * @examples * var set = new Set(), * obj1 = {}, * obj2 = {}, * fn1 = function fn1() {}, * fn2 = function fn2() {}; * * set.add('foo') // => true * set.add('foo') // => false * set.add(1) // => true * set.add(1) // => false * set.add('1') // => true * set.add('1') // => false * set.add(obj1) // => true * set.add(obj1) // => false * set.add(obj2) // => true * set.add(fn1) // => true * set.add(fn2) // => true * set.add(fn2) // => false * set.contains('__proto__') // => false * set.add('__proto__') // => true * set.add('__proto__') // => false * set.contains('add') // => false * set.add('add') // => true * set.add('add') // => false * set.contains(undefined) // => false * set.add(undefined) // => true * set.contains(undefined) // => true * set.contains('undefined') // => false * set.add('undefined') // => true * set.contains('undefined') // => true * set.contains(NaN) // => false * set.add(NaN) // => true * set.contains(NaN) // => true * set.contains('NaN') // => false * set.add('NaN') // => true * set.contains('NaN') // => true * set.contains('@foo') // => false * set.add('@foo') // => true * set.contains('@foo') // => true */ function Set() { this.table = {}; this.objects = []; } /** * Attempts to add a unique value to the set. * * @param {*} value The value to add. * @returns {boolean} True if the value was added to the set (meaning an equal * value was not already present), or else false. */ Set.prototype.add = function add(value) { var table = this.table, type = typeof value, // only applies for strings firstChar, // only applies for objects objects; switch (type) { case "number": case "boolean": case "undefined": if (!table[value]) { table[value] = true; return true; } return false; case "string": // Essentially, escape the first character if it could possibly collide // with a number, boolean, or undefined (or a string that happens to start // with the escape character!), OR if it could override a special property // such as '__proto__' or 'constructor'. switch (value.charAt(0)) { case "_": // e.g., __proto__ case "f": // for 'false' case "t": // for 'true' case "c": // for 'constructor' case "u": // for 'undefined' case "@": // escaped case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "N": // for NaN value = "@" + value; } if (!table[value]) { table[value] = true; return true; } return false; default: // For objects and functions, we can't really do anything other than store // them in an array and do a linear search for reference equality. objects = this.objects; if (!arrayContains(objects, value)) { objects.push(value); return true; } return false; } }; /** * Checks whether the set contains a value. * * @param {*} value The value to check for. * @returns {boolean} True if the set contains the value, or else false. */ Set.prototype.contains = function contains(value) { var type = typeof value, // only applies for strings firstChar; switch (type) { case "number": case "boolean": case "undefined": return !!this.table[value]; case "string": // Essentially, escape the first character if it could possibly collide // with a number, boolean, or undefined (or a string that happens to start // with the escape character!), OR if it could override a special property // such as '__proto__' or 'constructor'. switch (value.charAt(0)) { case "_": // e.g., __proto__ case "f": // for 'false' case "t": // for 'true' case "c": // for 'constructor' case "u": // for 'undefined' case "@": // escaped case "0": case "1": case "2": case "3": case "4": case "5": case "6": case "7": case "8": case "9": case "N": // for NaN value = "@" + value; } return !!this.table[value]; default: // For objects and functions, we can't really do anything other than store // them in an array and do a linear search for reference equality. return arrayContains(this.objects, value); } }; /** * A "rolling" queue, with a fixed capacity. As items are added to the head, * excess items are dropped from the tail. * * @private * @constructor * * @examples * var queue = new Queue(3); * * queue.add(1).toArray() // => [1] * queue.add(2).toArray() // => [1, 2] * queue.add(3).toArray() // => [1, 2, 3] * queue.add(4).toArray() // => [2, 3, 4] * queue.add(5).add(6).toArray() // => [4, 5, 6] * queue.add(7).add(8).toArray() // => [6, 7, 8] * * // also want to check corner cases * new Queue(1).add('foo').add('bar').toArray() // => ['bar'] * new Queue(0).add('foo').toArray() // => [] * new Queue(-1) // throws * * @benchmarks * function populateQueue(count, capacity) { * var q = new Queue(capacity); * for (var i = 0; i < count; ++i) { * q.add(i); * } * } * * function populateArray(count, capacity) { * var arr = []; * for (var i = 0; i < count; ++i) { * if (arr.length === capacity) { arr.shift(); } * arr.push(i); * } * } * * populateQueue(100, 10); // populating a Queue * populateArray(100, 10); // populating an Array */ function Queue(capacity) { this.contents = new Array(capacity); this.start = 0; this.count = 0; } /** * Adds an item to the queue, and returns the queue. */ Queue.prototype.add = function add(element) { var contents = this.contents, capacity = contents.length, start = this.start; if (this.count === capacity) { contents[start] = element; this.start = (start + 1) % capacity; } else { contents[this.count++] = element; } return this; }; /** * Returns an array containing snapshot of the queue's contents. */ Queue.prototype.toArray = function toArray() { var contents = this.contents, start = this.start, count = this.count; var snapshot = contents.slice(start, start + count); if (snapshot.length < count) { snapshot = snapshot.concat(contents.slice(0, count - snapshot.length)); } return snapshot; }; /** * Shared base method for defining new sequence types. */ function defineSequenceType(base, name, overrides) { /** @constructor */ var ctor = function ctor() {}; // Make this type inherit from the specified base. ctor.prototype = new base(); // Attach overrides to the new sequence type's prototype. for (var override in overrides) { ctor.prototype[override] = overrides[override]; } // Define a factory method that sets the new sequence's parent to the caller // and (optionally) applies any additional initialization logic. // Expose this as a chainable method so that we can do: // Lazy(...).map(...).filter(...).blah(...); var factory = function factory() { var sequence = new ctor(); // Every sequence needs a reference to its parent in order to work. sequence.parent = this; // If a custom init function was supplied, call it now. if (sequence.init) { sequence.init.apply(sequence, arguments); } return sequence; }; var methodNames = typeof name === 'string' ? [name] : name; for (var i = 0; i < methodNames.length; ++i) { base.prototype[methodNames[i]] = factory; } return ctor; } return Lazy; });