Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
['array.builders',
'array.selectors',
'collections.walk',
'function.arity',
'function.combinators',
'function.iterators',
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "underscore-contrib",
"version": "0.0.1",
"version": "0.1.0",
"main": "index.js",
"dependencies": {
"underscore": "*"
Expand All @@ -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"}
}
121 changes: 121 additions & 0 deletions test/collections.walk.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
11 changes: 11 additions & 0 deletions test/function.arity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");

});

});
14 changes: 14 additions & 0 deletions test/function.combinators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand Down
2 changes: 2 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<!-- contrib libraries -->
<script src="../underscore.array.builders.js"></script>
<script src="../underscore.array.selectors.js"></script>
<script src="../underscore.collections.walk.js"></script>
<script src="../underscore.function.arity.js"></script>
<script src="../underscore.function.combinators.js"></script>
<script src="../underscore.function.predicates.js"></script>
Expand All @@ -23,6 +24,7 @@
<!-- contrib tests -->
<script src="array.builders.js"></script>
<script src="array.selectors.js"></script>
<script src="collections.walk.js"></script>
<script src="function.arity.js"></script>
<script src="function.combinators.js"></script>
<script src="function.predicates.js"></script>
Expand Down
99 changes: 99 additions & 0 deletions underscore.collections.walk.js
Original file line number Diff line number Diff line change
@@ -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);
26 changes: 25 additions & 1 deletion underscore.function.arity.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
20 changes: 20 additions & 0 deletions underscore.function.combinators.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ----------------------------------
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);