Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions web/src/legal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ describe("dayOfYear", () => {
expect(dayOfYear(new Date(2026, 11, 31))).toBe(365);
});

// Characterizes CURRENT (mismatched) behavior: dayOfYear uses the real
// calendar (366 in a leap year) while the pipeline's _doy uses a fixed
// non-leap table (max 365). Plan 004 reconciles these; when it does,
// this assertion must change to reflect the new convention.
it("returns 366 for Dec 31 in a leap year (current mismatched behavior)", () => {
expect(dayOfYear(new Date(2028, 11, 31))).toBe(366);
// non-leap convention — must match pipeline/normalize.py _MONTH_START
it("returns 365 for Dec 31 in a leap year (non-leap convention)", () => {
expect(dayOfYear(new Date(2028, 11, 31))).toBe(365);
});

it("Feb 29 maps to the same value as Mar 1 (non-leap convention)", () => {
expect(dayOfYear(new Date(2028, 1, 29))).toBe(60);
expect(dayOfYear(new Date(2028, 2, 1))).toBe(60);
});

it("matches the pipeline's parse_window start for 05/01 (cross-convention anchor)", () => {
expect(dayOfYear(new Date(2026, 4, 1))).toBe(121);
});
});

Expand Down Expand Up @@ -135,4 +141,9 @@ describe("isOpen (combined)", () => {
it("permitted class and in season -> true", () => {
expect(evalOpen(["passenger"], 200, props)).toBe(true);
});

it("a window ending 12/31 (open_end=365) is OPEN on Dec 31 of a leap year (the leap-year bug this plan fixes)", () => {
const leapProps = { classes: ",passenger,", season: "seasonal", open_start: 121, open_end: 365 };
expect(evalOpen(["passenger"], dayOfYear(new Date(2028, 11, 31)), leapProps)).toBe(true);
});
});
16 changes: 10 additions & 6 deletions web/src/legal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
// Mirrors the data model from pipeline/normalize.py:
// classes ",passenger,motorcycle," (comma-delimited token list)
// season "yearlong" | "seasonal"
// open_start day-of-year 1..366
// open_end day-of-year 1..366 (may be < open_start for winter-wrapping)
// open_start day-of-year 1..365 (non-leap convention)
// open_end day-of-year 1..365 (non-leap convention) (may be < open_start for winter-wrapping)

import type { ExpressionSpecification } from "maplibre-gl";

/** Day-of-year (1..366) for a JS Date, in local time. */
// Cumulative day-of-year for the 1st of each month (non-leap); index 1..12.
// MUST match _MONTH_START in pipeline/normalize.py — the tiles encode season
// windows with this table, so the frontend has to count days the same way.
// Feb 29 intentionally maps to 60 (== Mar 1), same as the pipeline.
const MONTH_START = [0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];

/** Day-of-year (1..365, non-leap convention) for a JS Date, in local time. */
export function dayOfYear(d: Date): number {
const start = new Date(d.getFullYear(), 0, 0);
const diff = d.getTime() - start.getTime();
return Math.floor(diff / 86_400_000);
return MONTH_START[d.getMonth() + 1] + d.getDate();
}

/** True-expression: the route permits ANY of the profile's class tokens.
Expand Down
6 changes: 5 additions & 1 deletion web/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,11 @@ for (const p of VEHICLE_PROFILES) {
const paramVehicle = new URLSearchParams(location.search).get("vehicle");
vehicleSel.value =
VEHICLE_PROFILES.some((p) => p.key === paramVehicle) ? paramVehicle! : "moto_plated";
dateInput.value = new Date().toISOString().slice(0, 10);
// Local date, not the UTC ISO date string: in the evening (UTC is past
// midnight) the default must still be *today* here — "the night before" is
// the primary use.
const now = new Date();
dateInput.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
hideClosed.checked = new URLSearchParams(location.search).get("hideclosed") === "1";

function selectedTokens(): string[] {
Expand Down
Loading