1use unicode_normalization::UnicodeNormalization;
5use unicode_normalization::char::canonical_combining_class;
6
7use crate::error::{Error, Result};
8
9pub fn is_python_space(character: char) -> bool {
12 character.is_whitespace() || ('\u{1c}'..='\u{1f}').contains(&character)
13}
14
15pub fn str_repr(text: &str) -> String {
25 let quote = if text.contains('\'') && !text.contains('"') {
26 '"'
27 } else {
28 '\''
29 };
30 let mut written = String::new();
31 written.push(quote);
32 for character in text.chars() {
33 match character {
34 '\\' => written.push_str("\\\\"),
35 '\n' => written.push_str("\\n"),
36 '\r' => written.push_str("\\r"),
37 '\t' => written.push_str("\\t"),
38 _ if character == quote => {
39 written.push('\\');
40 written.push(character);
41 }
42 _ if character.is_control() || (is_python_space(character) && character != ' ') => {
43 let code = u32::from(character);
44 if code < 0x100 {
45 written.push_str(&format!("\\x{code:02x}"));
46 } else if code < 0x10000 {
47 written.push_str(&format!("\\u{code:04x}"));
48 } else {
49 written.push_str(&format!("\\U{code:08x}"));
50 }
51 }
52 _ => written.push(character),
53 }
54 }
55 written.push(quote);
56 written
57}
58
59pub fn whitespace_equal(a: &str, b: &str) -> bool {
69 a.chars()
70 .filter(|character| !is_python_space(*character))
71 .eq(b.chars().filter(|character| !is_python_space(*character)))
72}
73
74pub fn get_num_from_str(usr_str: &str, numbers: &str) -> (String, String) {
85 let mut found = String::new();
86 let mut remain = String::new();
87 for ch in usr_str.chars() {
88 if numbers.contains(ch) {
89 found.push(ch);
90 } else {
91 remain.push(ch);
92 }
93 }
94 (found, remain)
95}
96
97pub fn camel_case_to_hyphen(text: &str, replacement: char) -> String {
109 let characters: Vec<char> = text.chars().collect();
110 let mut first = String::new();
111 let mut index = 0;
112 while index < characters.len() {
113 let starts_word = characters[index] != '\n'
115 && characters
116 .get(index + 1)
117 .is_some_and(char::is_ascii_uppercase)
118 && characters
119 .get(index + 2)
120 .is_some_and(char::is_ascii_lowercase);
121 if starts_word {
122 first.push(characters[index]);
123 first.push(replacement);
124 first.push(characters[index + 1]);
125 index += 2;
126 while let Some(letter) = characters
127 .get(index)
128 .filter(|letter| letter.is_ascii_lowercase())
129 {
130 first.push(*letter);
131 index += 1;
132 }
133 } else {
134 first.push(characters[index]);
135 index += 1;
136 }
137 }
138 let characters: Vec<char> = first.chars().collect();
139 let mut second = String::new();
140 let mut index = 0;
141 while index < characters.len() {
142 let joins = (characters[index].is_ascii_lowercase() || characters[index].is_ascii_digit())
143 && characters
144 .get(index + 1)
145 .is_some_and(char::is_ascii_uppercase);
146 second.push(characters[index]);
147 if joins {
148 second.push(replacement);
149 second.push(characters[index + 1]);
150 index += 2;
151 } else {
152 index += 1;
153 }
154 }
155 second.to_lowercase()
156}
157
158pub fn camel_case_to_hyphen_checked(text: &str, replacement: &str) -> Result<String> {
165 let mut characters = replacement.chars();
166 let (Some(character), None) = (characters.next(), characters.next()) else {
167 return Err(Error::Value(
168 "Replacement must be a single character.".to_string(),
169 ));
170 };
171 if !character.to_lowercase().eq([character]) {
172 return Err(Error::Value(
173 "Replacement cannot be an uppercase character.".to_string(),
174 ));
175 }
176 Ok(camel_case_to_hyphen(text, character))
177}
178
179fn capitalize(word: &str) -> String {
182 let mut characters = word.chars();
183 let Some(first) = characters.next() else {
184 return String::new();
185 };
186 let title = match first {
187 'DŽ' | 'Dž' | 'dž' => "Dž".to_string(),
188 'LJ' | 'Lj' | 'lj' => "Lj".to_string(),
189 'NJ' | 'Nj' | 'nj' => "Nj".to_string(),
190 'DZ' | 'Dz' | 'dz' => "Dz".to_string(),
191 _ => first.to_uppercase().collect(),
192 };
193 title + &characters.as_str().to_lowercase()
194}
195
196pub fn hyphen_to_camel_case(text: &str, replacement: &str) -> Result<String> {
211 if replacement.is_empty() {
212 return Err(Error::Value("empty separator".to_string()));
213 }
214 let mut words = text.split(replacement);
215 let mut joined = words.next().unwrap_or_default().to_string();
216 for word in words {
217 joined.push_str(&capitalize(word));
218 }
219 Ok(joined)
220}
221
222pub fn space_camel_case(text: &str, replace_underscore: bool, keep_together: &[&str]) -> String {
236 const NUMBERS: &str = "0123456789.";
237 let mut spaced = String::new();
238 let mut last_is_number = false;
239 for (index, character) in text.chars().enumerate() {
240 let is_number = NUMBERS.contains(character);
241 let first_number = is_number && !last_is_number;
242 let first_letter = !is_number && last_is_number;
243 if index > 0 && (character.is_uppercase() || first_number || first_letter) {
244 spaced.push(' ');
245 }
246 spaced.push(character);
247 last_is_number = is_number;
248 }
249 for kept in keep_together {
250 let letters: Vec<String> = kept.chars().map(String::from).collect();
251 spaced = spaced.replace(&letters.join(" "), kept);
252 }
253 if replace_underscore {
254 spaced = spaced.replace('_', " ");
255 }
256 spaced
257}
258
259pub fn md5_hex(data: &[u8]) -> String {
268 const SHIFTS: [u32; 64] = [
269 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5,
270 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10,
271 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
272 ];
273 let constants: Vec<u32> = (0..64)
274 .map(|index: i32| (f64::from(index + 1).sin().abs() * 4_294_967_296.0) as u32)
275 .collect();
276 let mut message = data.to_vec();
277 let bit_length = (data.len() as u64).wrapping_mul(8);
278 message.push(0x80);
279 while message.len() % 64 != 56 {
280 message.push(0);
281 }
282 message.extend_from_slice(&bit_length.to_le_bytes());
283 let mut state: [u32; 4] = [0x6745_2301, 0xefcd_ab89, 0x98ba_dcfe, 0x1032_5476];
284 for block in message.chunks(64) {
285 let words: Vec<u32> = block
286 .chunks(4)
287 .map(|word| u32::from_le_bytes([word[0], word[1], word[2], word[3]]))
288 .collect();
289 let [mut a, mut b, mut c, mut d] = state;
290 for index in 0..64 {
291 let (mixed, word) = match index / 16 {
292 0 => ((b & c) | (!b & d), index),
293 1 => ((d & b) | (!d & c), (5 * index + 1) % 16),
294 2 => (b ^ c ^ d, (3 * index + 5) % 16),
295 _ => (c ^ (b | !d), (7 * index) % 16),
296 };
297 let rotated = a
298 .wrapping_add(mixed)
299 .wrapping_add(constants[index])
300 .wrapping_add(words[word])
301 .rotate_left(SHIFTS[index]);
302 (a, d, c) = (d, c, b);
303 b = b.wrapping_add(rotated);
304 }
305 for (held, added) in state.iter_mut().zip([a, b, c, d]) {
306 *held = held.wrapping_add(added);
307 }
308 }
309 state
310 .iter()
311 .flat_map(|word| word.to_le_bytes())
312 .map(|byte| format!("{byte:02x}"))
313 .collect()
314}
315
316pub fn strip_accents(text: &str) -> String {
328 text.nfkd()
329 .collect::<String>()
330 .replace('ß', "ss")
331 .replace(['“', '”'], "\"")
332 .replace(['‘', '’'], "'")
333 .chars()
334 .filter(|character| canonical_combining_class(*character) == 0)
335 .collect()
336}
337
338pub fn normalize_filename(name: &str) -> String {
352 let characters: Vec<char> = name.chars().collect();
353 let length = characters.len();
354 let (stem, extension) = if length > 5 && characters[length - 4] == '.' {
355 (
356 characters[..length - 4].iter().collect::<String>(),
357 Some(characters[length - 4..].iter().collect::<String>()),
358 )
359 } else {
360 (name.to_string(), None)
361 };
362 let mut normalized: String = strip_accents(&stem)
363 .chars()
364 .filter(char::is_ascii)
365 .map(|character| {
366 if character.is_ascii_alphanumeric() || character == '_' || character == '-' {
367 character
368 } else {
369 '_'
370 }
371 })
372 .collect();
373 if let Some(extension) = extension {
374 normalized.push_str(&extension);
375 }
376 normalized
377}
378
379pub fn remove_punctuation(text: &str) -> String {
388 text.chars()
389 .filter(|character| !character.is_ascii_punctuation())
390 .collect()
391}
392
393#[derive(Clone, Debug, PartialEq, Eq)]
396pub struct ParenthesesMatch {
397 pub start: usize,
399 pub end: usize,
401 pub text: String,
403 pub nested: Vec<ParenthesesMatch>,
405}
406
407pub fn parentheses_match(text: &str, open: &str, close: &str) -> Result<Vec<ParenthesesMatch>> {
425 if open.is_empty() || close.is_empty() {
426 return Err(Error::Value(
427 "Neither open nor close can be empty.".to_string(),
428 ));
429 }
430 let characters: Vec<char> = text.chars().collect();
431 let open: Vec<char> = open.chars().collect();
432 let close: Vec<char> = close.chars().collect();
433 let at = |index: usize, bracket: &[char]| {
434 characters.get(index..index + bracket.len()) == Some(bracket)
435 };
436 let mut stack = vec![ParenthesesMatch {
437 start: 0,
438 end: 0,
439 text: String::new(),
440 nested: Vec::new(),
441 }];
442 let mut escaped = false;
443 let mut index = 0;
444 while index < characters.len() {
445 if !escaped && at(index, &open) {
446 stack.push(ParenthesesMatch {
447 start: index + open.len(),
448 end: 0,
449 text: String::new(),
450 nested: Vec::new(),
451 });
452 index += open.len();
453 continue;
454 }
455 if !escaped && at(index, &close) {
456 if stack.len() <= 1 {
457 return Err(Error::Value(format!(
458 "Closing {} without {} at index {index}.",
459 str_repr(&close.iter().collect::<String>()),
460 str_repr(&open.iter().collect::<String>())
461 )));
462 }
463 let mut found = stack.pop().unwrap_or_else(|| unreachable!());
464 found.end = index;
465 found.text = characters[found.start..index].iter().collect();
466 if let Some(outer) = stack.last_mut() {
467 outer.nested.push(found);
468 }
469 index += close.len();
470 continue;
471 }
472 escaped = characters[index] == '\\' && !escaped;
473 index += 1;
474 }
475 if let Some(unclosed) = stack.get(1) {
476 return Err(Error::Value(format!(
477 "Opening {} at index {} was never closed",
478 str_repr(&open.iter().collect::<String>()),
479 unclosed.start - 1
480 )));
481 }
482 Ok(stack.pop().map(|outer| outer.nested).unwrap_or_default())
483}
484
485pub fn format_str<S: AsRef<str>>(parts: &[S]) -> String {
492 let joined: Vec<&str> = parts.iter().map(AsRef::as_ref).collect();
493 format!("{}\n", joined.join(" "))
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
502 fn camel_case_is_split_as_music21_splits_it() {
503 assert_eq!(camel_case_to_hyphen("movementName", '-'), "movement-name");
504 assert_eq!(
505 camel_case_to_hyphen("movementNameName", '_'),
506 "movement_name_name"
507 );
508 assert_eq!(camel_case_to_hyphen("fileName", '-'), "file-name");
509 assert_eq!(camel_case_to_hyphen("fileNameABC", '-'), "file-name-abc");
510 assert_eq!(camel_case_to_hyphen("Movement", '-'), "movement");
511 assert_eq!(camel_case_to_hyphen("SnapPizzicato", ' '), "snap pizzicato");
512 assert_eq!(camel_case_to_hyphen("OrganHeel", ' '), "organ heel");
513 assert_eq!(camel_case_to_hyphen("ABCd", '-'), "ab-cd");
514 assert_eq!(camel_case_to_hyphen("AbCdEf", '-'), "ab-cd-ef");
515 assert_eq!(camel_case_to_hyphen("x2Y", '-'), "x2-y");
516 }
517
518 #[test]
519 fn a_line_break_starts_no_word() {
520 assert_eq!(camel_case_to_hyphen("a\nBc", '-'), "a\nbc");
521 }
522
523 #[test]
524 fn replacements_music21_refuses() {
525 assert_eq!(
526 camel_case_to_hyphen_checked("fileName", "NotFound")
527 .unwrap_err()
528 .to_string(),
529 "Value error: Replacement must be a single character."
530 );
531 assert!(camel_case_to_hyphen_checked("fileName", "A").is_err());
532 assert_eq!(
533 camel_case_to_hyphen_checked("fileName", "_").unwrap(),
534 "file_name"
535 );
536 assert!(hyphen_to_camel_case("a-b", "").is_err());
537 assert_eq!(hyphen_to_camel_case("voice", "-").unwrap(), "voice");
538 }
539
540 #[test]
541 fn camel_case_is_spaced_as_music21_spaces_it() {
542 assert_eq!(
543 space_camel_case("thisIsATest", true, &["PMFC"]),
544 "this Is A Test"
545 );
546 assert_eq!(space_camel_case("movement3", true, &["PMFC"]), "movement 3");
547 assert_eq!(
548 space_camel_case("opus23402no219235", true, &["PMFC"]),
549 "opus 23402 no 219235"
550 );
551 assert_eq!(
552 space_camel_case("hello_myke", true, &["PMFC"]),
553 "hello myke"
554 );
555 }
556
557 #[test]
558 fn md5_of_rfc_1321s_examples() {
559 assert_eq!(md5_hex(b""), "d41d8cd98f00b204e9800998ecf8427e");
560 assert_eq!(md5_hex(b"abc"), "900150983cd24fb0d6963f7d28e17f72");
561 let long =
562 b"12345678901234567890123456789012345678901234567890123456789012345678901234567890";
563 assert_eq!(md5_hex(long), "57edf4a22be3c955ac49da2e2107b67a");
564 }
565
566 #[test]
567 fn accents_come_off_and_the_rest_stays() {
568 assert_eq!(strip_accents("Muß"), "Muss");
569 assert_eq!(strip_accents("音楽"), "音楽");
570 assert!(whitespace_equal("a\u{1c}b", "ab"));
571 }
572
573 #[test]
574 fn brackets_matched_or_refused() {
575 let found = parentheses_match(
576 r"Bologne wrote (a (whole) (lot) \(of\)) sym\(ph(on)ies\) concertantes.",
577 "(",
578 ")",
579 )
580 .unwrap();
581 assert_eq!((found[0].start, found[0].end), (15, 37));
582 assert_eq!(found[0].nested.len(), 2);
583 assert_eq!(
584 (found[1].start, found[1].end, found[1].text.as_str()),
585 (47, 49, "on")
586 );
587 let found = parentheses_match(
588 "Did you eat <<beans>> today <<Pythagoreas<<?>>>>",
589 "<<",
590 ">>",
591 )
592 .unwrap();
593 assert_eq!(found[1].text, "Pythagoreas<<?>>");
594 assert_eq!(found[1].nested[0].start, 43);
595 let unclosed = parentheses_match("My (parentheses (sometimes (continue", "(", ")");
596 assert_eq!(
597 unclosed.unwrap_err().to_string(),
598 "Value error: Opening '(' at index 3 was never closed"
599 );
600 let unopened = parentheses_match("This is a <bad> example>", "<", ">");
601 assert_eq!(
602 unopened.unwrap_err().to_string(),
603 "Value error: Closing '>' without '<' at index 23."
604 );
605 }
606
607 #[test]
608 fn test_basic() {
609 let (nums, rest) = get_num_from_str("23a", "0123456789");
610 assert_eq!(nums, "23");
611 assert_eq!(rest, "a");
612 }
613
614 #[test]
615 fn test_multiple_numbers() {
616 let (nums, rest) = get_num_from_str("23a954Hello", "0123456789");
617 assert_eq!(nums, "23954");
618 assert_eq!(rest, "aHello");
619 }
620
621 #[test]
622 fn test_empty_string() {
623 let (nums, rest) = get_num_from_str("", "0123456789");
624 assert_eq!(nums, "");
625 assert_eq!(rest, "");
626 }
627
628 #[test]
629 fn test_no_numbers() {
630 let (nums, rest) = get_num_from_str("Hello", "0123456789");
631 assert_eq!(nums, "");
632 assert_eq!(rest, "Hello");
633 }
634}