import { describe, expect, it } from 'vitest'; import { addCivilDays, civilDayOfWeek, compareCivil, minutesOfTime, toCivilDate, toCivilTime, } from './civil'; const ZURICH = 'Europe/Zurich'; describe('toCivilDate / toCivilTime', () => { it('converts an instant to the wall clock of the shop', () => { // 2026-09-20T08:15:00Z is 10:15 in Zurich (CEST, UTC+2). const instant = new Date('2026-09-20T08:15:00Z'); expect(toCivilDate(instant, ZURICH)).toBe('2026-09-20'); expect(toCivilTime(instant, ZURICH)).toBe('10:15'); }); it('keeps the local day when UTC has already rolled over', () => { // 22:30 UTC is 00:30 the next day in Zurich: the civil date must follow // the shop, not the server. const instant = new Date('2026-09-20T22:30:00Z'); expect(toCivilDate(instant, ZURICH)).toBe('2026-09-21'); expect(toCivilTime(instant, ZURICH)).toBe('00:30'); }); it('reads the same instant differently in winter and in summer', () => { // CET (UTC+1) in January, CEST (UTC+2) in July. expect(toCivilTime(new Date('2026-01-15T12:00:00Z'), ZURICH)).toBe('13:00'); expect(toCivilTime(new Date('2026-07-15T12:00:00Z'), ZURICH)).toBe('14:00'); }); }); describe('addCivilDays', () => { it('walks forward across a month boundary', () => { expect(addCivilDays('2026-09-30', 1)).toBe('2026-10-01'); }); it('walks forward across a year boundary', () => { expect(addCivilDays('2026-12-31', 1)).toBe('2027-01-01'); }); it('handles a leap day', () => { expect(addCivilDays('2028-02-28', 1)).toBe('2028-02-29'); expect(addCivilDays('2028-02-29', 1)).toBe('2028-03-01'); }); it('is unaffected by the spring daylight-saving switch', () => { // 2026-03-29 is the last Sunday of March: Zurich loses an hour that night. // Civil dates must still advance by exactly one day. expect(addCivilDays('2026-03-28', 1)).toBe('2026-03-29'); expect(addCivilDays('2026-03-29', 1)).toBe('2026-03-30'); }); it('is unaffected by the autumn daylight-saving switch', () => { // 2026-10-25 is the last Sunday of October: that day has 25 hours. expect(addCivilDays('2026-10-24', 1)).toBe('2026-10-25'); expect(addCivilDays('2026-10-25', 1)).toBe('2026-10-26'); }); it('walks backwards', () => { expect(addCivilDays('2026-01-01', -1)).toBe('2025-12-31'); }); }); describe('civilDayOfWeek', () => { it('matches JS getDay(), Sunday first', () => { expect(civilDayOfWeek('2026-09-20')).toBe(0); // Sunday expect(civilDayOfWeek('2026-09-21')).toBe(1); // Monday expect(civilDayOfWeek('2026-09-26')).toBe(6); // Saturday }); it('is stable across the daylight-saving switches', () => { expect(civilDayOfWeek('2026-03-29')).toBe(0); expect(civilDayOfWeek('2026-10-25')).toBe(0); }); }); describe('compareCivil', () => { it('orders dates chronologically', () => { expect(compareCivil('2026-09-20', '2026-09-21')).toBeLessThan(0); expect(compareCivil('2026-09-21', '2026-09-20')).toBeGreaterThan(0); expect(compareCivil('2026-09-20', '2026-09-20')).toBe(0); }); it('orders across year boundaries', () => { expect(compareCivil('2026-12-31', '2027-01-01')).toBeLessThan(0); }); }); describe('minutesOfTime', () => { it('converts a wall-clock time to minutes since midnight', () => { expect(minutesOfTime('00:00')).toBe(0); expect(minutesOfTime('10:00')).toBe(600); expect(minutesOfTime('18:30')).toBe(1110); expect(minutesOfTime('23:59')).toBe(1439); }); });