@ima-worldhealth/tree
Version:
Build a tree from an adjacency list and operate on it
222 lines (182 loc) • 6 kB
JavaScript
/**
* @class Tree
*
* @description
* This file contains the generic class definition of a tree. A tree is defined
* as an array of JSON objects having a parent key referring to another member
* of the array. The only exception is the root node, which does not need to be
* in the tree.
*/
const debug = require('debug')('Tree');
const clone = data => JSON.parse(JSON.stringify(data));
class Tree {
constructor(data = [], options = {
parentKey : 'parent',
rootId : 0,
}) {
this._parentKey = options.parentKey;
this._rootNode = {
id : options.rootId,
};
// build the tree with the provided root id and parentKey
this._rootNode.children = this.buildTreeFromArray(clone(data));
this.buildNodeIndex();
// build a node index
debug(`#constructor() built tree with ${data.length} nodes.`);
}
buildTreeFromArray(nodes, parentId = this._rootNode.id) {
debug(`#builtTreeFromArray() called with (Array(${nodes.length}), ${parentId}).`);
// recursion base-case: return nothing if empty array
if (nodes.length === 0) { return null; }
// find nodes which are the children of parentId
const children = nodes.filter(node => node[this._parentKey] === parentId);
// recurse - for each child node, compute their child-trees using the same
// buildTreeFromArray() command
children.forEach(node => {
node.children = this.buildTreeFromArray(nodes, node.id);
});
// return the list of children
return children;
}
buildNodeIndex() {
this._nodeIndex = {};
this.walk(node => {
this._nodeIndex[node.id] = node;
});
}
prune(fn) {
debug('#prune() called on tree structure.');
const markNodeToPruneFn = (node) => {
node._toPrune = fn(node);
};
this.walk(childNode => markNodeToPruneFn(childNode));
const prev = this.toArray();
const pruned = prev.filter(node => !node._toPrune);
debug(`#prune() removed ${prev.length - pruned.length} nodes from the tree`);
// return an array missing the pruned values
return pruned;
}
size() {
let size = 0;
this.walk(() => size++);
return size;
}
toArray() {
const array = [];
this.walk((node) => array.push(node));
return array;
}
getRootNode() {
return this._rootNode;
}
/**
* @method isRootNode
*
* @description
* Returns true if the node is the root node.
*
* @param node {Object} - a tree node to compare.
*/
isRootNode(node) {
return node.id === this._rootNode.id;
}
/**
* @method find
*
* @description
* Gets a node by its id.
*/
find(id) {
return this._nodeIndex[id];
}
/**
* @method walk
*
* @description
* Internal method to be used to walk through children, calling a function on
* each child. The caller can walk around the tree, calling a passed function
* on either the ascending or the descending step.
*
* @param fn {Function} - the function to call for each node in the tree
* @param callFnBeforeRecurse {Boolean} - specify whether to call the function
* on the descending or ascending direction of the recursion. The descending
* direction is before recursing through the children. The ascending direction
* is after all children have been looped through.
* @param currentNode {Object} - the current node in the walk.
* @param parentNode {Object} - the parent of the current node in the walk.
*/
walk(fn, callFnBeforeRecurse = true, currentNode = this._rootNode, parentNode = null) {
debug('#walk() called on tree structure.');
const callFnAfterRecurse = !callFnBeforeRecurse;
const recurse = () => {
currentNode.children.forEach(childNode => {
this.walk(fn, callFnBeforeRecurse, childNode, currentNode);
});
};
// if we start as the root node, then descend immediately.
if (this.isRootNode(currentNode)) {
recurse();
return;
}
// if we are supposed to call the function before recursion, we do that now
if (callFnBeforeRecurse) {
fn(currentNode, parentNode);
}
// recursive step: walk through children
recurse();
// if we are supposed to call the function after recursion, now is the time
if (callFnAfterRecurse) {
fn(currentNode, parentNode);
}
}
filterByLeaf(prop, value) {
// set the property of the child to the parent up to the top
this._rootNode.children.forEach(node => {
this.interate(node, prop, value, this._rootNode);
});
// let filter tree now
const data = this.toArray().filter(row => row[prop] === value);
this._rootNode.children = this.buildTreeFromArray(data);
this.buildNodeIndex();
}
// set the child's property to parent recursively up to the top
setPropertyToParent(node, prop, value) {
node[prop] = value;
if (node.parentNode) {
this.setPropertyToParent(node.parentNode, prop, value);
}
}
// walk around the tree
// search the node by property's value
interate(node, prop, value, parent) {
node.parentNode = parent;
if (node[prop] === value && !parent[prop]) {
this.setPropertyToParent(parent, prop, value);
}
if (node.children) {
node.children.forEach(child => {
this.interate(child, prop, value, node);
});
}
delete node.parentNode;
}
/**
* @method sort
*
* @description
* Sorts the child nodes in place by a provided comparison function.
*/
sort(comparisonFn) {
this.walk((childNode, parentNode) => parentNode.children.sort(comparisonFn));
}
}
// common functions used throughout the application.
Tree.common = {
computeNodeDepth : (currentNode, parentNode) => {
currentNode.depth = (parentNode.depth || 0) + 1;
},
sumOnProperty : (property, defaultValue = 0) => (currentNode, parentNode) => {
parentNode[property] = (parentNode[property] || defaultValue) + currentNode[property];
},
};
module.exports = Tree;