I created a procedural implementation of the nested set model in JavaScript. The use case for this small library was that the front-end(presented as an MVC) needs to traverse hierarchical data from back-ends.
The library that I created satisfies the following:
- DSL-like architecture, with clear distinction between a tree and a node.
- Searching through the set for a node matching a given partial of it
- Retrieve parents for any given node
- Retrieve children for any given node
- Retrieve descendants for any particular node
- Utility methods determining "what" a node is, given it's coordinates
- A lesser distinguished way of finding siblings (parents[0] -> children = siblings)
The library is divided into two classes.
NestedSetModel
is the base class and acts as a starting point for traversing the tree.
It's first function is to order the set by left values ascendantly. This is done for the presumption that descendants are always at a higher index(of the set) than parents. The second function is to create an index of all right values.
var NestedSetModel = function(model) {
this.model = [];
this.index = {};
// sort the set for deterministic order
model.sort(function(a, b)
{
return a.left - b.left;
});
var self = this;
// create an index
for(var index in model) {
if (!model.hasOwnProperty(index)) {
continue;
}
this.index[model[index].right] = index;
}
for(var entry in model) {
if (!model.hasOwnProperty(entry)) {
continue;
}
try {
var node = new NestedSetModelNode(model[entry], model, this.index);
this.model.push(node);
} catch(e) {}
}
return this;
}
NestedSetModel.prototype.compareNodes = function(a, b, strict) {
var strict = strict || false;
if (a === b) {
return true;
}
var keys = [
Object.keys(a),
Object.keys(b)
];
if (strict && keys[0].length !== keys[1].length) {
return false;
}
for (var i = 0; i <= keys[1].length; i++) {
var prop = keys[1][i];
if (a[prop] !== b[prop]) {
return false;
}
}
if (!strict) {
return true;
}
for (var prop in keys[0]) {
if (b[prop] !== undefined && a[prop] !== b[prop]) {
return false;
}
if (typeof a[prop] === 'object'
&& this.compareNodes(a[prop], b[prop], true) === false) {
return false
}
}
return true;
}
NestedSetModel.prototype.find = function(partial, strict) {
for (var key in this.model) {
if (!this.model.hasOwnProperty(key)) {
continue;
} else if (this.compareNodes(this.model[key], partial, strict)) {
return new NestedSetModelNode(this.model[key], this.model, this.index);
}
}
}
The second and final class is NestedSetModelNode
. It contains methods related to relationships between nodes. I've omitted methods that perform rudementy calculations.
var NestedSetModelNode = function(node, model, index) {
var self = this;
Object.keys(node).forEach(function(prop) {
self[prop] = node[prop];
});
this.model = model;
this.index = index;
}
NestedSetModelNode.prototype.parents = function() {
var parents = [];
var self = this;
this.model.map(function(node) {
if (self.left > node.left && self.right < node.right) {
parents.push(new NestedSetModelNode(node, self.model, self.index));
}
});
return parents;
}
NestedSetModelNode.prototype.descendants = function() {
var descendants = [];
var num_items = Math.floor((this.right - this.left) / 2);
for(var right in this.index) {
if (right < this.right && right > this.left) {
var node = this.model[this.index[right]];
descendants.push(new NestedSetModelNode(node, this.model, this.index));
}
}
return descendants;
}
NestedSetModelNode.prototype.children = function() {
var children = [];
var right = this.right - 1;
while(true) {
if (right === this.left) {
break;
}
var child = this.model[this.index[right]];
children.push(new NestedSetModelNode(child, this.model, this.index));
right = child.left - 1;
}
return children.reverse();
}
Semantics aside, what improvements can I implement that will make this library more consistent and practical(performance wise) to compute client-side?
So far, I have created an index tree of right values for optimization of r - l > 1
lookups.
This increases performance, as only half the amount of objects are actually traversed and compared.