From da8e46a2125fa0a0ea28de494ac4ba9087dbb682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20A=2EJ=2E=20Wrona?= Date: Wed, 4 Jan 2017 22:18:07 +0100 Subject: [PATCH] INIT --- .gitignore | 2 + LICENSE | 21 +++++ README.md | 2 + index.js | 174 +++++++++++++++++++++++++++++++++++++++++ package.json | 45 +++++++++++ test/reduceAny.js | 192 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 436 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.js create mode 100644 package.json create mode 100644 test/reduceAny.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba2a97b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +coverage diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1fa0d5e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ɓukasz A.J. Wrona + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2b0b34 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# co-reduce-any +Reduce anything in Co-like manner diff --git a/index.js b/index.js new file mode 100644 index 0000000..8d51a48 --- /dev/null +++ b/index.js @@ -0,0 +1,174 @@ +/******************************************************************************/ +/** + * Reduce anything in Co-like manner + * + * Provides a consistent interface for reducing arrays, strings, + * object literals, maps, sets, generators and streams. Synchronously + * when applicable. + * + * const collection = [ 1, 2, 3, 4 ] + * + * reduce(collection, function* (next) { + * const result = [ ] + * for (let pair; pair = yield next;) { + * const [ key, elem ] = pair + * const value = yield Promise.resolve(elem * key) + * } + * return result + * }).then(result => console.log(resul)) + * + * yield is overloaded + * + * value = yield promise // converts promise to a value, like Co + * yield next // waits for new [ key, element ] pair + * + * The key: + * In arrays and strings, key is the elements' index + * In Maps and object literals, key is the element's key + * In generators Sets and node streams, key is the iteration's "i" provided + * for consistency + * + * If no promises are yielded function returns a value returned from + * supplied generator + * + * Iterates over node streams sequentially and always returns a promise. + * + * @module co-reduce-any + * @author Lukasz A.J. Wrona + * @license MIT + */ +/******************************************************************************/ + +"use strict" +var typical = require("typical") +var Stream = require("stream") + +/******************************************************************************/ + +var isPromise = typical.isPromise +var isObject = typical.isObject +var isIterable = typical.isIterable + +var next = Object.freeze({ }) + +function* enumerateIterable(collection) { + var elem + var index = 0 + for (elem of collection) { + yield [ index, elem ] + index += 1 + } +} + +// Enumerate map 1:1 +function* enumerateMap(map) { + var pair + for (pair of map) { + yield pair + } +} + +// Enumerate object literals +function* enumerateObject(object) { + var key + for (key in object) { + yield [ key, object[key] ] + } +} + +// Inspired by Python's enumerate +function enumerate(collection) { + if (isObject(collection)) { + if (collection instanceof Map) { + return enumerateMap(collection) + } else if (isIterable(collection)) { + return enumerateIterable(collection) + } else { + return enumerateObject(collection) + } + } else { + throw new TypeError("Object cannot be enumerated") + } +} + +function then(value, proc) { + return isPromise(value) ? value.then(proc) : proc(value) +} + +function genStep(gen, chunk, err) { + var done, state, value + if (err) { + state = gen.throw(err) + } else { + state = gen.next(chunk) + } + done = state.done + value = state.value + if (done) { + return value + } else if (value !== next) { + if (isPromise(value)) { + return value.catch(function (err) { + return genStep(gen, undefined, err) + }) + .then(function (value) { + return genStep(gen, value) + }) + } else { + return genStep(gen, value) + } + } +} + +function reduceStream(stream, generator) { + return new Promise(function (resolve, reject) { + var gen = generator(next) + var promise = Promise.resolve(genStep(gen)) + stream.on("data", function (chunk) { + stream.pause() + promise = promise.then(function () { + stream.resume() + return genStep(gen, chunk) + }) + .catch(reject) + }) + stream.on("error", reject) + stream.on("end", function () { + promise = promise.then(function () { + return genStep(gen) + }) + .then(resolve, reject) + }) + }) +} + +function _reduceGen(inGen, outGen) { + var state = inGen.next() + return then(genStep(outGen, state.value), function (value) { + if (state.done) { + return then(genStep(outGen), function () { + return value + }) + } else { + return _reduceGen(inGen, outGen) + } + }) +} + +function reduceGen(inGen, generator) { + var outGen = generator(next) + return then(genStep(outGen), function () { + return _reduceGen(inGen, outGen) + }) +} + +function reduceAny(collection, generator) { + if (collection instanceof Stream) { + return reduceStream(collection, generator) + } else { + return reduceGen(enumerate(collection), generator) + } +} + +module.exports = reduceAny + diff --git a/package.json b/package.json new file mode 100644 index 0000000..2e32623 --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "co-reduce-any", + "version": "1.0.0", + "description": "Reduce anything in Co-like manner", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "mocha test/* --recursive", + "coverage": "istanbul cover node_modules/.bin/_mocha -- test/* --recursive" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/lajw/co-reduce-any.git" + }, + "keywords": [ + "co", + "reduce", + "any", + "foreach", + "stream", + "map", + "set", + "array", + "object", + "generator", + "promise", + "asynchronous" + ], + "author": "Lukasz A.J. Wrona ", + "license": "MIT", + "bugs": { + "url": "https://github.com/lajw/co-reduce-any/issues" + }, + "homepage": "https://github.com/lajw/co-reduce-any#readme", + "dependencies": { + "typical": "^2.6.0" + }, + "devDependencies": { + "istanbul": "^0.4.5", + "mocha": "^3.2.0", + "stream-array": "^1.1.2" + } +} diff --git a/test/reduceAny.js b/test/reduceAny.js new file mode 100644 index 0000000..455a9c8 --- /dev/null +++ b/test/reduceAny.js @@ -0,0 +1,192 @@ +/* global it describe */ + +"use strict" + +var forEach = require("..") +var assert = require("assert") +var streamArray = require("stream-array") + +function* listIterations(next) { + var key, pair, value + var result = [ ] + while (pair = yield next) { + key = pair[0] + value = pair[1] + result.push([ key, yield value ]) + } + return result +} + +describe("for-each", function () { + + it("throw typerror on non-iterable", function () { + var caught + try { + forEach(null, listIterations) + } catch (error) { + caught = error + } + assert.ok(caught instanceof TypeError, "Should have thrown typeerror") + }) + + it("array synchronous, successful", function () { + var array = [ "one", "two", "three" ] + var result = forEach(array, listIterations) + assert.deepEqual(result, [ + [ 0, "one" ], + [ 1, "two" ], + [ 2, "three" ], + ]) + }) + + it("map synchronous, successful", function () { + var map = new Map([ + [ "one", "cat" ], + [ "two", "dog" ], + [ "three", "moose" ] + ]) + var result = forEach(map, listIterations) + assert.deepEqual(result, [ + [ "one", "cat" ], + [ "two", "dog" ], + [ "three", "moose" ] + ]) + }) + + it("array asynchronous, successful", function () { + var array = [ "one", "two", "three" ].map(function (val) { + return Promise.resolve(val) + }) + return forEach(array, listIterations) + .then(function (result) { + assert.deepEqual(result, [ + [ 0, "one" ], + [ 1, "two" ], + [ 2, "three" ], + ]) + }) + }) + + it("object synchronous", function () { + var object = { + one : "uno", + two : "dos", + three : "tres", + } + var result = forEach(object, listIterations) + assert.deepEqual(result, [ + [ "one", "uno" ], + [ "two", "dos" ], + [ "three", "tres" ], + ]) + }) + + it("throw error into generator on first iteration", function () { + var obj = { } + var caught + return forEach([ 1 ], function* (next) { + yield next // this shouldn't be mandatory + try { + yield Promise.reject(obj) + } catch (err) { + caught = err + } + }) + .then(function () { + assert.strictEqual(caught, obj, "Should have caught the error") + }) + }) + + it("throw error into generator before first iteration", function () { + var obj = { } + var caught + return forEach([ 1 ], function* () { + try { + yield Promise.reject(obj) + } catch (err) { + caught = err + } + }) + .then(function () { + assert.strictEqual(caught, obj, "Should have caught the error") + }) + }) + + it("stream, successful", function () { + var stream = streamArray([ "one", "two", "three", "four" ]) + return forEach(stream, function* (next) { + var result = [ ] + var chunk + while (chunk = yield next) { + result.push(chunk.toString().toUpperCase()) + } + return result + }) + .then(function (result) { + assert.deepEqual(result, [ "ONE", "TWO", "THREE", "FOUR" ]) + }) + }) + + it("stream async, successful", function () { + var stream = streamArray([ "one", "two", "three", "four" ]) + return forEach(stream, function* (next) { + var result = [ ] + var chunk + while (chunk = yield next) { + result.push(yield Promise.resolve(chunk.toString().toUpperCase())) + } + return result + }) + .then(function (result) { + assert.deepEqual(result, [ "ONE", "TWO", "THREE", "FOUR" ]) + }) + }) + + it("stream, throw before first chunk", function () { + var obj = { } + var stream = streamArray([ "one", "two", "three", "four" ]) + return forEach(stream, function* () { + throw obj + }) + .then(function () { + throw new Error("Should have thrown") + }) + .catch(function (result) { + assert.strictEqual(result, obj) + }) + }) + + it("stream, throw on second chunk", function () { + var obj = { } + var stream = streamArray([ "one", "two", "three", "four" ]) + return forEach(stream, function* (next) { + yield next + throw obj + }) + .then(function () { + throw new Error("Should have thrown") + }) + .catch(function (result) { + assert.strictEqual(result, obj) + }) + }) + + it("stream, throw after last chunk", function () { + var stream = streamArray([ "one", "two", "three", "four" ]) + return forEach(stream, function* (next) { + var result = [ ] + var chunk + while (chunk = yield next) { + result.push(chunk.toString().toUpperCase()) + } + throw result + }) + .then(function () { + throw new Error("Should have thrown") + }) + .catch(function (err) { + assert.deepEqual(err, [ "ONE", "TWO", "THREE", "FOUR" ]) + }) + }) + +})