#[must_use]
pub fn unicode_accidentals(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_uppercase() && (b'A'..=b'G').contains(&c) && i + 1 < bytes.len() {
let next = bytes[i + 1];
if next == b'b' {
out.push(c as char);
out.push('\u{266D}');
i += 2;
continue;
}
if next == b'#' {
out.push(c as char);
out.push('\u{266F}');
i += 2;
continue;
}
}
if (c == b'b' || c == b'#') && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
let prev_is_note =
i > 0 && (b'A'..=b'G').contains(&bytes[i - 1]) && bytes[i - 1].is_ascii_uppercase();
if !prev_is_note {
out.push(if c == b'b' { '\u{266D}' } else { '\u{266F}' });
i += 1;
continue;
}
}
let len = utf8_char_len(c);
let end = (i + len).min(bytes.len());
out.push_str(&s[i..end]);
i = end;
}
out
}
fn utf8_char_len(lead: u8) -> usize {
if lead < 0x80 {
1
} else if lead < 0xC0 {
1
} else if lead < 0xE0 {
2
} else if lead < 0xF0 {
3
} else {
4
}
}
#[must_use]
pub fn tempo_marking_for(bpm: f32) -> Option<&'static str> {
if !bpm.is_finite() || bpm <= 0.0 {
return None;
}
if bpm < 40.0 {
return Some("Grave");
}
if bpm < 60.0 {
return Some("Largo");
}
if bpm < 66.0 {
return Some("Larghetto");
}
if bpm < 76.0 {
return Some("Adagio");
}
if bpm < 108.0 {
return Some("Andante");
}
if bpm < 120.0 {
return Some("Moderato");
}
if bpm < 168.0 {
return Some("Allegro");
}
if bpm < 177.0 {
return Some("Vivace");
}
if bpm < 200.0 {
return Some("Presto");
}
Some("Prestissimo")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unicode_accidentals_basic() {
assert_eq!(unicode_accidentals("Bb"), "B\u{266D}");
assert_eq!(unicode_accidentals("Eb7"), "E\u{266D}7");
assert_eq!(unicode_accidentals("F#m"), "F\u{266F}m");
assert_eq!(unicode_accidentals("Bb/Eb"), "B\u{266D}/E\u{266D}");
assert_eq!(unicode_accidentals("Am"), "Am");
assert_eq!(unicode_accidentals("Cdim"), "Cdim");
assert_eq!(unicode_accidentals("Cmaj7"), "Cmaj7");
assert_eq!(unicode_accidentals("Bbm7"), "B\u{266D}m7");
}
#[test]
fn unicode_accidentals_leaves_non_root_letters_alone() {
assert_eq!(unicode_accidentals("Verse"), "Verse");
assert_eq!(unicode_accidentals("中文"), "中文");
}
#[test]
fn unicode_accidentals_extension_alterations() {
assert_eq!(unicode_accidentals("Gb7(b9)"), "G\u{266D}7(\u{266D}9)");
assert_eq!(unicode_accidentals("Cmaj7#11"), "Cmaj7\u{266F}11");
assert_eq!(unicode_accidentals("D7b13"), "D7\u{266D}13");
assert_eq!(unicode_accidentals("C7b13"), "C7\u{266D}13");
assert_eq!(
unicode_accidentals("G7(b9,#11)"),
"G7(\u{266D}9,\u{266F}11)",
);
assert_eq!(unicode_accidentals("Cm7"), "Cm7");
assert_eq!(unicode_accidentals("Bbm7"), "B\u{266D}m7");
}
#[test]
fn tempo_marking_table() {
assert_eq!(tempo_marking_for(30.0), Some("Grave"));
assert_eq!(tempo_marking_for(50.0), Some("Largo"));
assert_eq!(tempo_marking_for(62.0), Some("Larghetto"));
assert_eq!(tempo_marking_for(70.0), Some("Adagio"));
assert_eq!(tempo_marking_for(90.0), Some("Andante"));
assert_eq!(tempo_marking_for(110.0), Some("Moderato"));
assert_eq!(tempo_marking_for(120.0), Some("Allegro"));
assert_eq!(tempo_marking_for(140.0), Some("Allegro"));
assert_eq!(tempo_marking_for(170.0), Some("Vivace"));
assert_eq!(tempo_marking_for(180.0), Some("Presto"));
assert_eq!(tempo_marking_for(220.0), Some("Prestissimo"));
assert_eq!(tempo_marking_for(0.0), None);
assert_eq!(tempo_marking_for(-1.0), None);
assert_eq!(tempo_marking_for(f32::NAN), None);
}
}