Files
pocket-chords/crates/domain/src/note.rs

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);
}
}