From ecdc95accef1450e7c8fd9465e5886d02dc83a26 Mon Sep 17 00:00:00 2001 From: Muthu Kumar Date: Wed, 16 May 2018 18:47:12 +0530 Subject: [PATCH] [rewrite] Using ES6 classes --- example.js | 25 ------ index.js | 258 ++++++++++++++++++++++++++++++++++++++------------------- utils/cycle.js | 182 ++++++++++++++++++++++++++++++++++++++++ utils/index.js | 19 +++++ 4 files changed, 376 insertions(+), 108 deletions(-) delete mode 100644 example.js create mode 100644 utils/cycle.js create mode 100644 utils/index.js diff --git a/example.js b/example.js deleted file mode 100644 index c0e906a..0000000 --- a/example.js +++ /dev/null @@ -1,25 +0,0 @@ -const infiniteList = require('.'); - -const increment = i => i + 2; -const infinite = infiniteList.create(0, increment, 10); - -console.log(infinite.top()) -console.log(infinite.last()) - -console.log(infinite.get(5)) -console.log(infinite.get(5)) -console.log(infinite.get(6)) -console.log(infinite.get(14).next()) - -console.log(infinite.toString()) -console.log(infinite.top().next().next().toString()) - -console.log(infinite.take(5)) - -for(let i of infinite) { - if(i.index > 100) break; - // console.log(infinite.get(5).value); - console.log(i.value.toString()); -} - -console.log(infinite.clearCache()); \ No newline at end of file diff --git a/index.js b/index.js index efca296..3fb8d2b 100644 --- a/index.js +++ b/index.js @@ -1,114 +1,206 @@ -const always = x => _ => x; +/** + * ∞ + * Infinity: Create infinitely generating lists in JavaScript. + * @version 0.2.0 + * @author Muthu Kumar (MKRhere) + */ +// Utils +const { always, isNonZeroFalsy, stringify, areNumbers } = require('./utils'); + +/** + * An item of the InfiniteList class. Created when calling .get(n) on an InfiniteList. + * Exposed for instanceof utility sake. Not to be called directly. + * @class InfiniteListItem + */ class InfiniteListItem { - constructor (list, value, index) { - this.value = value, - this.index = index, - this.next = () => list.get(index + 1), - this.previous = () => list.get(index - 1), - this[Symbol.iterator] = () => ({ - next: () => ({ - done: false, - value: list.get(index + 1) - }) - }), - this.toString = () => { - const val = list.get(i).value; - return ( 'InfiniteListItem [ ..., ' - + ((typeof val === 'object' && val !== null) - ? JSON.stringify(val, null, 2) - : val.toString()) - + ', ... ]' ) + /** + * Creates an instance of InfiniteListItem. + * @param {any} list Parent list, instance of InfiniteList + * @param {any} value Current value + * @param {any} index Current index + * @memberof InfiniteListItem + */ + constructor(list, value, index) { + this.value = value; + this.index = index; + this.next = z => (!z ? list.get(index + 1) : list.get(index + z)); + this.previous = z => (!z ? list.get(index - 1) : list.get(index - z)); + + // Check if Symbol exists + if (typeof Symbol !== 'undefined' && Symbol.iterator) { + /** + * ES6 Symbol.iterator + * @returns {Iterable.<*>} + */ + this[Symbol.iterator] = () => ({ + next: () => ({ + done: false, + value: list.get(index + 1) + }) + }); } } + + /** + * toString method for pretty printing InfiniteListItem instance. + * @returns {String} Decycled and beautified string + */ + toString() { + return ('InfiniteListItem [ .. ' + + stringify(this.value) + + ' .. ]') + }; } -const infiniteList = { +class InfiniteList { /** - * InfiniteList Constructor. Iterates infinitely until index value is found. - * - * @param {any} start - * @param {any} next - * @returns InfiniteList instance + * InfiniteList Constructor. + * Iterates infinitely until index value is found. + * Stores cache in closure so when the same index is requested again, + * it's returned immediately. + * @param {*} start Starting value + * @param {Function} next Function to find next item + * Accepts current value and optionally previous value + * @constructs InfiniteList */ - create (start, next) { + constructor(start, next) { // Closure magic! let cache = []; let j = 0; - // Get list item of index i - function get(i) { + /** + * Get InfiniteListItem at index. + * @param {Number} index A non-negative integer representing index + * @returns {InfiniteListItem} + */ + this.get = function (index) { // Validation - if( - // i is a falsy value except 0 - (!i && i !== 0) - // is not a number - || (typeof i !== 'number') - // is... not a number - || Number.isNaN(i) + if ( + // i is a non-zero falsy value, or is negative + (isNonZeroFalsy(index) || index < 0) + || !areNumbers(index) ) return; - //TODO: Cache limiting. (Removed after unexpected behaviour) - + //TODO: Cache limiting. (Removed for unexpected behaviour) + // Initializing first item if it doesn't exist - if(!cache[0]) cache[0] = start; + if (!cache[0]) cache[0] = start; // If index were to be infinity, value and index are infinity - if(i === Infinity) return new InfiniteListItem(this, Infinity, Infinity) + if (index === Infinity) return new InfiniteListItem(this, Infinity, Infinity) // If index exists in cache, return the value - if(i in cache) return new InfiniteListItem(this, cache[i], i); + if (index in cache) return new InfiniteListItem(this, cache[index], index); // If i doesn't exist in cache - if(!(i in cache)) { - if(cache.length <= i && (cache.length - 1) in cache) - while (cache.length <= i) - cache[cache.length] = next(cache[cache.length - 1]); + if (!(index in cache)) { + if (cache.length <= index && (cache.length - 1) in cache) + while (cache.length <= index) + cache[cache.length] = next(cache[cache.length - 1], cache[cache.length - 2]); } - return new InfiniteListItem(this, cache[i], i); + return new InfiniteListItem(this, cache[index], index); } - const take = (from, to) => { - const arr = []; - let source, target; - // "from" number of elements - if(!to) { source = 0; target = from; } - // "target" is the end index! - else { source = from; target = to + 1 }; - for(let i = source; i < target; i ++) { - arr.push(get(i)); + /** + * Clear cache manually. + * Forces destroy reference to cache, and creates a new cache. + * Old cache will be GC'd. + * @returns {undefined} + */ + this.clearCache = () => (cache = [], undefined); + + // Check if Symbol exists + if (typeof Symbol !== 'undefined' && Symbol.iterator) { + /** + * ES6 Symbol.iterator + * @returns {Iterable.<*>} + */ + this[Symbol.iterator] = function () { + return { + next: () => ({ + done: false, + value: this.get(j++) + }) + } }; - return arr; - }; - - // Clear cache manually. - const clearCache = () => (cache = [], undefined); - - const top = function () { return this.get(0) }; - const end = function () { return this.get(Infinity) }; - const returns = { - get, - take, - top, - first: top, - end, - last: end, - clearCache, - [Symbol.iterator]: () => ({ - next: () => ({ - done: false, - value: get(j++) - }) - }), - toString: () => 'InfiniteList [ ' - + take(0, 10).map(x => (' ' + x.value.toString())) - + ' ... ]' - }; - return returns; + } } } -module.exports = infiniteList; -module.exports.InfiniteListItem = InfiniteListItem; +/** + * Takes a given number of elements from the InfiniteList. + * @param {Number} from Number of elements or starting index + * @param {Number} to Optional ending index + * @returns {Array} An array of InfiniteListItems + */ +InfiniteList.prototype.take = function (from, to) { + const arr = []; + + if( + isNonZeroFalsy(from) + || (from === 0 && isNonZeroFalsy(to)) // Take 0 elements? + || (!areNumbers(from) && isNonZeroFalsy(to)) + ) return arr; + + let source, target; + // "from" number of elements + if (isNonZeroFalsy(to)) { + source = 0; + target = from; + } + // "target" is the end index! + else { + source = from; + target = to + 1 + }; + + for (let i = source; i < target; i++) { + arr.push(this.get(i)); + }; + return arr; +}; + +/** + * Returns first element of InfiniteList. + * @returns {InfiniteListItem} Instance of InfiniteListItem + */ +InfiniteList.prototype.top = function () { + return this.get(0) +}; + +/** + * Returns last element of InfiniteList (Infinity). + * @returns {InfiniteListItem} Instance of InfiniteListItem + */ +InfiniteList.prototype.end = function () { + return this.get(Infinity) +}; + +/** + * toString method for pretty printing InfiniteList instance. + * Snips at 2 elements for arrays and objects, or 5 elements otherwise. + * @returns {String} Pretty printed InfiniteList + */ +InfiniteList.prototype.toString = function () { + const length = typeof this.first() === 'object' ? 2 : 5; + return [ + 'InfiniteList [', + this + .take(length) + .map(x => (' ' + stringify(x.value))) + + ',', + '... ]' + ] + .join(' '); +} + +/* Convenience methods */ +InfiniteList.prototype.first = InfiniteList.prototype.top; +InfiniteList.prototype.last = InfiniteList.prototype.end; + +// Exports +module.exports = InfiniteList; +module.exports.InfiniteListItem = InfiniteListItem; \ No newline at end of file diff --git a/utils/cycle.js b/utils/cycle.js new file mode 100644 index 0000000..5e63ccb --- /dev/null +++ b/utils/cycle.js @@ -0,0 +1,182 @@ +/* + cycle.js + 2018-05-15 + Public Domain. + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. +*/ + +// The file uses the WeakMap feature of ES6. + +/*jslint eval */ + +/*property + $ref, decycle, forEach, get, indexOf, isArray, keys, length, push, + retrocycle, set, stringify, test +*/ + +if (typeof JSON.decycle !== "function") { + JSON.decycle = function decycle(object, replacer) { + "use strict"; + + // Make a deep copy of an object or array, assuring that there is at most + // one instance of each object or array in the resulting structure. The + // duplicate references (which might be forming cycles) are replaced with + // an object of the form + + // {"$ref": PATH} + + // where the PATH is a JSONPath string that locates the first occurance. + + // So, + + // var a = []; + // a[0] = a; + // return JSON.stringify(JSON.decycle(a)); + + // produces the string '[{"$ref":"$"}]'. + + // If a replacer function is provided, then it will be called for each value. + // A replacer function receives a value and returns a replacement value. + + // JSONPath is used to locate the unique object. $ indicates the top level of + // the object or array. [NUMBER] or [STRING] indicates a child element or + // property. + + var objects = new WeakMap(); // object to path mappings + + return (function derez(value, path) { + + // The derez function recurses through the object, producing the deep copy. + + var old_path; // The path of an earlier occurance of value + var nu; // The new object or array + + // If a replacer function was provided, then call it to get a replacement value. + + if (replacer !== undefined) { + value = replacer(value); + } + + // typeof null === "object", so go on if this value is really an object but not + // one of the weird builtin objects. + + if ( + typeof value === "object" && + value !== null && + !(value instanceof Boolean) && + !(value instanceof Date) && + !(value instanceof Number) && + !(value instanceof RegExp) && + !(value instanceof String) + ) { + + // If the value is an object or array, look to see if we have already + // encountered it. If so, return a {"$ref":PATH} object. This uses an + // ES6 WeakMap. + + old_path = objects.get(value); + if (old_path !== undefined) { + return { + $ref: old_path + }; + } + + // Otherwise, accumulate the unique value and its path. + + objects.set(value, path); + + // If it is an array, replicate the array. + + if (Array.isArray(value)) { + nu = []; + value.forEach(function (element, i) { + nu[i] = derez(element, path + "[" + i + "]"); + }); + } else { + + // If it is an object, replicate the object. + + nu = {}; + Object.keys(value).forEach(function (name) { + nu[name] = derez( + value[name], + path + "[" + JSON.stringify(name) + "]" + ); + }); + } + return nu; + } + return value; + }(object, "$")); + }; +} + + +if (typeof JSON.retrocycle !== "function") { + JSON.retrocycle = function retrocycle($) { + "use strict"; + + // Restore an object that was reduced by decycle. Members whose values are + // objects of the form + // {$ref: PATH} + // are replaced with references to the value found by the PATH. This will + // restore cycles. The object will be mutated. + + // The eval function is used to locate the values described by a PATH. The + // root object is kept in a $ variable. A regular expression is used to + // assure that the PATH is extremely well formed. The regexp contains nested + // * quantifiers. That has been known to have extremely bad performance + // problems on some browsers for very long strings. A PATH is expected to be + // reasonably short. A PATH is allowed to belong to a very restricted subset of + // Goessner's JSONPath. + + // So, + // var s = '[{"$ref":"$"}]'; + // return JSON.retrocycle(JSON.parse(s)); + // produces an array containing a single element which is the array itself. + + var px = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\(?:[\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*")\])*$/; + + (function rez(value) { + + // The rez function walks recursively through the object looking for $ref + // properties. When it finds one that has a value that is a path, then it + // replaces the $ref object with a reference to the value that is found by + // the path. + + if (value && typeof value === "object") { + if (Array.isArray(value)) { + value.forEach(function (element, i) { + if (typeof element === "object" && element !== null) { + var path = element.$ref; + if (typeof path === "string" && px.test(path)) { + value[i] = eval(path); + } else { + rez(element); + } + } + }); + } else { + Object.keys(value).forEach(function (name) { + var item = value[name]; + if (typeof item === "object" && item !== null) { + var path = item.$ref; + if (typeof path === "string" && px.test(path)) { + value[name] = eval(path); + } else { + rez(item); + } + } + }); + } + } + }($)); + return $; + }; +} + +module.exports = JSON; \ No newline at end of file diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..1d7460f --- /dev/null +++ b/utils/index.js @@ -0,0 +1,19 @@ +const JSON = require('./cycle'); + +const always = x => _ => x; +const isNonZeroFalsy = _ => ( + (Boolean(_) === false) && _ !== 0 +); +const stringify = _ => { + return ((typeof _ === 'object' && _ !== null) ? + JSON.stringify(JSON.decycle(_), null, 2) : + _.toString()) +}; +const areNumbers = (...items) => items.every(x => (typeof x === 'number' && x !== NaN)); + +module.exports = { + always, + isNonZeroFalsy, + stringify, + areNumbers +} \ No newline at end of file