Adds the game and its Game8 events page. One `SOURCES` entry against the existing game8 parser, plus one date shape that page needs: `parseLabelledStartEnd` reads a duration cell holding `Start: <date>` and `End: <date>` split by a `<br>`, which a tag-stripping reader flattens into one run of text. Four of the seven events say `End: Permanent`. That is the source telling us there is no deadline, so they publish with `endsAt: null` and `endPrecision: "unknown"` rather than a plausible date. The labelled cell is also structure end to end, so it never becomes a summary — stripping the leading half would leave "End: Permanent" standing where a description belongs. Verified by re-extracting the page with a throwaway script independent of the parser: 7 rows in, 7 events out, every date matching. Note the source itself is stale — its "Current Events" table still lists January-May 2025 and "Upcoming Events" says there are none. The adapter is right; the page looks abandoned, and Nikki coverage worth relying on needs a second source at a higher priority. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
167 lines
5.7 KiB
TypeScript
167 lines
5.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
import {
|
||
parseFullRange,
|
||
parseLabelledStartEnd,
|
||
parseMonthDayRange,
|
||
parseMonthDayYear,
|
||
parseOpenRange,
|
||
parseSlashDateTimeRange,
|
||
} from "../src/ingest/dates.ts";
|
||
|
||
describe("parseMonthDayYear", () => {
|
||
test("parses a full date at day precision", () => {
|
||
expect(parseMonthDayYear("August 12, 2026")).toEqual({
|
||
iso: "2026-08-12T00:00:00.000Z",
|
||
precision: "day",
|
||
});
|
||
});
|
||
|
||
test("accepts abbreviated months with and without a period", () => {
|
||
expect(parseMonthDayYear("Apr. 29, 2026")?.iso).toBe(
|
||
"2026-04-29T00:00:00.000Z",
|
||
);
|
||
expect(parseMonthDayYear("Sept 3, 2026")?.iso).toBe(
|
||
"2026-09-03T00:00:00.000Z",
|
||
);
|
||
});
|
||
|
||
test("returns null rather than guessing a missing year", () => {
|
||
expect(parseMonthDayYear("August 12")).toBeNull();
|
||
});
|
||
|
||
test("rejects impossible calendar dates", () => {
|
||
expect(parseMonthDayYear("February 30, 2026")).toBeNull();
|
||
});
|
||
|
||
test("rejects an unknown month name", () => {
|
||
expect(parseMonthDayYear("Smarch 3, 2026")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parseMonthDayRange", () => {
|
||
test("applies the stated year to both ends", () => {
|
||
expect(parseMonthDayRange("August 12 - September 21, 2026")).toEqual({
|
||
start: { iso: "2026-08-12T00:00:00.000Z", precision: "day" },
|
||
end: { iso: "2026-09-21T00:00:00.000Z", precision: "day" },
|
||
});
|
||
});
|
||
|
||
test("rolls the start back a year when the range crosses New Year", () => {
|
||
// "December 28 - January 4, 2027" — the stated year belongs to the end.
|
||
const r = parseMonthDayRange("December 28 - January 4, 2027");
|
||
expect(r?.start.iso).toBe("2026-12-28T00:00:00.000Z");
|
||
expect(r?.end.iso).toBe("2027-01-04T00:00:00.000Z");
|
||
});
|
||
|
||
test("handles en dash and abbreviated months", () => {
|
||
const r = parseMonthDayRange("Apr. 29 – May 13, 2026");
|
||
expect(r?.start.iso).toBe("2026-04-29T00:00:00.000Z");
|
||
expect(r?.end.iso).toBe("2026-05-13T00:00:00.000Z");
|
||
});
|
||
|
||
test("returns null for a year-less range", () => {
|
||
// Game8 summary tables render "08/12 - 08/24". Guessing the year here is
|
||
// exactly the failure the product exists to prevent.
|
||
expect(parseMonthDayRange("08/12 - 08/24")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parseSlashDateTimeRange", () => {
|
||
test("parses a timed range at exact precision", () => {
|
||
expect(
|
||
parseSlashDateTimeRange("2021/01/16 04:00 - 2021/01/31 03:59"),
|
||
).toEqual({
|
||
start: { iso: "2021-01-16T04:00:00.000Z", precision: "exact" },
|
||
end: { iso: "2021-01-31T03:59:00.000Z", precision: "exact" },
|
||
});
|
||
});
|
||
|
||
test("ignores trailing prose after the range", () => {
|
||
const r = parseSlashDateTimeRange(
|
||
"2021/01/16 04:00 - 2021/01/31 03:59 Currently Unavailable",
|
||
);
|
||
expect(r?.end.iso).toBe("2021-01-31T03:59:00.000Z");
|
||
});
|
||
|
||
test("returns null for non-date prose", () => {
|
||
expect(parseSlashDateTimeRange("Permanently Available")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parseFullRange", () => {
|
||
test("reads a year from each side", () => {
|
||
expect(parseFullRange("Aug. 14, 2026 - Aug. 24, 2026")).toEqual({
|
||
start: { iso: "2026-08-14T00:00:00.000Z", precision: "day" },
|
||
end: { iso: "2026-08-24T00:00:00.000Z", precision: "day" },
|
||
});
|
||
});
|
||
|
||
test("ignores prose trailing the range", () => {
|
||
const r = parseFullRange(
|
||
"July 30, 2026 - August 13, 2026 Reach Union Level 8 to unlock",
|
||
);
|
||
expect(r?.end.iso).toBe("2026-08-13T00:00:00.000Z");
|
||
});
|
||
|
||
test("does not fire on a range with the year only at the end", () => {
|
||
// That shape belongs to parseMonthDayRange, which rolls the start year.
|
||
expect(parseFullRange("August 12 - September 21, 2026")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parseOpenRange", () => {
|
||
test("keeps a real start when the end is not a date", () => {
|
||
// "End of 4.6" and "Permanent" are honest unknowns, not parse failures.
|
||
for (const input of [
|
||
"Jul. 24, 2026 - End of 4.6",
|
||
"July 10, 2026 - Permanent",
|
||
"August 3, 2026",
|
||
]) {
|
||
const r = parseOpenRange(input);
|
||
expect(r?.end).toBeNull();
|
||
expect(r?.start.iso.slice(0, 4)).toBe("2026");
|
||
}
|
||
});
|
||
|
||
test("returns null when there is no full date at all", () => {
|
||
expect(parseOpenRange("08/12 - 08/24")).toBeNull();
|
||
expect(parseOpenRange("Releases in Version 3.6")).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe("parseLabelledStartEnd", () => {
|
||
test("reads a labelled cell", () => {
|
||
expect(
|
||
parseLabelledStartEnd("Start: April 28, 2025 End: June 12, 2025"),
|
||
).toEqual({
|
||
start: { iso: "2025-04-28T00:00:00.000Z", precision: "day" },
|
||
end: { iso: "2025-06-12T00:00:00.000Z", precision: "day" },
|
||
});
|
||
});
|
||
|
||
test("reports no end when the end half is not a date", () => {
|
||
// "Permanent" is the source telling us there is no deadline. Inventing one
|
||
// is the failure this codebase exists to prevent.
|
||
const permanent = parseLabelledStartEnd(
|
||
"Start: January 24, 2025 End: Permanent",
|
||
);
|
||
expect(permanent?.start.iso).toBe("2025-01-24T00:00:00.000Z");
|
||
expect(permanent?.end).toBeNull();
|
||
|
||
expect(parseLabelledStartEnd("Start: January 24, 2025 End: TBD")?.end).toBeNull();
|
||
expect(parseLabelledStartEnd("Start: January 24, 2025")?.end).toBeNull();
|
||
});
|
||
|
||
test("returns null when the start half is not a date", () => {
|
||
expect(parseLabelledStartEnd("Start: After maintenance End: TBD")).toBeNull();
|
||
expect(parseLabelledStartEnd("Ends after 30 days of making your account.")).toBeNull();
|
||
});
|
||
|
||
test("requires the colons, so prose containing 'end' does not split", () => {
|
||
// Without them, any sentence mentioning an end would look like a boundary.
|
||
expect(
|
||
parseLabelledStartEnd("Start the quest before August 12, 2026 to end the arc"),
|
||
).toBeNull();
|
||
});
|
||
});
|