fix: дерево ОСВ устойчиво к самоссылке, циклам и дублям id; key документа (I-1, M-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EAKRDNzXtRrwmWz9z6PZu3
This commit is contained in:
parent
5d1c28d0de
commit
edaa2b6d58
@ -54,7 +54,7 @@ export function DocumentPage() {
|
||||
/>
|
||||
<main style={{ flex: '1 1 auto', minHeight: 0, background: 'white', borderRadius: 12, padding: 12, overflow: 'auto' }}>
|
||||
{broken && <Empty description='Документ не удалось распознать — данных для проверки нет' />}
|
||||
{!broken && kind === 'osv' && <OsvDocument document={doc} onChanged={load} />}
|
||||
{!broken && kind === 'osv' && <OsvDocument key={doc.id} document={doc} onChanged={load} />}
|
||||
{!broken && kind === 'statement' && <Empty description='Проверка формы — в следующей задаче' />}
|
||||
{kind === 'none' && <Empty description='Документ без данных для проверки' />}
|
||||
</main>
|
||||
|
||||
@ -31,4 +31,20 @@ describe('buildEntryTree', () => {
|
||||
it('ребёнок с неизвестным родителем — в корень', () => {
|
||||
expect(buildEntryTree([e('x', 'missing', 1)]).map((n) => n.id)).toEqual(['x']);
|
||||
});
|
||||
it('самоссылка — в корень, без рекурсии', () => {
|
||||
const tree = buildEntryTree([e('a', 'a', 1), e('b', null, 2)]);
|
||||
expect(tree.map((n) => n.id)).toEqual(['a', 'b']);
|
||||
expect(tree[0].children).toBeUndefined();
|
||||
});
|
||||
it('цикл из двух — оба в корень, ничего не теряется', () => {
|
||||
const tree = buildEntryTree([e('a', 'b', 1), e('b', 'a', 2)]);
|
||||
expect(tree.map((n) => n.id)).toEqual(['a', 'b']);
|
||||
expect(tree.every((n) => n.children === undefined)).toBe(true);
|
||||
});
|
||||
it('дубль id — второй экземпляр пропускается', () => {
|
||||
const tree = buildEntryTree([e('a', null, 1), e('a', null, 2), e('c', 'a', 3)]);
|
||||
expect(tree.map((n) => n.id)).toEqual(['a']);
|
||||
expect(tree[0].row_number).toBe(1);
|
||||
expect(tree[0].children?.map((n) => n.id)).toEqual(['c']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,14 +2,29 @@ import type { EntryOut } from '../../types/api';
|
||||
|
||||
export type EntryNode = EntryOut & { key: string; children?: EntryNode[] };
|
||||
|
||||
/** Дерево строк ОСВ по parent_entry_id; порядок — как пришёл (по row_number). */
|
||||
export function buildEntryTree(entries: EntryOut[]): 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 ? nodes.get(entry.parent_entry_id) : undefined;
|
||||
const parent = entry.parent_entry_id && !inCycle(entry) ? nodes.get(entry.parent_entry_id) : undefined;
|
||||
if (parent) {
|
||||
parent.children = [...(parent.children ?? []), node];
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user