feat(domain): add Note enum with semitone math and display
This commit is contained in:
3
crates/domain/src/lib.rs
Normal file
3
crates/domain/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod note;
|
||||||
|
|
||||||
|
pub use note::Note;
|
||||||
107
crates/domain/src/note.rs
Normal file
107
crates/domain/src/note.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
#[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, use_sharps: bool) -> 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 from_str(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, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flat_of(root: Note) -> Option<Note> {
|
||||||
|
if root.semitone() == 0 { return None; } // Cb is unusual, skip
|
||||||
|
Some(Note::from_semitone((root.semitone() + 11) % 12, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn semitone_roundtrip() {
|
||||||
|
assert_eq!(Note::from_semitone(Note::A.semitone(), true), Note::A);
|
||||||
|
assert_eq!(Note::from_semitone(Note::FSharpGFlat.semitone(), true), Note::FSharpGFlat);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::from_str("F#"), Some(Note::FSharpGFlat));
|
||||||
|
assert_eq!(Note::from_str("Gb"), Some(Note::FSharpGFlat));
|
||||||
|
assert_eq!(Note::from_str("A"), Some(Note::A));
|
||||||
|
assert_eq!(Note::from_str("X"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user