UNPKG

tree-walk

Version:

Helpers for traversing, inspecting, and transforming arbitrary trees.

369 lines (292 loc) 12.2 kB
'use strict'; /* global document */ var test = require('tape'), _ = require('underscore'); var walk = require('../'); function getSimpleTestTree() { return { val: 0, l: { val: 1, l: { val: 2 }, r: { val: 3 } }, r: { val: 4, l: { val: 5 }, r: { val: 6 } } }; } function getMixedTestTree() { 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 } ] }; } function getVal(node) { return node.val; } test('basic', function(t) { // 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); t.equal(tree.val, 16, 'should visit subtrees first'); tree = getSimpleTestTree(); walk.preorder(tree, visitor); t.equal(tree.val, 5, 'should visit subtrees after the node itself'); var f = function(x, y) {}; var visited = walk.map(f, walk.preorder, _.identity); t.deepEquals(visited, [f], 'function w/ no properties is treated as a leaf'); f.foo = 3; visited = walk.map(f, walk.preorder, _.identity); t.deepEqual(visited, [f, 3], 'own property of function is be visited'); (function(x) { visited = walk.map(arguments, walk.preorder, _.identity); })('x', 99); t.equal(visited.length, 3); t.deepEqual(visited.slice(1), ['x', 99], 'arguments is treated like an array'); t.end(); }); test('circularRefs', function(t) { var tree = getSimpleTestTree(); tree.l.l.r = tree; t.throws(function() { walk.preorder(tree, _.identity); }, /cycle/, 'preorder t.throws an exception'); t.throws(function() { walk.postorder(tree, _.identity); }, /cycle/, 'postorder t.throws an exception'); tree = getSimpleTestTree(); tree.r.l = tree.r; t.throws(function() { walk.preorder(tree, _.identity); }, /cycle/, 'exception for a self-referencing node'); tree = getSimpleTestTree(); tree.l.l = tree.l.r = {}; t.ok(walk.reduce(tree, function() { return true; }), "same object can be in diff't branches"); tree.l.l = tree.l.r = 'hai'; t.ok(walk.reduce(tree, function() { return true; }), "same string can be in diff't branches"); // Create a custom walker that reuses the same object as the walk target. // This should not be considered a cycle. var wrapper = []; var walker = walk(function(node, key) { if (node && node.hasOwnProperty('val')) { wrapper[0] = node.l; wrapper[1] = node.r; return wrapper; } return node; }); t.ok(walker.reduce(tree, function() { return true; }), 'target object can be the same'); t.end(); }); test('simpleMap', function(t) { var visitor = function(node, key) { if (_.has(node, 'val')) return node.val; if (key !== 'val') throw new Error('Leaf node with incorrect key'); return this && this.leafChar || '-'; }; var visited = walk.map(getSimpleTestTree(), walk.preorder, visitor).join(''); t.equal(visited, '0-1-2-3-4-5-6-', 'pre-order map'); visited = walk.map(getSimpleTestTree(), walk.postorder, visitor).join(''); t.equal(visited, '---2-31--5-640', 'post-order map'); var context = { leafChar: '*' }; visited = walk.map(getSimpleTestTree(), walk.preorder, visitor, context).join(''); t.equal(visited, '0*1*2*3*4*5*6*', 'pre-order with context'); visited = walk.map(getSimpleTestTree(), walk.postorder, visitor, context).join(''); t.equal(visited, '***2*31**5*640', 'post-order with context'); if (typeof document !== 'undefined') { var root = document.querySelector('#map-test'); var ids = walk.map(root, walk.preorder, function(el) { return el.id; }); t.deepEqual(ids, ['map-test', 'id1', 'id2'], 'preorder map with DOM elements'); ids = walk.map(root, walk.postorder, function(el) { return el.id; }); t.deepEqual(ids, ['id1', 'id2', 'map-test'], 'postorder map with DOM elements'); } t.end(); }); test('mixedMap', function(t) { var visitor = function(node) { return _.isString(node) ? node.toLowerCase() : null; }; var tree = getMixedTestTree(); var preorderResult = walk.map(tree, walk.preorder, visitor); t.equal(preorderResult.length, 19, 'all nodes are visited'); t.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); t.deepEqual(preorderResult.sort(), postorderResult.sort(), 'post-order map on a mixed tree'); tree = [['foo'], tree]; var result = walk.map(tree, walk.postorder, visitor); t.deepEqual(_.difference(result, postorderResult), ['foo'], 'map on list of trees'); t.end(); }); test('pluck', function(t) { var tree = getSimpleTestTree(); tree.val = { val: 'z' }; var plucked = walk.pluckRec(tree, 'val'); t.equal(plucked.shift(), tree.val); t.equal(plucked.join(''), 'z123456', 'pluckRec is recursive'); plucked = walk.pluck(tree, 'val'); t.equal(plucked.shift(), tree.val); t.equal(plucked.join(''), '123456', 'regular pluck is not recursive'); tree.l.r.foo = 42; t.equal(walk.pluck(tree, 'foo').shift(), 42, 'pluck a value from deep in the tree'); tree = getMixedTestTree(); t.deepEqual(walk.pluck(tree, 'city'), ['Munich', 'San Francisco', 'Toronto'], 'pluck from a mixed tree'); tree = [tree, { city: 'Loserville', population: 'you' }]; t.deepEqual(walk.pluck(tree, 'population'), [1378000, 812826, 2615000, 'you'], 'pluck from a list of trees'); t.end(); }); test('reduce', function(t) { var add = function(a, b) { return a + b; }; var leafMemo = []; var sum = function(memo, node) { if (_.isObject(node)) return _.reduce(memo, add, 0); t.strictEqual(memo, leafMemo); return node; }; var tree = getSimpleTestTree(); t.equal(walk.reduce(tree, sum, leafMemo), 21); // A more useful example: transforming a tree. // Returns a new node where the left and right subtrees are swapped. var mirror = function(memo, node) { if (!_.has(node, 'r')) return node; return _.extend(_.clone(node), { l: memo.r, r: memo.l }); }; // Returns the '-' for internal nodes, and the value itself for leaves. var toString = function(node) { return _.has(node, 'val') ? '-' : node; }; tree = walk.reduce(getSimpleTestTree(), mirror); t.equal(walk.reduce(tree, sum, leafMemo), 21); t.equal(walk.map(tree, walk.preorder, toString).join(''), '-0-4-6-5-1-3-2', 'pre-order map'); t.end(); }); test('find', function(t) { var tree = getSimpleTestTree(); // Returns a visitor function that will succeed when a node with the given // value is found, and then raise an exception the next time it's called. var findValue = function(value) { var found = false; return function(node) { if (found) throw new Error('already found!'); found = (node.val === value); return found; }; }; t.equal(walk.find(tree, findValue(0)).val, 0); t.equal(walk.find(tree, findValue(6)).val, 6); t.deepEqual(walk.find(tree, findValue(99)), undefined); t.end(); }); test('filter', function(t) { var tree = getSimpleTestTree(); tree.r.val = '.oOo.'; // Remove one of the numbers. var isEvenNumber = function(x) { return _.isNumber(x) && x % 2 === 0; }; t.equal(walk.filter(tree, walk.preorder, _.isObject).length, 7, 'filter objects'); t.equal(walk.filter(tree, walk.preorder, _.isNumber).length, 6, 'filter numbers'); t.equal(walk.filter(tree, walk.postorder, _.isNumber).length, 6, 'postorder filter numbers'); t.equal(walk.filter(tree, walk.preorder, isEvenNumber).length, 3, 'filter even numbers'); // With the identity function, only the value '0' should be omitted. t.equal(walk.filter(tree, walk.preorder, _.identity).length, 13, 'filter on identity function'); t.end(); }); test('reject', function(t) { var tree = getSimpleTestTree(); tree.r.val = '.oOo.'; // Remove one of the numbers. t.equal(walk.reject(tree, walk.preorder, _.isObject).length, 7, 'reject objects'); t.equal(walk.reject(tree, walk.preorder, _.isNumber).length, 8, 'reject numbers'); t.equal(walk.reject(tree, walk.postorder, _.isNumber).length, 8, 'postorder reject numbers'); // With the identity function, only the value '0' should be kept. t.equal(walk.reject(tree, walk.preorder, _.identity).length, 1, 'reject with identity function'); t.end(); }); test('custom traversal', function(t) { var tree = getSimpleTestTree(); // Set up a walker that will not traverse the 'val' properties. var walker = walk(function(node) { return _.omit(node, 'val'); }); t.equal(walker.pluck(tree, 'val').length, 7, 'pluck with custom traversal'); t.equal(walker.pluckRec(tree, 'val').length, 7, 'pluckRec with custom traversal'); t.equal(walker.map(tree, walk.postorder, _.identity).length, 7, 'traversal strategy is dynamically scoped'); // Check that the default walker is unaffected. t.equal(walk.map(tree, walk.postorder, _.identity).length, 14, 'default map still works'); t.equal(walk.pluckRec(tree, 'val').join(''), '0123456', 'default pluckRec still works'); t.end(); }); test('custom traversal shorthand', function(t) { var tree = getSimpleTestTree(); t.deepEqual(walk(['l']).map(tree, walk.preorder, getVal), [0, 1, 2]); t.deepEqual(walk(['l', 'r']).map(tree, walk.preorder, getVal), [0, 1, 2, 3, 4, 5, 6]); t.deepEqual(walk(['l', 'z']).map(tree, walk.preorder, getVal), [0, 1, 2]); t.deepEqual(walk([]).map(tree, walk.preorder, getVal), [0], 'with empty array, just visits root'); t.deepEqual(walk('z').map(tree, walk.preorder, getVal), [0], 'with unknown keys, just visits root'); // When just a string is passed as the traversal strategy, the behaviour is // subtly different: the traversal target (in this case, the array of child // nodes) is not treated as a node. tree = { val: 0, children: [{ val: 1, children: [] }, { val: 2 }]}; t.deepEqual(walk('children').map(tree, walk.preorder, getVal), [0, 1, 2]); t.deepEqual(walk(['children']).map(tree, walk.preorder, getVal), [0, undefined]); t.deepEqual(walk('z').map(tree, walk.preorder, getVal), [0], 'with unknown key, just visits root'); t.end(); }); test('reduce with custom traversal', function(t) { var tree = { op: '+', operands: [ { value: 1 }, { op: '+', operands: [ { value: 2 }, { value: 3 }] } ]}; var walker = walk(function(node) { return node.operands; }); var answer = walker.reduce(tree, function(subResults, node) { t.ok(_.isObject(node), 'should only visit objects, not primitives'); if ('value' in node) return node.value; t.ok(_.isArray(subResults), 'child results should be an array'); if (node.op === '+') return subResults[0] + subResults[1]; }); t.equal(answer, 6); t.end(); }); test('attributes', function(t) { var tree = getSimpleTestTree(); // Set up a walker that will not traverse the 'val' properties. var walker = walk(function(node) { return _.omit(node, 'val'); }); var count = 0; var min = walker.createAttribute(function(subResults, node) { count++; if (subResults) return Math.min(node.val, Math.min(subResults.l, subResults.r)); return node.val; }); t.equal(min(tree), 0); t.equal(count, 7); t.equal(min(tree), 0); t.equal(min(tree.l), 1); t.equal(min(tree.r), 4); t.equal(count, 7, 'visitor should be memoized for all nodes'); var max = walker.createAttribute(function(subResults, node) { if (subResults) return Math.max(node.val, Math.max(subResults.l, subResults.r)); return node.val; }); t.equal(max(tree), 6); t.end(); }); test('stopping recursion', function(t) { var tree = getSimpleTestTree(); var vals = []; walk.preorder(tree, function(node, key) { if (key === 'l') { return walk.STOP_RECURSION; } if (_.isObject(node)) { vals.push(node.val); } }); t.deepEqual(vals, [0, 4, 6]); t.end(); });