UNPKG

xduce

Version:

Composable algorithmic transformations library for JavaScript

401 lines (388 loc) 17.6 kB
/* * Copyright (c) 2017 Thomas Otterson * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** * Functions that deal with iteration - the first step in transduction, which is taking the input collection and * breaking it into its component parts. * * Iteration is the simplest to deal with because it's already well-supported natively by JavaScript. This file is * largely dedicated to implementing consistent iteration in pre-ES2015 environments, as well as adding iteration * possibilities for those objects not already supported natively. * * @module iteration * @private */ const { bmpCharAt, bmpLength, isArray, isFunction, isObject, isString } = require('./util'); const { protocols, isImplemented } = require('./protocol'); const p = protocols; /** * Creates an iterator over strings. ES2015 strings already satisfy the iterator protocol, so this function will not * be used for them. This is for ES5 strings where the iterator protocol doesn't exist. As with ES2015 iterators, it * takes into account double-width BMP characters and will return the entire character as a two-character string. * * @private * * @param {string} str The string to be iterated over.@author [author] * @return {module:xduce~iterator} An iterator that returns one character per call to `next`. */ function stringIterator(str) { let index = 0; return { next() { return index < bmpLength(str) ? { value: bmpCharAt(str, index++), done: false } : { done: true }; } }; } /** * Creates an iterator over strings. ES2015 strings already satisfy the iterator protocol, so this function will not * be used for them. This is for ES5 strings where the iterator protocol doesn't exist. * * @private * * @param {array} array The array to be iterated over. * @return {module:xduce~iterator} An iterator that returns one element per call to `next`. */ function arrayIterator(array) { let index = 0; return { next() { return index < array.length ? { value: array[index++], done: false } : { done: true }; } }; } /** * Creates an iterator over objcts. * * Objects are not generally iterable, as there is no defined order for an object, and each "element" of an object * actually has two values, unlike any other collection (a key and a property). However, it's tremendously useful to * be able to use at least some transformers with objects as well. This iterator adds support in two different * ways to make that possible. * * The first is that a sort order is defined. Quite simply, it's done alphabetically by key. There is also an option - * through the second parameter `sort` - to provide a different sort function. This should be a function in the style * of `Array.prototype.sort`, where two parameters are compared and -1 is returned if the first is larger, 1 is returned * if the second is larger, and 0 is returned if they're equal. This is applied ONLY TO THE KEYS of the object. If you * wish to sort on values, consider iterating into an array and then sorting the elements by value. * * In the public API, this sort function can only be passed through the `{@link module:xduce.iterator|iterator}` * function. If you wish to use an object sorted in a non-default order, you should create an iterator out of it and * transform that iterator. For example: * * | DEFAULT ORDER | CUSTOM ORDER | * | ------------------------------------ | ---------------------------------------------------- | * | `var result = sequence(obj, xform);` | `var result = asObject(iterator(obj, sort), xform);` | * * The second support feature is the alternative "kv-form" objects. A reasonable way to iterate over objects would be to * produce single-property objects, one per property on the original object (i.e., `{a: 1, b: 2}` would become two * elements: `{a: 1}` and `{b: 2}`). This is fine for straight iteration and reduction, but it can present a challenge * to use a transformer with. Consider this example code, which uppercases the key and adds one to the value. * * ``` * function doObjectSingle(obj) { * var key = Object.keys(obj)[0]; * var result = {}; * result[key.toUpperCase()] = obj[key] + 1; * return result; * } * ``` * * This is a little unwieldy, so the iterator provides for another kind of iteration. Setting the third parameter, * `kv`, to `true` (which is the default), objects will be iterated into two-property objects with `k` and `v` as the * property names. For example, `{a: 1, b: 2}` will become two elements: `{k: 'a', v: 1}` and `{k: 'b', v: 2}`. This * turns the mapping function shown above into something simpler. * * ``` * function doObjectKv(obj) { * var result = {}; * result[obj.k.toUpperCase()]: obj.v + 1; * return result; * } * ``` * * This is the default iteration form for objects internally. If you want to iterate an object into the `{key: value}` * form, for which you would have to use the `doObjectSingle` style transformer, you must call * `{@link module:xduce.iterator|iterator}` with the third parameter explicitly set to `false` and then pass that * iterator to the transducing function. This is availabe in particular for those writing their own transducers. * * Still, while this is nice, we can do better. The built-in reducers for arrays, objects, strings, and iterators * recognize the kv-form and know how to reduce it back into a regular key-value form for output. So instead of that * first `doObjectKv`, we could write it this way. * * ``` * function doObjectKvImproved(obj) { * return {k: obj.k.toUpperCase(), v: obj.v + 1}; * } * ``` * * The reducer will recognize the form and reduce it correctly. The upshot is that in this library, `doObjectKv` and * `doObjectKvImproved` will produce the SAME RESULT. Which function to use is purely a matter of preference. IMPORTANT * NOTE: If you're adding transducer support to non-supported types, remember that you must decide whether to have your * `step` function recognize kv-form objects and reduce them into key-value. If you don't, then the style of * `doObjectKvImproved` will not be available. * * ANOTHER IMPORTANT NOTE: The internal reducers recognize kv-form very explicitly. The object must have exactly two * enumerable properties, and those properties must be named 'k' and 'v'. This is to reduce the chance as much as * possible of having errors because an object that was meant to be two properties was turned into one. (It is possible * to have a transformation function return an object of more than one property; if that happens, and if that object is * not a kv-form object, then all of the properties will be merged into the final object.) * * One final consideration: you have your choice of mapping function styles, but the better choice may depend on * language. The above examples are in ES5. If you're using ES2015, however, you have access to destructuring and * dynamic object keys. That may make `doObjectKv` look better, because with those features it can be written like this: * * ``` * doObjectKv = ({k, v}) => {[k.toUpperCase()]: v + 1}; * ``` * * And that's about as concise as it gets. Note that some languages that compile into JavaScript, like CoffeeScript and * LiveScript, also support these features. * * @private * * TL;DR: * 1. Iteration order of objects is alphabetical by key, though that can be changed by passing a sort function to * `{@link module:xduce.iterator|iterator}`. * 2. Iteration is done internally in kv-form. * 3. Transformation functions can output objects in key-value, which is easier in ES2015. * 4. Transformation functions can output objects in kv-form, which is easier in ES5. * @param {object} obj The object to iterate over. * @param {module:xduce~sort} [sort] An optional sort function. This is applied to the keys of the object to * determine the order of iteration. * @param {boolean} [kv=false] Whether or not this object should be iterated into kv-form (if false, it remains in the * normal key-value form). * @return {module:xduce~iterator} An iterator that returns one key-value pair per call to `next`. */ function objectIterator(obj, sort, kv = false) { let keys = Object.keys(obj); keys = typeof sort === 'function' ? keys.sort(sort) : keys.sort(); let index = 0; return { next() { if (index < keys.length) { const k = keys[index++]; const value = {}; if (kv) { value.k = k; value.v = obj[k]; } else { value[k] = obj[k]; } return { value, done: false }; } return { done: true }; } }; } /** * Creates an iterator that runs a function for each `next` value. * * The function in question is provided two arguments: the current 0-based index (which starts at `0` and increases by * one for each run) and the return value for the prior calling of the function (which is `undefined` if the function * has not yet been run). The return value of the function is used as the value that comes from `next` the next time * it's called. * * If the function returns `undefined` at any point, the iteration terminates there and the `done` property of the * object returned with the next call to `next` becomes `true`. * * @private * * @param {function} fn A function that is run once for each step of the iteration. It is provided two arguments: the * `0`-based index of that run (starts at `0` and increments each run) and the return value of the last call to the * function (`undefined` if it hasn't been called yet). If the function returns `undefined` at any point, the * iteration terminates. * @return {function} A generator wrapping `fn`, which is suitable for use as an iterator. */ function functionIterator(fn) { return function* () { let current; let index = 0; for (;;) { current = fn(index++, current); if (current === undefined) { break; } yield current; } }(); } /** * Determines whether an object is in kv-form. This used by the reducers that must recognize this form and reduce those * elements back into key-value form. * * This determination is made by simply checking that the object has exactly two properties and that they are named * `k` and `v`. * * @private * * @param {object} obj The object to be tested. * @return {boolean} Either `true` if the object is in kv-form or `false` if it isn't. */ function isKvFormObject(obj) { const keys = Object.keys(obj); if (keys.length !== 2) { return false; } return !!~keys.indexOf('k') && !!~keys.indexOf('v'); } /** * **Creates an iterator over the provided collection.** * * For collections that are not objects, it's as simple as that. Pass in the collection, get an iterator over that * collection. * * ``` * const iter = iterator([1, 2, 3]); * iter.next().value === 1; // true * iter.next().value === 2; // true * iter.next().value === 3; // true * iter.next().done === true; // true * ``` * * Objects get special support though, as noted in the section above on iterating over objects. Objects are iterated in * alphabetical order by key, unless a second parameter is passed to `iterator`. This must be a function that takes two * parameters (which will be object keys) and returns `-1` if the first is less than the second, `1` if the second is * less than the first, and `0` if they're equal. * * Also, `iterator` by default iterates objects into key/value form. However, if a third parameter of `true` is passed, * it will instead render the object in kv-form. This is the form used internally when a transducer is invoked. * * The second and third parameters are ignored if the input collection is not an object. * * ``` * const iter = iterator({ b: 2, a: 4 }); * iter.next().value.a === 4; // true * iter.next().value.b === 2; // true * iter.next().done === true; // true * * const kvIter = iterator({ b: 2, a: 4 }, null, true); * const { k: k1, v: v1 } = kvIter.next().value; * k1 === 'a' && v1 === 4; // true * const { k: k2, v: v2 } = kvIter.next().value; * k2 === 'b' && v2 === 2; // true * iter.next().done === true; // true * ``` * * Note that if this function is passed an object that looks like an iterator (an object that has a `next` function), * the object itself is returned. It is assumed that a function called `next` conforms to the iteration protocol. * * If this function is provided a *function* as its first argument, an iterator is returned which runs that function * one time for each call to `next`. That function is provided two arguments: the index of the call (starting at `0` * for the first time it's called and increasing by 1 per invocation after that) and the return value of the previous * call to the function (starting at `undefined` for the first run before the function is ever called). If the function * ever returns `undefined`, the iterator will terminate and set the `done` property of its return value to `true` at * that point. * * Note that since the initial value of the second argument is `undefined`, using default arguments is an excellent way * of providing the function an initial value. * * ``` * const constIter = iterator(() => 6); // Bert's favorite number * constIter.next().value === 6; // true * constIter.next().value === 6; // true; * // This will go on forever, as long as `next` keeps getting called * * const indexIter = iterator(x => x * x); * indexIter.next().value === 0; // true * indexIter.next().value === 1; // true * indexIter.next().value === 4; // true * indexIter.next().value === 9; // true * // Again, this will go on forever, or until the numbers get to big JS to handle * * // Using default value on `last` parameter for initial value * const lastIter = iterator((index, last = 1) => last * (index + 1)); // Factorial * lastIter.next().value === 1; // true * lastIter.next().value === 2; // true * lastIter.next().value === 6; // true * lastIter.next().value === 24; // true * // Again, forever, though factorials get big quickly * * // This iterator will terminate when the function returns `undefined` * const stopIter = iterator(x => x < 2 ? x : undefined); * stopIter.next().value === 0; // true * stopIter.next().value === 1; // true * stopIter.next().done === true; // true * ``` * * @memberof module:xduce * * @param {*} obj The value to be iterated over. * @param {module:xduce~sort} [sort] A function used to determine the sorting of keys for an object iterator. It has * no effect when iterating over anything that is not a plain object. * @param {boolean} [kv=false] Whether an object should be iterated into kv-form. This is only relevant when iterating * over an object; otherwise its value is ignored. * @return {module:xduce~iterator} An iterator over the provided value. If the value is not iterable (it's not an * array, object, or string, and it doesn't have a protocol-defined iterator), `null` is returned. */ function iterator(obj, sort, kv) { switch (true) { case isFunction(obj[p.iterator]): return obj[p.iterator](); case isFunction(obj.next): return obj; case isFunction(obj): return functionIterator(obj); case isString(obj): return stringIterator(obj); case isArray(obj): return arrayIterator(obj); case isObject(obj): return objectIterator(obj, sort, kv); default: return null; } } /** * Determines whether the passed object is iterable, in terms of what 'iterable' means to this library. In other words, * objects and ES5 arrays and strings will return `true`, as will objects with a `next` function. For that reason this * function is only really useful within the library and therefore isn't exported. * * @private * * @param {*} obj The value to test for iterability. * @return {boolean} Either `true` if the value is iterable (`{@link module:xduce.iterator}` will return an iterator * for it) or `false` if it is not. */ function isIterable(obj) { return isImplemented(obj, 'iterator') || isString(obj) || isArray(obj) || isObject(obj); } module.exports = { isKvFormObject, iterator, isIterable };