- New cell format "Dauer" ([h]:mm): hours keep counting past 24 (37:30). A formula left on "Automatisch" whose numeric inputs are all times or durations (=SUM over working times, =end-start) displays as a duration. Typing 24 or more hours (37:30) enters a duration. - "Datum + Zeit" shows a bare time of day (< 1 day) without the epoch date 30.12.1899. - New functions: YEAR, MONTH, DAY, HOUR, MINUTE, SECOND, WEEKDAY, ISOWEEKNUM, WEEKNUM, EDATE, EOMONTH, DAYS, DATEDIF, NETWORKDAYS, WORKDAY, DATEVALUE, TIMEVALUE. Where Excel defaults to US conventions, ISO 8601 is the default: weeks start Monday (WEEKDAY -> Monday = 1), WEEKNUM is the ISO week. Excel's other types stay available. - Command reference and README document all of it; the README test count is updated to 553. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.4 KiB
TypeScript
126 lines
5.4 KiB
TypeScript
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);
|
|
});
|
|
});
|