simple-tree-utils
Version:
Simple Tree Utils is the library to convert and manipulate with tree-like structures.
439 lines (438 loc) • 14.9 kB
JavaScript
const p = "id", P = "parentId", u = "children";
class o {
/**
* Constructor of class
* @param config - to configure class, if configuration option is omitted, default one is used
*/
constructor(e) {
this.idProp = (e == null ? void 0 : e.idProp) || p, this.parentIdProp = (e == null ? void 0 : e.parentIdProp) || P, this.childrenProp = (e == null ? void 0 : e.childrenProp) || u;
}
/**
* Convert list to tree like structure
* @param list list of objects, objects need to have id (as you configured, or 'id' by default) and parentId property (as you configured, or 'parentId' by default)
* @param rootParentId - id of root parent nodes (if not specified, root nodes are nodes with parentId of null)
* @returns tree structure
*/
list2Tree(e, t = null) {
return e.filter((r) => r[this.parentIdProp] === t).map((r) => ({
...r,
[this.childrenProp]: this.list2Tree(e, r[this.idProp])
}));
}
/**
* Convert tree like structure to list
* @param tree - tree of objects, objects need to have children (as you configured, or 'children' by default) and parentId property (as you configured, or 'parentId' by default)
* @returns list
*/
tree2List(e) {
return this._tree2List(o.deepCopy(e));
}
/**
* Convert tree like structure to list (helper method)
* @param tree tree of objects, objects need to have children (as you configured, or 'children' by default) and parentId property (as you configured, or 'parentId' by default)
* @param parentId - id of parent node
* @private
* @returns list
*/
_tree2List(e, t = null) {
return o.deepCopy(e).reduce((r, s) => {
const { [this.childrenProp]: i, ...h } = s;
return [
...r,
{ ...h, [this.parentIdProp]: t },
...i != null && i.length ? this._tree2List(i, h[this.idProp]) : []
];
}, []);
}
/**
* Method to get node in tree structure by given id
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns found node
*/
get(e, t) {
return this.find(e, (r) => r[this.idProp] === t);
}
/**
* Method to find node in tree structure by given callback function
* @param tree - tree structure to search in
* @param fn - callback function to find node
* @returns found node
* @example
* ```ts
* utils.find(tree, item => item.id === myId);
* ```
*/
find(e, t) {
const r = e.find((s) => t(s));
return r || e.reduce(
(s, i) => s || this.find(i[this.childrenProp] || [], t),
null
);
}
/**
* Method to iterate over all nodes
* @param tree - tree structure to iterate over
* @param fn - callback function to perform
*/
forEach(e, t) {
e == null || e.forEach((r) => {
t(r), this.forEach(r[this.childrenProp], t);
});
}
/**
* Method to find all nodes in tree structure by given callback function
* @param tree - tree structure to search in
* @param fn - callback function to find all nodes
* @returns all found nodes
* @example
* ```ts
* utils.filter(tree, item => item.id === myId);
* ```
*/
filter(e, t) {
const r = e.filter((s) => t(s));
return e.reduce((s, i) => [...s, ...i[this.childrenProp].length ? this.filter(i[this.childrenProp], t) : []], r);
}
/**
* Method to delete node in tree by given id (mutable operation!)
* @param tree - tree structure for node deleting
* @param id - identifier of node to delete
* @returns deleted node, if nothing deleted then returns null
*/
delete(e, t) {
return this.deleteBy(e, (r) => r[this.idProp] === t)[0] || null;
}
/**
* Method to delete node in tree by given callback function (mutable operation!)
* @param tree - tree structure for node deleting
* @param fn - callback function to remove all nodes
* @returns deleted nodes
* @example
* ```ts
* utils.deleteBy(tree, item => item.id === myId);
* ```
*/
deleteBy(e, t) {
const s = e.map((i, h) => ({ index: h, filter: t(i) })).filter(({ filter: i }) => i).map(({ index: i }) => i).reverse().reduce((i, h) => [...i, ...e.splice(h, 1)], []);
return e.reduce((i, h) => [...i, ...this.deleteBy(h[this.childrenProp] ?? [], t)], s);
}
/**
* Method to add new node to tree, node will be added as last child (mutable operation!)
* @param tree - tree structure for node adding
* @param parentId - identifier of parent node, null if new node should be on root level
* @param childData - data of new node
* @param anotherChildData - data of new additional nodes
*/
add(e, t, r, ...s) {
this._add("push", e, t, r, ...s);
}
/**
* Method to add new node to tree, node will be added as first child (mutable operation!)
* @param tree - tree structure for node adding
* @param parentId - identifier of parent node, null if new node should be on root level
* @param childData - data of new node
* @param anotherChildData - data of new additional nodes
*/
addUnshift(e, t, r, ...s) {
this._add("unshift", e, t, r, ...s);
}
/**
* Method to add new node to tree at the end or at the beginning (helper method)
* @param operation - how item should be added into tree
* @param tree - tree structure for node adding
* @param parentId - identifier of parent node, null if new node should be on root level
* @param childData - data of new node
* @param anotherChildData - data of new additional nodes
*/
_add(e, t, r, s, ...i) {
if (r == null) {
t[e](s, ...i);
return;
}
const h = t.findIndex((n) => n[this.idProp] == r);
if (h != -1) {
t[h][this.childrenProp][e](
{ [this.childrenProp]: [], ...s },
...i.map((n) => ({ [this.childrenProp]: [], ...n }))
);
return;
}
t.forEach((n) => this.add(n[this.childrenProp], r, s, ...i));
}
/**
* Method to update node by id with given data in tree (mutable operation!)
* @param tree - tree structure for node editing
* @param id - identifier of node to be updated
* @param data - new data of node (you should also pass children if you want to keep it)
*/
edit(e, t, r) {
const s = e.findIndex((i) => i[this.idProp] == t);
if (s != -1) {
e[s] = { [this.idProp]: e[s][this.idProp], [this.childrenProp]: [], ...r };
return;
}
e.forEach((i) => this.edit(i[this.childrenProp], t, r));
}
/**
* Method to get descendant nodes of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns all found children nodes
*/
getDescendants(e, t) {
const r = this.get(e, t);
return r ? this._getDescendants(r) : [];
}
/**
* Helper method to recursively get all descendant nodes of given node in tree structure
* @param node - we want to get all of its children
* @private
* @returns all found children nodes
*/
_getDescendants(e) {
return [
...e[this.childrenProp],
...e[this.childrenProp].reduce((t, r) => [...t, ...this._getDescendants(r)], [])
];
}
/**
* Method to get ancestors of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns all found parent nodes
*/
getAncestors(e, t) {
const r = [];
let s = this.getParent(e, t);
for (; s; )
r.push(s), s = this.getParent(e, s[this.idProp]);
return r.reverse();
}
/**
* Method to get nodes that are part of path from root
* Alias for {@link TreeUtils.getAncestors | getAncestors} method
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns all nodes that are part of path (ordered from root)
*/
getPathNodes(e, t) {
return this.getAncestors(e, t);
}
/**
* Method to get parent of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns found parent node, otherwise null
*/
getParent(e, t) {
return this._getParent(e, t);
}
/**
* Method to get parent of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @param parent - parent node, if we found something (for recursion only)
* @returns found parent node, otherwise null
*/
_getParent(e, t, r = null) {
return e.find((i) => i[this.idProp] === t) ? r : e.reduce(
(i, h) => i || this._getParent(h[this.childrenProp] || [], t, h),
null
);
}
/**
* Method to get children of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns children nodes of node
*/
getChildren(e, t) {
var r;
return ((r = this.get(e, t)) == null ? void 0 : r[this.childrenProp]) || [];
}
/**
* Method to get neighbours (neighbour is parent or child) of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns neighbours of node
*/
getNeighbours(e, t) {
return [this.getParent(e, t), ...this.getChildren(e, t)].filter((r) => r);
}
/**
* Method to get siblings of given node in tree structure
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns siblings of node
*/
getSiblings(e, t) {
var r;
return (((r = this.getParent(e, t)) == null ? void 0 : r[this.childrenProp]) || []).filter((s) => s[this.idProp] !== t);
}
/**
* Method to get leafs of subtree from given node
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns leafs of nodes
*/
getLeafs(e, t) {
return this.filter(this.getSubTree(e, t), (r) => !r[this.childrenProp].length);
}
/**
* Method to get subtree from given node (children of node)
* Alias for {@link TreeUtils.getChildren | getChildren} method
* @param tree - tree structure to search in
* @param id - identifier of node
* @returns subtree
*/
getSubTree(e, t) {
return this.getChildren(e, t);
}
/**
* Method to get size of subtree (number of nodes in the subtree)
* @param tree - tree structure
* @param id - identifier of node
* @returns size of subtree
*/
getSize(e, t) {
return this.tree2List(this.getSubTree(e, t)).length + 1;
}
/**
* Method to get breath of subtree (the number of leaves in subtree)
* @param tree - tree structure
* @param id - identifier of node
* @returns breath of subtree
*/
getBreath(e, t) {
return this.getLeafs(e, t).length;
}
/**
* Method to get depth of node (the depth of a node is the length of the path to its root i.e., its root path)
* @param tree - tree structure
* @param id - identifier of node
* @returns depth of node
*/
getDepth(e, t) {
return this.getPathNodes(e, t).length;
}
/**
* Method to get level of node (the level of a node is the number of edges along the unique path between it and the root node)
* @param tree - tree structure
* @param id - identifier of node
* @returns level of node
*/
getLevel(e, t) {
return this.getDepth(e, t) + 1;
}
/**
* Method to get degree of node (for a given node, its number of children. A leaf, by definition, has degree zero)
* @param tree - tree structure
* @param id - identifier of node
* @returns degree of node
*/
getDegree(e, t) {
return this.getChildren(e, t).length;
}
/**
* Method to get degree of tree (the degree of a tree is the maximum degree of a node in the tree)
* @param tree - tree structure
* @returns degree of tree
*/
getTreeDegree(e) {
return e.reduce((t, r) => Math.max(t, r[this.childrenProp].length, this.getTreeDegree(r[this.childrenProp])), 0);
}
/**
* Method to get nodes in tree at specific level
* @param tree - tree structure to search in
* @param level - desired level
* @returns all nodes, that are on specific level
*/
getNodesAtLevel(e, t) {
return this._getNodesAtLevel(e, t);
}
/**
* Helper method to get nodes in tree at specific level recursively
* @param tree - tree structure to search in
* @param level - desired level
* @param actualLevel - actual level, that is searched
* @private
* @returns all nodes, that are on specific level
*/
_getNodesAtLevel(e, t, r = 0) {
return e.reduce((s, i) => [
...s,
...t === r ? [i] : [],
...r < t ? this._getNodesAtLevel(i[this.childrenProp], t, r + 1) : []
], []);
}
/**
* Method to get width on level in tree (the number of nodes in a level)
* @param tree - tree structure
* @param level - desired level
* @returns width on desired level
*/
getWidth(e, t) {
return this.getNodesAtLevel(e, t).length;
}
/**
* Method to get height of node (the height of a node is the length of the longest downward path to a leaf from that node)
* @param tree - tree structure
* @param id - identifier of node
* @returns height of node
*/
getHeight(e, t) {
return this.getHeightNode(this.getSubTree(e, t));
}
/**
* Helper method to get height of node from children recursively
* @param tree - tree structure
* @param height - actual computed height
* @private
* @returns height of node
*/
getHeightNode(e, t = 0) {
return e.reduce((r, s) => Math.max(r, this.getHeightNode(s[this.childrenProp], t + 1)), t);
}
/**
* Method to get distance between 2 nodes
* @param tree - tree structure
* @param id1 - identifier of first node
* @param id2 - identifier of second node
* @returns distance between 2 nodes, returns -1, if there is no connection between nodes
*/
getDistance(e, t, r) {
const s = [...this.getPathNodes(e, t), this.get(e, t)], i = [...this.getPathNodes(e, r), this.get(e, r)], h = [...s].reverse().find((d) => i.includes(d));
if (!h)
return -1;
const n = s.findIndex((d) => d.id === h.id), l = i.findIndex((d) => d.id === h.id);
return s.length - n - 1 + (i.length - l - 1);
}
/**
* Method to compute paths for nodes (mutable operation)
* path property will be added into each node
* e.g. {path: "parent/child"...}
* @param tree - tree structure
* @param pathComputationProperty - property to use for path computation
* @param delimiter - to delimit path
* @param pathProperty - property where path will be stored
* @param originPath - path of top level nodes
*/
computePaths(e, t, r = "/", s = "path", i = "/") {
e == null || e.forEach((h) => {
h[s] = i, this.computePaths(h[this.childrenProp], t, r, s, `${h.path}${h[t]}${r}`);
});
}
/**
* Helper method to deep clone object
* @param obj - object to be cloned
* @private
* @returns deep cloned object
*/
static deepCopy(e) {
return JSON.parse(JSON.stringify(e));
}
}
export {
o as TreeUtils
};