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);