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