-
-
Save zcaudate/ea5e317f4840f20ac065bd754c5cb3c0 to your computer and use it in GitHub Desktop.
Using ES6 Generator prototype model to implement lazy chaining. When you set the prototype of multiple Generator Functions to a common prototype you can chain them. Examples at the bottom. (this code currently only works in Firefox Aurora, but will eventually work in all JS engines).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const module = { exports: {} }; | |
let exports = module.exports; | |
const GeneratorFunction = function*(){}.constructor; | |
const iteratorSymbol = (typeof Symbol === "function" && Symbol.iterator) || "@@iterator"; | |
const MISSING = {}; | |
/** | |
* A generator function is always called as a constructor. If multiple | |
* generators share the same prototype, and you put generators on that shared | |
* prototype as methods, you can do chaining. | |
*/ | |
function* Chainable(iterable) { | |
if (iterable && typeof iterable[iteratorSymbol] === "function") { | |
yield* iterable; | |
} | |
} | |
module.exports = exports = Chainable; | |
// Generators don't set their "constructor" property like normal functions do. | |
Object.defineProperty(Chainable.prototype, "constructor", { | |
configurable: true, | |
writable: true, | |
value: Chainable | |
}); | |
/** | |
* Add a helper function. | |
* @param Function f | |
* The helper function. | |
* @return Function | |
* The helper function. | |
*/ | |
function helper(f) { | |
if (f instanceof GeneratorFunction) { | |
f.prototype = Chainable.prototype; | |
} | |
return f; | |
} | |
/** | |
* Export a function and set it to use the shared prototype. | |
*/ | |
function exportFunction(f) { | |
helper(f); | |
return exports[f.name] = f; | |
} | |
/** | |
* Export a function and also add a method version of it to the shared | |
* prototype. This allows it to be used to chain from other generators produced | |
* by the generator functions in this module. | |
* | |
* @see exportFunction | |
*/ | |
function exportMethod(f) { | |
exportFunction(f); | |
Chainable.prototype[f.name] = function(...args) { | |
return f(this, ...args); | |
}; | |
} | |
/** | |
* Get an iterator from an iterable. | |
* @param Iterable iterable | |
* The iterable to get the iterator from. | |
* @return Iterator | |
* The iterator for the iterable. | |
*/ | |
const iter = exportFunction(function iter(iterable) { | |
if (iterable && typeof iterable[iteratorSymbol] === "function") { | |
return iterable[iteratorSymbol](); | |
} | |
throw new TypeError("target is not iterable"); | |
}); | |
/** | |
* An infinite generator that counts up. | |
* | |
* @param Number [start] | |
* The number to start from. | |
* @param Number [step] | |
* The amount to change each iteration. | |
* @return Chainable | |
*/ | |
exportFunction(function* count(start = 0, step = 1) { | |
for (;;) { | |
yield start; | |
start += step; | |
} | |
}); | |
/** | |
* A generator that generates numbers in a range using a given step. Can be | |
* called with parameters [stop] or [start, stop] or [start, stop, step]. | |
* | |
* @param Number [start] | |
* The number to start from. | |
* @param Number stop | |
* The amount to stop at. | |
* @param Number [step] | |
* The amount to change each iteration. | |
* @return Chainable | |
*/ | |
exportFunction(function* range(start = 0, stop, step = 1) { | |
if (arguments.length < 2) { | |
stop = start; | |
start = 0; | |
} | |
const count = Math.max(Math.ceil((stop - start) / step), 0); | |
for (let i = 0; i < count; i++) { | |
yield start; | |
start += step; | |
} | |
}); | |
/** | |
* A generator that repeatedly yields a value. By default this is an infinite | |
* generator. | |
* | |
* @param Any value | |
* The value to yield. | |
* @param Number [count] | |
* The amount of times to repeat the value. | |
* @return Chainable | |
*/ | |
exportFunction(function* repeat(value, count = Infinity) { | |
while (count-- > 0) { | |
yield value; | |
} | |
}); | |
/** | |
* A generator that yield the for..in keys for an object. | |
* @param Object object | |
* The object to get the keys for. | |
* @return Array | |
* The array of keys. | |
*/ | |
function* enumerate(object) { | |
for (let key in object) { | |
yield key; | |
} | |
} | |
/** | |
* A generator that yields keys for an object's own properties. | |
* | |
* @param Object object | |
* The object to yield keys from. | |
* @param Boolean [ownProperties]f | |
* Whether to include only own properties or also prototype properties. | |
* @return Chainable | |
*/ | |
exportFunction(function* keys(object, ownProperties = true) { | |
const getKeys = ownProperties ? Object.keys : enumerate; | |
yield* getKeys(object); | |
}); | |
/** | |
* A generator that yields the values for an object's own properties. | |
* | |
* @param Object object | |
* The object to yield values from. | |
* @param Boolean [ownProperties] | |
* Whether to include only own properties or also prototype properties. | |
* @return Chainable | |
*/ | |
exportFunction(function* values(object, ownProperties = true) { | |
const getKeys = ownProperties ? Object.keys : enumerate; | |
for (let key of getKeys(object)) { | |
yield object[key]; | |
} | |
}); | |
/** | |
* A generator that yields [key, value] pairs for an object's own properties. | |
* | |
* @param Object object | |
* The object to yield [key, value] pairs from. | |
* @param Boolean [ownProperties] | |
* Whether to include only own properties or also prototype properties. | |
* @return Chainable | |
*/ | |
exportFunction(function* entries(object, ownProperties = true) { | |
const getKeys = ownProperties ? Object.keys : enumerate; | |
for (let key of getKeys(object)) { | |
yield [key, object[key]]; | |
} | |
}); | |
/** | |
* Repeatedly execs a RegExp against a string, yielding the captures each time. | |
* | |
* @param RegExp regexp | |
* The RegExp to exec. | |
* @param String string | |
* @return Chainable | |
*/ | |
exportFunction(function* exec(regexp, string) { | |
for (let match; match = regexp.exec(string); yield match); | |
}); | |
/** | |
* A generator that creates a subproduct from a provided iterable and another | |
* product to chain from. | |
* | |
* @param Iterable chained | |
* The Iterable to chain from. | |
* @param Iterable iterable | |
* The Iterable to add values from. | |
* @return Chainable | |
*/ | |
const _product = helper(function*(chained, iterable) { | |
for (let tuple of chained) { | |
for (let item of iterable) { | |
yield tuple.concat(item); | |
} | |
} | |
}); | |
/** | |
* A generator to create the cartesian product of the provided iterables. | |
* | |
* @param Iterable... iterables | |
* The Iterables to create the product of. | |
* @return Chainable | |
*/ | |
exportMethod(function product(...iterables) { | |
return iterables.reduce(_product, iter([[]])); | |
}); | |
/** | |
* A generator that doesn't yield values until a given callback returns truthy. | |
* After that it yields all of the remaining values. | |
* | |
* @param Iterable iterable | |
* The iterable to drop values from. | |
* @param Function callback | |
* The callback to test each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* dropWhile(iterable, callback, thisArg) { | |
for (let item of iterable) { | |
if (!callback.call(thisArg, item)) { | |
break; | |
} | |
} | |
yield* iterable; | |
}); | |
/** | |
* A generator that yields values until a given callback returns falsey. | |
* | |
* @param Iterable iterable | |
* The iterable to take values from. | |
* @param Function callback | |
* The callback to test each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* takeWhile(iterable, callback, thisArg) { | |
for (let item of iterable) { | |
if (!callback.call(thisArg, item)) { | |
return; | |
} | |
yield item; | |
} | |
}); | |
/** | |
* A generator that skips a given amount of items and stops at a given end. | |
* | |
* @param Iterable iterable | |
* The iterable to take values from. | |
* @param Number [start] | |
* How many items to skip at the beginning. | |
* @param Number end | |
* The total amount of items after which to end at. | |
* @return Chainable | |
*/ | |
exportMethod(function* slice(iterable, start, end) { | |
if (arguments.length < 3) { | |
end = start; | |
start = 0; | |
} | |
if (end <= start) { | |
return; | |
} | |
let index = 0; | |
for (let item of iterable) { | |
if (index >= start) { | |
yield item; | |
} | |
if (index++ === end) { | |
return; | |
} | |
} | |
}); | |
/** | |
* A generator that yields values that return truthy from a given callback. | |
* | |
* @param Iterable iterable | |
* The iterable to filter values from. | |
* @param Function callback | |
* The callback to test each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* filter(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
if (callback.call(thisArg, item, index++, iterable)) { | |
yield item; | |
} | |
} | |
}); | |
/** | |
* A generator that produces filters it so each value is only seen once. | |
* | |
* @param Iterable iterable | |
* The iterables to filter by uniques. | |
* @return Chainable | |
*/ | |
exportMethod(function* unique(iterable) { | |
const seen = new Set(); | |
for (let item of iterable) { | |
if (!seen.has(item)) { | |
seen.add(item); | |
yield item; | |
} | |
} | |
seen.clear(); | |
}); | |
/** | |
* A generator that yields values transformed by a given callback. | |
* | |
* @param Iterable iterable | |
* The iterable to map values from. | |
* @param Function callback | |
* The callback to transform each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* map(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
yield callback.call(thisArg, item, index++, iterable); | |
} | |
}); | |
/** | |
* A generator that yields values from a callback that is called with arguments | |
* yielded from a given Iterable. | |
* | |
* @param Iterable iterable | |
* The iterable to map values from. | |
* @param Function callback | |
* The callback to transform each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* starMap(iterable, callback, thisArg) { | |
for (let args of iterable) { | |
yield callback.call(thisArg, ...args); | |
} | |
}); | |
/** | |
* A generator that calls a given callback for each value, but yields it | |
* directly through unchanged. | |
* | |
* @param Iterable iterable | |
* The iterable to tap values from. | |
* @param Function callback | |
* The callback that each action will be reported to. Called with | |
* arguments for action and value. Action is one of | |
* ["next", "throw", "return"]. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Chainable | |
*/ | |
exportMethod(function* tap(iterable, callback, thisArg) { | |
const iterator = iter(iterable); | |
let action = "next"; | |
let nextValue; | |
for (;;) { | |
const {done, value} = iterator[action](sentValue); | |
if (done) { | |
callback.call(thisArg, "return", value); | |
return value; | |
} | |
callback.call(thisArg, action, value); | |
try { | |
nextValue = yield value; | |
action = "next"; | |
} catch (e) { | |
nextValue = e; | |
action = "throw"; | |
} | |
} | |
}); | |
/** | |
* A generator that wraps a given iterable to handle errors, optionally | |
* reporting them to a callback. | |
* | |
* @param Iterable iterable | |
* The iterable to tap values from. | |
* @param Function [callback] | |
* The callback to any encountered errors will be reported to. | |
* @return Chainable | |
*/ | |
exportMethod(function* squelch(iterable, callback = () => {}) { | |
let iterator; | |
try { | |
iterator = iter(iterable); | |
} catch (e) { | |
callback(e); | |
return; | |
} | |
let sendValue; | |
for (;;) { | |
let done, value; | |
try { | |
({done, value}) = iterator.next(sendValue); | |
if (done) { | |
return value; | |
} | |
} catch (e) { | |
callback(e); | |
continue; | |
} | |
sendValue = yield value; | |
} | |
}); | |
/** | |
* A generator that yields [index, value] pairs from the values in a given | |
* iterable. | |
* | |
* @param Iterable iterable | |
* The iterables to yield indexed values from. | |
* @param Number [start] | |
* The index to start from. Defaults to 0. | |
* @param Number [step] | |
* The amount to increase for each index. Defaults to 1. | |
* @return Chainable | |
*/ | |
exportMethod(function* index(iterable, start = 0, step = 1) { | |
for (let value of iterable) { | |
yield [start, value]; | |
start += step; | |
} | |
}); | |
/** | |
* A generator that yields a given property from each value from the provided | |
* iterable. | |
* | |
* @param Iterable iterable | |
* The iterable to pluck properties from. | |
* @param String key | |
* The key of the property to pluck from each value. | |
* @return Chainable | |
*/ | |
exportMethod(function* pluck(iterable, key) { | |
for (let item of iterable) { | |
yield item[key]; | |
} | |
}); | |
/** | |
* A generator that yields the resulted of invoking a given method on each | |
* value from a provided iterable with the given arguments. | |
* | |
* @param Iterable iterable | |
* The iterable to pluck properties from. | |
* @param String key | |
* The key of the property to pluck from each value. | |
* @param Any... args | |
* Any additional arguments to pass to the method. | |
* @return Chainable | |
*/ | |
exportMethod(function* invoke(iterable, key, ...args) { | |
for (let item of iterable) { | |
yield item[key](...args); | |
} | |
}); | |
/** | |
* A generator that repeatedly loops through a given iterable's initial value | |
* set. This is an infinite generator. By default this is an infinite generator. | |
* | |
* @param Iterable iterable | |
* The iterable to map values from. | |
* @param Number [count] | |
* The amount of times to cycle the iterable. | |
* @return Chainable | |
*/ | |
exportMethod(function* cycle(iterable, count = Infinity) { | |
const cache = []; | |
for (let item of iterable) { | |
yield item; | |
cache.push(item); | |
} | |
if (cache.length) { | |
for (let i = 0; i < count; i++) { | |
yield* cache; | |
} | |
} | |
}); | |
/** | |
* A generator that combines any number of iterables into a single iterable, | |
* yielding from start to finish. | |
* | |
* @param Iterable... iterables | |
* The iterables to combine into one iterable. | |
* @return Chainable | |
*/ | |
exportMethod(function* chain(...iterables) { | |
for (let iterable of iterables) { | |
yield* iterable; | |
} | |
}); | |
const _zip = helper(function*(iterables, longest) { | |
if (!iterables.length) { | |
return; | |
} | |
const iterators = iterables.map(iter); | |
for (;;) { | |
const values = []; | |
for (let iterator of iterators) { | |
const {done, value} = iterator.next(); | |
if (done) { | |
if (!longest) { | |
return; | |
} | |
values.length++; | |
} else { | |
values.push(value); | |
} | |
} | |
yield values; | |
} | |
}); | |
/** | |
* A generator that executes multiple iterators in parallel, yielding an array | |
* for each set until the first one is exhausted. | |
* | |
* @param Iterable... iterables | |
* The iterables to combine in parallel. | |
* @return Chainable | |
*/ | |
exportMethod(function zip(...iterables) { | |
return _zip(iterables, false); | |
}); | |
/** | |
* A generator that executes multiple iterators in parallel, yielding an array | |
* for each set until the first one is exhausted. | |
* | |
* @param Iterable... iterables | |
* The iterables to combine in parallel. | |
* @return Chainable | |
*/ | |
exportMethod(function zipLongest(...iterables) { | |
return _zip(iterables, true); | |
}); | |
const QUEUE_COMPACT_SIZE = 500; | |
/** | |
* An efficient implementation of the queue data structure. | |
* @param Iterable [iterable] | |
* Optional iterable to initialize the values in the Queue. | |
*/ | |
function Queue(iterable) { | |
this._head = 0; | |
this._items = iterable ? [...iterable] : []; | |
this.size = this._items.length; | |
} | |
Queue.prototype.enqueue = function(item) { | |
this._items.push(item); | |
return ++this.size; | |
}; | |
Queue.prototype.dequeue = function() { | |
if (!this.size) { | |
return; | |
} | |
const item = this._items[this._head]; | |
if (this._head === QUEUE_COMPACT_SIZE) { | |
this._items = this._items.slice(this._head + 1); | |
this._head = 0; | |
} else { | |
this._items[this._head++] = MISSING; | |
} | |
return item; | |
}; | |
Queue.prototype[iteratorSymbol] = function*() { | |
if (!this.size) { | |
return; | |
} | |
const {front} = this; | |
const items = this._items; | |
let index = this._head; | |
yield front; | |
for (; index < items.length; index++) { | |
const item = items[index]; | |
if (item !== MISSING) { | |
yield item; | |
} | |
} | |
}; | |
/** | |
* A generator that works in tandem with multiple other generators to | |
* individually yield values from a a generator that yields tuples. | |
* | |
* @param Iterator iterator | |
* Source Iterator to draw tuples from. | |
* @param Number index | |
* Index that this generator should draw values using from the source | |
* generator's tuples. | |
* @param Queue... queues | |
* Array of Queues for all the generators unzipping from this iterator. | |
* Indices in this array will match the index in the tuple. | |
* @return Chainable | |
*/ | |
const _unzip = helper(function*(iterator, index, queues) { | |
const queue = queues[index]; | |
for (;;) { | |
if (queue.size) { | |
yield queue.dequeue(); | |
} else { | |
const {value, done} = iterator.next(); | |
if (done) { | |
return; | |
} | |
let item = MISSING; | |
for (let i = 0; i < queues.length; i++) { | |
if (i in value) { | |
if (index === i) { | |
item = value[i]; | |
} else { | |
queues[i].enqueue(value[i]); | |
} | |
} | |
} | |
if (item !== MISSING) { | |
yield item; | |
} | |
} | |
} | |
}); | |
/** | |
* Takes an iterator that yields tuples of values and creates multiple | |
* generators, each yielding a values from a given index in the yielded tuplees. | |
* | |
* @param Iterable iterable | |
* The iterable to split into multiple iterables. | |
* @return Chainable... | |
*/ | |
exportMethod(function unzip(iterable) { | |
const iterator = iter(iterable); | |
const {done, value} = iterator.next(); | |
if (done) { | |
return []; | |
} | |
const count = value.length; | |
const iterables = new Array(count); | |
const queues = new Array(count); | |
while (count--) { | |
queues[count] = new Queue(); | |
queues[count].enqueue(value[count]); | |
iterables[count] = _unzip(iterator, count, queues); | |
} | |
return iterables; | |
}); | |
/** | |
* A generator that filters elements from an iterable based on the truthiness | |
* of the values yielded from a second iterable. | |
* | |
* @param Iterable iterable | |
* The Iterable to filter. | |
* @param Iterable selectors | |
* The Iterable who's values' truthiness will be used as a filter. | |
* @return Chainable | |
*/ | |
exportMethod(function* compress(iterable, selectors) { | |
selectors = iter(selectors); | |
for (let item of iterable) { | |
if (selectors.next().value) { | |
yield item; | |
} | |
} | |
}); | |
/** | |
* Groupby description. | |
* | |
* @param Iterable iterable | |
* The Iterable to filter. | |
* @param Function [keyCallback] | |
* The optional callback to provide a key for a given value. | |
* @return Chainable | |
*/ | |
exportMethod(function* groupBy(iterable, keyCallback = x => x) { | |
function* group(targetKey) { | |
while (currentKey === targetKey) { | |
yield currentValue; | |
let {done, value} = iterator.next(); | |
if (done) { | |
return; | |
} | |
currentKey = keyCallback(currentValue = value); | |
} | |
} | |
const iterator = iter(iterable); | |
let targetKey = MISSING; | |
let currentKey = MISSING; | |
let currentValue = MISSING; | |
for (;;) { | |
while (currentKey === targetKey) { | |
let {done, value} = iterator.next(); | |
if (done) { | |
return; | |
} | |
currentKey = keyCallback(currentValue = value); | |
} | |
yield [currentKey, group(targetKey = currentKey)]; | |
} | |
}); | |
/** | |
* A generator that yields tuples of values yielded from a given iterable. | |
* | |
* @param Iterable iterable | |
* The source Iterable. | |
* @param Number chunkSize | |
* The amount of yielded values to collect into each array to be yielded. | |
* @param Boolean [abbreviate] | |
* Optionally discard the last tuple if its not full because the Iterable | |
* ended early. | |
* @return Chainable | |
*/ | |
exportMethod(function* chunk(iterable, chunkSize, abbreviate = false) { | |
let items = []; | |
let index = 0; | |
for (let item of iterable) { | |
items[index++] = item; | |
if (index === chunkSize) { | |
yield items; | |
items = []; | |
index = 0; | |
} | |
} | |
if (index && (index === chunkSize || !abbreviate)) { | |
yield items; | |
} | |
}); | |
/** | |
* A generator that caches values and is capable of working in tandem with | |
* multiple other instances using the same iterator. Used by tee. | |
* | |
* @param Iterator iterator | |
* The source Iterator. | |
* @param Array cache | |
* Stores values until all iterators have yielded the value. | |
* @return Chainable | |
*/ | |
const _tee = helper(function*(iterator, cache) { | |
const {items} = cache; | |
let index = 0; | |
for (;;) { | |
if (index === items.length) { | |
let {done, value} = iterator.next(); | |
if (done) { | |
if (cache.returned === MISSING) { | |
cache.returned = value; | |
} | |
break; | |
} | |
yield items[index++] = value; | |
} else if (index === cache.tail) { | |
let value = items[index]; | |
if (index === QUEUE_COMPACT_SIZE) { | |
items = cache.items = items.slice(index); | |
index = 0; | |
cache.tail = 0; | |
} else { | |
items[index] = undefined; | |
cache.tail = ++index; | |
} | |
yield value; | |
} else { | |
yield items[index++]; | |
} | |
} | |
if (cache.tail === index) { | |
items.length = 0; | |
} | |
return cache.returned; | |
}); | |
/** | |
* Creates multiple generators that each yield the same output as a single input | |
* iterable. They can be iterated at different times and speeds and still yield | |
* the same values in the same order. | |
* | |
* @param Iterable iterable | |
* The Iterable to copy. | |
* @param Number [count] | |
* The number of copies to make. | |
* @return Chainable... | |
* An array containing the replicated generators. | |
*/ | |
exportMethod(function tee(iterable, count = 2) { | |
const iterator = iter(iterable); | |
const iterables = new Array(count); | |
const cache = { tail: 0, items: [], returned: MISSING }; | |
while (count--) { | |
iterables[count] = _tee(iterator, cache); | |
} | |
return iterables; | |
}); | |
/** | |
* Consumes an iterable and calls a callback for each value. | |
* | |
* @param Iterable iterable | |
* The iterable to test values from. | |
* @param Function callback | |
* The callback to call with each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
*/ | |
exportMethod(function forEach(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
callback.call(thisArg, item, index++, iterable); | |
} | |
}); | |
/** | |
* Reduce an iterable to a single value by repeatedly calling a callback. | |
* | |
* @param Iterable iterable | |
* The iterable to map values from. | |
* @param Function callback | |
* The callback to call with the accumulated value and the current value. | |
* @param Any [initial] | |
* Optional initial value. If none is provided, then the first value | |
* returned from the iterator will be used. | |
* @return Any | |
* The final accumulated value. | |
*/ | |
exportMethod(function reduce(iterable, callback, initial) { | |
const iterator = iter(iterable); | |
let current = initial; | |
if (arguments.length < 3) { | |
const {value, done} = iterator.next(); | |
if (done) { | |
return; | |
} | |
current = value; | |
} | |
for (let item of iterator) { | |
current = callback(current, item, iterable); | |
} | |
return current; | |
}); | |
/** | |
* A generator that reduces an iterable to a single value by repeatedly calling | |
* a callback, yielding the current accumulated value on each iteration. | |
* | |
* @param Iterable iterable | |
* The iterable to map values from. | |
* @param Function callback | |
* The callback to call with the accumulated value and the current value. | |
* @param Any [initial] | |
* Optional initial value. If none is provided, then the first value | |
* returned from the iterator will be used. | |
* @return Chainable | |
*/ | |
exportMethod(function* ireduce(iterable, callback, initial) { | |
const iterator = iter(iterable); | |
let current = initial; | |
if (arguments.length < 3) { | |
const {value, done} = iterator.next(); | |
if (done) { | |
return; | |
} | |
current = value; | |
} | |
yield current; | |
for (let item of iterator) { | |
yield current = callback(current, item, iterable); | |
} | |
return current; | |
}); | |
/** | |
* Consumes an iterable and returns whether every item passes a callback test | |
* for truthiness. | |
* | |
* @param Iterable iterable | |
* The iterable to test values from. | |
* @param Function callback | |
* The callback to call with each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Boolean | |
* Whether every item in the iterable passed the test. | |
*/ | |
exportMethod(function every(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
if (!callback.call(thisArg, item, index++, iterable)) { | |
return false; | |
} | |
} | |
return true; | |
}); | |
/** | |
* Consumes and iterable and returns whether any item passes a callback test | |
* for truthiness. | |
* | |
* @param Iterable iterable | |
* The iterable to test values from. | |
* @param Function callback | |
* The callback to call with each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Boolean | |
* Whether any item in the iterable passed the test. | |
*/ | |
exportMethod(function some(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
if (callback.call(thisArg, item, index++, iterable)) { | |
return true; | |
} | |
} | |
return false; | |
}); | |
/** | |
* Consumes and iterable and returns the first value to pass the callback test | |
* for truthiness. | |
* | |
* @param Iterable iterable | |
* The iterable to test values from. | |
* @param Function callback | |
* The callback to call with each value. | |
* @param Any [thisArg] | |
* Optional |this| value the callback will be called with. | |
* @return Any | |
* The first value to pass the test, or undefined. | |
*/ | |
exportMethod(function find(iterable, callback, thisArg) { | |
let index = 0; | |
for (let item of iterable) { | |
if (callback.call(thisArg, item, index++, iterable)) { | |
return item; | |
} | |
} | |
}); | |
/** | |
* Return the first value from a given iterable. | |
* @param Iterable iterable | |
* The iterable to get the first value from. | |
* @param Any [defaultValue] | |
* The default value to return if the iterable is done. | |
* @return Any | |
*/ | |
exportMethod(function first(iterable, defaultValue) { | |
const iterator = iter(iterable); | |
const {value, done} = iterator.next(); | |
return done ? defaultValue : value; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const iterutils = exports; | |
const {iter} = iterutils; | |
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty); | |
function take(iterable, count = Infinity) { | |
return [...iterutils.slice(iterable, 0, count)]; | |
} | |
const testStack = []; | |
let errors = []; | |
let currentTest; | |
function test(name, callback) { | |
testStack.push(name); | |
try { | |
callback(); | |
} catch (e) { | |
errors.push(e); | |
} | |
testStack.pop(); | |
} | |
function ok(value) { | |
if (!value) { | |
throw new Error(testStack.join(" ") + " - not ok"); | |
} | |
} | |
function strictEqual(a, b) { | |
return a === b; | |
} | |
function similarIterables(a, b, options = {}) { | |
const comparator = options.comparator || strictEqual; | |
const count = options.count || Infinity; | |
const iterA = iter(a); | |
const iterB = iter(b); | |
let index = 0; | |
let isSame = true; | |
for (;;) { | |
if (++index >= count) { | |
break; | |
} | |
const {done: doneA, value: valueA} = iterA.next(); | |
const {done: doneB, value: valueB} = iterB.next(); | |
isSame = isSame && comparator(valueA, valueB); | |
if (doneA || doneB) { | |
isSame = isSame && doneA === doneB; | |
break; | |
} | |
} | |
return isSame; | |
} | |
function similarProperties(a, b, options = {}) { | |
if (a === b) { | |
return true; | |
} | |
const comparator = options.comparator || strictEqual; | |
const keys = Object.keys(a); | |
const keysCompare = new Set(Object.keys(b)); | |
if (keys.length !== keysCompare.size) { | |
return false; | |
} | |
for (let key of keys) { | |
if (!keysCompare.has(key) || !comparator(a[key], b[key])) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function similarResult(iterator, {done, value}) { | |
const result = iterator.next(); | |
return done === result.done && similarProperties(value, result.value); | |
} | |
function isDone(iterator, value) { | |
const result = iterator.next(); | |
return result.done && result.value === value; | |
} | |
function isEqual(a, b) { | |
return a === b; | |
} | |
function isInstance(a, b) { | |
return a instanceof b; | |
} | |
function makeAssertable(f) { | |
return (...args) => { | |
if (!f(...args)) { | |
throw new Error(testStack.join(" ") + " - not ok"); | |
} | |
} | |
} | |
const assertSimilarIterables = makeAssertable(similarIterables); | |
const assertSimilarProperties = makeAssertable(similarProperties); | |
const assertSimilarResult = makeAssertable(similarResult); | |
const assertIsDone = makeAssertable(isDone); | |
const assertIsEqual = makeAssertable(isEqual); | |
const assertIsInstance = makeAssertable(isInstance); | |
const assertOk = makeAssertable(Boolean); | |
function item(value, done) { | |
return { value: value, done: !!done }; | |
} | |
function done(value) { | |
return { value: value, done: true }; | |
} | |
const testObj = { | |
a: 5, | |
b: 10, | |
__proto__: { | |
__proto__: null, | |
c: 20 | |
} | |
}; | |
test("iter", () => { | |
// Manually check values here because `similar` uses `iter`. | |
const iterator = itertools.iter([1, 2, 3]); | |
assertSimilarResult(iterator, item(1)); | |
assertSimilarResult(iterator, item(2)); | |
assertSimilarResult(iterator, item(3)); | |
assertIsDone(iterator); | |
}); | |
test("count", () => { | |
let iterator = itertools.count(5); | |
assertSimilarIterables(iterator, [5, 6, 7], { count: 3 }); | |
iterator = itertools.count(10, 3); | |
assertSimilarIterables(iterator, [10, 13, 16], { count: 3 }); | |
}); | |
test("range", () => { | |
let iterator = itertools.range(3); | |
assertSimilarIterables(iterator, [0, 1, 2]); | |
iterator = itertools.range(10, 13); | |
assertSimilarIterables(iterator, [10, 11, 12]); | |
iterator = itertools.range(10, 16, 2); | |
assertSimilarIterables(iterator, [10, 12, 14]); | |
}); | |
test("repeat", () => { | |
let iterator = itertools.repeat("f", 3); | |
assertSimilarIterables(iterator, ["f", "f", "f"]); | |
}); | |
test("keys", () => { | |
let iterator = itertools.keys(testObj); | |
assertSimilarIterables(iterator, ["a", "b"]); | |
iterator = itertools.keys(testObj, false); | |
assertSimilarIterables(iterator, ["a", "b", "c"]); | |
}); | |
test("values", () => { | |
let iterator = itertools.values(testObj); | |
assertSimilarIterables(iterator, [5, 10]); | |
iterator = itertools.values(testObj, false); | |
assertSimilarIterables(iterator, [5, 10, 20]); | |
}); | |
test("entries", () => { | |
let iterator = itertools.entries(testObj); | |
assertSimilarIterables(iterator, [["a", 5], ["b", 10]]); | |
iterator = itertools.entries(testObj, false); | |
assertSimilarIterables(iterator, [["a", 5], ["b", 10], ["c", 20]]); | |
}); | |
test("exec", () => { | |
}); | |
test("product", () => { | |
let iterator = itertools.product("abc", "def"); | |
}); | |
test("dropWhile", () => { | |
let iterator = itertools.dropWhile([5, 10, 20, 50], v => v < 20); | |
assertSimilarIterables(iterator, [20, 50]); | |
}); | |
test("takeWhile", () => { | |
let iterator = itertools.takeWhile([5, 10, 20, 50], v => v < 20); | |
assertSimilarIterables(iterator, [5, 10]); | |
}); | |
test("slice", () => { | |
let iterator = itertools.slice([5, 10, 20], 1); | |
assertSimilarIterables(iterator, [10, 20]); | |
iterator = itertools.slice([5, 10, 20], 1, 2); | |
assertSimilarIterables(iterator, [10]); | |
}); | |
test("filter", () => { | |
let iterator = itertools.filter([5, 10, 20], v => v !== 10); | |
assertSimilarIterables(iterator, [5, 20]); | |
}); | |
test("unique", () => { | |
let iterator = itertools.unique([5, 5, 5, 10, 20, 20]); | |
assertSimilarIterables(iterator, [5, 10, 20]); | |
}); | |
test("map", () => { | |
let iterator = itertools.unique([5, 10, 20], v => v * 10); | |
assertSimilarIterables(iterator, [50, 100, 200]); | |
}); | |
test("starMap", () => { | |
}); | |
test("tap", () => { | |
}); | |
test("squelch", () => { | |
}); | |
test("index", () => { | |
let iterator = itertools.index([5, 10, 20], 1, 2); | |
assertSimilarIterables(iterator, [[1, 5], [3, 10], [5, 20]]); | |
}); | |
test("pluck", () => { | |
const objs = [{ foo: 5 }, { foo: 10 }, { foo: 20 }]; | |
let iterator = itertools.pluck(objs, "foo"); | |
assertSimilarIterables(iterator, [5, 10, 20]); | |
}); | |
test("invoke", () => { | |
}); | |
test("cycle", () => { | |
let iterator = itertools.cycle([5, 10, 20]); | |
assertSimilarIterables(iterator, [5, 10, 20, 5, 10, 20], { count: 6 }); | |
}); | |
test("chain", () => { | |
let iterator = itertools.chain([5, 10, 20], "foo"); | |
assertSimilarIterables(iterator, [5, 10, 20, "f", "o", "o"]); | |
}); | |
test("zip", () => { | |
let iterator = itertools.zip([5, 10, 20], "foobar"); | |
assertSimilarIterables(iterator, [[5, "f"], [10, "o"], [20, "o"]]); | |
}); | |
test("zipLongest", () => { | |
let iterator = itertools.zip([5, 10, 20], "foobar"); | |
assertSimilarIterables(iterator, [[5, "f"], [10, "o"], [20, "o"], [,"b"], [,"a"], [,"r"]]); | |
}); | |
test("unzip", () => { | |
let [a, b] = itertools.unzip([[5, "f"], [10, "o"], [20, "o"], [,"b"], [,"a"], [,"r"]]); | |
assertSimilarIterables(a, [5, 10, 20]); | |
assertSimilarIterables(b, "foobar"); | |
}); | |
test("compress", () => { | |
let iterator = itertools.compress([5, 10, 20], [true, false, true]); | |
assertSimilarIterables(a, [5, 20]); | |
}); | |
test("groupBy", () => { | |
}); | |
test("chunk", () => { | |
}); | |
test("tee", () => { | |
const iterators = itertools.tee([5, 10, 20], 3); | |
let count = 0; | |
for (let iterator of iterators) { | |
count++; | |
assertSimilarIterables(iterator, [5, 10, 20]); | |
} | |
assertIsEqual(count, 3); | |
}); | |
test("forEach", () => { | |
const items = new Set([5, 10, 20]); | |
itertools.forEach([5, 10, 20], item => { | |
assertOk(items.has(item)); | |
items.delete(item); | |
}); | |
assertIsEqual(items.size, 0); | |
}); | |
test("reduce", () => { | |
}); | |
test("ireduce", () => { | |
}); | |
test("every", () => { | |
}); | |
test("some", () => { | |
}); | |
test("find", () => { | |
}); | |
test("first", () => { | |
let iterator = itertools.iter([5, 10, 20]); | |
assertIsEqual(iterator.first(), 5); | |
assertIsEqual(iterator.first(), 10); | |
assertIsEqual(iterator.first(), 20); | |
assertIsEqual(iterator.first(), undefined); | |
assertIsEqual(iterator.first(null), null); | |
}); | |
test("chaining", () => { | |
let index = 0; | |
let iterator = itertools.iter((for (v of [5, 10, 20]) ++index && v)); | |
assertIsEqual(index, 0); | |
assertIsInstance(iterator, Chainable); | |
iterator = iterator.cycle(2); | |
assertIsEqual(index, 0); | |
assertIsInstance(iterator, Chainable); | |
iterator = iterator.map(x => x * 10); | |
assertIsEqual(index, 0); | |
assertIsInstance(iterator, Chainable); | |
assertSimilarIterables(iterator, [50, 100, 200, 50, 100, 200]); | |
assertIsEqual(index, 3); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[...exports.count() // count is an infinite generator | |
.dropWhile(n => n < 64) // limit the start point | |
.takeWhile(n => n < 91) // limit the end point | |
.map(String.fromCharCode) // transform | |
].join("") | |
// ---> | |
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment