Files
pocket-chords/crates/domain/src/value_objects/note.rs
Gabriel Kaszewski d13df586dd refactor: DDD/CQRS architecture, unified crate layout
- crates: common→application, api→presentation, infrastructure/*→adapters/*
- new crates: api-types, infra-wiring
- domain: errors/, models/, value_objects/, ports/, services/
- application: CQRS use cases (songs/, tabs/) w/ commands, queries, deps
- unified DomainError replaces RepositoryError
- workspace deps, unused dep cleanup
- fix: parse plain-text chord lines (UG drops spans mid-song)
- tests extracted to separate modules (tests/ dirs)
2026-07-11 21:02:10 +02:00

130 lines
3.1 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",
}
}
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)]
#[path = "../tests/note.rs"]
mod tests;