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