feat(domain): add Chord struct with parsing and display
This commit is contained in:
13
crates/domain/Cargo.toml
Normal file
13
crates/domain/Cargo.toml
Normal 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 }
|
||||||
85
crates/domain/src/chord.rs
Normal file
85
crates/domain/src/chord.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user