Reputation: 48600
Attempting to reduce a flat list of items (each with a list of their own statuses) into a tree, where the status of the child node influences the parent.
const Status = {
UNKNOWN : { name: 'UNKNOWN' , priority: 0 },
DEBUG : { name: 'DEBUG' , priority: 1 },
INFO : { name: 'INFO' , priority: 2 },
WARNING : { name: 'WARNING' , priority: 3 },
ERROR : { name: 'ERROR' , priority: 4 },
};
const { UNKNOWN, DEBUG, INFO, WARNING, ERROR } = Status;
const maxStatus = (statuses) =>
statuses.reduce((max, curr) =>
curr.priority > max.priority
? curr
: max, UNKNOWN);
class Node {
constructor({ id, parentId, status }) {
this.id = id;
this.parentId = parentId;
this.status = status ?? UNKNOWN;
this.children = [];
}
};
const buildTree = (items) => {
const
root = new Node({ id: 0, parentId: null }),
nodeList = { 0 : root };
items.forEach(({ id, parentId, data }) => {
if (!nodeList[id]) {
nodeList[id] = new Node({
id,
parentId,
status: maxStatus(data)
});
} else {
Object.assign(nodeList[id], {
parentId,
status: maxStatus(data)
});
}
if (!nodeList[parentId]) {
nodeList[parentId] = new Node({ id: parentId });
}
nodeList[parentId].children.push(nodeList[id]);
});
return root;
};
const calculateStatus = (tree) => {
let status = UNKNOWN;
if (tree.children.length) {
tree.children.forEach(child => {
const localMax = calculateStatus(child);
status = maxStatus([ status, localMax ]);
});
tree.status = status;
}
}
const printNode = (node, depth) => {
const indent = ' '.repeat(depth);
const { id, status: { name } } = node;
console.log(`${indent}${id} ${name}`);
node.children.forEach(child =>
printNode(child, depth + 1));
}
const printTree = (tree) => printNode(tree, 0);
const items = [
{ id: 1, parentId: 0, data: [ ] },
{ id: 2, parentId: 0, data: [ DEBUG, DEBUG ] },
{ id: 3, parentId: 0, data: [ INFO, DEBUG ] },
{ id: 4, parentId: 1, data: [ INFO, DEBUG ] },
{ id: 5, parentId: 1, data: [ DEBUG ] },
{ id: 6, parentId: 1, data: [ WARNING ] },
{ id: 7, parentId: 2, data: [ INFO, DEBUG ] },
{ id: 8, parentId: 7, data: [ DEBUG ] },
{ id: 9, parentId: 8, data: [ ] },
{ id: 10, parentId: 3, data: [ ERROR, ERROR ] },
];
const tree = buildTree(items);
printTree(tree);
.as-console-wrapper { top: 0; max-height: 100% !important; }
0 ERROR
1 WARNING
4 INFO
5 DEBUG
6 WARNING
2 INFO
7 INFO
8 DEBUG
9 UNKNOWN
3 ERROR
10 ERROR
Here is a graphical representation of the algorithm:
The initial status shows each node as UNKNOWN
.
Before the tree is traversed, each node has a pre-calculated status based on its data.
While traversing the tree (recursively) in a post-order traversal strategy, each node asks its children what their maximum status is.
Upvotes: 1
Views: 765
Reputation: 50787
Another approach which treats your tree as immutable (and built out of plain JS objects, not constructed Node
s) can use a simpler recursion for the core of your buildTree
and calculateStatus
functions. We could write it like this:
const Status = {UNKNOWN: {name: 'UNKNOWN', priority: 0}, DEBUG: {name: 'DEBUG', priority: 1}, INFO: {name: 'INFO', priority: 2}, WARNING: {name: 'WARNING', priority: 3}, ERROR: {name: 'ERROR', priority: 4}}
const {UNKNOWN, DEBUG, INFO, WARNING, ERROR} = Status
const maxStatus = (statuses) => statuses .reduce (
(max, curr) => curr .priority > max .priority ? curr : max,
UNKNOWN
)
const buildForest = (items, target) =>
items .filter (({parentId}) => parentId == target) .map (({id, parentId, data}) => ({
id,
parentId,
status: maxStatus (data),
children: buildForest (items, id)
}))
// Assumes there really is a single root.
const buildTree = (items) =>
({id: 0, parentId: null, status: UNKNOWN, children: buildForest (items, 0)})
const calculateStatus = (node, children = node .children .map (c => calculateStatus (c))) => ({
...node,
status: maxStatus ([node .status, ...children .map (c => c .status)]),
children,
})
const printNode = (node, depth) => {
const indent = ' '.repeat(depth);
const { id, status: { name } } = node;
console.log(`${indent}${id} ${name}`)
node.children.forEach(child =>
printNode(child, depth + 1))
}
const printTree = (tree) => printNode (tree, 0)
const items = [{id: 1, parentId: 0, data: []}, {id: 2, parentId: 0, data: [DEBUG, DEBUG]}, {id: 3, parentId: 0, data: [INFO, DEBUG]}, {id: 4, parentId: 1, data: [INFO, DEBUG]}, {id: 5, parentId: 1, data: [DEBUG]}, {id: 6, parentId: 1, data: [WARNING]}, {id: 7, parentId: 2, data: [INFO, DEBUG]}, {id: 8, parentId: 7, data: [DEBUG]}, {id: 9, parentId: 8, data: []}, {id: 10, parentId: 3, data: [ERROR, ERROR]}]
const tree = buildTree (items)
const newTree = calculateStatus (tree)
// stringify just to avoid SO's /**id:N**/ & /**ref:N**/ notation. Statuses ARE references
// Note that `tree` was not modified in calling `calculateStatus`
console .log (JSON.stringify(tree, null, 4))
printTree (newTree)
.as-console-wrapper {max-height: 100% !important; top: 0}
buildTree
is built atop buildForest
which recursively scans the list of elements for the next set of children. Note that this creates plain JS objects, and does not use something like your Node
constructor.
calculateStatus
builds a new node from a current one by first recurring on any children, extracting their status
properties, and then calls maxStatus
with them and the current node's status to derive the new status. Then it creates a new node with those properties. Note that other properties of the node than children
and status
will be shared by reference, if they are reference types. (It doesn't matter with this example as id
and parentId
are primitives. But it does show that this technique reuses as much as it can.)
Because it's being called with map
and we add a default parameter, you cannot replace node .children .map (c => calculateStatus (c))
with node .children .map (calculateStatus)
, which certainly would feel cleaner. If you don't like that default parameter, we could easily recast this as:
const calculateStatus = (node) => {
const children = node .children .map (calculateStatus)
return {
...node,
status: maxStatus ([node .status, ...children .map (c => c .status)]),
children,
}
}
I left printNode
/printTree
alone; they work fine with these plain JS objects.
You may not find this of use if you really prefer to mutate your data, but I find it a cleaner way to work.
It wasn't clear to me what was meant by this comment:
I am going to throw the tree away, so mutating it would not be an issue.
But if it meant that the intermediate tree is of no value whatsoever, and is only there as a step to create the final tree, then we can build that final tree directly, with code something like this:
const buildForest = (items, target) =>
items .filter (({parentId}) => parentId == target) .map (
({id, parentId, data}) => {
const children = buildForest (items, id)
const status = maxStatus ([...data, ...children .map (c => c .status)])
return {id, parentId, status, children}
})
const buildTree = (items) => {
const children = buildForest (items, 0)
const status = maxStatus (children .map (c => c .status))
return {id: 0, parentId: null, status, children}
}
Upvotes: 1
Reputation: 350202
There are two issues, both in calculateStatus
:
It lacks a return statement, and so the recursive call will always return undefined
. It should end with return status;
The initial value of status
should not be UNKNOWN
. When at a leaf, and tree.children.length
is zero, you don't want to return UNKNOWN
, but the actual status
property value of the node. So start with let status = tree.status
Corrected:
const calculateStatus = (tree) => {
let status = tree.status; // <--
if (tree.children.length) {
tree.children.forEach(child => {
const localMax = calculateStatus(child);
status = maxStatus([ status, localMax ]);
});
tree.status = status;
}
return status; // <--
}
And of course, you need to call calculateStatus
in the main code.
const Status = {
UNKNOWN : { name: 'UNKNOWN' , priority: 0 },
DEBUG : { name: 'DEBUG' , priority: 1 },
INFO : { name: 'INFO' , priority: 2 },
WARNING : { name: 'WARNING' , priority: 3 },
ERROR : { name: 'ERROR' , priority: 4 },
};
const { UNKNOWN, DEBUG, INFO, WARNING, ERROR } = Status;
const maxStatus = (statuses) => {
return statuses.reduce((max, curr) =>
curr.priority > max.priority
? curr
: max, UNKNOWN);
};
class Node {
constructor({ id, parentId, status }) {
this.id = id;
this.parentId = parentId;
this.status = status ?? UNKNOWN;
this.children = [];
}
};
const buildTree = (items) => {
const
root = new Node({ id: 0, parentId: null }),
nodeList = { 0 : root };
items.forEach(({ id, parentId, data }) => {
if (!nodeList[id]) {
nodeList[id] = new Node({
id,
parentId,
status: maxStatus(data)
});
} else {
Object.assign(nodeList[id], {
parentId,
status: maxStatus(data)
});
}
if (!nodeList[parentId]) {
nodeList[parentId] = new Node({ id: parentId });
}
nodeList[parentId].children.push(nodeList[id]);
});
return root;
};
const calculateStatus = (tree) => {
let status = tree.status; // <--
if (tree.children.length) {
tree.children.forEach(child => {
const localMax = calculateStatus(child);
status = maxStatus([ status, localMax ]);
});
tree.status = status;
}
return status; // <--
}
const printNode = (node, depth) => {
const indent = ' '.repeat(depth);
const { id, status: { name } } = node;
console.log(`${indent}${id} ${name}`);
node.children.forEach(child =>
printNode(child, depth + 1));
}
const printTree = (tree) => printNode(tree, 0);
const items = [
{ id: 1, parentId: 0, data: [ ] },
{ id: 2, parentId: 0, data: [ DEBUG, DEBUG ] },
{ id: 3, parentId: 0, data: [ INFO, DEBUG ] },
{ id: 4, parentId: 1, data: [ INFO, DEBUG ] },
{ id: 5, parentId: 1, data: [ DEBUG ] },
{ id: 6, parentId: 1, data: [ WARNING ] },
{ id: 7, parentId: 2, data: [ INFO, DEBUG ] },
{ id: 8, parentId: 7, data: [ DEBUG ] },
{ id: 9, parentId: 8, data: [ ] },
{ id: 10, parentId: 3, data: [ ERROR, ERROR ] },
];
const tree = buildTree(items);
calculateStatus(tree); // <--
printTree(tree);
Upvotes: 1