diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f744573 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://gumroad.com/l/hGYGh diff --git a/README.md b/README.md index 9508e37..15c6b6e 100644 --- a/README.md +++ b/README.md @@ -25,29 +25,112 @@ Essential.js is an alternative to [Underscore.js](http://underscorejs.org/) that ## API -```coffeescript -module.exports = { - # Core - _, id, K, - builtin, toArray, - variadic, apply, applyNew, - ncurry, λ, curry, partial, - flip, flip3, nflip, - unary, binary, nary, - compose, pcompose, sequence, over, - notF, not:notF, eq, notEq, typeOf, isType, - toObject, extend, deepExtend, deepClone, forOwn, - fold, fold1, foldr, foldr1, map, filter, any, all, each, indexOf, concat, - slice, first, last, rest, initial, take, drop, - inArray, remove, tails, uniqueBy, unique, dups, - flatten, union, intersection, flatMap, - pluck, deepPluck, where, deepWhere, - values, pairs, interleave, intersperse, intercalate, - zip, zipWith, zipObject, unzipObject, - range, shuffle, - sortBy, groupBy, countBy, - format, template, gmatch, permutations, combinations, powerset, - # Fantasy - fmap, ap, chain, liftA, seqM -} -``` + + + Function Examplecall, Expected output + -------------------------------------- ------------------------------------------------------------------- + + ncurry ncurry(2, add)(1)(2), 3 + curry curry(add)(1)(2), 3 + partial - in order partial(add, 1)(2), 3 + partial - with placeholder partial(add, _, 1)(2), 3 + partial - with placeholder interleaved partial(append, _, 'b', _)('a','c'), 'abc' + apply apply(add, [1,2]), 3 + applyNew applyNew(Person, ['Josh',25]) instanceof Person, true + flip flip(sub)(2, 3), 1 + flip3 flip3((x, y, z) -> x - y - z)(2, 3, 5), 0 + nflip nflip(append)('a','b','c'), 'cba' + unary [1,2,3].map(unary variadic), [[1], [2], [3]] + binary [1,2,3].map(binary variadic), [[1,0], [2,1], [3,2]] + nary [1,2,3].map(nary 1, variadic), [[1], [2], [3]] + compose compose(curry(add)(1), curry(mul)(2))(2), 5 + sequence sequence(curry(add)(1), curry(mul)(2))(2), 6 + seq - shorthand for sequence seq(curry(add)(1), curry(mul)(2))(2), 6 + pipe functions thru eachother pipe(curry(add)(1), curry(mul)(2))(2), 6 + pcompose pcompose(add(1), mul(2), add(3))([1,2,3]), [2,4,6] + over over(add, mul(2), 3, 4), 14 + notF notF(even)(2), false + eq eq('foo')('foo'), true + notEq notEq('foo')('bar'), true + isType [isType('Array',[]), isType('Object',{})], [true, true] + toObject toObject(['a',1,'b',2,'c',3]), {a:1, b:2, c:3} + extend extend(obj, {b:2, c:3}, {c:4}), {a:1, b:2, c:4} + extend - mutates target obj, {a:1, b:2, c:4} + deepExtend - object deepExtend(x, y), {a: {b: 123, c: 2}} + deepClone a, deepClone a + forOwn forOwn([], ((acc, k, v) -> acc.concat [k, v]), {a:1, b:2, c:3}), ['a',1,'b',2,'c',3] + fold fold(0, add, [1,2,3]), 6 + foldr foldr(6, sub, [1,2,3]), 0 + map map(curry(add)(1), [1,2,3]), [2,3,4] + filter filter(even, [1,2,3,4]), [2,4] + any any(even, [1,2,3,4]), true + all all(even, [1,2,3,4]), false + indexOf indexOf(2, [1,2,3]), 1 + concat concat([1,2], [3,4], [5,6]), [1,2,3,4,5,6] + first first([1,2,3]), 1 + last last([1,2,3]), 3 + rest rest([1,2,3]), [2,3] + initial initial([1,2,3]), [1,2] + take take(2, [1,2,3]), [1,2] + drop drop(2, [1,2,3]), [3] + inArray inArray([1,2,3], 2), true + uniqueBy uniqueBy(((x) -> x.length), ['a','b','aa','bb']), ['a','aa'] + unique unique([1,1,2,2,3,3]), [1,2,3] + dups dups([1,1,2,2,3,4]), [1,2] + flatten flatten([1,[2,[3,[4]]]]), [1,2,3,4] + union union([1,2], [2,3], [3,4]), [1,2,3,4] + intersection intersection([1,2,3],[1,2,4],[1,2,5]), [1,2] + flatMap flatMap([1,2], (x) -> flatMap([3,4], (y) -> x + y)), [4,5,5,6] + pluck array pluck(1, [1,2,3]), 2 + pluck object pluck('a', {a:1, b:2, c:3}), 1 + deepPluck true, true + where where({name:'Peter'}, [{name:'Peter'},{name:'Jon'},{name:'Mike'}]), [{name:'Peter'}] + deepWhere deepWhere({a:{b:1}}, [{a:{b:1}}, {c:2}]), [{a:{b:1}}] + values values({a:1, b:2, c:3}), [1,2,3] + pairs pairs({a:1, b:2, c:3}), [['a',1],['b',2],['c',3]] + zip zip([1,2,3], [4,5,6]), [[1,4], [2,5], [3,6]] + zipWith zipWith(add, [1,2,3], [4,5,6]), [5,7,9] + zipObject zipObject(['a','b','c'],[1,2,3]), {a:1, b:2, c:3} + unzipObject unzipObject({a:1, b:2}), [['a','b'], [1,2]] + range range(0,10), [0..10] + shuffle - copies array shuffle(xs) is xs, false + sortBy - collection sortBy(id, [3,4,2,5,1]), [1,2,3,4,5] + groupBy groupBy(Math.round, [1.1,1.2,1.3,1.6,1.7,1.8]), {1:[1.1,1.2,1.3], 2:[1.6,1.7,1.8]} + groupBy - collection groupBy(((x) -> x.n), [{n:1},{n:1},{n:2},{n:2}]), {1:[{n:1},{n:1}],2:[{n:2},{n:2}]} + countBy countBy(Math.round, [1.1,1.2,1.3,1.6,1.7,1.8]), {1:3, 2:3} + format - formatting strings format(['a','c'], '%1b%2d'), 'abcd' + template - string evaluation template({a:'a', c:'c'}, '#{a}b#{c}d'), 'abcd' + gmatch gmatch(/\{(.+?)\}/g, '{a}b{c}d'), ['a','c'] + either - handy for fallback either( ((id) -> null), ((id) -> 'not found'), 1234 ), 'not found' + bindAll - bind obj functions to scope bindAll( obj, [scope] ) + + + # async map + var arr, done; + arr = [400, 300, 200, 100]; + done = function(err) { + console.log("async done"); + if( err ) console.log( err.toString() ); + }; + mapAsync(arr, done, function(v, k, next) { + return setTimeout(function() { + return next(); + }, v); + }); + + # reactive streams + onbuttonpress = pipe( + createEventStream("#mybutton",'click'), + pick('target'), + pick('value') + ) + +## Local scope + +If you [don't want to use global scope using expose()](http://stackoverflow.com/questions/2613310/ive-heard-global-variables-are-bad-what-alternative-solution-should-i-use), but +you still want to use FP without `_.` syntax-noise, you can include it in your local scope like this: + + require('essentialjs').local()(require) + diff --git a/lib/essential.js b/lib/essential.js index 0110f11..0db4610 100644 --- a/lib/essential.js +++ b/lib/essential.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.9.3 +// Generated by CoffeeScript 1.10.0 /* * Essential.js 1.1.20 @@ -7,7 +7,7 @@ */ (function() { - var K, _, all, any, ap, apply, applyNew, binary, builtin, chain, combinations, compose, concat, countBy, curry, deepClone, deepExtend, deepPluck, deepWhere, drop, dups, each, eq, extend, filter, first, flatMap, flatten, flip, flip3, fmap, fold, fold1, foldr, foldr1, forOwn, format, gmatch, groupBy, id, inArray, indexOf, initial, intercalate, interleave, intersection, intersperse, isType, last, liftA, map, nary, ncurry, nflip, notEq, notF, over, pairs, partial, pcompose, permutations, pluck, powerset, range, remove, rest, seqM, sequence, shuffle, slice, sortBy, tails, take, template, toArray, toObject, typeOf, unary, union, unique, uniqueBy, unzipObject, values, variadic, where, zip, zipObject, zipWith, λ, + var K, _, add, all, any, ap, append, apply, applyNew, binary, bindAll, builtin, chain, combinations, compose, concat, countBy, createEventStream, curry, deepClone, deepExtend, deepPluck, deepWhere, drop, dups, each, either, eq, extend, filter, first, flatMap, flatten, flip, flip3, fmap, fold, fold1, foldr, foldr1, forOwn, format, gmatch, groupBy, id, inArray, indexOf, initial, intercalate, interleave, intersection, intersperse, isType, last, liftA, map, mapAsync, mapKeys, mul, nary, ncurry, nflip, notEq, notF, over, pairs, partial, pcompose, permutations, pipe, pluck, powerset, psequence, range, remove, rest, seq, seqM, sequence, shuffle, slice, sortBy, sub, tails, take, template, toArray, toObject, typeOf, unary, union, unique, uniqueBy, unzipObject, values, variadic, where, zip, zipObject, zipWith, λ, slice1 = [].slice, hasProp = {}.hasOwnProperty, indexOf1 = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; @@ -141,7 +141,9 @@ }; }; - sequence = nflip(compose); + pipe = seq = sequence = nflip(compose); + + psequence = nflip(pcompose); over = λ(function(f, g, x, y) { return f(g(x), g(y)); @@ -239,9 +241,25 @@ return foldr(xs[0], f, xs); }); - map = flip(builtin(Array.prototype.map)); + map = ncurry(2, function() { + var input, obj, result; + obj = false; + input = (isType('Object', arguments[1]) ? (obj = unzipObject(arguments[1]))[1] : arguments[1]); + result = input.map(arguments[0]); + return (obj ? zipObject(obj[0], result) : result); + }); - filter = flip(builtin(Array.prototype.filter)); + mapKeys = function(obj, f) { + return zipObject(map(arguments[0], Object.keys(arguments[1])), unzipObject(arguments[1])[1]); + }; + + filter = ncurry(2, function() { + var input, obj, result; + obj = false; + input = (isType('Object', arguments[1]) ? (obj = unzipObject(arguments[1]))[1] : arguments[1]); + result = input.filter(arguments[0]); + return (obj ? zipObject(obj[0], result) : result); + }); any = flip(builtin(Array.prototype.some)); @@ -591,12 +609,94 @@ }, ctor.of([])); }); + add = λ(function(x, y) { + return x + y; + }); + + mul = λ(function(x, y) { + return x * y; + }); + + sub = λ(function(x, y) { + return x - y; + }); + + append = function() { + var as; + as = 1 <= arguments.length ? slice1.call(arguments, 0) : []; + return as.reduce(add); + }; + + either = curry(function(a, b, data) { + return a(data) || b(data); + }); + + bindAll = function(obj, scope) { + if (scope == null) { + scope = obj; + } + return forOwn(obj, function(k, v) { + if (typeof v === "function") { + return obj[k] = v.bind(scope); + } + }); + }; + + mapAsync = function(arr, done, cb) { + if( !arr || arr.length == 0 ) done() + var f, funcs, i, k, v; + funcs = []; + i = 0; + for (k in arr) { + v = arr[k]; + f = function(i, v) { + return function() { + var e, error; + try { + if (funcs[i + 1] != null) { + return cb(v, i, funcs[i + 1]); + } else { + return cb(v, i, done); + } + } catch (error) { + e = error; + return done(new Error(e)); + } + }; + }; + funcs.push(f(i++, v)); + } + return funcs[0](); + }; + + createEventStream = function(selector, event) { + return function(next) { + var element, elements, l, len, results; + if (selector[0] === "#") { + element = document.querySelector(selector); + if (element) { + return element.addEventListener(event, next); + } + } else { + elements = document.querySelectorAll(selector); + results = []; + for (l = 0, len = elements.length; l < len; l++) { + element = elements[l]; + results.push(element.addEventListener(event, next)); + } + return results; + } + }; + }; + module.exports = { _: _, id: id, K: K, builtin: builtin, toArray: toArray, + mapAsync: mapAsync, + createEventStream: createEventStream, variadic: variadic, apply: apply, applyNew: applyNew, @@ -613,6 +713,8 @@ compose: compose, pcompose: pcompose, sequence: sequence, + seq: seq, + pipe: pipe, over: over, notF: notF, not: notF, @@ -630,6 +732,7 @@ foldr: foldr, foldr1: foldr1, map: map, + mapKeys: mapKeys, filter: filter, any: any, all: all, @@ -677,6 +780,12 @@ permutations: permutations, combinations: combinations, powerset: powerset, + bindAll: bindAll, + add: add, + mul: mul, + sub: sub, + append: append, + either: either, fmap: fmap, ap: ap, chain: chain, @@ -686,4 +795,21 @@ module.exports.expose = partial(extend, _, module.exports); + module.exports.local = (function() { + var code; + code = function() { + var k, str, v; + str = 'var ejs = require("essentialjs")'; + for (k in this) { + v = this[k]; + if (k !== '_') { + str += " ;" + k + " = ejs." + k; + } + } + return str + ";"; + }; + code.apply(this); + return new Function('require', code.apply(this)); + }).bind(module.exports); + }).call(this); diff --git a/package.json b/package.json index 98d5f1a..1193932 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "elclanrs", "name": "essentialjs", "description": "Essential helpers for functional JavaScript", - "version": "1.1.20", + "version": "1.1.3", "main": "lib/essential.js", "repository": { "type": "git", @@ -12,8 +12,9 @@ "coffeeify": "^0.7.0" }, "scripts": { - "compile": "coffee -c -o lib src/essential.coffee ", + "compile": "coffee -c -o lib src/essential.coffee ; browserify --outfile essential.min.js -r .", "test": "coffee test", + "gendoc": "grep '^test ' test/index.coffee | sed \"s/^test //g;s/^'//g;s/',/@/\" | column -t -s'@' | awk '{print \" \"$0}'", "prepublish": "npm test && npm run-script compile" }, "licenses": [{ diff --git a/src/essential.coffee b/src/essential.coffee index df7376f..78a8a25 100644 --- a/src/essential.coffee +++ b/src/essential.coffee @@ -41,7 +41,8 @@ nary = λ (n, f) -> (as...) -> f as[0...n]... compose = (fs...) -> fs.reduce (f, g) -> (as...) -> f g as... pcompose = (fs...) -> (xs) -> xs.map (x, i) -> fs[i]? x -sequence = nflip compose +pipe = seq = sequence = nflip compose +psequence = nflip pcompose over = λ (f, g, x, y) -> f g(x), g y @@ -90,8 +91,21 @@ fold = flip3 builtin Array::reduce fold1 = λ (f, xs) -> fold xs[0], f, xs foldr = flip3 builtin Array::reduceRight foldr1 = λ (f, xs) -> foldr xs[0], f, xs -map = flip builtin Array::map -filter = flip builtin Array::filter +map = ncurry( 2, () -> + obj = false + input = ( if isType 'Object', arguments[1] then (obj = unzipObject(arguments[1]))[1] else arguments[1] ) + result = input.map arguments[0] + return ( if obj then zipObject obj[0], result else result ) +) +mapKeys = (obj,f) -> zipObject map( arguments[0], Object.keys arguments[1]), unzipObject(arguments[1])[1] + +filter = ncurry( 2, () -> + obj = false + input = ( if isType 'Object', arguments[1] then (obj = unzipObject(arguments[1]))[1] else arguments[1] ) + result = input.filter arguments[0] + return ( if obj then zipObject obj[0], result else result ) +) + any = flip builtin Array::some all = flip builtin Array::every each = flip builtin Array::forEach @@ -277,20 +291,58 @@ seqM = λ (ctor, ms) -> ctor.of [] ) +add = λ (x, y) -> x + y +mul = λ (x, y) -> x * y +sub = λ (x, y) -> x - y +append = (as...) -> as.reduce add + +# either provides an easy fallback for default data (`either foo(),default()`) +either = curry (a,b,data) -> a(data) || b(data) + +# easily map all functions of a module to itself (or another scope) +# to ensure `this` reference from changing +bindAll = (obj,scope) -> + scope = obj if not scope? + forOwn obj, (k,v) -> obj[k] = v.bind scope if typeof v is "function" + +mapAsync = (arr,done,cb) -> + done() if not arr or arr.length == 0 + funcs = [] ; i=0 + for k,v of arr + f = (i,v) -> + () -> + try + if funcs[i+1]? + cb v,i, funcs[i+1] + else cb v,i, done + catch e + done new Error(e) + funcs.push f(i++,v) + funcs[0]() + +createEventStream = (selector,event) -> + (next) -> + if selector[0] == "#" + element = document.querySelector(selector) + element.addEventListener( event, next ) if element + else + elements = document.querySelectorAll(selector) + element.addEventListener( event, next ) for element in elements + # Exports # module.exports = { # Core _, id, K, - builtin, toArray, + builtin, toArray, mapAsync, createEventStream, variadic, apply, applyNew, ncurry, λ, curry, partial, flip, flip3, nflip, unary, binary, nary, - compose, pcompose, sequence, over, + compose, pcompose, sequence, seq, pipe, over, notF, not:notF, eq, notEq, typeOf, isType, toObject, extend, deepExtend, deepClone, forOwn, - fold, fold1, foldr, foldr1, map, filter, any, all, each, indexOf, concat, + fold, fold1, foldr, foldr1, map, mapKeys, filter, any, all, each, indexOf, concat, slice, first, last, rest, initial, take, drop, inArray, remove, tails, uniqueBy, unique, dups, flatten, union, intersection, flatMap, @@ -299,9 +351,19 @@ module.exports = { zip, zipWith, zipObject, unzipObject, range, shuffle, sortBy, groupBy, countBy, - format, template, gmatch, permutations, combinations, powerset, + format, template, gmatch, permutations, combinations, powerset, bindAll + # Math / Logical + add, mul, sub, append, either, # Fantasy fmap, ap, chain, liftA, seqM } module.exports.expose = partial extend, _, module.exports +module.exports.local = ( () -> + code = () -> + str = 'var ejs = require("essentialjs")' + str += " ;#{k} = ejs.#{k}" for k,v of @ when k != '_' + return str+";" + code.apply @ + new Function( 'require', code.apply @ ) +).bind module.exports diff --git a/test/index.coffee b/test/index.coffee index 6a7d02b..911cafc 100644 --- a/test/index.coffee +++ b/test/index.coffee @@ -2,13 +2,8 @@ require('../src/essential.coffee').expose global assert = require('assert').deepEqual test = (name, result, expected) -> - assert result, expected, "#{name}: expected #{JSON.stringify expected} but got #{JSON.stringify result}" - console.log "#{name} ✓" - -add = λ (x, y) -> x + y -mul = λ (x, y) -> x * y -sub = λ (x, y) -> x - y -append = (as...) -> as.reduce add + assert JSON.stringify(result), JSON.stringify(expected), "#{name}: expected #{JSON.stringify expected} but got #{JSON.stringify result}" + console.log "#{name}✓" even = (x) -> x % 2 is 0 @@ -37,6 +32,8 @@ test 'nary', [1,2,3].map(nary 1, variadic), [[1], [2], [3]] test 'compose', compose(curry(add)(1), curry(mul)(2))(2), 5 test 'sequence', sequence(curry(add)(1), curry(mul)(2))(2), 6 +test 'seq - shorthand for sequence', seq(curry(add)(1), curry(mul)(2))(2), 6 +test 'pipe functions thru eachother', pipe(curry(add)(1), curry(mul)(2))(2), 6 test 'pcompose', pcompose(add(1), mul(2), add(3))([1,2,3]), [2,4,6] test 'over', over(add, mul(2), 3, 4), 14 @@ -64,6 +61,8 @@ test 'forOwn', forOwn([], ((acc, k, v) -> acc.concat [k, v]), {a:1, b:2, c:3}), test 'fold', fold(0, add, [1,2,3]), 6 test 'foldr', foldr(6, sub, [1,2,3]), 0 test 'map', map(curry(add)(1), [1,2,3]), [2,3,4] +test 'map object values', map( curry(add)(1), {id:1,foo:1}), {id:2,foo:2} +test 'map object keys', mapKeys( curry(add)(1), {id:1,foo:1}), {'1id':1,'1foo':1} test 'filter', filter(even, [1,2,3,4]), [2,4] test 'any', any(even, [1,2,3,4]), true test 'all', all(even, [1,2,3,4]), false @@ -109,15 +108,28 @@ test 'range', range(0,10), [0..10] xs = [1..5] test 'shuffle - copies array', shuffle(xs) is xs, false -test 'sortBy', sortBy(id, [3,4,2,5,1]), [1,2,3,4,5] +test 'sortBy - collection', sortBy(id, [3,4,2,5,1]), [1,2,3,4,5] test 'groupBy', groupBy(Math.round, [1.1,1.2,1.3,1.6,1.7,1.8]), {1:[1.1,1.2,1.3], 2:[1.6,1.7,1.8]} test 'groupBy - collection', groupBy(((x) -> x.n), [{n:1},{n:1},{n:2},{n:2}]), {1:[{n:1},{n:1}],2:[{n:2},{n:2}]} test 'countBy', countBy(Math.round, [1.1,1.2,1.3,1.6,1.7,1.8]), {1:3, 2:3} -test 'format', format(['a','c'], '%1b%2d'), 'abcd' -test 'template', template({a:'a', c:'c'}, '#{a}b#{c}d'), 'abcd' +test 'format - formatting strings', format(['a','c'], '%1b%2d'), 'abcd' +test 'template - string evaluation', template({a:'a', c:'c'}, '#{a}b#{c}d'), 'abcd' test 'gmatch', gmatch(/\{(.+?)\}/g, '{a}b{c}d'), ['a','c'] +test 'either - handy for fallback', either( ((id) -> null), ((id) -> 'not found'), 1234 ), 'not found' + +test 'bindAll - bind obj functions to scope', true, true + + +# async map over array +arr = [400,300,200,100] +done = (err) -> console.log "async done" +mapAsync arr, done, (v,k,next) -> + setTimeout () -> + next() + ,v + ### TODO remove tails