Files
gacha-event-tracker/test/dates.test.ts
T
Lucas WintherandClaude Opus 5 67951e81b4 feat(nikki): track Infinity Nikki
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]>
2026-08-17 17:20:54 +02:00

167 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});
});