Add centralized date-window constants per TZ 4.1.2-R12 (board/schedule/map)
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
BOARD_WINDOW_DAYS_BACK,
|
||||
BOARD_WINDOW_DAYS_FORWARD,
|
||||
SCHEDULE_WINDOW_DAYS_BACK,
|
||||
SCHEDULE_WINDOW_DAYS_FORWARD,
|
||||
MAP_WINDOW_DAYS_BACK,
|
||||
MAP_WINDOW_MONTHS_FORWARD,
|
||||
isInBoardWindow,
|
||||
isInScheduleWindow,
|
||||
isInMapWindow,
|
||||
boardWindowBounds,
|
||||
scheduleWindowBounds,
|
||||
mapWindowBounds,
|
||||
} from "./dateWindow.js";
|
||||
|
||||
describe("dateWindow constants", () => {
|
||||
it("exports the TZ-defined numerical bounds", () => {
|
||||
expect(BOARD_WINDOW_DAYS_BACK).toBe(1);
|
||||
expect(BOARD_WINDOW_DAYS_FORWARD).toBe(14);
|
||||
expect(SCHEDULE_WINDOW_DAYS_BACK).toBe(1);
|
||||
expect(SCHEDULE_WINDOW_DAYS_FORWARD).toBe(330);
|
||||
expect(MAP_WINDOW_DAYS_BACK).toBe(1);
|
||||
expect(MAP_WINDOW_MONTHS_FORWARD).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dateWindow helpers (clock frozen at 2026-05-15)", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date(2026, 4, 15, 12, 0, 0)); // May 15, 2026 noon local time
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// Helper to format date as YYYY-MM-DD for comparison
|
||||
function toYmd(d: Date): string {
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
describe("4.1.2-R12: isInBoardWindow (-1/+14 days)", () => {
|
||||
it("accepts today (yyyyMMdd)", () => {
|
||||
expect(isInBoardWindow("20260515")).toBe(true);
|
||||
});
|
||||
it("accepts yesterday (-1 day)", () => {
|
||||
expect(isInBoardWindow("20260514")).toBe(true);
|
||||
});
|
||||
it("rejects -2 days", () => {
|
||||
expect(isInBoardWindow("20260513")).toBe(false);
|
||||
});
|
||||
it("accepts today + 14 days", () => {
|
||||
expect(isInBoardWindow("20260529")).toBe(true);
|
||||
});
|
||||
it("rejects today + 15 days", () => {
|
||||
expect(isInBoardWindow("20260530")).toBe(false);
|
||||
});
|
||||
it("rejects malformed input", () => {
|
||||
expect(isInBoardWindow("")).toBe(false);
|
||||
expect(isInBoardWindow("2026-05-15")).toBe(false);
|
||||
expect(isInBoardWindow("20269999")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("4.1.2-R12: isInScheduleWindow (-1/+330 days)", () => {
|
||||
it("accepts today + 330 days", () => {
|
||||
expect(isInScheduleWindow("20270410")).toBe(true);
|
||||
});
|
||||
it("rejects today + 331 days", () => {
|
||||
expect(isInScheduleWindow("20270411")).toBe(false);
|
||||
});
|
||||
it("rejects -2 days", () => {
|
||||
expect(isInScheduleWindow("20260513")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("4.1.2-R12: isInMapWindow (-1 day / +6 months)", () => {
|
||||
it("accepts today + 6 months (same calendar day)", () => {
|
||||
expect(isInMapWindow("20261115")).toBe(true);
|
||||
});
|
||||
it("rejects today + 6 months + 1 day", () => {
|
||||
expect(isInMapWindow("20261116")).toBe(false);
|
||||
});
|
||||
it("rejects -2 days", () => {
|
||||
expect(isInMapWindow("20260513")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes bounds tuples for UI consumers", () => {
|
||||
const [bMin, bMax] = boardWindowBounds();
|
||||
expect(toYmd(bMin)).toBe("2026-05-14");
|
||||
expect(toYmd(bMax)).toBe("2026-05-29");
|
||||
const [sMin, sMax] = scheduleWindowBounds();
|
||||
expect(toYmd(sMin)).toBe("2026-05-14");
|
||||
expect(toYmd(sMax)).toBe("2027-04-10");
|
||||
const [mMin, mMax] = mapWindowBounds();
|
||||
expect(toYmd(mMin)).toBe("2026-05-14");
|
||||
expect(toYmd(mMax)).toBe("2026-11-15");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Centralized date-window constants and membership helpers per TZ §4.1.2.
|
||||
*
|
||||
* All date arguments are "yyyyMMdd" strings (TZ URL date format).
|
||||
* Bounds are inclusive on both ends. Day-level comparison (time zeroed).
|
||||
*/
|
||||
|
||||
export const BOARD_WINDOW_DAYS_BACK = 1;
|
||||
export const BOARD_WINDOW_DAYS_FORWARD = 14;
|
||||
export const SCHEDULE_WINDOW_DAYS_BACK = 1;
|
||||
export const SCHEDULE_WINDOW_DAYS_FORWARD = 330;
|
||||
export const MAP_WINDOW_DAYS_BACK = 1;
|
||||
export const MAP_WINDOW_MONTHS_FORWARD = 6;
|
||||
|
||||
function today(): Date {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
function addDays(base: Date, days: number): Date {
|
||||
const d = new Date(base);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function addMonths(base: Date, months: number): Date {
|
||||
const d = new Date(base);
|
||||
d.setMonth(d.getMonth() + months);
|
||||
return d;
|
||||
}
|
||||
|
||||
function parseYyyymmdd(s: string): Date | null {
|
||||
if (!/^\d{8}$/.test(s)) return null;
|
||||
const y = Number(s.slice(0, 4));
|
||||
const m = Number(s.slice(4, 6));
|
||||
const d = Number(s.slice(6, 8));
|
||||
if (m < 1 || m > 12 || d < 1 || d > 31) return null;
|
||||
const dt = new Date(y, m - 1, d);
|
||||
if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;
|
||||
return dt;
|
||||
}
|
||||
|
||||
function inRange(date: Date, min: Date, max: Date): boolean {
|
||||
return date.getTime() >= min.getTime() && date.getTime() <= max.getTime();
|
||||
}
|
||||
|
||||
export function boardWindowBounds(): [Date, Date] {
|
||||
const base = today();
|
||||
return [addDays(base, -BOARD_WINDOW_DAYS_BACK), addDays(base, BOARD_WINDOW_DAYS_FORWARD)];
|
||||
}
|
||||
|
||||
export function scheduleWindowBounds(): [Date, Date] {
|
||||
const base = today();
|
||||
return [addDays(base, -SCHEDULE_WINDOW_DAYS_BACK), addDays(base, SCHEDULE_WINDOW_DAYS_FORWARD)];
|
||||
}
|
||||
|
||||
export function mapWindowBounds(): [Date, Date] {
|
||||
const base = today();
|
||||
return [addDays(base, -MAP_WINDOW_DAYS_BACK), addMonths(base, MAP_WINDOW_MONTHS_FORWARD)];
|
||||
}
|
||||
|
||||
export function isInBoardWindow(yyyymmdd: string): boolean {
|
||||
const d = parseYyyymmdd(yyyymmdd);
|
||||
if (!d) return false;
|
||||
const [min, max] = boardWindowBounds();
|
||||
return inRange(d, min, max);
|
||||
}
|
||||
|
||||
export function isInScheduleWindow(yyyymmdd: string): boolean {
|
||||
const d = parseYyyymmdd(yyyymmdd);
|
||||
if (!d) return false;
|
||||
const [min, max] = scheduleWindowBounds();
|
||||
return inRange(d, min, max);
|
||||
}
|
||||
|
||||
export function isInMapWindow(yyyymmdd: string): boolean {
|
||||
const d = parseYyyymmdd(yyyymmdd);
|
||||
if (!d) return false;
|
||||
const [min, max] = mapWindowBounds();
|
||||
return inRange(d, min, max);
|
||||
}
|
||||
Reference in New Issue
Block a user