import { describe, expect, it } from 'vitest'; import { addDays, addMonths, addYears, daysInMonth, formatDate, isSameDay, isValid, isWithin, monthGrid, parseDate, startOfWeek, weekdayNames, } from './date-utils'; const at = (year: number, month: number, day: number, hours = 0, minutes = 0) => new Date(year, month - 1, day, hours, minutes); describe('date maths', () => { it('crosses a month boundary', () => { expect(addDays(at(2024, 1, 31), 1)).toEqual(at(2024, 2, 1)); }); it('handles a leap day', () => { expect(addDays(at(2024, 2, 28), 1)).toEqual(at(2024, 2, 29)); expect(addDays(at(2023, 2, 28), 1)).toEqual(at(2023, 3, 1)); }); it('clamps the day when a month is too short', () => { /* 31 January plus one month is 29 February in 2024, not 2 March. */ expect(addMonths(at(2024, 1, 31), 1)).toEqual(at(2024, 2, 29)); expect(addMonths(at(2023, 1, 31), 1)).toEqual(at(2023, 2, 28)); }); it('steps years, clamping 29 February', () => { expect(addYears(at(2024, 2, 29), 1)).toEqual(at(2025, 2, 28)); }); it('counts the days in a month', () => { expect(daysInMonth(2024, 1)).toBe(29); expect(daysInMonth(2023, 1)).toBe(28); expect(daysInMonth(2024, 0)).toBe(31); }); it('compares by day, ignoring the time', () => { expect(isSameDay(at(2024, 3, 7, 9), at(2024, 3, 7, 23))).toBe(true); expect(isSameDay(at(2024, 3, 7), at(2024, 3, 8))).toBe(false); expect(isSameDay(null, at(2024, 3, 7))).toBe(false); }); it('treats a range as inclusive and direction-agnostic', () => { expect(isWithin(at(2024, 3, 7), at(2024, 3, 1), at(2024, 3, 31))).toBe(true); expect(isWithin(at(2024, 3, 1), at(2024, 3, 1), at(2024, 3, 31))).toBe(true); expect(isWithin(at(2024, 4, 1), at(2024, 3, 1), at(2024, 3, 31))).toBe(false); /* Endpoints given backwards still describe the same span. */ expect(isWithin(at(2024, 3, 7), at(2024, 3, 31), at(2024, 3, 1))).toBe(true); }); it('rejects an invalid date', () => { expect(isValid(new Date('nonsense'))).toBe(false); expect(isValid(at(2024, 3, 7))).toBe(true); expect(isValid(null)).toBe(false); }); describe('week and grid', () => { it('anchors the week to the given first day', () => { /* 2024-03-07 is a Thursday. */ expect(startOfWeek(at(2024, 3, 7), 1)).toEqual(at(2024, 3, 4)); // Monday expect(startOfWeek(at(2024, 3, 7), 0)).toEqual(at(2024, 3, 3)); // Sunday }); it('always draws six full weeks', () => { /* A jagged grid would move the paging buttons under the cursor. */ expect(monthGrid(at(2024, 3, 1), 1)).toHaveLength(42); expect(monthGrid(at(2024, 2, 1), 1)).toHaveLength(42); }); it('starts the grid on the first day of the week', () => { const grid = monthGrid(at(2024, 3, 1), 1); expect(grid[0].getDay()).toBe(1); expect(grid[0]).toEqual(at(2024, 2, 26)); }); it('orders weekday names from the given first day', () => { expect(weekdayNames('en-US', 1)[0]).toBe('Mon'); expect(weekdayNames('en-US', 0)[0]).toBe('Sun'); }); /* Chrome's ICU has no `az` and falls back to the CLDR root, which formats weekdays in English while still reporting `az-AZ` as resolved. The names are carried in date-utils for that reason; this pins them. */ it('names the Azerbaijani weekdays itself', () => { expect(weekdayNames('az-AZ', 1)).toEqual(['B.E.', 'Ç.A.', 'Ç.', 'C.A.', 'C.', 'Ş.', 'B.']); expect(weekdayNames('az', 0)[0]).toBe('B.'); /* A different script is still the platform's job. */ expect(weekdayNames('az-Cyrl-AZ', 1)[0]).not.toBe('B.E.'); }); }); }); describe('formatDate', () => { it('fills the numeric tokens, padded and unpadded', () => { const date = at(2024, 3, 7, 9, 5); expect(formatDate(date, 'yyyy-MM-dd')).toBe('2024-03-07'); expect(formatDate(date, 'd/M/yyyy')).toBe('7/3/2024'); expect(formatDate(date, 'yyyy-MM-dd HH:mm')).toBe('2024-03-07 09:05'); expect(formatDate(date, 'yy')).toBe('24'); }); it('uses locale month names', () => { expect(formatDate(at(2024, 3, 7), 'MMMM yyyy', 'en-US')).toBe('March 2024'); expect(formatDate(at(2024, 3, 7), 'MMM', 'en-US')).toBe('Mar'); }); it('uses the built-in Azerbaijani month names', () => { expect(formatDate(at(2024, 3, 7), 'MMMM yyyy', 'az-AZ')).toBe('mart 2024'); expect(formatDate(at(2024, 3, 7), 'MMM', 'az-AZ')).toBe('mar'); }); it('passes separators through untouched', () => { expect(formatDate(at(2024, 3, 7), 'dd.MM.yyyy')).toBe('07.03.2024'); }); it('returns an empty string for an invalid date', () => { expect(formatDate(new Date('nonsense'), 'yyyy-MM-dd')).toBe(''); }); }); describe('parseDate', () => { it('round-trips what formatDate produced', () => { const date = at(2024, 3, 7); expect(parseDate(formatDate(date, 'yyyy-MM-dd'), 'yyyy-MM-dd')).toEqual(date); }); it('accepts unpadded numbers', () => { /* Someone typing 2024-3-7 means the same thing as 2024-03-07. */ expect(parseDate('2024-3-7', 'yyyy-MM-dd')).toEqual(at(2024, 3, 7)); }); it('reads month names', () => { expect(parseDate('March 2024', 'MMMM yyyy')).toEqual(at(2024, 3, 1)); expect(parseDate('mar 2024', 'MMM yyyy')).toEqual(at(2024, 3, 1)); }); it('reads the built-in Azerbaijani month names back', () => { expect(parseDate('sentyabr 2024', 'MMMM yyyy', 'az-AZ')).toEqual(at(2024, 9, 1)); expect(parseDate('iyn 2024', 'MMM yyyy', 'az-AZ')).toEqual(at(2024, 6, 1)); }); it('parses a time', () => { expect(parseDate('2024-03-07 14:30', 'yyyy-MM-dd HH:mm')).toEqual(at(2024, 3, 7, 14, 30)); }); it('rejects a date that never existed', () => { /* `new Date(2024, 1, 31)` would silently become 2 March. */ expect(parseDate('2024-02-31', 'yyyy-MM-dd')).toBeNull(); expect(parseDate('2023-02-29', 'yyyy-MM-dd')).toBeNull(); expect(parseDate('2024-02-29', 'yyyy-MM-dd')).toEqual(at(2024, 2, 29)); }); it('rejects out-of-range parts', () => { expect(parseDate('2024-13-01', 'yyyy-MM-dd')).toBeNull(); expect(parseDate('2024-03-07 25:00', 'yyyy-MM-dd HH:mm')).toBeNull(); }); it('rejects text that does not match the pattern', () => { expect(parseDate('07/03/2024', 'yyyy-MM-dd')).toBeNull(); expect(parseDate('not a date', 'yyyy-MM-dd')).toBeNull(); expect(parseDate('', 'yyyy-MM-dd')).toBeNull(); }); it('does not guess at an ambiguous order', () => { /* 03/04/2024 is day-first here because the pattern says so, not because anything inferred it. */ expect(parseDate('03/04/2024', 'dd/MM/yyyy')).toEqual(at(2024, 4, 3)); expect(parseDate('03/04/2024', 'MM/dd/yyyy')).toEqual(at(2024, 3, 4)); }); it('stays in local time', () => { /* Parsing through `Date.parse` would shift the day for anyone west of Greenwich; the parts have to be applied locally. */ const parsed = parseDate('2024-03-07', 'yyyy-MM-dd')!; expect(parsed.getDate()).toBe(7); expect(parsed.getHours()).toBe(0); }); });