feat(stammbaum): order siblings/branches by birthYear NULLS LAST, displayName, id (#724)
Net-new ordering coverage: roots and every unit's children sort by birthYear ASC (undated last), then displayName, then stable id — so horizontal x never depends on Map iteration order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -108,3 +108,34 @@ describe('buildFamilyForest — loose-spouse absorption', () => {
|
||||
expect(alb.members).toEqual(['alb', 'w1910', 'w1925', 'wNull']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildFamilyForest — sibling/branch ordering', () => {
|
||||
it('orders children by birthYear ASC, NULLS LAST, then displayName, then id', () => {
|
||||
// Provided out of order; the comparator must reorder to 1910, 1920, then
|
||||
// the two undated by displayName/id (NULLS LAST).
|
||||
const undatedB: PersonNodeDTO = { id: 'u-b', displayName: 'Zoe', familyMember: true };
|
||||
const undatedA: PersonNodeDTO = { id: 'u-a', displayName: 'Anna', familyMember: true };
|
||||
const forest = buildFamilyForest(
|
||||
[
|
||||
person('P', { birthYear: 1880 }),
|
||||
person('c1920', { birthYear: 1920 }),
|
||||
undatedB,
|
||||
person('c1910', { birthYear: 1910 }),
|
||||
undatedA
|
||||
],
|
||||
[parent('P', 'c1920'), parent('P', 'u-b'), parent('P', 'c1910'), parent('P', 'u-a')]
|
||||
);
|
||||
|
||||
const p = findUnit(forest.roots, 'P')!;
|
||||
// 1910, 1920 (dated ASC), then undated by displayName (Anna < Zoe).
|
||||
expect(p.children.map((u) => u.id)).toEqual(['c1910', 'c1920', 'u-a', 'u-b']);
|
||||
});
|
||||
|
||||
it('orders roots by the same rule', () => {
|
||||
const forest = buildFamilyForest(
|
||||
[person('late', { birthYear: 1900 }), person('early', { birthYear: 1850 })],
|
||||
[]
|
||||
);
|
||||
expect(forest.roots.map((r) => r.id)).toEqual(['early', 'late']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,6 +174,19 @@ export function buildFamilyForest(nodes: PersonNodeDTO[], edges: RelationshipDTO
|
||||
else unitObj.get(hp)!.children.push(unitObj.get(p)!);
|
||||
}
|
||||
|
||||
// Sibling/branch order: birthYear ASC NULLS LAST → displayName → id. Applied
|
||||
// to roots and to every unit's children so x never depends on Map order.
|
||||
const branchOrder = (u: Unit, v: Unit) => {
|
||||
const a = byId.get(u.id)!;
|
||||
const b = byId.get(v.id)!;
|
||||
const ya = a.birthYear ?? Number.POSITIVE_INFINITY;
|
||||
const yb = b.birthYear ?? Number.POSITIVE_INFINITY;
|
||||
if (ya !== yb) return ya - yb;
|
||||
return a.displayName.localeCompare(b.displayName) || (u.id < v.id ? -1 : 1);
|
||||
};
|
||||
roots.sort(branchOrder);
|
||||
for (const u of unitObj.values()) u.children.sort(branchOrder);
|
||||
|
||||
// --- Cross-links: displaced parent → absorbed-spouse edges. ---
|
||||
const crossLinks: CrossLink[] = [];
|
||||
for (const k of absorbedInto.keys()) {
|
||||
|
||||
Reference in New Issue
Block a user