livesplit_title_abbreviations/
lib.rs1#![no_std]
2
3extern crate alloc;
4
5use alloc::{boxed::Box, format, string::String, vec, vec::Vec};
6use unicase::UniCase;
7
8fn ends_with_roman_numeral(name: &str) -> bool {
11 name.split_whitespace().rev().next().map_or(false, |n| {
12 n.chars().all(|c| c == 'I' || c == 'V' || c == 'X')
13 })
14}
15
16fn ends_with_numeric(name: &str) -> bool {
17 name.chars().last().map_or(false, |c| c.is_numeric())
18}
19
20fn series_subtitle_handling(name: &str, split_token: &str, list: &mut Vec<Box<str>>) -> bool {
21 let mut iter = name.splitn(2, split_token);
22 if let (Some(series), Some(subtitle)) = (iter.next(), iter.next()) {
23 let series_abbreviations = abbreviate(series);
24 let subtitle_abbreviations = abbreviate(subtitle);
25 let series_trimmed = series.trim_end();
26
27 let is_series_representative =
28 ends_with_numeric(series_trimmed) || ends_with_roman_numeral(series_trimmed);
29
30 let is_there_only_one_series_abbreviation = series_abbreviations.len() == 1;
31
32 for subtitle_abbreviation in &subtitle_abbreviations {
33 for series_abbreviation in &series_abbreviations {
34 if is_series_representative
35 || &**series_abbreviation != series
36 || is_there_only_one_series_abbreviation
37 {
38 list.push(
39 format!("{series_abbreviation}{split_token}{subtitle_abbreviation}").into(),
40 );
41 }
42 }
43 }
44
45 if is_series_representative {
46 list.extend(series_abbreviations);
47 }
48 list.extend(subtitle_abbreviations);
49
50 true
51 } else {
52 false
53 }
54}
55
56fn left_right_handling(name: &str, split_token: &str, list: &mut Vec<Box<str>>) -> bool {
57 let mut iter = name.splitn(2, split_token);
58 if let (Some(series), Some(subtitle)) = (iter.next(), iter.next()) {
59 let series_abbreviations = abbreviate(series);
60 let subtitle_abbreviations = abbreviate(subtitle);
61
62 for subtitle_abbreviation in &subtitle_abbreviations {
63 for series_abbreviation in &series_abbreviations {
64 list.push(
65 format!("{series_abbreviation}{split_token}{subtitle_abbreviation}").into(),
66 );
67 }
68 }
69
70 true
71 } else {
72 false
73 }
74}
75
76fn and_handling(name: &str, list: &mut Vec<Box<str>>) -> bool {
77 let and = UniCase::new("and");
78 for word in name.split_whitespace() {
79 if UniCase::new(word) == and {
80 let index = word.as_ptr() as usize - name.as_ptr() as usize;
81 let (left, rest) = name.split_at(index);
82 let right = &rest[word.len()..];
83 let name = format!("{left}&{right}");
84 list.extend(abbreviate(&name));
85 return true;
86 }
87 }
88 false
89}
90
91fn remove_prefix_word<'a>(text: &'a str, word: &str) -> Option<&'a str> {
92 let first_word = text.split_whitespace().next()?;
93 if unicase::eq(first_word, word) {
94 Some(text[first_word.len()..].trim_start())
95 } else {
96 None
97 }
98}
99
100fn is_all_caps_or_digits(text: &str) -> bool {
101 text.chars().all(|c| c.is_uppercase() || c.is_numeric())
102}
103
104pub fn abbreviate(name: &str) -> Vec<Box<str>> {
105 let name = name.trim();
106 let mut list = vec![];
107 if name.is_empty() {
108 return list;
109 }
110
111 let parenthesis = name
112 .char_indices()
113 .rev()
114 .find(|&(_, c)| c == '(')
115 .and_then(|(start, _)| {
116 name[start + 1..]
117 .char_indices()
118 .find(|&(_, c)| c == ')')
119 .map(|(end, _)| (start, end + 2))
120 });
121
122 if let Some((start, end)) = parenthesis {
123 let (before_parenthesis, rest) = name.split_at(start);
124 let after_parenthesis = &rest[end..];
125 let name = format!(
126 "{} {}",
127 before_parenthesis.trim_end(),
128 after_parenthesis.trim_start()
129 );
130 list.extend(abbreviate(&name));
131 } else if series_subtitle_handling(name, ": ", &mut list)
132 || series_subtitle_handling(name, " - ", &mut list)
133 || left_right_handling(name, " | ", &mut list)
134 || and_handling(name, &mut list)
135 {
136 } else {
137 if let Some(rest) =
138 remove_prefix_word(name, "the").or_else(|| remove_prefix_word(name, "a"))
139 {
140 list.push(rest.into());
141 }
142
143 if name.contains(char::is_whitespace) {
144 let mut abbreviated = String::new();
145 for word in name.split(|c: char| c.is_whitespace() || c == '-') {
146 if let Some(first_char) = word.chars().next() {
147 let word_mapped = word.chars().map(|c| if c == '&' { 'a' } else { c });
148
149 if first_char.is_numeric() {
150 abbreviated.extend(word_mapped);
151 } else if word.len() <= 4 && is_all_caps_or_digits(word) {
152 if !abbreviated.is_empty() {
153 abbreviated.push(' ');
154 }
155 abbreviated.extend(word_mapped);
156 } else {
157 abbreviated.push(first_char);
158 }
159 }
160 }
161 list.push(abbreviated.into());
162 }
163 }
164
165 list.sort_unstable();
166 list.dedup();
167
168 if let Some(idx) = list.iter().position(|x| name == x.as_ref()) {
169 let last = list.len() - 1;
170 list.swap(idx, last);
171 } else {
172 list.push(name.into());
173 }
174
175 list
176}
177
178pub fn abbreviate_category(category: &str) -> Vec<Box<str>> {
179 let mut abbrevs = Vec::new();
180
181 let mut splits = category.splitn(2, '(');
182 let before = splits.next().unwrap().trim();
183
184 if let Some(rest) = splits.next() {
185 splits = rest.splitn(2, ')');
186 let inside = splits.next().unwrap();
187 if let Some(after) = splits.next() {
188 let after = after.trim_end();
189
190 let mut buf = String::with_capacity(category.len());
191 buf.push_str(before);
192 buf.push_str(" (");
193
194 let mut splits = inside.split(',');
195 let mut variable = splits.next().unwrap();
196 for next_variable in splits {
197 buf.push_str(variable);
198 let old_len = buf.len();
199
200 buf.push(')');
201 buf.push_str(after);
202 abbrevs.push(buf.as_str().into());
203
204 buf.drain(old_len..);
205 buf.push(',');
206 variable = next_variable;
207 }
208
209 if after.trim().is_empty() {
210 buf.drain(before.len()..);
211 } else {
212 buf.drain(before.len() + 1..);
213 buf.push_str(after);
214 }
215
216 abbrevs.push(buf.into());
217 }
218 }
219
220 abbrevs.push(category.into());
221
222 abbrevs
223}
224
225#[cfg(test)]
226mod tests {
227 use super::abbreviate;
228 use alloc::boxed::Box;
229 use alloc::vec;
230
231 #[test]
238 fn game_test_1() {
239 let abbreviations = abbreviate("Burnout 3: Takedown");
240
241 let expected = vec![
242 Box::from("B3"),
243 Box::from("B3: Takedown"),
244 Box::from("Burnout 3"),
245 Box::from("Takedown"),
246 Box::from("Burnout 3: Takedown"),
247 ];
248
249 assert_eq!(abbreviations, expected);
250 }
251
252 #[test]
253 fn game_test_2() {
254 let abbreviations = abbreviate("The Legend of Zelda: The Wind Waker");
255
256 let expected = vec![
257 Box::from("Legend of Zelda: TWW"),
258 Box::from("Legend of Zelda: The Wind Waker"),
259 Box::from("Legend of Zelda: Wind Waker"),
260 Box::from("TLoZ: TWW"),
261 Box::from("TLoZ: The Wind Waker"),
262 Box::from("TLoZ: Wind Waker"),
263 Box::from("TWW"),
264 Box::from("The Wind Waker"),
265 Box::from("Wind Waker"),
266 Box::from("The Legend of Zelda: The Wind Waker"),
267 ];
268
269 assert_eq!(abbreviations, expected);
270 }
271
272 #[test]
273 fn game_test_3() {
274 let abbreviations = abbreviate("SpongeBob SquarePants: Battle for Bikini Bottom");
275
276 let expected = vec![
277 Box::from("Battle for Bikini Bottom"),
278 Box::from("BfBB"),
279 Box::from("SS: Battle for Bikini Bottom"),
280 Box::from("SS: BfBB"),
281 Box::from("SpongeBob SquarePants: Battle for Bikini Bottom"),
282 ];
283
284 assert_eq!(abbreviations, expected);
285 }
286
287 #[test]
288 #[rustfmt::skip]
289 fn game_test_4() {
290 let abbreviations = abbreviate("Super Mario 64");
291
292 let expected = vec![
293 Box::from("SM64"),
294 Box::from("Super Mario 64"),
295 ];
296
297 assert_eq!(abbreviations, expected);
298 }
299
300 #[test]
301 fn contains_original_title() {
302 let abbreviations = abbreviate("test title: the game");
303 assert!(abbreviations.contains(&Box::from("test title: the game")));
304 }
305
306 #[test]
307 fn removes_parens() {
308 let abbreviations = abbreviate("test title (the game)");
309 assert!(abbreviations.contains(&Box::from("test title")));
310 }
311
312 #[test]
313 fn original_title_is_last() {
314 let abbreviations = abbreviate("test title: the game");
315 let last = abbreviations.last().unwrap();
316
317 assert_eq!("test title: the game", last.as_ref())
318 }
319}