feat(stammbaum): add familyForest.pickStructuralOwner (#724)
Structural-owner rule for couples: earlier birth year wins, missing year sorts last, ties break on stable id. The single definition reused by the cross-link, cycle and intra-family paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { pickStructuralOwner } from './familyForest';
|
||||
|
||||
type Person = { id: string; birthYear?: number };
|
||||
|
||||
describe('pickStructuralOwner', () => {
|
||||
const p = (id: string, birthYear?: number): Person => ({ id, birthYear });
|
||||
|
||||
it('picks the earlier-born spouse as structural owner', () => {
|
||||
expect(pickStructuralOwner(p('a', 1900), p('b', 1920))).toBe('a');
|
||||
expect(pickStructuralOwner(p('a', 1920), p('b', 1900))).toBe('b');
|
||||
});
|
||||
|
||||
it('sorts a missing birthYear last (the dated spouse owns)', () => {
|
||||
expect(pickStructuralOwner(p('a'), p('b', 1900))).toBe('b');
|
||||
expect(pickStructuralOwner(p('a', 1900), p('b'))).toBe('a');
|
||||
});
|
||||
|
||||
it('breaks ties on stable id when birth years match or are both missing', () => {
|
||||
expect(pickStructuralOwner(p('zzz', 1900), p('aaa', 1900))).toBe('aaa');
|
||||
expect(pickStructuralOwner(p('zzz'), p('aaa'))).toBe('aaa');
|
||||
});
|
||||
});
|
||||
34
frontend/src/lib/person/genealogy/layout/familyForest.ts
Normal file
34
frontend/src/lib/person/genealogy/layout/familyForest.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Domain-aware construction of the genealogy "family forest" (#724).
|
||||
//
|
||||
// This module owns every piece of genealogy knowledge the layout needs —
|
||||
// spouse runs, birth-year ordering, structural-owner selection, intra-family
|
||||
// marriage resolution and cross-links — and flattens it into the abstract
|
||||
// { id, width, children } nodes that the domain-agnostic tidyTree.ts packs.
|
||||
// buildLayout.ts orchestrates the two and maps run positions back to persons.
|
||||
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type PersonNodeDTO = components['schemas']['PersonNodeDTO'];
|
||||
|
||||
/**
|
||||
* Choose the structural owner of a couple: the spouse who keeps the bloodline
|
||||
* (hierarchy) position. Earlier birth year wins; a missing birth year sorts
|
||||
* last; ties break on the stable id. Shared by the cycle, cross-link and
|
||||
* intra-family paths so the rule is defined exactly once.
|
||||
*/
|
||||
export function pickStructuralOwner(
|
||||
a: { id: string; birthYear?: number | null },
|
||||
b: { id: string; birthYear?: number | null }
|
||||
): string {
|
||||
const ra = ownerRank(a);
|
||||
const rb = ownerRank(b);
|
||||
if (ra !== rb) return ra < rb ? a.id : b.id;
|
||||
return a.id <= b.id ? a.id : b.id;
|
||||
}
|
||||
|
||||
// Lower is "more owning". Missing birth year → +Infinity (sorts last).
|
||||
function ownerRank(p: { birthYear?: number | null }): number {
|
||||
return p.birthYear == null ? Number.POSITIVE_INFINITY : p.birthYear;
|
||||
}
|
||||
|
||||
export type { PersonNodeDTO };
|
||||
Reference in New Issue
Block a user