feat(dates): read a bare ISO date as one boundary
bluearchive.wiki gives each boundary its own column and writes it as a plain 2026-08-04, so unlike every reader here there is no range to split and no field order to infer. Day precision, because the page states no time of day. Anchored at both ends, which matters more for this shape than for any other in the module: it is the least distinctive one, and unanchored it would happily find a date inside an article slug or a version string. Also names the two readers the paragraph below the table was describing. "The last two" stopped being true when rows were appended after them, and the examples belong to parseAdjacentFullRange either way. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
02dd7b3ed4
commit
77921e88f5
+4
-2
@@ -101,13 +101,15 @@ All live in `src/ingest/dates.ts`, each returning null rather than inferring any
|
||||
| `parseAdjacentFullRange` | `July 30, 2026 August 13, 2026` (halves split by an `<hr>`) | Persona 5: The Phantom X |
|
||||
| `parseYearFirstSlashRange` | `2026/07/30 – 2026/08/20` (year first, so field order is not inferred) | Arknights |
|
||||
| `parseOrdinalDateTimeRange` | `November 9th, 05:00 - December 4th, 2023, 04:59 (UTC-5)` (ordinal days, stated offset) | Reverse: 1999 |
|
||||
| `parseIsoDay` | `2026-08-04` (one boundary per column, so nothing to split) | Blue Archive |
|
||||
| `parseOpenRange` | `Jul. 24, 2026 - End of 4.6`, `July 10, 2026 - Permanent` | Star Rail, Wuthering Waves |
|
||||
|
||||
`parseOpenRange` is tried last because it is the most permissive — it accepts any leading full date
|
||||
and reports no end.
|
||||
|
||||
The last two are anchored at both ends and require a year on each half, which is what keeps them from
|
||||
eating prose. `August 12, 2026 Day 3 rewards are doubled` would otherwise read "Day 3" as an end, and
|
||||
`parseAdjacentFullRange` and `parseYearFirstSlashRange` are anchored at both ends and require a year
|
||||
on each half, which is what keeps them from eating prose. `August 12, 2026 Day 3 rewards are doubled`
|
||||
would otherwise read "Day 3" as an end, and
|
||||
`June 25, 2026 July 16/30, 2026` names *two* candidate ends — so it takes neither, and the leftover is
|
||||
not shown as a summary either (a date the parser refused to trust must not reappear dressed as
|
||||
information).
|
||||
|
||||
@@ -52,6 +52,25 @@ export function parseMonthDayYear(input: string): ParsedInstant | null {
|
||||
return value === null ? null : { iso: value, precision: "day" };
|
||||
}
|
||||
|
||||
/**
|
||||
* "2026-08-04" → 2026-08-04T00:00:00.000Z, day precision.
|
||||
*
|
||||
* A whole cell, anchored at both ends, because this is the least distinctive
|
||||
* shape here: unanchored it would find a date inside a version string or an
|
||||
* article slug. Blue Archive's wiki gives each boundary its own column and
|
||||
* writes it as a bare ISO date, so unlike every range above there is nothing to
|
||||
* split and no field order to infer.
|
||||
*
|
||||
* Rejects an impossible calendar date (`2026-02-30`) through `iso`, and states
|
||||
* `day` rather than `exact` because the source publishes no time of day.
|
||||
*/
|
||||
export function parseIsoDay(input: string): ParsedInstant | null {
|
||||
const m = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})\s*$/.exec(input);
|
||||
if (!m) return null;
|
||||
const value = iso(Number(m[1]), Number(m[2]), Number(m[3]));
|
||||
return value === null ? null : { iso: value, precision: "day" };
|
||||
}
|
||||
|
||||
/**
|
||||
* "August 12 - September 21, 2026" → both instants, year taken from the end.
|
||||
* A range whose end carries no year is unresolvable and returns null.
|
||||
|
||||
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
parseAdjacentFullRange,
|
||||
parseFullRange,
|
||||
parseIsoDay,
|
||||
parseLabelledStartEnd,
|
||||
parseMonthDayRange,
|
||||
parseMonthDayYear,
|
||||
@@ -330,3 +331,39 @@ describe("parseOrdinalDateTimeRange", () => {
|
||||
expect(range?.start.iso).toBe("2026-08-13T10:00:00.000Z");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseIsoDay", () => {
|
||||
test("parses a bare ISO date at day precision", () => {
|
||||
expect(parseIsoDay("2026-08-04")).toEqual({
|
||||
iso: "2026-08-04T00:00:00.000Z",
|
||||
precision: "day",
|
||||
});
|
||||
});
|
||||
|
||||
test("tolerates the whitespace a table cell carries", () => {
|
||||
expect(parseIsoDay(" 2026-09-15 ")?.iso).toBe("2026-09-15T00:00:00.000Z");
|
||||
});
|
||||
|
||||
test("rejects an impossible calendar date", () => {
|
||||
expect(parseIsoDay("2026-02-30")).toBeNull();
|
||||
expect(parseIsoDay("2026-13-01")).toBeNull();
|
||||
});
|
||||
|
||||
test("refuses a date embedded in anything else", () => {
|
||||
// Anchored at both ends because this is the least distinctive shape in this
|
||||
// module. Unanchored it finds dates in article slugs and version strings.
|
||||
expect(parseIsoDay("Rerun 2026-08-04")).toBeNull();
|
||||
expect(parseIsoDay("2026-08-04 - 2026-08-18")).toBeNull();
|
||||
// The page's other schedule tables write their boundaries this way, with a
|
||||
// wall clock and no timezone anywhere. Reading one as UTC would invent the
|
||||
// fact that matters most, so this reader takes none of them.
|
||||
expect(parseIsoDay("08/12/2026 11:00")).toBeNull();
|
||||
});
|
||||
|
||||
test("does not accept a partial date", () => {
|
||||
expect(parseIsoDay("2026-08")).toBeNull();
|
||||
expect(parseIsoDay("2026")).toBeNull();
|
||||
expect(parseIsoDay("TBA")).toBeNull();
|
||||
expect(parseIsoDay("")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user