112 lines
3.6 KiB
Rust
112 lines
3.6 KiB
Rust
#[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<Note> {
|
|
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<Note> {
|
|
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);
|
|
}
|
|
}
|