import { describe, it, expect } from 'vitest'; import { FUNCTIONS } from '../functions'; import { dateToSerial, timeSerial } from '../dates'; import { isError, type CellValue } from '../types'; const call = (name: string, ...args: CellValue[]) => FUNCTIONS[name](args); const d = (y: number, m: number, day: number) => dateToSerial(y, m, day); describe('date parts', () => { const t = dateToSerial(2026, 9, 2, 14, 30, 15); it('YEAR / MONTH / DAY / HOUR / MINUTE / SECOND', () => { expect(call('YEAR', t)).toBe(2026); expect(call('MONTH', t)).toBe(9); expect(call('DAY', t)).toBe(2); expect(call('HOUR', t)).toBe(14); expect(call('MINUTE', t)).toBe(30); expect(call('SECOND', t)).toBe(15); }); it('rounds to the whole second, so float noise never shows as :59', () => { const noisy = timeSerial(14, 29, 59) + 1 / 86400; expect(call('HOUR', noisy)).toBe(14); expect(call('MINUTE', noisy)).toBe(30); expect(call('SECOND', noisy)).toBe(0); }); it('checks argument count and type', () => { expect(isError(call('YEAR'))).toBe(true); expect(isError(call('YEAR', 1, 2))).toBe(true); expect(isError(call('YEAR', 'kein Datum'))).toBe(true); }); }); describe('WEEKDAY (ISO default: Monday = 1)', () => { const wed = d(2026, 9, 2); it('defaults to ISO numbering', () => { expect(call('WEEKDAY', wed)).toBe(3); expect(call('WEEKDAY', d(2026, 9, 6))).toBe(7); // Sunday }); it('supports Excel types 1, 2, 3', () => { expect(call('WEEKDAY', wed, 1)).toBe(4); // Sunday = 1 expect(call('WEEKDAY', wed, 2)).toBe(3); expect(call('WEEKDAY', wed, 3)).toBe(2); // Monday = 0 expect(isError(call('WEEKDAY', wed, 7))).toBe(true); }); }); describe('ISO week numbers', () => { it('handles the year boundaries', () => { expect(call('ISOWEEKNUM', d(2026, 1, 1))).toBe(1); // Thursday expect(call('ISOWEEKNUM', d(2027, 1, 1))).toBe(53); // Friday -> last week of 2026 expect(call('ISOWEEKNUM', d(2021, 1, 3))).toBe(53); // Sunday -> week 53 of 2020 expect(call('ISOWEEKNUM', d(2024, 12, 30))).toBe(1); // Monday -> week 1 of 2025 expect(call('ISOWEEKNUM', d(2026, 9, 2))).toBe(36); }); it('WEEKNUM is ISO by default, US-style with type 1/2', () => { expect(call('WEEKNUM', d(2021, 1, 3))).toBe(53); expect(call('WEEKNUM', d(2026, 1, 3), 1)).toBe(1); // Saturday, same week as 1 Jan expect(call('WEEKNUM', d(2026, 1, 4), 1)).toBe(2); // Sunday starts week 2 expect(call('WEEKNUM', d(2026, 1, 4), 2)).toBe(1); // ... but not when weeks start Monday expect(call('WEEKNUM', d(2026, 1, 5), 2)).toBe(2); }); }); describe('month arithmetic', () => { it('EDATE clamps to the end of a shorter month', () => { expect(call('EDATE', d(2026, 1, 31), 1)).toBe(d(2026, 2, 28)); expect(call('EDATE', d(2024, 1, 31), 1)).toBe(d(2024, 2, 29)); expect(call('EDATE', d(2026, 3, 31), -1)).toBe(d(2026, 2, 28)); expect(call('EDATE', d(2026, 9, 2), 12)).toBe(d(2027, 9, 2)); }); it('EOMONTH', () => { expect(call('EOMONTH', d(2026, 9, 2), 0)).toBe(d(2026, 9, 30)); expect(call('EOMONTH', d(2026, 9, 2), -1)).toBe(d(2026, 8, 31)); expect(call('EOMONTH', d(2026, 1, 15), 1)).toBe(d(2026, 2, 28)); }); }); describe('differences', () => { it('DAYS counts whole calendar days, ignoring the time', () => { expect(call('DAYS', d(2026, 12, 31), d(2026, 1, 1))).toBe(364); expect(call('DAYS', d(2026, 1, 2) + 0.9, d(2026, 1, 1) + 0.1)).toBe(1); }); it('DATEDIF in every unit', () => { const s = d(2020, 2, 29); const e = d(2026, 9, 2); expect(call('DATEDIF', s, e, 'Y')).toBe(6); expect(call('DATEDIF', s, e, 'M')).toBe(78); expect(call('DATEDIF', s, e, 'D')).toBe(e - s); expect(call('DATEDIF', s, e, 'YM')).toBe(6); expect(call('DATEDIF', s, e, 'MD')).toBe(4); // 29.08. -> 02.09. expect(call('DATEDIF', s, e, 'yd')).toBe(d(2026, 9, 2) - d(2026, 2, 28)); expect(isError(call('DATEDIF', e, s, 'D'))).toBe(true); expect(isError(call('DATEDIF', s, e, 'W'))).toBe(true); }); }); describe('working days (Monday-Friday)', () => { it('NETWORKDAYS counts both ends and skips holidays on weekdays', () => { expect(call('NETWORKDAYS', d(2026, 9, 1), d(2026, 9, 30))).toBe(22); expect(call('NETWORKDAYS', d(2026, 9, 1), d(2026, 9, 30), d(2026, 9, 15))).toBe(21); expect(call('NETWORKDAYS', d(2026, 9, 1), d(2026, 9, 30), d(2026, 9, 5))).toBe(22); // Saturday expect(call('NETWORKDAYS', d(2026, 9, 30), d(2026, 9, 1))).toBe(-22); }); it('WORKDAY steps over weekends and holidays, both directions', () => { expect(call('WORKDAY', d(2026, 9, 4), 1)).toBe(d(2026, 9, 7)); // Fri -> Mon expect(call('WORKDAY', d(2026, 9, 7), -1)).toBe(d(2026, 9, 4)); expect(call('WORKDAY', d(2026, 9, 4), 1, d(2026, 9, 7))).toBe(d(2026, 9, 8)); expect(call('WORKDAY', d(2026, 9, 2), 0)).toBe(d(2026, 9, 2)); }); }); describe('DATEVALUE / TIMEVALUE', () => { it('read the same formats a cell entry is recognized in', () => { expect(call('DATEVALUE', '2026-09-02')).toBe(d(2026, 9, 2)); expect(call('DATEVALUE', '02.09.2026 14:30')).toBe(d(2026, 9, 2)); expect(call('TIMEVALUE', '14:30')).toBeCloseTo(timeSerial(14, 30, 0), 12); expect(call('TIMEVALUE', '2026-09-02 14:30')).toBeCloseTo(timeSerial(14, 30, 0), 12); }); it('pass a number through and reject other text', () => { expect(call('DATEVALUE', d(2026, 9, 2) + 0.5)).toBe(d(2026, 9, 2)); expect(isError(call('DATEVALUE', 'hallo'))).toBe(true); }); });