when-am-i / files / src / utils / historicalDate.test.ts
historicalDate.test.ts
34c4dd59 2026-06-18 Pavel Safronov | 1 | import { describe, it, expect } from 'vitest'; |
4014e5e0 2026-06-18 Pavel Safronov | 2 | import { |
| 3 | daysInMonth, | |
| 4 | daysInYear, | |
| 5 | dayOfYear, | |
| 6 | fromDayOfYear, | |
| 7 | toHistoricalDate, | |
| 8 | fromHistoricalDate, | |
| 9 | clampDay, | |
| 10 | } from './historicalDate'; | |
34c4dd59 2026-06-18 Pavel Safronov | 11 | |
| 12 | describe('daysInMonth', () => { | |
| 13 | it('handles February leap years (Gregorian rule)', () => { | |
| 14 | expect(daysInMonth(2000, 2)).toBe(29); // divisible by 400 | |
| 15 | expect(daysInMonth(1900, 2)).toBe(28); // divisible by 100, not 400 | |
| 16 | expect(daysInMonth(2004, 2)).toBe(29); | |
| 17 | expect(daysInMonth(2001, 2)).toBe(28); | |
| 18 | }); | |
| 19 | ||
| 20 | it('handles 30- and 31-day months', () => { | |
| 21 | expect(daysInMonth(2026, 4)).toBe(30); | |
| 22 | expect(daysInMonth(2026, 12)).toBe(31); | |
| 23 | }); | |
| 24 | }); | |
| 25 | ||
4014e5e0 2026-06-18 Pavel Safronov | 26 | describe('day-of-year helpers', () => { |
| 27 | it('daysInYear handles leap years', () => { | |
| 28 | expect(daysInYear(2000)).toBe(366); | |
| 29 | expect(daysInYear(1900)).toBe(365); | |
| 30 | expect(daysInYear(2001)).toBe(365); | |
| 31 | }); | |
| 32 | ||
| 33 | it('dayOfYear counts from Jan 1', () => { | |
| 34 | expect(dayOfYear(2001, 1, 1)).toBe(1); | |
| 35 | expect(dayOfYear(2001, 3, 1)).toBe(60); // 31 + 28 + 1 | |
| 36 | expect(dayOfYear(2000, 3, 1)).toBe(61); // leap: 31 + 29 + 1 | |
| 37 | expect(dayOfYear(2001, 12, 31)).toBe(365); | |
| 38 | }); | |
| 39 | ||
| 40 | it('fromDayOfYear inverts dayOfYear', () => { | |
| 41 | for (const [year, m, d] of [[2001, 3, 1], [2000, 3, 1], [2026, 6, 18], [1999, 12, 31]] as const) { | |
| 42 | expect(fromDayOfYear(year, dayOfYear(year, m, d))).toEqual({ month: m, day: d }); | |
| 43 | } | |
| 44 | }); | |
| 45 | ||
| 46 | it('fromDayOfYear clamps out-of-range input', () => { | |
| 47 | expect(fromDayOfYear(2001, 999)).toEqual({ month: 12, day: 31 }); | |
| 48 | expect(fromDayOfYear(2001, 0)).toEqual({ month: 1, day: 1 }); | |
| 49 | }); | |
| 50 | }); | |
| 51 | ||
34c4dd59 2026-06-18 Pavel Safronov | 52 | describe('toHistoricalDate / fromHistoricalDate', () => { |
| 53 | it('passes CE years through unchanged', () => { | |
| 54 | expect(toHistoricalDate({ year: 1700, month: 3, day: 22, era: 'CE' })).toEqual({ year: 1700, month: 3, day: 22 }); | |
| 55 | }); | |
| 56 | ||
| 57 | it('maps BCE onto astronomical numbering (1 BCE = 0)', () => { | |
| 58 | expect(toHistoricalDate({ year: 1, month: 1, day: 1, era: 'BCE' })).toEqual({ year: 0, month: 1, day: 1 }); | |
| 59 | expect(toHistoricalDate({ year: 44, month: 3, day: 15, era: 'BCE' })).toEqual({ year: -43, month: 3, day: 15 }); | |
| 60 | }); | |
| 61 | ||
| 62 | it('round-trips both directions', () => { | |
| 63 | for (const d of [ | |
| 64 | { year: 2026, month: 6, day: 18, era: 'CE' as const }, | |
| 65 | { year: 1, month: 1, day: 1, era: 'BCE' as const }, | |
| 66 | { year: 44, month: 3, day: 15, era: 'BCE' as const }, | |
| 67 | ]) { | |
| 68 | expect(fromHistoricalDate(toHistoricalDate(d))).toEqual(d); | |
| 69 | } | |
| 70 | }); | |
| 71 | }); | |
| 72 | ||
| 73 | describe('clampDay', () => { | |
| 74 | it('clamps a day past the end of the month', () => { | |
| 75 | expect(clampDay({ year: 2025, month: 2, day: 31, era: 'CE' })).toMatchObject({ month: 2, day: 28 }); | |
| 76 | expect(clampDay({ year: 2024, month: 2, day: 31, era: 'CE' })).toMatchObject({ day: 29 }); | |
| 77 | }); | |
| 78 | ||
| 79 | it('leaves a valid day alone', () => { | |
| 80 | expect(clampDay({ year: 2026, month: 6, day: 15, era: 'CE' })).toMatchObject({ day: 15 }); | |
| 81 | }); | |
| 82 | }); |