diff --git a/scripts/backfill/__tests__/match-tournament.test.ts b/scripts/backfill/__tests__/match-tournament.test.ts new file mode 100644 index 0000000..484ee30 --- /dev/null +++ b/scripts/backfill/__tests__/match-tournament.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from "vitest"; +import { extractTournamentIdentity } from "../match-tournament"; + +describe("extractTournamentIdentity", () => { + it("uses eventDate year when the name has no trailing year", () => { + const identity = extractTournamentIdentity({ + name: "Masters Tournament", + eventDate: "2026-04-09", + eventType: "tournament", + }); + + expect(identity).toEqual({ name: "Masters Tournament", year: 2026 }); + }); + + it("derives the year from eventDate for a simple tournament name", () => { + const identity = extractTournamentIdentity({ + name: "Wimbledon", + eventDate: "2026-07-01", + eventType: "tournament", + }); + + expect(identity).toEqual({ name: "Wimbledon", year: 2026 }); + }); + + it("strips a trailing year from the name and uses it as the canonical year", () => { + const identity = extractTournamentIdentity({ + name: "Wimbledon 2026", + eventDate: "2026-07-01", + eventType: "tournament", + }); + + expect(identity).toEqual({ name: "Wimbledon", year: 2026 }); + }); + + it("throws when neither the name nor eventDate supply a year", () => { + expect(() => + extractTournamentIdentity({ + name: "Wimbledon", + eventDate: null, + eventType: "tournament", + }), + ).toThrow(/cannot determine year/); + }); +}); diff --git a/scripts/backfill/match-tournament.ts b/scripts/backfill/match-tournament.ts new file mode 100644 index 0000000..e7c7c92 --- /dev/null +++ b/scripts/backfill/match-tournament.ts @@ -0,0 +1,57 @@ +/** + * Pure function(s) for extracting canonical tournament identity + * (name + year) from a `scoring_events` row. + * + * Used by the Phase 2 backfill to group per-window events into + * canonical `tournaments` rows. + */ + +export interface ScoringEventInput { + name: string; + eventDate: string | null; + eventType: string; +} + +export interface TournamentIdentity { + /** Tournament name with any trailing 4-digit year stripped. */ + name: string; + year: number; +} + +const TRAILING_YEAR_RE = / (\d{4})$/; + +/** + * Extracts the canonical `(name, year)` identity for a tournament + * from a scoring event. + * + * Resolution order for year: + * 1. A trailing 4-digit year on the event name (e.g. "Wimbledon 2026"). + * The year is stripped from the returned name. + * 2. The first 4 characters of `eventDate` (format `YYYY-MM-DD`). + * + * Throws if neither source supplies a year. + */ +export function extractTournamentIdentity( + ev: ScoringEventInput, +): TournamentIdentity { + const trimmedName = ev.name.trim(); + const match = trimmedName.match(TRAILING_YEAR_RE); + + if (match) { + const yearFromName = Number(match[1]); + const nameWithoutYear = trimmedName.slice(0, match.index).trim(); + return { name: nameWithoutYear, year: yearFromName }; + } + + if (ev.eventDate) { + const yearStr = ev.eventDate.slice(0, 4); + const yearFromDate = Number(yearStr); + if (Number.isFinite(yearFromDate) && yearStr.length === 4) { + return { name: trimmedName, year: yearFromDate }; + } + } + + throw new Error( + `cannot determine year for scoring event "${ev.name}" (eventDate=${ev.eventDate ?? "null"})`, + ); +}