Files
time-tools/tests/time/core.test.ts
T
2026-09-01 02:54:04 +02:00

132 lines
3.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { addBusinessDays, compareArithmetic } from "../../src/time/arithmetic";
import { inspectEpoch } from "../../src/time/epoch";
import { createUtcEvent, foldIcsLine } from "../../src/time/ics";
import { previewCron, previewRRule } from "../../src/time/recurrence";
import {
compareTimeZones,
nextTransitions,
resolveLocalDateTime,
} from "../../src/time/zones";
describe("exact epoch conversion", () => {
it("preserves negative nanoseconds", () => {
const result = inspectEpoch("-0.000000001", "seconds");
expect(result.epochNanoseconds).toBe(-1n);
expect(result.iso).toBe("1969-12-31T23:59:59.999999999Z");
});
it("rejects sub-nanosecond precision", () => {
expect(() => inspectEpoch("0.0000000001", "seconds")).toThrow(
/smaller than one nanosecond/u,
);
});
});
describe("time-zone handling", () => {
it("classifies a spring gap and autumn overlap", () => {
expect(
resolveLocalDateTime("2026-03-29T02:30", "Europe/Berlin").status,
).toBe("skipped");
const overlap = resolveLocalDateTime("2026-10-25T02:30", "Europe/Berlin");
expect(overlap.status).toBe("ambiguous");
expect(overlap.choices.map((choice) => choice.offset)).toEqual([
"+02:00",
"+01:00",
]);
});
it("compares the same instant without changing its epoch", () => {
const values = compareTimeZones("2026-01-01T00:00:00Z", [
"UTC",
"Asia/Tokyo",
]);
expect(values[0]?.epochNanoseconds).toBe(values[1]?.epochNanoseconds);
expect(values[1]?.local).toBe("2026-01-01T09:00:00");
});
it("rejects a non-integer transition count", () => {
expect(() =>
nextTransitions("2026-01-01T00:00:00Z", "Europe/Berlin", 1.5),
).toThrow(/between 1 and 32/u);
});
});
describe("calendar arithmetic", () => {
it("shows wall-clock and elapsed days diverging across DST", () => {
const result = compareArithmetic(
"2026-03-28T12:00:00Z",
"Europe/Berlin",
"P1D",
);
expect(result.differenceSeconds).toBe("-3600");
});
it("adds business days with explicit holidays", () => {
expect(addBusinessDays("2026-12-24", 2, ["2026-12-25"]).result).toBe(
"2026-12-29",
);
});
it("bounds explicit holiday allocation", () => {
expect(() =>
addBusinessDays("2026-01-01", 1, Array(10_001).fill("2026-01-02")),
).toThrow(/10,000/u);
});
it("does not silently truncate a fractional business-day count", () => {
expect(() => addBusinessDays("2026-01-01", 1.5)).toThrow(/±100,000/u);
});
});
describe("bounded recurrence previews", () => {
it("previews five-field cron in its named zone", () => {
const result = previewCron(
"0 9 * * MON-FRI",
"2026-08-31T08:00:00Z",
"Europe/Berlin",
2,
"5-part",
);
expect(result).toHaveLength(2);
expect(result[0]?.zoned).toContain("T09:00:00");
});
it("previews a strict RRULE", () => {
const result = previewRRule(
"FREQ=DAILY;COUNT=3",
"2026-09-01T09:00",
"Europe/Berlin",
10,
);
expect(result).toHaveLength(3);
expect(result[0]?.zoned).toContain("2026-09-01T09:00:00");
});
});
describe("iCalendar output", () => {
it("creates CRLF UTC event lines", () => {
const result = createUtcEvent({
startLocal: "2026-09-01T10:00",
timeZone: "Europe/Berlin",
duration: "PT1H",
summary: "Review, plan",
});
expect(result.ics).toContain("DTSTART:20260901T080000Z\r\n");
expect(result.ics).toContain("SUMMARY:Review\\, plan");
expect(result.ics.endsWith("\r\n")).toBe(true);
});
it("folds Unicode lines by UTF-8 octets", () => {
const folded = foldIcsLine(`DESCRIPTION:${"ä".repeat(50)}`);
expect(folded).toContain("\r\n ");
expect(
Math.max(
...folded
.split("\r\n")
.map((line) => new TextEncoder().encode(line).length),
),
).toBeLessThanOrEqual(75);
});
});