feat: add Girls' Frontline 2, from IOP Wiki

The best date material this project has after wiki.gg: every row states an
exact instant on both boundaries and names the zone, so nothing is converted
and nothing is assumed. `parseIsoClockRangeUtc` requires that `(UTC)` rather
than defaulting to it — a row that ever loses it should drop out instead of
landing hours off a boundary the reader is watching in-game.

The hazard here is the Server column. CN, EN and JP rows share one table and
the Chinese schedule runs about a year ahead, which is the akwiki CN-column
problem verbatim: publish EN, skip the rest. Betas are fenced off separately,
being dated exactly like everything else and playable by nobody, and the page
is an archive back to 2023, so currency is decided against ctx.now as in
bawiki. That leaves one live event today — thin, and true.

No resetOffsets: the EN boundaries land on three different clocks, which is a
patch window rather than a reset hour.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Lucas Winther
2026-08-19 03:50:05 +02:00
co-authored by Claude Opus 5
parent c1cd1148d9
commit 4dff14087f
14 changed files with 6029 additions and 21 deletions
+56
View File
@@ -11,6 +11,7 @@ import {
parseSlashClockZone,
parseSlashDateTimeRange,
parseYearFirstSlashRange,
parseIsoClockRangeUtc,
} from "../src/ingest/dates.ts";
describe("parseMonthDayYear", () => {
@@ -416,3 +417,58 @@ describe("parseSlashClockZone", () => {
).toBeNull();
});
});
describe("parseIsoClockRangeUtc", () => {
test("reads both boundaries exact, converting nothing", () => {
// IOP Wiki states the zone itself, so this is the one range reader here
// that assumes no offset anywhere.
const range = parseIsoClockRangeUtc(
"2026-08-06 13:00 - 2026-08-26 22:59 (UTC)",
);
expect(range?.start.iso).toBe("2026-08-06T13:00:00.000Z");
expect(range?.end.iso).toBe("2026-08-26T22:59:00.000Z");
expect(range?.start.precision).toBe("exact");
expect(range?.end.precision).toBe("exact");
});
test("accepts the en dash and the non-breaking spaces the page emits", () => {
// The wiki writes `&#160;-&#160;`, which `text()` decodes to spaces.
expect(
parseIsoClockRangeUtc("2025-01-16 17:00 2025-02-06 02:59 (UTC)")?.end
.iso,
).toBe("2025-02-06T02:59:00.000Z");
});
test("requires the stated zone rather than defaulting to UTC", () => {
// The failure this prevents is silent and hours wide: a wall clock with no
// zone is a missing fact, exactly as it is in `parseSlashClockZone`.
expect(parseIsoClockRangeUtc("2026-08-06 13:00 - 2026-08-26 22:59")).toBeNull();
expect(
parseIsoClockRangeUtc("2026-08-06 13:00 - 2026-08-26 22:59 (UTC+8)"),
).toBeNull();
});
test("rejects an impossible calendar day", () => {
expect(
parseIsoClockRangeUtc("2026-02-30 13:00 - 2026-03-26 22:59 (UTC)"),
).toBeNull();
expect(
parseIsoClockRangeUtc("2026-08-06 25:00 - 2026-08-26 22:59 (UTC)"),
).toBeNull();
});
test("is anchored at the start, so it cannot find a range inside prose", () => {
expect(
parseIsoClockRangeUtc("Runs 2026-08-06 13:00 - 2026-08-26 22:59 (UTC)"),
).toBeNull();
});
test("tolerates the ICS widget's leftovers after the zone", () => {
// The period cell also carries an export widget; the parser strips its
// markup but the container survives as trailing whitespace.
expect(
parseIsoClockRangeUtc("2026-08-06 13:00 - 2026-08-26 22:59 (UTC) ")
?.start.iso,
).toBe("2026-08-06T13:00:00.000Z");
});
});