Files
SixFlagsSuperCalendar/lib/park-meta.ts
josh e9da6f3120 fix: robust coaster matching + dark carnival color scheme
Matching fixes:
- normalize() now strips all non-word/non-space chars via [^\w\s] instead of
  a hand-rolled list, catching !, curly apostrophe (U+2019), and any future edge cases
- Add isCoaster() helper with prefix matching (min 5 chars) to handle subtitle
  mismatches in either direction (e.g. "Apocalypse" vs "Apocalypse the Ride",
  "The New Revolution - Classic" vs "New Revolution")
- Fix top-level rides loop which still used coasterNames.has(normalize()) instead
  of isCoaster() — this was the recurring bug causing top-level rides to miss

UI:
- Dark neutral base (#111) replacing cold navy and muddy purple
- Neon accent palette: hot pink, electric green, vivid yellow, cyan
- Park page max-width 960→1280px, calendar cells 72→96px tall
- Scrollbar accent matches theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 15:22:59 -04:00

87 lines
3.0 KiB
TypeScript

/**
* park-meta.json — persisted alongside the SQLite DB in data/
*
* This file stores per-park metadata that doesn't belong in the schedule DB:
* - rcdb_id: user-supplied RCDB park ID (fills into https://rcdb.com/{id}.htm)
* - coasters: list of operating roller coaster names scraped from RCDB
* - coasters_scraped_at: ISO timestamp of last RCDB scrape
*
* discover.ts: ensures every park has a skeleton entry (rcdb_id null by default)
* scrape.ts: populates coasters[] for parks with a known rcdb_id (30-day staleness)
*/
import fs from "fs";
import path from "path";
const META_PATH = path.join(process.cwd(), "data", "park-meta.json");
export interface ParkMeta {
/** RCDB park page ID — user fills this in manually after discover creates the skeleton */
rcdb_id: number | null;
/** Operating roller coaster names scraped from RCDB */
coasters: string[];
/** ISO timestamp of when coasters was last scraped from RCDB */
coasters_scraped_at: string | null;
}
export type ParkMetaMap = Record<string, ParkMeta>;
export function readParkMeta(): ParkMetaMap {
try {
return JSON.parse(fs.readFileSync(META_PATH, "utf8")) as ParkMetaMap;
} catch {
return {};
}
}
export function writeParkMeta(meta: ParkMetaMap): void {
fs.mkdirSync(path.dirname(META_PATH), { recursive: true });
fs.writeFileSync(META_PATH, JSON.stringify(meta, null, 2) + "\n");
}
/** Default skeleton entry for a park that has never been configured. */
export function defaultParkMeta(): ParkMeta {
return { rcdb_id: null, coasters: [], coasters_scraped_at: null };
}
const COASTER_STALE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
/** Returns true when the coaster list needs to be re-scraped from RCDB. */
export function areCoastersStale(entry: ParkMeta): boolean {
if (!entry.coasters_scraped_at) return true;
return Date.now() - new Date(entry.coasters_scraped_at).getTime() > COASTER_STALE_MS;
}
/**
* Normalize a ride name for fuzzy matching between data sources.
*
* Queue-Times uses branded names (BATMAN™ The Ride, THE JOKER™ Funhouse Coaster)
* while RCDB uses clean names (Batman The Ride, Joker Funhouse Coaster).
*
* Normalization steps:
* 1. Strip trademark/copyright symbols (™ ® ©)
* 2. Strip leading "THE " / "THE" prefix
* 3. Replace punctuation (- : ' ") with spaces
* 4. Collapse runs of whitespace
* 5. Lowercase and trim
*/
export function normalizeRideName(name: string): string {
return name
.replace(/[\u2122\u00ae\u00a9™®©]/g, "")
.replace(/^the\s+/i, "")
.replace(/[^\w\s]/g, " ")
.replace(/\s+/g, " ")
.toLowerCase()
.trim();
}
/**
* Returns a Set of normalized coaster names for fast membership checks.
* Returns null when no coaster data exists for the park.
*/
export function getCoasterSet(parkId: string, meta: ParkMetaMap): Set<string> | null {
const entry = meta[parkId];
if (!entry || entry.coasters.length === 0) return null;
return new Set(entry.coasters.map(normalizeRideName));
}