fuzzyworld

when-am-i / files / src / utils / timelineBands.test.ts

timelineBands.test.ts

Blame: the commit that last changed each line · file · history

68dcf3ed 2026-06-25 Pavel Safronov1import { describe, it, expect } from 'vitest';
2import { effectiveErasForPoint, yearBandsForPoint, dayBandsForPoint } from './timelineBands';
3import { daysInYear } from './historicalDate';
4import type { CalendarRegion, Geometry } from '../types';
5
6const square: Geometry = {
7 type: 'Polygon',
8 coordinates: [[[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]],
9};
10
11// A region that switches calendars mid-year, on 22 September 1792 (the French
12// Republican start) — a real intra-year changeover for the day-band tests.
13const switchover: CalendarRegion[] = [
14 {
15 region: { name: 'France', geo: square },
16 sourceFile: 'src/data/calendars/france.json',
17 eras: [
18 { name: 'Ancien régime', start: null, end: '1792-09-22', calendar: 'Gregorian' },
19 { name: 'Republic', start: '1792-09-22', end: '1806-01-01', calendar: 'French Republican calendar' },
20 ],
21 },
22];
23
24describe('effectiveErasForPoint', () => {
25 it('returns just the modern Gregorian default outside every region', () => {
26 // The default is appended unconditionally (mirroring the old year-band
27 // behavior); the day/year mappers clip it out of pre-modern years.
28 const eras = effectiveErasForPoint(50, 50, switchover);
29 expect(eras).toHaveLength(1);
30 expect(eras[0].name).toBe('Modern (global civil calendar)');
31 expect(eras[0].calendar).toBe('Gregorian');
32 });
33
34 it('appends the modern Gregorian default for a region without modern Gregorian', () => {
35 const eras = effectiveErasForPoint(5, 5, switchover);
36 expect(eras.map((e) => e.calendar)).toEqual([
37 'Gregorian', // ancien régime, ends 1792 (does not reach modern)
38 'French Republican calendar',
39 'Gregorian', // appended global default
40 ]);
41 expect(eras[eras.length - 1].name).toBe('Modern (global civil calendar)');
42 });
43
44 it('does not append the default when a region already has modern Gregorian', () => {
45 const modern: CalendarRegion[] = [
46 { region: { name: 'X', geo: square }, eras: [{ name: 'Now', start: '1900-01-01', end: null, calendar: 'Gregorian' }] },
47 ];
48 expect(effectiveErasForPoint(5, 5, modern)).toHaveLength(1);
49 });
50
51 it('suppresses the default when an era opts out and reaches modern times', () => {
52 const isolated: CalendarRegion[] = [
53 { region: { name: 'I', geo: square }, eras: [{ name: 'None', start: null, end: null, calendar: 'None', suppressModernDefault: true }] },
54 ];
55 const eras = effectiveErasForPoint(5, 5, isolated);
56 expect(eras).toHaveLength(1);
57 expect(eras[0].calendar).toBe('None');
58 });
59});
60
61describe('yearBandsForPoint', () => {
62 it('maps eras to year spans with ±Infinity for open ends', () => {
63 const bands = yearBandsForPoint(5, 5, switchover);
64 expect(bands[0]).toEqual({ start: -Infinity, end: 1792, calendar: 'Gregorian' });
65 expect(bands[1]).toEqual({ start: 1792, end: 1806, calendar: 'French Republican calendar' });
66 // appended modern default
67 expect(bands[2]).toEqual({ start: 1927, end: Infinity, calendar: 'Gregorian' });
68 });
69});
70
71describe('dayBandsForPoint', () => {
72 it('splits the changeover year at the exact day', () => {
73 // 1792 is a leap year; 22 Sept is day 266. The half-open boundary means
74 // the old calendar covers days 1–265 and the new one 266–366.
75 const bands = dayBandsForPoint(5, 5, switchover, 1792);
76 expect(daysInYear(1792)).toBe(366);
77 expect(bands).toEqual([
78 { startDoy: 1, endDoy: 265, calendar: 'Gregorian' },
79 { startDoy: 266, endDoy: 366, calendar: 'French Republican calendar' },
80 ]);
81 });
82
83 it('yields a single full-year band for a year with no changeover', () => {
84 const bands = dayBandsForPoint(5, 5, switchover, 1800);
85 expect(bands).toEqual([{ startDoy: 1, endDoy: 365, calendar: 'French Republican calendar' }]);
86 });
87
88 it('drops eras that do not reach into the selected year', () => {
89 // 1850: the region's eras have lapsed and the global default starts 1927,
90 // so no band covers this year here.
91 expect(dayBandsForPoint(5, 5, switchover, 1850)).toEqual([]);
92 });
93
94 it('covers the whole year with the modern default once it applies', () => {
95 // 2000 is a leap year (divisible by 400), so 366 days.
96 const bands = dayBandsForPoint(5, 5, switchover, 2000);
97 expect(bands).toEqual([{ startDoy: 1, endDoy: 366, calendar: 'Gregorian' }]);
98 });
99});