feat(person): formatLifeDateRange takes date + precision, delegates to formatDocumentDate
New formatLifeDate single-date helper carries no glyph so cards can wrap * / † in aria-hidden spans. Missing precision falls back to YEAR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,113 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { formatLifeDateRange } from './personLifeDates';
|
import { formatLifeDate, formatLifeDateRange } from './personLifeDates';
|
||||||
|
|
||||||
|
// Delegates all precision rendering to formatDocumentDate — these tests pin the
|
||||||
|
// composition (glyphs, dash, empty sides) and one rendering per precision so a
|
||||||
|
// regression in the delegation is caught here, not on a person card.
|
||||||
describe('formatLifeDateRange', () => {
|
describe('formatLifeDateRange', () => {
|
||||||
it('returns both dates when birth and death year are given', () => {
|
describe('both dates (de default)', () => {
|
||||||
expect(formatLifeDateRange(1882, 1944)).toBe('* 1882 – † 1944');
|
it('renders DAY precision as full dates', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-14', 'DAY', '1944-11-02', 'DAY')).toBe(
|
||||||
|
'* 14. März 1901 – † 2. November 1944'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns only birth year when only birthYear is given', () => {
|
it('renders MONTH precision as month + year', () => {
|
||||||
expect(formatLifeDateRange(1882, undefined)).toBe('* 1882');
|
expect(formatLifeDateRange('1901-03-01', 'MONTH', '1944-11-01', 'MONTH')).toBe(
|
||||||
|
'* März 1901 – † November 1944'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns only death year when only deathYear is given', () => {
|
it('renders YEAR precision as bare years', () => {
|
||||||
expect(formatLifeDateRange(undefined, 1944)).toBe('† 1944');
|
expect(formatLifeDateRange('1901-01-01', 'YEAR', '1944-01-01', 'YEAR')).toBe(
|
||||||
|
'* 1901 – † 1944'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty string when neither year is given', () => {
|
it('renders mixed precisions per side', () => {
|
||||||
expect(formatLifeDateRange(undefined, undefined)).toBe('');
|
expect(formatLifeDateRange('1901-03-14', 'DAY', '1944-01-01', 'YEAR')).toBe(
|
||||||
|
'* 14. März 1901 – † 1944'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty string when both are null', () => {
|
it('renders APPROX precision with the ca. prefix (legacy imports)', () => {
|
||||||
expect(formatLifeDateRange(null, null)).toBe('');
|
expect(formatLifeDateRange('1901-01-01', 'APPROX', '1944-01-01', 'APPROX')).toBe(
|
||||||
|
'* ca. 1901 – † ca. 1944'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('single sides and empty states', () => {
|
||||||
|
it('renders birth only without dash or dagger', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-14', 'DAY', null, null)).toBe('* 14. März 1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders death only without dash or asterisk', () => {
|
||||||
|
expect(formatLifeDateRange(null, null, '1944-11-02', 'DAY')).toBe('† 2. November 1944');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders YEAR birth only', () => {
|
||||||
|
expect(formatLifeDateRange('1882-01-01', 'YEAR', null, null)).toBe('* 1882');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders APPROX death only', () => {
|
||||||
|
expect(formatLifeDateRange(null, null, '1944-01-01', 'APPROX')).toBe('† ca. 1944');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when both dates are null', () => {
|
||||||
|
expect(formatLifeDateRange(null, null, null, null)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string when both dates are null even with UNKNOWN precisions', () => {
|
||||||
|
expect(formatLifeDateRange(null, 'UNKNOWN', null, 'UNKNOWN')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to YEAR rendering when a precision is missing', () => {
|
||||||
|
expect(formatLifeDateRange('1901-01-01', null, null, null)).toBe('* 1901');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('locales (German-month-leak guard)', () => {
|
||||||
|
it('renders DAY precision in English', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-14', 'DAY', null, null, 'en')).toBe('* March 14, 1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders MONTH precision in English', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-01', 'MONTH', null, null, 'en')).toBe('* March 1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders DAY precision in Spanish', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-14', 'DAY', null, null, 'es')).toBe(
|
||||||
|
'* 14 de marzo de 1901'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders MONTH precision in Spanish', () => {
|
||||||
|
expect(formatLifeDateRange('1901-03-01', 'MONTH', null, null, 'es')).toBe('* marzo de 1901');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Single-date helper for components that must keep the * / † glyphs in their own
|
||||||
|
// aria-hidden markup (PersonCard, PersonHoverCard) instead of in the string.
|
||||||
|
describe('formatLifeDate', () => {
|
||||||
|
it('renders a DAY-precision date without any glyph', () => {
|
||||||
|
expect(formatLifeDate('1901-03-14', 'DAY')).toBe('14. März 1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders an APPROX-precision date (legacy imports)', () => {
|
||||||
|
expect(formatLifeDate('1901-01-01', 'APPROX')).toBe('ca. 1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to YEAR rendering when precision is missing', () => {
|
||||||
|
expect(formatLifeDate('1901-01-01', null)).toBe('1901');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty string for a null date', () => {
|
||||||
|
expect(formatLifeDate(null, 'DAY')).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders in the requested locale', () => {
|
||||||
|
expect(formatLifeDate('1901-03-14', 'DAY', 'en')).toBe('March 14, 1901');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,44 @@
|
|||||||
|
import { formatDocumentDate, type DatePrecision } from '$lib/shared/utils/documentDate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formats the life date range for a person.
|
* Formats one life date (birth or death) at the precision the data claims,
|
||||||
|
* delegating all rendering to {@link formatDocumentDate}. Returns '' for a
|
||||||
|
* missing date. Carries no * / † glyph — components that need the glyphs wrap
|
||||||
|
* them in their own `aria-hidden` markup so screen readers only hear the date.
|
||||||
|
*
|
||||||
|
* A missing precision falls back to YEAR: pre-V76 rows only knew a year, and
|
||||||
|
* a bare year is the only safe rendering for a date without precision metadata.
|
||||||
|
*/
|
||||||
|
export function formatLifeDate(
|
||||||
|
date: string | null | undefined,
|
||||||
|
precision: DatePrecision | null | undefined,
|
||||||
|
locale?: string
|
||||||
|
): string {
|
||||||
|
if (!date) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return formatDocumentDate(date, precision ?? 'YEAR', null, null, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the full life date range as plain text, e.g. for dropdown subtitles.
|
||||||
* Examples:
|
* Examples:
|
||||||
* * 1882 – † 1944 (both)
|
* * 14. März 1901 – † 2. November 1944 (both, DAY precision)
|
||||||
* * 1882 (birth only)
|
* * 1882 (birth only, YEAR precision)
|
||||||
* † 1944 (death only)
|
* † ca. 1944 (death only, APPROX precision)
|
||||||
* "" (neither)
|
* "" (neither)
|
||||||
*/
|
*/
|
||||||
export function formatLifeDateRange(birthYear?: number | null, deathYear?: number | null): string {
|
export function formatLifeDateRange(
|
||||||
if (birthYear && deathYear) {
|
birthDate: string | null | undefined,
|
||||||
return `* ${birthYear} – † ${deathYear}`;
|
birthDatePrecision: DatePrecision | null | undefined,
|
||||||
|
deathDate: string | null | undefined,
|
||||||
|
deathDatePrecision: DatePrecision | null | undefined,
|
||||||
|
locale?: string
|
||||||
|
): string {
|
||||||
|
const birth = birthDate ? `* ${formatLifeDate(birthDate, birthDatePrecision, locale)}` : null;
|
||||||
|
const death = deathDate ? `† ${formatLifeDate(deathDate, deathDatePrecision, locale)}` : null;
|
||||||
|
if (birth && death) {
|
||||||
|
return `${birth} – ${death}`;
|
||||||
}
|
}
|
||||||
if (birthYear) {
|
return birth ?? death ?? '';
|
||||||
return `* ${birthYear}`;
|
|
||||||
}
|
|
||||||
if (deathYear) {
|
|
||||||
return `† ${deathYear}`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user