Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
36 lines
1.6 KiB
TypeScript
36 lines
1.6 KiB
TypeScript
import type { EntryOut } from '../../types/api';
|
||
|
||
export type EntryNode = EntryOut & { key: string; children?: EntryNode[] };
|
||
|
||
/** Дерево строк ОСВ по parent_entry_id; порядок — как пришёл (по row_number).
|
||
* Защита от «грязных» данных: дубль id — второй экземпляр пропускается; самоссылка и цикл — такие строки идут в корень. */
|
||
export function buildEntryTree(input: EntryOut[]): EntryNode[] {
|
||
const seen = new Set<string>();
|
||
const entries = input.filter((entry) => !seen.has(entry.id) && seen.add(entry.id));
|
||
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
||
|
||
// Строка в цикле, если, идя вверх по родителям, мы вернёмся к ней самой.
|
||
const inCycle = (start: EntryOut): boolean => {
|
||
let current = start.parent_entry_id ? byId.get(start.parent_entry_id) : undefined;
|
||
for (let steps = 0; current && steps <= entries.length; steps++) {
|
||
if (current.id === start.id) return true;
|
||
current = current.parent_entry_id ? byId.get(current.parent_entry_id) : undefined;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
const nodes = new Map<string, EntryNode>();
|
||
for (const entry of entries) nodes.set(entry.id, { ...entry, key: entry.id });
|
||
const roots: EntryNode[] = [];
|
||
for (const entry of entries) {
|
||
const node = nodes.get(entry.id) as EntryNode;
|
||
const parent = entry.parent_entry_id && !inCycle(entry) ? nodes.get(entry.parent_entry_id) : undefined;
|
||
if (parent) {
|
||
parent.children = [...(parent.children ?? []), node];
|
||
} else {
|
||
roots.push(node);
|
||
}
|
||
}
|
||
return roots;
|
||
}
|