test: add coaster name matching test suite

Extract matching logic into lib/coaster-match.ts (isCoasterMatch + normalizeForMatch)
so it can be imported by both the scraper and tests without duplication.

Add tests/coaster-matching.test.ts covering all known match/false-positive cases:
- Trademark symbols, leading THE, possessives, punctuation
- Subtitle variants in both directions (Apocalypse, New Revolution - Classic)
- Space-split brand words (BAT GIRL vs Batgirl)
- 4D subtitle extension (THE JOKER™ 4D Free Fly Coaster vs Joker)
- False positives: Joker y Harley Quinn, conjunction connectors

Run with: npm test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:43:20 -04:00
parent dc4fbeb7ec
commit 9cac86d241
5 changed files with 161 additions and 51 deletions

View File

@@ -9,45 +9,7 @@
const BASE = "https://queue-times.com/parks";
/**
* Normalize a ride name for fuzzy matching between Queue-Times and RCDB.
*
* - Strips trademark/copyright symbols (™ ® © and Unicode variants)
* - Strips leading "THE " prefix
* - Replaces ALL non-word, non-space characters with a space
* (handles !, -, :, ', ' U+2019, ", and any other punctuation)
* - Collapses whitespace, lowercases, trims
*/
function normalize(name: string): string {
return name
.replace(/[\u2122\u00ae\u00a9™®©]/g, "")
.replace(/^the\s+/i, "")
.replace(/[^\w\s]/g, " ")
.replace(/\s+/g, " ")
.toLowerCase()
.trim();
}
/**
* Check if a Queue-Times ride name matches any coaster in the RCDB set.
*
* Exact normalized match covers most cases. Prefix matching handles cases
* where one source drops or adds a subtitle:
* "Apocalypse" (QT) vs "Apocalypse the Ride" (RCDB)
* "The New Revolution - Classic" (QT) vs "New Revolution" (RCDB)
*
* Minimum 5 chars on the shorter side prevents accidental short matches.
*/
function isCoaster(name: string, coasterSet: Set<string>): boolean {
const norm = normalize(name);
if (coasterSet.has(norm)) return true;
for (const c of coasterSet) {
const shorter = norm.length <= c.length ? norm : c;
const longer = norm.length <= c.length ? c : norm;
if (shorter.length >= 5 && longer.startsWith(shorter)) return true;
}
return false;
}
import { isCoasterMatch, normalizeForMatch } from "../coaster-match.ts";
const HEADERS = {
"User-Agent":
@@ -131,7 +93,7 @@ export async function fetchLiveRides(
isOpen: r.is_open,
waitMinutes: r.wait_time ?? 0,
lastUpdated: r.last_updated,
isCoaster: coasterNames ? isCoaster(r.name, coasterNames) : false,
isCoaster: coasterNames ? isCoasterMatch(r.name, coasterNames) : false,
});
}
}