diff --git a/src/sim/calendar.ts b/src/sim/calendar.ts new file mode 100644 index 0000000..f76111e --- /dev/null +++ b/src/sim/calendar.ts @@ -0,0 +1,54 @@ +export interface GameDate { + year: number; + month: number; // 1-12 + day: number; +} + +const MS_PER_DAY = 86_400_000; + +export const START_DATE: GameDate = { year: 1990, month: 9, day: 28 }; +export const EURO_DATE: GameDate = { year: 2002, month: 1, day: 1 }; + +function toUTC(d: GameDate): number { + return Date.UTC(d.year, d.month - 1, d.day); +} + +function fromUTC(ms: number): GameDate { + const js = new Date(ms); + return { year: js.getUTCFullYear(), month: js.getUTCMonth() + 1, day: js.getUTCDate() }; +} + +export function addDays(d: GameDate, n: number): GameDate { + return fromUTC(toUTC(d) + n * MS_PER_DAY); +} + +export function daysBetween(a: GameDate, b: GameDate): number { + return Math.round((toUTC(b) - toUTC(a)) / MS_PER_DAY); +} + +export function compareDates(a: GameDate, b: GameDate): number { + return toUTC(a) - toUTC(b); +} + +export function isSunday(d: GameDate): boolean { + return new Date(toUTC(d)).getUTCDay() === 0; +} + +export function isLastDayOfMonth(d: GameDate): boolean { + return addDays(d, 1).month !== d.month; +} + +export function isEuroEra(d: GameDate): boolean { + return compareDates(d, EURO_DATE) >= 0; +} + +export function dateToISO(d: GameDate): string { + const mm = String(d.month).padStart(2, '0'); + const dd = String(d.day).padStart(2, '0'); + return `${d.year}-${mm}-${dd}`; +} + +export function parseISO(s: string): GameDate { + const [year, month, day] = s.split('-').map(Number); + return { year, month, day }; +} diff --git a/tests/sim/calendar.test.ts b/tests/sim/calendar.test.ts new file mode 100644 index 0000000..4a7f4ee --- /dev/null +++ b/tests/sim/calendar.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest'; +import { + addDays, compareDates, dateToISO, daysBetween, isEuroEra, + isLastDayOfMonth, isSunday, parseISO, START_DATE, +} from '../../src/sim/calendar'; + +describe('calendar', () => { + it('starts on the European Game Boy launch (Friday 28/09/1990)', () => { + expect(START_DATE).toEqual({ year: 1990, month: 9, day: 28 }); + expect(isSunday(START_DATE)).toBe(false); + }); + + it('detects Sundays', () => { + expect(isSunday({ year: 1990, month: 9, day: 30 })).toBe(true); + expect(isSunday({ year: 1990, month: 9, day: 29 })).toBe(false); + }); + + it('adds days across month and year boundaries', () => { + expect(addDays({ year: 1990, month: 12, day: 31 }, 1)).toEqual({ year: 1991, month: 1, day: 1 }); + expect(addDays({ year: 1990, month: 9, day: 28 }, 3)).toEqual({ year: 1990, month: 10, day: 1 }); + }); + + it('detects the last day of a month, including leap years', () => { + expect(isLastDayOfMonth({ year: 1992, month: 2, day: 29 })).toBe(true); + expect(isLastDayOfMonth({ year: 1992, month: 2, day: 28 })).toBe(false); + expect(isLastDayOfMonth({ year: 1990, month: 9, day: 30 })).toBe(true); + }); + + it('switches to the euro on 01/01/2002', () => { + expect(isEuroEra({ year: 2001, month: 12, day: 31 })).toBe(false); + expect(isEuroEra({ year: 2002, month: 1, day: 1 })).toBe(true); + }); + + it('computes days between dates', () => { + expect(daysBetween({ year: 1990, month: 9, day: 28 }, { year: 1990, month: 10, day: 28 })).toBe(30); + expect(daysBetween({ year: 1990, month: 10, day: 28 }, { year: 1990, month: 9, day: 28 })).toBe(-30); + }); + + it('round-trips ISO strings and compares dates', () => { + expect(dateToISO(parseISO('1990-09-28'))).toBe('1990-09-28'); + expect(compareDates(parseISO('1990-09-28'), parseISO('1991-01-01'))).toBeLessThan(0); + expect(compareDates(parseISO('1991-01-01'), parseISO('1991-01-01'))).toBe(0); + }); +});