feat(relationship): add formatRelationshipDateRange helper

Plain-text "from – to" range formatter for relationship dates, delegating all
precision rendering to the shared formatDocumentDate (zero new precision logic).
Lives in $lib/person next to personLifeDates and reuses the existing
person → shared boundary. From-only renders just the start (no trailing dash),
no dates renders nothing.

REQ-015

Refs #837
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-14 18:45:55 +02:00
parent 6d2b6f3d2b
commit 4d9b165a2d
2 changed files with 112 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { formatRelationshipDateRange } from './relationshipDates';
// Delegates all precision rendering to formatDocumentDate — these tests pin the
// composition (dash, single sides, empty state) and one rendering per precision,
// plus en/es for DAY/MONTH so a German-month leak is caught here, not on a card.
describe('formatRelationshipDateRange', () => {
describe('both dates (de default)', () => {
it('renders DAY precision as full dates', () => {
expect(formatRelationshipDateRange('1923-05-12', 'DAY', '1958-06-13', 'DAY')).toBe(
'12. Mai 1923 13. Juni 1958'
);
});
it('renders MONTH precision as month + year', () => {
expect(formatRelationshipDateRange('1923-05-01', 'MONTH', '1958-06-01', 'MONTH')).toBe(
'Mai 1923 Juni 1958'
);
});
it('renders YEAR precision as bare years', () => {
expect(formatRelationshipDateRange('1923-01-01', 'YEAR', '1958-01-01', 'YEAR')).toBe(
'1923 1958'
);
});
it('renders mixed precisions per side', () => {
expect(formatRelationshipDateRange('1923-05-12', 'DAY', '1958-01-01', 'YEAR')).toBe(
'12. Mai 1923 1958'
);
});
});
describe('single sides and empty states', () => {
it('renders from only without a trailing dash', () => {
expect(formatRelationshipDateRange('1923-05-12', 'DAY', null, null)).toBe('12. Mai 1923');
});
it('renders to only with a leading dash', () => {
expect(formatRelationshipDateRange(null, null, '1958-06-13', 'DAY')).toBe(' 13. Juni 1958');
});
it('renders nothing when both dates are missing (UNKNOWN)', () => {
expect(formatRelationshipDateRange(null, 'UNKNOWN', null, 'UNKNOWN')).toBe('');
});
it('renders nothing for a from-only with a null date', () => {
expect(formatRelationshipDateRange(null, null, null, null)).toBe('');
});
});
describe('localized months (catch German-month leak)', () => {
it('renders DAY in English with no German month name', () => {
const out = formatRelationshipDateRange('1923-05-12', 'DAY', null, null, 'en');
expect(out).toContain('May');
expect(out).not.toContain('Mai');
expect(out).toContain('1923');
});
it('renders MONTH in Spanish', () => {
const out = formatRelationshipDateRange('1923-05-01', 'MONTH', null, null, 'es');
expect(out.toLowerCase()).toContain('mayo');
});
});
});

View File

@@ -0,0 +1,47 @@
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
/**
* Formats one relationship endpoint (start or end) at the precision the data
* claims, delegating all rendering to {@link formatDocumentDate}. Returns '' for
* a missing date. A missing precision falls back to YEAR — pre-V78 rows only knew
* a year. Mirrors {@link formatLifeDate} for the person life-date pattern.
*/
function formatEnd(
date: string | null | undefined,
precision: DatePrecision | null | undefined,
locale?: string
): string {
if (!date) {
return '';
}
return formatDocumentDate(date, precision ?? 'YEAR', null, null, locale);
}
/**
* Formats a relationship's startend range as plain text, e.g. for a marriage row.
* Examples (de):
* 12. Mai 1923 13. Juni 1958 (both)
* 12. Mai 1923 (start only — no trailing dash)
* 13. Juni 1958 (end only)
* "" (neither — the caller renders no date line)
*/
export function formatRelationshipDateRange(
fromDate: string | null | undefined,
fromDatePrecision: DatePrecision | null | undefined,
toDate: string | null | undefined,
toDatePrecision: DatePrecision | null | undefined,
locale?: string
): string {
const from = formatEnd(fromDate, fromDatePrecision, locale);
const to = formatEnd(toDate, toDatePrecision, locale);
if (from && to) {
return `${from} ${to}`;
}
if (from) {
return from;
}
if (to) {
return ` ${to}`;
}
return '';
}