Add tree utility and tests
This commit is contained in:
1 parent
c3e2cf5ed3
commit
fe31376f67
2 files changed
+82
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { buildCategoryTree, CategoryNode } from './tree.utility.ts';
|
||||||
|
|
||||||
|
describe('Tree Utility - buildCategoryTree()', () => {
|
||||||
|
it('should transform a flat list of ltree rows into a nested JSON structure', () => {
|
||||||
|
const flatNodes: CategoryNode[] = [
|
||||||
|
{ id: 1, name: 'Root', path: 'Top' },
|
||||||
|
{ id: 3, name: 'Astronomy', path: 'Top.Science.Astronomy' },
|
||||||
|
{ id: 2, name: 'Science', path: 'Top.Science' },
|
||||||
|
{ id: 4, name: 'Gardening', path: 'Top.Hobbies.Gardening' },
|
||||||
|
{ id: 5, name: 'Hobbies', path: 'Top.Hobbies' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const tree = buildCategoryTree(flatNodes);
|
||||||
|
|
||||||
|
// Verify root nodes count
|
||||||
|
assert.equal(tree.length, 1);
|
||||||
|
assert.equal(tree[0].name, 'Root');
|
||||||
|
|
||||||
|
// Verify second level ("Science" and "Hobbies")
|
||||||
|
const rootChildren = tree[0].children;
|
||||||
|
assert.equal(rootChildren.length, 2);
|
||||||
|
|
||||||
|
const scienceNode = rootChildren.find(c => c.name === 'Science');
|
||||||
|
const hobbiesNode = rootChildren.find(c => c.name === 'Hobbies');
|
||||||
|
|
||||||
|
assert.ok(scienceNode);
|
||||||
|
assert.ok(hobbiesNode);
|
||||||
|
|
||||||
|
// Verify third level deeply nested child ("Astronomy")
|
||||||
|
assert.equal(scienceNode.children.length, 1);
|
||||||
|
assert.equal(scienceNode.children[0].name, 'Astronomy');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
export interface CategoryNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeResponseNode {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
children: TreeResponseNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCategoryTree(nodes: CategoryNode[]): TreeResponseNode[] {
|
||||||
|
const tree: TreeResponseNode[] = [];
|
||||||
|
const nodeMap = new Map<string, TreeResponseNode>();
|
||||||
|
|
||||||
|
// Sort nodes by path depth (nlevel) implicitly by splitting path strings
|
||||||
|
// This guarantees parents are always processed before their children
|
||||||
|
const sortedNodes = [...nodes].sort((a, b) => a.path.split('.').length - b.path.split('.').length);
|
||||||
|
|
||||||
|
// Build the map and establish relationships
|
||||||
|
for (const node of sortedNodes) {
|
||||||
|
const treeNode: TreeResponseNode = { ...node, children: [] };
|
||||||
|
nodeMap.set(node.path, treeNode);
|
||||||
|
|
||||||
|
const pathParts = node.path.split('.');
|
||||||
|
|
||||||
|
if (pathParts.length === 1) {
|
||||||
|
// Root level node (e.g., "Top")
|
||||||
|
tree.push(treeNode);
|
||||||
|
} else {
|
||||||
|
// Find parent path by dropping the last element (e.g., "Top.Science" -> "Top")
|
||||||
|
const parentPath = pathParts.slice(0, -1).join('.');
|
||||||
|
const parentNode = nodeMap.get(parentPath);
|
||||||
|
|
||||||
|
if (parentNode) {
|
||||||
|
parentNode.children.push(treeNode);
|
||||||
|
} else {
|
||||||
|
// Fallback: If parent isn't in the dataset, treat it as an orphan root
|
||||||
|
tree.push(treeNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user