diff --git a/index.js b/index.js index 7972baf..a1c320e 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ ['array.builders', 'array.selectors', + 'collections.walk', 'function.arity', 'function.combinators', 'function.iterators', diff --git a/package.json b/package.json index 1b06477..1020a02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "underscore-contrib", - "version": "0.0.1", + "version": "0.1.0", "main": "index.js", "dependencies": { "underscore": "*" @@ -10,7 +10,7 @@ "url": "https://github.com/documentcloud/underscore-contrib.git" }, "license": "MIT", - "author": {"name": "Fogus", - "email": "me@fogus.me", + "author": {"name": "Fogus", + "email": "me@fogus.me", "url": "http://www.fogus.me"} } diff --git a/test/collections.walk.js b/test/collections.walk.js new file mode 100644 index 0000000..c591cda --- /dev/null +++ b/test/collections.walk.js @@ -0,0 +1,121 @@ +$(document).ready(function() { + + module("underscore.collections.walk"); + + var getSimpleTestTree = function() { + return { + val: 0, + l: { val: 1, l: { val: 2 }, r: { val: 3 } }, + r: { val: 4, l: { val: 5 }, r: { val: 6 } }, + }; + }; + + var getMixedTestTree = function() { + return { + current: + { city: 'Munich', aliases: ['Muenchen'], population: 1378000 }, + previous: [ + { city: 'San Francisco', aliases: ['SF', 'San Fran'], population: 812826 }, + { city: 'Toronto', aliases: ['TO', 'T-dot'], population: 2615000 }, + ] + }; + }; + + test("basic", function() { + // Updates the value of `node` to be the sum of the values of its subtrees. + // Ignores leaf nodes. + var visitor = function(node) { + if (node.l && node.r) + node.val = node.l.val + node.r.val; + }; + + var tree = getSimpleTestTree(); + _.walk.postorder(tree, visitor); + equal(tree.val, 16, 'should visit subtrees first'); + + tree = getSimpleTestTree(); + _.walk.preorder(tree, visitor); + equal(tree.val, 5, 'should visit subtrees after the node itself'); + }); + + test("circularRefs", function() { + var tree = getSimpleTestTree(); + tree.l.l.r = tree; + throws(function() { _.walk.preorder(tree, _.identity) }, TypeError, 'preorder throws an exception'); + throws(function() { _.walk.postrder(tree, _.identity) }, TypeError, 'postorder throws an exception'); + + tree = getSimpleTestTree(); + tree.r.l = tree.r; + throws(function() { _.walk.preorder(tree, _.identity) }, TypeError, 'exception for a self-referencing node'); + }); + + test("simpleMap", function() { + var visitor = function(node, key, parent) { + if (_.has(node, 'val')) return node.val; + if (key !== 'val') throw Error('Leaf node with incorrect key'); + return this.leafChar || '-'; + }; + var visited = _.walk.map(getSimpleTestTree(), _.walk.preorder, visitor).join(''); + equal(visited, '0-1-2-3-4-5-6-', 'pre-order map'); + + visited = _.walk.map(getSimpleTestTree(), _.walk.postorder, visitor).join(''); + equal(visited, '---2-31--5-640', 'post-order map'); + + var context = { leafChar: '*' }; + visited = _.walk.map(getSimpleTestTree(), _.walk.preorder, visitor, context).join(''); + equal(visited, '0*1*2*3*4*5*6*', 'pre-order with context'); + + visited = _.walk.map(getSimpleTestTree(), _.walk.postorder, visitor, context).join(''); + equal(visited, '***2*31**5*640', 'post-order with context'); + + if (document.querySelector) { + var root = document.querySelector('#map-test'); + var ids = _.walk.map(root, _.walk.preorder, function(el) { return el.id; }); + deepEqual(ids, ['map-test', 'id1', 'id2'], 'preorder map with DOM elements'); + + ids = _.walk.map(root, _.walk.postorder, function(el) { return el.id; }); + deepEqual(ids, ['id1', 'id2', 'map-test'], 'postorder map with DOM elements'); + } + }); + + test("mixedMap", function() { + var visitor = function(node, key, parent) { + return _.isString(node) ? node.toLowerCase() : null; + }; + + var tree = getMixedTestTree(); + var preorderResult = _.walk.map(tree, _.walk.preorder, visitor); + equal(preorderResult.length, 19, 'all nodes are visited'); + deepEqual(_.reject(preorderResult, _.isNull), + ['munich', 'muenchen', 'san francisco', 'sf', 'san fran', 'toronto', 'to', 't-dot'], + 'pre-order map on a mixed tree'); + + var postorderResult = _.walk.map(tree, _.walk.postorder, visitor); + deepEqual(preorderResult.sort(), postorderResult.sort(), 'post-order map on a mixed tree'); + + tree = [['foo'], tree]; + var result = _.walk.map(tree, _.walk.postorder, visitor); + deepEqual(_.difference(result, postorderResult), ['foo'], 'map on list of trees'); + }); + + test("pluck", function() { + var tree = getSimpleTestTree(); + tree.val = { val: 'z' }; + + var plucked = _.walk.pluckRec(tree, 'val'); + equal(plucked.shift(), tree.val); + equal(plucked.join(''), 'z123456', 'pluckRec is recursive'); + + plucked = _.walk.pluck(tree, 'val'); + equal(plucked.shift(), tree.val); + equal(plucked.join(''), '123456', 'regular pluck is not recursive'); + + tree.l.r.foo = 42; + equal(_.walk.pluck(tree, 'foo'), 42, 'pluck a value from deep in the tree'); + + tree = getMixedTestTree(); + deepEqual(_.walk.pluck(tree, 'city'), ['Munich', 'San Francisco', 'Toronto'], 'pluck from a mixed tree'); + tree = [tree, { city: 'Loserville', population: 'you' }]; + deepEqual(_.walk.pluck(tree, 'population'), [1378000, 812826, 2615000, 'you'], 'pluck from a list of trees'); + }); +}); diff --git a/test/function.arity.js b/test/function.arity.js index 6735f47..411d1d3 100644 --- a/test/function.arity.js +++ b/test/function.arity.js @@ -51,4 +51,15 @@ $(document).ready(function() { equal(flipWithArity(echo).length, echo.length, "flipWithArity gets its arity correct"); }); + + test("curry2", function () { + + function echo () { return [].slice.call(arguments, 0); } + + deepEqual(echo(1, 2), [1, 2], "Control test"); + deepEqual(_.curry2(echo)(1, 2), [1, 2], "Accepts arguments greedily"); + deepEqual(_.curry2(echo)(1)(2), [1, 2], "Accepts curried arguments"); + + }); + }); diff --git a/test/function.combinators.js b/test/function.combinators.js index f64aba1..93b0123 100644 --- a/test/function.combinators.js +++ b/test/function.combinators.js @@ -90,6 +90,20 @@ $(document).ready(function() { deepEqual(echo3(1,2,3), [[1], 2, 3], 'should return the arguments provded'); deepEqual(echo3(1,2,3,4), [[1, 2], 3, 4], 'should return the arguments provded'); }); + + test("mapArgsWith", function () { + var echo = _.unsplatl(function (args) { return args; }); + function double (n) { return n * 2; } + function plusOne (n) { return n + 1; } + + deepEqual(_.mapArgsWith(double, echo)(), [], "should handle the empty case") + deepEqual(_.mapArgsWith(double, echo)(42), [84], "should handle one arg") + deepEqual(_.mapArgsWith(plusOne, echo)(1, 2, 3), [2, 3, 4], "should handle many args") + + deepEqual(_.mapArgsWith(double)(echo)(), [], "should handle the empty case") + deepEqual(_.mapArgsWith(double)(echo)(42), [84], "should handle one arg") + deepEqual(_.mapArgsWith(plusOne)(echo)(1, 2, 3), [2, 3, 4], "should handle many args") + }); test("flip2", function() { var div = function(n, d) { return n/d; }; diff --git a/test/index.html b/test/index.html index e905c3b..b0c8b9e 100644 --- a/test/index.html +++ b/test/index.html @@ -11,6 +11,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/underscore.collections.walk.js b/underscore.collections.walk.js new file mode 100644 index 0000000..04a3fd3 --- /dev/null +++ b/underscore.collections.walk.js @@ -0,0 +1,99 @@ +// Underscore-contrib (underscore.collections.walk.js 0.0.1) +// (c) 2013 Patrick Dubroy +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // An internal object that can be returned from a visitor function to + // prevent a top-down walk from walking subtrees of a node. + var breaker = {}; + + var notTreeError = 'Not a tree: same object found in two different branches'; + + // Walk the tree recursively beginning with `root`, calling `beforeFunc` + // before visiting an objects descendents, and `afterFunc` afterwards. + function walk(root, beforeFunc, afterFunc, context) { + var visited = []; + (function _walk(value, key, parent) { + if (beforeFunc && beforeFunc.call(context, value, key, parent) === breaker) + return; + + if (_.isObject(value) || _.isArray(value)) { + // Keep track of objects that have been visited, and throw an exception + // when trying to visit the same object twice. + if (visited.indexOf(value) >= 0) throw new TypeError(notTreeError); + visited.push(value); + + // Recursively walk this object's descendents. If it's a DOM node, walk + // its DOM children. + _.each(_.isElement(value) ? value.children : value, _walk, context); + } + + if (afterFunc) afterFunc.call(context, value, key, parent); + })(root); + } + + function pluck(obj, propertyName, recursive) { + var results = []; + _.walk.preorder(obj, function(value, key) { + if (key === propertyName) { + results[results.length] = value; + if (!recursive) return breaker; + } + }); + return results; + } + + // Add the `walk` namespace + // ------------------------ + + _.walk = walk; + _.extend(walk, { + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object only after traversing its children. + postorder: function(obj, visitor, context) { + walk(obj, null, visitor, context); + }, + + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object before traversing its children. + preorder: function(obj, visitor, context) { + walk(obj, visitor, null, context) + }, + + // Produces a new array of values by recursively traversing `obj` and + // mapping each value through the transformation function `visitor`. + // `strategy` is the traversal function to use, e.g. `preorder` or + // `postorder`. + map: function(obj, strategy, visitor, context) { + var results = []; + strategy.call(null, obj, function(value, key, parent) { + results[results.length] = visitor.call(context, value, key, parent); + }); + return results; + }, + + // Return the value of properties named `propertyName` reachable from the + // tree rooted at `obj`. Results are not recursively searched; use + // `pluckRec` for that. + pluck: function(obj, propertyName) { + return pluck(obj, propertyName, false); + }, + + // Version of `pluck` which recursively searches results for nested objects + // with a property named `propertyName`. + pluckRec: function(obj, propertyName) { + return pluck(obj, propertyName, true); + } + }); + _.walk.collect = _.walk.map; // Alias `map` as `collect`. +})(this); diff --git a/underscore.function.arity.js b/underscore.function.arity.js index ca70cf6..ecefe91 100644 --- a/underscore.function.arity.js +++ b/underscore.function.arity.js @@ -64,8 +64,32 @@ return function quaternary (a, b, c, d) { return fun.call(this, a, b, c, d); }; + }, + + // greedy currying for functions taking two arguments. + curry2: function (fun) { + return function curried (first, optionalLast) { + if (arguments.length === 1) { + return function (last) { + return fun(first, last); + }; + } + else return fun(first, optionalLast); + }; + }, + + // greedy flipped currying for functions taking two arguments. + curry2flipped: function (fun) { + return function curried (last, optionalFirst) { + if (arguments.length === 1) { + return function (first) { + return fun(first, last); + }; + } + else return fun(optionalFirst, last); + }; } - + }); _.arity = (function () { diff --git a/underscore.function.combinators.js b/underscore.function.combinators.js index f68d04e..7fa5007 100644 --- a/underscore.function.combinators.js +++ b/underscore.function.combinators.js @@ -17,6 +17,17 @@ var truthy = function(x) { return (x !== false) && existy(x); }; var __reverse = [].reverse; var __slice = [].slice; + var __map = [].map; + + // n.b. depends on underscore.function.arity.js + + // Takes a target function and a mapping function. Returns a function + // that applies the mapper to its arguments before evaluating the body. + function baseMapArgs (fun, mapFun) { + return _.arity(fun.length, function () { + return fun.apply(this, __map.call(arguments, mapFun)); + }); + }; // Mixing in the combinator functions // ---------------------------------- @@ -149,6 +160,9 @@ } } }, + + // map the arguments of a function + mapArgs: _.curry2(baseMapArgs), // Returns a function that returns an array of the calls to each // given function for some arguments. @@ -208,5 +222,11 @@ }); _.unsplatr = _.unsplat; + + // map the arguments of a function, takes the mapping function + // first so it can be used as a combinator + _.mapArgsWith = _.curry2(_.flip(baseMapArgs)); + + })(this);