├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tern-port 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) Bryce B. Baril 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | through2-reduce 2 | =============== 3 | 4 | [![NPM](https://nodei.co/npm/through2-reduce.png)](https://nodei.co/npm/through2-reduce/) 5 | 6 | `through2-reduce` is a thin wrapper around [through2](http://npm.im/through2) that works like `Array.prototype.reduce` but for streams. 7 | 8 | This is a *much* less common use-case with streams, but it can occasionally be useful to do a Reduce function on a stream. 9 | 10 | **EXPERIMENTAL** This is a bit of a bizarre one, so I wouldn't be surprised if there are some dangerous edge cases around flushing and pausing and such. Use at your own risk. 11 | 12 | This stream will only ever emit a *single* chunk. For more traditional `stream.Transform` filters or transforms, consider `through2` `through2-filter` or `through2-map`. 13 | 14 | Also, if your stream never ends, Reduce will never end. 15 | 16 | ```js 17 | 18 | var reduce = require("through2-reduce") 19 | 20 | var sum = reduce({objectMode: true}, function (previous, current) { return previous + current }) 21 | 22 | // vs. with through2: 23 | function combine (value, encoding, callback) { 24 | if (this.total == undefined) { 25 | this.total = value 26 | return callback() 27 | } 28 | this.total += value 29 | return callback() 30 | } 31 | function flush (callback) { 32 | this.push(this.total) 33 | return callback() 34 | } 35 | var sum = through2({objectMode: true}, combine, flush) 36 | 37 | // Then use your reduce: (e.g. source is an objectMode stream of numbers) 38 | source.pipe(sum).pipe(sink) 39 | 40 | // Works like `Array.prototype.reduce` meaning you can specify a function that 41 | // takes up to three* arguments: fn(previous, current, index) AND you can specify 42 | // an initial value 43 | var mean = reduce({objectMode: true}, function (prev, curr, index) { 44 | return prev - (prev - curr) / (index + 1) 45 | }, 0) 46 | 47 | ``` 48 | 49 | *Differences from `Array.prototype.reduce`: 50 | * No fourth `array` callback argument. That would require realizing the entire stream, which is generally counter-productive to stream operations. 51 | * `Array.prototype.reduce` doesn't modify the source Array, which is somewhat nonsensical when applied to streams. 52 | 53 | API 54 | ---- 55 | 56 | `reduce([options,] fn [,initial])` 57 | 58 | Create a Reduce *instance* 59 | 60 | `reduce.ctor([options,] fn [,initial])` 61 | 62 | Create a Reduce *class* 63 | 64 | `reduce.obj([options,] fn [,initial])` 65 | 66 | Create a Reduce *instance* that defaults to `objectMode: true`. 67 | 68 | `reduce.objCtor([options,] fn [,initial])` 69 | 70 | Just like ctor, but with `objectMode: true` defaulting to true. 71 | 72 | Options 73 | ------- 74 | 75 | * wantStrings: Automatically call chunk.toString() for the super lazy. 76 | * all other through2 options 77 | 78 | LICENSE 79 | ======= 80 | 81 | MIT 82 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = make 2 | module.exports.ctor = ctor 3 | module.exports.objCtor = objCtor 4 | module.exports.obj = obj 5 | 6 | var through2 = require("through2") 7 | var xtend = require("xtend") 8 | 9 | function ctor(options, fn, initial) { 10 | if (typeof options == "function") { 11 | initial = fn 12 | fn = options 13 | options = {} 14 | } 15 | 16 | var Reduce = through2.ctor(options, function (chunk, encoding, callback) { 17 | if (this.options.wantStrings) chunk = chunk.toString() 18 | 19 | // First chunk with no initial value set 20 | if (this._reduction === undefined && this._index == 0) { 21 | this._reduction = chunk 22 | return callback() 23 | } 24 | 25 | try { 26 | this._reduction = fn.call(this, this._reduction, chunk, this._index++) 27 | } catch (e) { 28 | var err = e 29 | } 30 | return callback(err) 31 | }, function (callback) { 32 | this.push(this._reduction) 33 | callback() 34 | }) 35 | Reduce.prototype._index = 0 36 | Reduce.prototype._reduction = initial 37 | return Reduce 38 | } 39 | 40 | function make(options, fn, initial) { 41 | return ctor(options, fn, initial)() 42 | } 43 | 44 | function objCtor(options, fn, initial) { 45 | if (typeof options == "function") { 46 | initial = fn 47 | fn = options 48 | options = {} 49 | } 50 | options = xtend({objectMode: true, highWaterMark: 16}, options) 51 | return ctor(options, fn, initial) 52 | } 53 | 54 | function obj(options, fn, initial) { 55 | if (typeof options == "function") { 56 | initial = fn 57 | fn = options 58 | options = {} 59 | } 60 | options = xtend({objectMode: true, highWaterMark: 16}, options) 61 | return make(options, fn, initial) 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "through2-reduce", 3 | "version": "1.1.1", 4 | "description": "A through2 wrapper to emulate Array.prototype.reduce for streams.", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "node test/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:brycebaril/through2-reduce.git" 15 | }, 16 | "keywords": [ 17 | "streams", 18 | "through", 19 | "through2", 20 | "reduce" 21 | ], 22 | "author": "Bryce B. Baril", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/brycebaril/through2-reduce/issues" 26 | }, 27 | "devDependencies": { 28 | "isnumber": "~1.0.0", 29 | "stream-spigot": "~3.0.5", 30 | "tape": "~4.0.0", 31 | "terminus": "~1.0.12" 32 | }, 33 | "dependencies": { 34 | "through2": "~2.0.0", 35 | "xtend": "~4.0.1" 36 | }, 37 | "homepage": "https://github.com/brycebaril/through2-reduce" 38 | } 39 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var test = require("tape").test 2 | 3 | var reduce = require("../") 4 | var spigot = require("stream-spigot") 5 | var concat = require("terminus").concat 6 | var isnumber = require("isnumber") 7 | 8 | test("ctor", function (t) { 9 | t.plan(2) 10 | 11 | var Sum = reduce.ctor(function (prev, curr) { 12 | return prev + curr 13 | }) 14 | 15 | function combine(result) { 16 | t.equals(result.length, 1, "Only one record passed") 17 | t.equals(result[0], 40, "Summed") 18 | } 19 | 20 | spigot({objectMode: true}, [2, 4, 8, 2, 6, 8, 10]) 21 | .pipe(new Sum({objectMode: true})) 22 | .pipe(concat({objectMode: true},combine)) 23 | }) 24 | 25 | test("ctor initial value", function (t) { 26 | t.plan(2) 27 | 28 | var Sum = reduce.ctor(function (prev, curr) { 29 | return prev + curr 30 | }, 5) 31 | 32 | function combine(result) { 33 | t.equals(result.length, 1, "Only one record passed") 34 | t.equals(result[0], 45, "Summed") 35 | } 36 | 37 | spigot({objectMode: true}, [2, 4, 8, 2, 6, 8, 10]) 38 | .pipe(new Sum({objectMode: true})) 39 | .pipe(concat({objectMode: true},combine)) 40 | }) 41 | 42 | test("ctor options initial value", function (t) { 43 | t.plan(2) 44 | 45 | var Sum = reduce.ctor({objectMode: true}, function (prev, curr) { 46 | return prev + curr 47 | }, 5) 48 | 49 | function combine(result) { 50 | t.equals(result.length, 1, "Only one record passed") 51 | t.equals(result[0], 45, "Summed") 52 | } 53 | 54 | spigot({objectMode: true}, [2, 4, 8, 2, 6, 8, 10]) 55 | .pipe(new Sum()) 56 | .pipe(concat({objectMode: true},combine)) 57 | }) 58 | 59 | test("use index & initial", function (t) { 60 | t.plan(10) 61 | 62 | var mean = reduce({objectMode: true, foo: "bar"}, function (prev, curr, index) { 63 | t.equals(this.options.foo, "bar", "can see options") 64 | return prev - (prev - curr) / (index + 1) 65 | }, 0) 66 | 67 | function combine(result) { 68 | t.equals(result.length, 1, "Only one record passed") 69 | t.equals(result[0], 5.25, "Averaged") 70 | } 71 | 72 | spigot({objectMode: true}, [2, 4, 8, 2, 6, 8, 10, 2]) 73 | .pipe(mean) 74 | .pipe(concat({objectMode: true},combine)) 75 | }) 76 | 77 | test("object", function (t) { 78 | t.plan(2) 79 | 80 | var mean = reduce({objectMode: true}, function (prev, curr, index) { 81 | var meanWidgets = prev.widgets - (prev.widgets - curr.widgets) / (index + 1) 82 | prev.widgets = meanWidgets 83 | prev.time = curr.time 84 | return prev 85 | }, {time: 0, widgets: 0}) 86 | 87 | function combine(result) { 88 | t.equals(result.length, 1, "Only one record passed") 89 | t.deepEquals(result[0], {time: 8, widgets: 5.25}, "Averaged") 90 | } 91 | 92 | spigot({objectMode: true}, [ 93 | {time: 1, widgets: 2}, 94 | {time: 2, widgets: 4}, 95 | {time: 3, widgets: 8}, 96 | {time: 4, widgets: 2}, 97 | {time: 5, widgets: 6}, 98 | {time: 6, widgets: 8}, 99 | {time: 7, widgets: 10}, 100 | {time: 8, widgets: 2}, 101 | ]) 102 | .pipe(mean) 103 | .pipe(concat({objectMode: true},combine)) 104 | }) 105 | 106 | test("obj", function (t) { 107 | t.plan(2) 108 | 109 | var mean = reduce.obj(function (prev, curr, index) { 110 | var meanWidgets = prev.widgets - (prev.widgets - curr.widgets) / (index + 1) 111 | prev.widgets = meanWidgets 112 | prev.time = curr.time 113 | return prev 114 | }, {time: 0, widgets: 0}) 115 | 116 | function combine(result) { 117 | t.equals(result.length, 1, "Only one record passed") 118 | t.deepEquals(result[0], {time: 8, widgets: 5.25}, "Averaged") 119 | } 120 | 121 | spigot({objectMode: true}, [ 122 | {time: 1, widgets: 2}, 123 | {time: 2, widgets: 4}, 124 | {time: 3, widgets: 8}, 125 | {time: 4, widgets: 2}, 126 | {time: 5, widgets: 6}, 127 | {time: 6, widgets: 8}, 128 | {time: 7, widgets: 10}, 129 | {time: 8, widgets: 2}, 130 | ]) 131 | .pipe(mean) 132 | .pipe(concat({objectMode: true},combine)) 133 | }) 134 | 135 | test("objCtor", function (t) { 136 | t.plan(2) 137 | 138 | var Mean = reduce.objCtor(function (prev, curr, index) { 139 | var meanWidgets = prev.widgets - (prev.widgets - curr.widgets) / (index + 1) 140 | prev.widgets = meanWidgets 141 | prev.time = curr.time 142 | return prev 143 | }, {time: 0, widgets: 0}) 144 | 145 | function combine(result) { 146 | t.equals(result.length, 1, "Only one record passed") 147 | t.deepEquals(result[0], {time: 8, widgets: 5.25}, "Averaged") 148 | } 149 | 150 | spigot({objectMode: true}, [ 151 | {time: 1, widgets: 2}, 152 | {time: 2, widgets: 4}, 153 | {time: 3, widgets: 8}, 154 | {time: 4, widgets: 2}, 155 | {time: 5, widgets: 6}, 156 | {time: 6, widgets: 8}, 157 | {time: 7, widgets: 10}, 158 | {time: 8, widgets: 2}, 159 | ]) 160 | .pipe(new Mean) 161 | .pipe(concat({objectMode: true},combine)) 162 | }) 163 | 164 | test("wantStrings", function (t) { 165 | t.plan(1) 166 | 167 | var Sort = reduce.ctor({wantStrings: true}, function (prev, curr) { 168 | if (prev < curr) return prev 169 | return curr 170 | }) 171 | 172 | function combine(result) { 173 | t.equals(result.toString(), "Bird", "First word alphabetically") 174 | } 175 | 176 | spigot(["Cat", "Dog", "Bird", "Rabbit", "Elephant"]) 177 | .pipe(new Sort()) 178 | .pipe(concat({objectMode: true},combine)) 179 | }) 180 | 181 | test("error", function (t) { 182 | t.plan(2) 183 | 184 | var Sum = reduce.ctor(function (prev, curr) { 185 | if (!isnumber(curr)) { 186 | this.emit("error", new Error("Values must be numeric")) 187 | } 188 | return prev + parseFloat(curr) 189 | }) 190 | 191 | function combine(result) { 192 | t.notOk(1, "Should not complete pipeline when error") 193 | } 194 | 195 | var summer = new Sum({objectMode: true}) 196 | summer.on("error", function (err) { 197 | t.ok(err) 198 | t.equals(err.message, "Values must be numeric") 199 | }) 200 | 201 | spigot({objectMode: true}, [2, 4, 8, 2, "cat", 8, 10]) 202 | .pipe(summer) 203 | .pipe(concat({objectMode: true},combine)) 204 | }) 205 | 206 | test("throw", function (t) { 207 | t.plan(2) 208 | 209 | var Sum = reduce.ctor(function (prev, curr) { 210 | if (!isnumber(curr)) { 211 | throw new Error("Values must be numeric") 212 | } 213 | return prev + parseFloat(curr) 214 | }) 215 | 216 | function combine(result) { 217 | t.notOk(1, "Should not complete pipeline when error") 218 | } 219 | 220 | var summer = new Sum({objectMode: true}) 221 | summer.on("error", function (err) { 222 | t.ok(err) 223 | t.equals(err.message, "Values must be numeric") 224 | }) 225 | 226 | spigot({objectMode: true}, [2, 4, 8, 2, "cat", 8, 10]) 227 | .pipe(summer) 228 | .pipe(concat({objectMode: true},combine)) 229 | }) 230 | --------------------------------------------------------------------------------