holdings: a dash-led edition tail normalises like a bracketed one, so the catalogue's "Pinkerton - Deluxe Edition" finds the member who holds Pinkerton

This commit is contained in:
KeelrLeek committed 2026-09-09 12:12:55 -05:00
1 parent 1c66602b89
commit aaacfc9ff5
1 file changed
+58 -3
+58 -3
View File
@@ -87,10 +87,13 @@ fn key(username: &str) -> String {
// ---------------------------------------------------------------- names // ---------------------------------------------------------------- names
/// Bracketed chunks go — "(2019)", "[FLAC]", "{web}" — then only letters and /// Bracketed chunks go — "(2019)", "[FLAC]", "{web}" — and so does a
/// digits remain, lowercased; a leading "the" goes too. "Sigur Rós" and /// dash-led edition tail — "Pinkerton - Deluxe Edition", "Foo - EP" — then
/// "sigur ros" are not the same string here, and that is a known miss. /// only letters and digits remain, lowercased; a leading "the" goes too.
/// "Sigur Rós" and "sigur ros" are not the same string here, and that is a
/// known miss.
pub(crate) fn norm(s: &str) -> String { pub(crate) fn norm(s: &str) -> String {
let s = strip_edition_tail(s);
let mut out = String::with_capacity(s.len()); let mut out = String::with_capacity(s.len());
let mut depth = 0i32; let mut depth = 0i32;
for c in s.chars() { for c in s.chars() {
@@ -115,6 +118,41 @@ pub(crate) fn norm(s: &str) -> String {
out out
} }
/// The catalogue spells an edition after a spaced dash where a folder would
/// bracket it — "Pinkerton - Deluxe Edition" is the gap card, "Pinkerton
/// (Deluxe Edition)" the folder — so a dash-led tail that reads as an
/// edition goes the way brackets do, and only that tail: "Boards of Canada -
/// Geogaddi" is a name with a dash in it and stays whole. Found 2026-09-09
/// when the deluxe card beside a marked Pinkerton carried no "has it".
fn strip_edition_tail(s: &str) -> &str {
let mut s = s.trim_end();
loop {
let Some((at, len)) = [" - ", " – ", " — "]
.iter()
.filter_map(|d| s.rfind(d).map(|i| (i, d.len())))
.max_by_key(|(i, _)| *i)
else {
return s;
};
let tail: String = s[at + len..]
.chars()
.filter(|c| c.is_alphanumeric())
.flat_map(|c| c.to_lowercase())
.collect();
if !is_edition_tail(&tail) {
return s;
}
s = s[..at].trim_end();
}
}
fn is_edition_tail(t: &str) -> bool {
matches!(t, "ep" | "single")
|| ["deluxe", "remaster", "edition", "expanded", "anniversary", "reissue", "bonus", "explicit"]
.iter()
.any(|w| t.contains(w))
}
fn is_disc_folder(s: &str) -> bool { fn is_disc_folder(s: &str) -> bool {
let n: String = s.to_lowercase().chars().filter(|c| c.is_alphanumeric()).collect(); let n: String = s.to_lowercase().chars().filter(|c| c.is_alphanumeric()).collect();
for p in ["cd", "disc", "disk", "vol", "volume", "side"] { for p in ["cd", "disc", "disk", "vol", "volume", "side"] {
@@ -352,4 +390,21 @@ mod tests {
assert_eq!(norm("2000 - Spirit They're Gone Spirit They've Vanished"), "spirittheyregonespirittheyvevanished"); assert_eq!(norm("2000 - Spirit They're Gone Spirit They've Vanished"), "spirittheyregonespirittheyvevanished");
assert!(is_disc_folder("Disc 2") && is_disc_folder("cd1") && !is_disc_folder("Discovery")); assert!(is_disc_folder("Disc 2") && is_disc_folder("cd1") && !is_disc_folder("Discovery"));
} }
#[test]
fn dash_led_editions_go_the_way_brackets_do() {
assert_eq!(norm("Pinkerton - Deluxe Edition"), "pinkerton");
assert_eq!(norm("Pinkerton – Deluxe Edition"), "pinkerton");
assert_eq!(norm("Pinkerton — Deluxe Edition"), "pinkerton");
assert_eq!(norm("Pinkerton - Deluxe Edition (2010)"), "pinkerton");
assert_eq!(norm("Album - Deluxe Edition - Remastered"), "album");
assert_eq!(norm("Kid A - 2019 Remaster"), "kida");
assert_eq!(norm("Foo - EP"), "foo");
assert_eq!(norm("Foo - Single"), "foo");
assert_eq!(norm("Pinkerton (Deluxe Edition) [2010]"), norm("Pinkerton - Deluxe Edition"));
// A dash inside a name is not an edition.
assert_eq!(norm("Boards of Canada - Geogaddi"), "boardsofcanadageogaddi");
assert_eq!(norm("Album - Live"), "albumlive");
assert_eq!(norm("1996 - Expert Knob Twiddlers"), "expertknobtwiddlers");
}
} }