game_manager_lib/utils/
text.rs1const EDITION_SUFFIXES: &[&str] = &[
6 "Collector's Edition",
7 "Collectors Edition",
8 "Complete Edition",
9 "Game of the Year Edition",
10 "Definitive Edition",
11 "Special Edition",
12 "Deluxe Edition",
13 "Premium Edition",
14 "Enhanced Edition",
15 "GOTY Edition",
16 "GOTY",
17 "Ultimate Edition",
18];
19
20pub fn strip_trademark_symbols(name: &str) -> String {
25 name.chars()
26 .filter(|c| !matches!(c, '™' | '®' | '©'))
27 .collect::<String>()
28 .split_whitespace()
29 .collect::<Vec<_>>()
30 .join(" ")
31}
32
33pub fn normalize_for_matching(name: &str) -> String {
36 strip_trademark_symbols(name)
37 .to_lowercase()
38 .replace([':', ',', '\'', '\u{2019}'], "")
39 .trim()
40 .to_string()
41}
42
43pub fn contains_word_boundary(haystack: &str, needle: &str) -> bool {
47 let h = haystack.as_bytes();
48 let n = needle.as_bytes();
49 if n.is_empty() || n.len() > h.len() {
50 return false;
51 }
52 for start in 0..=(h.len() - n.len()) {
53 if &h[start..start + n.len()] == n {
54 let before_ok = start == 0 || !(h[start - 1] as char).is_alphanumeric();
55 let end = start + n.len();
56 let after_ok = end == h.len() || !(h[end] as char).is_alphanumeric();
57 if before_ok && after_ok {
58 return true;
59 }
60 }
61 }
62 false
63}
64
65pub fn is_likely_non_base_game(name: &str) -> bool {
70 let lower = name.to_lowercase();
71 if name.trim().len() <= 2 {
72 return true;
73 }
74
75 let boundary_keywords = ["demo", "trial"];
77 if boundary_keywords
78 .iter()
79 .any(|kw| contains_word_boundary(&lower, kw))
80 {
81 return true;
82 }
83
84 let substring_keywords = [
86 "season pass",
87 "dlc",
88 "add-on",
89 "addon",
90 "expansion",
91 "pre-order",
92 "preorder",
93 "pre order",
94 "starter pack",
95 "pack",
96 "soundtrack",
97 "artbook",
98 "art book",
99 "playtest",
100 "goodie",
101 ];
102 substring_keywords.iter().any(|kw| lower.contains(kw))
103}
104
105pub fn strip_edition_suffix(name: &str) -> String {
108 let trimmed = name.trim();
109 for suffix in EDITION_SUFFIXES {
110 if let Some(rest) = trimmed.to_lowercase().strip_suffix(&suffix.to_lowercase()) {
111 return trimmed[..rest.len()]
112 .trim_end_matches(['-', ':', ',', ' '])
113 .trim()
114 .to_string();
115 }
116 }
117 trimmed.to_string()
118}
119
120pub fn slugify(name: &str) -> String {
125 strip_trademark_symbols(name)
126 .to_lowercase()
127 .chars()
128 .map(|c| if c.is_alphanumeric() { c } else { ' ' })
129 .collect::<String>()
130 .split_whitespace()
131 .collect::<Vec<_>>()
132 .join("-")
133}