#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Note { A, ASharpBFlat, B, C, CSharpDFlat, D, DSharpEFlat, E, F, FSharpGFlat, G, GSharpAFlat, } impl Note { pub fn semitone(&self) -> u8 { match self { Note::C => 0, Note::CSharpDFlat => 1, Note::D => 2, Note::DSharpEFlat => 3, Note::E => 4, Note::F => 5, Note::FSharpGFlat => 6, Note::G => 7, Note::GSharpAFlat => 8, Note::A => 9, Note::ASharpBFlat => 10, Note::B => 11, } } pub fn from_semitone(s: u8) -> Note { match s % 12 { 0 => Note::C, 1 => Note::CSharpDFlat, 2 => Note::D, 3 => Note::DSharpEFlat, 4 => Note::E, 5 => Note::F, 6 => Note::FSharpGFlat, 7 => Note::G, 8 => Note::GSharpAFlat, 9 => Note::A, 10 => Note::ASharpBFlat, 11 => Note::B, _ => unreachable!(), } } pub fn to_sharp_str(&self) -> &'static str { match self { Note::C => "C", Note::CSharpDFlat => "C#", Note::D => "D", Note::DSharpEFlat => "D#", Note::E => "E", Note::F => "F", Note::FSharpGFlat => "F#", Note::G => "G", Note::GSharpAFlat => "G#", Note::A => "A", Note::ASharpBFlat => "A#", Note::B => "B", } } pub fn to_flat_str(&self) -> &'static str { match self { Note::C => "C", Note::CSharpDFlat => "Db", Note::D => "D", Note::DSharpEFlat => "Eb", Note::E => "E", Note::F => "F", Note::FSharpGFlat => "Gb", Note::G => "G", Note::GSharpAFlat => "Ab", Note::A => "A", Note::ASharpBFlat => "Bb", Note::B => "B", } } /// Parse just the note portion from the start of a string. /// Returns (Note, chars_consumed) or None. pub fn parse_prefix(s: &str) -> Option<(Note, usize)> { let mut chars = s.chars(); let root = match chars.next()? { 'A' => Note::A, 'B' => Note::B, 'C' => Note::C, 'D' => Note::D, 'E' => Note::E, 'F' => Note::F, 'G' => Note::G, _ => return None, }; match chars.next() { Some('#') => Some((Self::sharp_of(root), 2)), Some('b') if s.len() > 1 => { let flatted = Self::flat_of(root)?; Some((flatted, 2)) } _ => Some((root, 1)), } } pub fn parse(s: &str) -> Option { let (note, consumed) = Self::parse_prefix(s)?; if consumed == s.len() { Some(note) } else { None } } fn sharp_of(root: Note) -> Note { Note::from_semitone((root.semitone() + 1) % 12) } fn flat_of(root: Note) -> Option { Some(Note::from_semitone((root.semitone() + 11) % 12)) } } #[cfg(test)] mod tests { use super::*; #[test] fn semitone_roundtrip() { assert_eq!(Note::from_semitone(Note::A.semitone()), Note::A); assert_eq!(Note::from_semitone(Note::FSharpGFlat.semitone()), Note::FSharpGFlat); } #[test] fn parse_cb_enharmonic() { assert_eq!(Note::parse("Cb"), Some(Note::B)); } #[test] fn sharp_display() { assert_eq!(Note::ASharpBFlat.to_sharp_str(), "A#"); assert_eq!(Note::FSharpGFlat.to_sharp_str(), "F#"); } #[test] fn flat_display() { assert_eq!(Note::ASharpBFlat.to_flat_str(), "Bb"); assert_eq!(Note::CSharpDFlat.to_flat_str(), "Db"); } #[test] fn parse_note() { assert_eq!(Note::parse("F#"), Some(Note::FSharpGFlat)); assert_eq!(Note::parse("Gb"), Some(Note::FSharpGFlat)); assert_eq!(Note::parse("A"), Some(Note::A)); assert_eq!(Note::parse("X"), None); } }