feat(domain): add Chord struct with parsing and display

This commit is contained in:
2026-04-08 01:31:17 +02:00
parent ac1f1bd3da
commit 422afbb3f2
3 changed files with 100 additions and 0 deletions

13
crates/domain/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "domain"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
async-trait = { workspace = true }

View File

@@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
use crate::Note;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(into = "String", try_from = "String")]
pub struct Chord {
pub root: Note,
pub descriptor: Option<String>,
}
impl Chord {
pub fn from_str(s: &str) -> Option<Self> {
let (root, consumed) = Note::parse_prefix(s)?;
let descriptor = if consumed < s.len() {
Some(s[consumed..].to_string())
} else {
None
};
Some(Chord { root, descriptor })
}
/// Display chord name. use_sharps=true → "F#m", false → "Gbm".
pub fn name(&self, use_sharps: bool) -> String {
let root_str = if use_sharps {
self.root.to_sharp_str()
} else {
self.root.to_flat_str()
};
match &self.descriptor {
Some(d) => format!("{}{}", root_str, d),
None => root_str.to_string(),
}
}
}
impl From<Chord> for String {
fn from(c: Chord) -> String {
c.name(true)
}
}
impl TryFrom<String> for Chord {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
Chord::from_str(&s).ok_or_else(|| format!("invalid chord: {}", s))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
let c = Chord::from_str("Em").unwrap();
assert_eq!(c.root, crate::Note::E);
assert_eq!(c.descriptor.as_deref(), Some("m"));
}
#[test]
fn parse_no_descriptor() {
let c = Chord::from_str("G").unwrap();
assert_eq!(c.root, crate::Note::G);
assert!(c.descriptor.is_none());
}
#[test]
fn parse_flat_root() {
let c = Chord::from_str("Bb").unwrap();
assert_eq!(c.root, crate::Note::ASharpBFlat);
assert!(c.descriptor.is_none());
}
#[test]
fn name_sharp() {
let c = Chord { root: crate::Note::FSharpGFlat, descriptor: Some("m".into()) };
assert_eq!(c.name(true), "F#m");
}
#[test]
fn name_flat() {
let c = Chord { root: crate::Note::ASharpBFlat, descriptor: None };
assert_eq!(c.name(false), "Bb");
}
}

View File

@@ -1,3 +1,5 @@
pub mod note; pub mod note;
pub mod chord;
pub use note::Note; pub use note::Note;
pub use chord::Chord;