Skip to main content

use_chord/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8    pub use crate::{
9        ChordAlteration, ChordError, ChordExtension, ChordInversion, ChordName, ChordQuality,
10        ChordSymbol, ChordToneRole, ChordVoicingKind, SeventhChordKind, TriadKind,
11    };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ChordName(String);
15
16impl ChordName {
17    pub fn new(value: impl AsRef<str>) -> Result<Self, ChordError> {
18        non_empty_text(value).map(Self)
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24
25    pub fn value(&self) -> &str {
26        self.as_str()
27    }
28
29    pub fn into_string(self) -> String {
30        self.0
31    }
32}
33
34impl AsRef<str> for ChordName {
35    fn as_ref(&self) -> &str {
36        self.as_str()
37    }
38}
39
40impl fmt::Display for ChordName {
41    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42        formatter.write_str(self.as_str())
43    }
44}
45
46impl FromStr for ChordName {
47    type Err = ChordError;
48
49    fn from_str(value: &str) -> Result<Self, Self::Err> {
50        Self::new(value)
51    }
52}
53
54impl TryFrom<&str> for ChordName {
55    type Error = ChordError;
56
57    fn try_from(value: &str) -> Result<Self, Self::Error> {
58        Self::new(value)
59    }
60}
61#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub struct ChordSymbol(String);
63
64impl ChordSymbol {
65    pub fn new(value: impl AsRef<str>) -> Result<Self, ChordError> {
66        non_empty_text(value).map(Self)
67    }
68
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72
73    pub fn value(&self) -> &str {
74        self.as_str()
75    }
76
77    pub fn into_string(self) -> String {
78        self.0
79    }
80}
81
82impl AsRef<str> for ChordSymbol {
83    fn as_ref(&self) -> &str {
84        self.as_str()
85    }
86}
87
88impl fmt::Display for ChordSymbol {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        formatter.write_str(self.as_str())
91    }
92}
93
94impl FromStr for ChordSymbol {
95    type Err = ChordError;
96
97    fn from_str(value: &str) -> Result<Self, Self::Err> {
98        Self::new(value)
99    }
100}
101
102impl TryFrom<&str> for ChordSymbol {
103    type Error = ChordError;
104
105    fn try_from(value: &str) -> Result<Self, Self::Error> {
106        Self::new(value)
107    }
108}
109#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum ChordQuality {
111    Major,
112    Minor,
113    Diminished,
114    Augmented,
115    Suspended2,
116    Suspended4,
117    Dominant,
118    HalfDiminished,
119    MajorMinor,
120    Power,
121    Custom,
122}
123
124impl ChordQuality {
125    pub const ALL: &'static [Self] = &[
126        Self::Major,
127        Self::Minor,
128        Self::Diminished,
129        Self::Augmented,
130        Self::Suspended2,
131        Self::Suspended4,
132        Self::Dominant,
133        Self::HalfDiminished,
134        Self::MajorMinor,
135        Self::Power,
136        Self::Custom,
137    ];
138
139    pub const fn as_str(self) -> &'static str {
140        match self {
141            Self::Major => "major",
142            Self::Minor => "minor",
143            Self::Diminished => "diminished",
144            Self::Augmented => "augmented",
145            Self::Suspended2 => "suspended-2",
146            Self::Suspended4 => "suspended-4",
147            Self::Dominant => "dominant",
148            Self::HalfDiminished => "half-diminished",
149            Self::MajorMinor => "major-minor",
150            Self::Power => "power",
151            Self::Custom => "custom",
152        }
153    }
154}
155
156impl fmt::Display for ChordQuality {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        formatter.write_str(self.as_str())
159    }
160}
161
162impl FromStr for ChordQuality {
163    type Err = ChordError;
164
165    fn from_str(value: &str) -> Result<Self, Self::Err> {
166        match normalized_label(value)?.as_str() {
167            "major" => Ok(Self::Major),
168            "minor" => Ok(Self::Minor),
169            "diminished" => Ok(Self::Diminished),
170            "augmented" => Ok(Self::Augmented),
171            "suspended-2" => Ok(Self::Suspended2),
172            "suspended-4" => Ok(Self::Suspended4),
173            "dominant" => Ok(Self::Dominant),
174            "half-diminished" => Ok(Self::HalfDiminished),
175            "major-minor" => Ok(Self::MajorMinor),
176            "power" => Ok(Self::Power),
177            "custom" => Ok(Self::Custom),
178            _ => Err(ChordError::UnknownLabel),
179        }
180    }
181}
182#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
183pub enum ChordToneRole {
184    Root,
185    Third,
186    Fifth,
187    Seventh,
188    Ninth,
189    Eleventh,
190    Thirteenth,
191    AddedTone,
192    AlteredTone,
193}
194
195impl ChordToneRole {
196    pub const ALL: &'static [Self] = &[
197        Self::Root,
198        Self::Third,
199        Self::Fifth,
200        Self::Seventh,
201        Self::Ninth,
202        Self::Eleventh,
203        Self::Thirteenth,
204        Self::AddedTone,
205        Self::AlteredTone,
206    ];
207
208    pub const fn as_str(self) -> &'static str {
209        match self {
210            Self::Root => "root",
211            Self::Third => "third",
212            Self::Fifth => "fifth",
213            Self::Seventh => "seventh",
214            Self::Ninth => "ninth",
215            Self::Eleventh => "eleventh",
216            Self::Thirteenth => "thirteenth",
217            Self::AddedTone => "added-tone",
218            Self::AlteredTone => "altered-tone",
219        }
220    }
221}
222
223impl fmt::Display for ChordToneRole {
224    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
225        formatter.write_str(self.as_str())
226    }
227}
228
229impl FromStr for ChordToneRole {
230    type Err = ChordError;
231
232    fn from_str(value: &str) -> Result<Self, Self::Err> {
233        match normalized_label(value)?.as_str() {
234            "root" => Ok(Self::Root),
235            "third" => Ok(Self::Third),
236            "fifth" => Ok(Self::Fifth),
237            "seventh" => Ok(Self::Seventh),
238            "ninth" => Ok(Self::Ninth),
239            "eleventh" => Ok(Self::Eleventh),
240            "thirteenth" => Ok(Self::Thirteenth),
241            "added-tone" => Ok(Self::AddedTone),
242            "altered-tone" => Ok(Self::AlteredTone),
243            _ => Err(ChordError::UnknownLabel),
244        }
245    }
246}
247#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
248pub enum ChordVoicingKind {
249    Closed,
250    Open,
251    Drop2,
252    Drop3,
253    Quartal,
254    Cluster,
255    Shell,
256    Custom,
257}
258
259impl ChordVoicingKind {
260    pub const ALL: &'static [Self] = &[
261        Self::Closed,
262        Self::Open,
263        Self::Drop2,
264        Self::Drop3,
265        Self::Quartal,
266        Self::Cluster,
267        Self::Shell,
268        Self::Custom,
269    ];
270
271    pub const fn as_str(self) -> &'static str {
272        match self {
273            Self::Closed => "closed",
274            Self::Open => "open",
275            Self::Drop2 => "drop-2",
276            Self::Drop3 => "drop-3",
277            Self::Quartal => "quartal",
278            Self::Cluster => "cluster",
279            Self::Shell => "shell",
280            Self::Custom => "custom",
281        }
282    }
283}
284
285impl fmt::Display for ChordVoicingKind {
286    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
287        formatter.write_str(self.as_str())
288    }
289}
290
291impl FromStr for ChordVoicingKind {
292    type Err = ChordError;
293
294    fn from_str(value: &str) -> Result<Self, Self::Err> {
295        match normalized_label(value)?.as_str() {
296            "closed" => Ok(Self::Closed),
297            "open" => Ok(Self::Open),
298            "drop-2" => Ok(Self::Drop2),
299            "drop-3" => Ok(Self::Drop3),
300            "quartal" => Ok(Self::Quartal),
301            "cluster" => Ok(Self::Cluster),
302            "shell" => Ok(Self::Shell),
303            "custom" => Ok(Self::Custom),
304            _ => Err(ChordError::UnknownLabel),
305        }
306    }
307}
308#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
309pub enum TriadKind {
310    Major,
311    Minor,
312    Diminished,
313    Augmented,
314    Suspended2,
315    Suspended4,
316    Power,
317}
318
319impl TriadKind {
320    pub const ALL: &'static [Self] = &[
321        Self::Major,
322        Self::Minor,
323        Self::Diminished,
324        Self::Augmented,
325        Self::Suspended2,
326        Self::Suspended4,
327        Self::Power,
328    ];
329
330    pub const fn as_str(self) -> &'static str {
331        match self {
332            Self::Major => "major",
333            Self::Minor => "minor",
334            Self::Diminished => "diminished",
335            Self::Augmented => "augmented",
336            Self::Suspended2 => "suspended-2",
337            Self::Suspended4 => "suspended-4",
338            Self::Power => "power",
339        }
340    }
341}
342
343impl fmt::Display for TriadKind {
344    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345        formatter.write_str(self.as_str())
346    }
347}
348
349impl FromStr for TriadKind {
350    type Err = ChordError;
351
352    fn from_str(value: &str) -> Result<Self, Self::Err> {
353        match normalized_label(value)?.as_str() {
354            "major" => Ok(Self::Major),
355            "minor" => Ok(Self::Minor),
356            "diminished" => Ok(Self::Diminished),
357            "augmented" => Ok(Self::Augmented),
358            "suspended-2" => Ok(Self::Suspended2),
359            "suspended-4" => Ok(Self::Suspended4),
360            "power" => Ok(Self::Power),
361            _ => Err(ChordError::UnknownLabel),
362        }
363    }
364}
365#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
366pub enum SeventhChordKind {
367    MajorSeventh,
368    MinorSeventh,
369    Dominant,
370    HalfDiminished,
371    Diminished,
372    MinorMajor,
373}
374
375impl SeventhChordKind {
376    pub const ALL: &'static [Self] = &[
377        Self::MajorSeventh,
378        Self::MinorSeventh,
379        Self::Dominant,
380        Self::HalfDiminished,
381        Self::Diminished,
382        Self::MinorMajor,
383    ];
384
385    pub const fn as_str(self) -> &'static str {
386        match self {
387            Self::MajorSeventh => "major-seventh",
388            Self::MinorSeventh => "minor-seventh",
389            Self::Dominant => "dominant",
390            Self::HalfDiminished => "half-diminished",
391            Self::Diminished => "diminished",
392            Self::MinorMajor => "minor-major",
393        }
394    }
395}
396
397impl fmt::Display for SeventhChordKind {
398    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
399        formatter.write_str(self.as_str())
400    }
401}
402
403impl FromStr for SeventhChordKind {
404    type Err = ChordError;
405
406    fn from_str(value: &str) -> Result<Self, Self::Err> {
407        match normalized_label(value)?.as_str() {
408            "major-seventh" => Ok(Self::MajorSeventh),
409            "minor-seventh" => Ok(Self::MinorSeventh),
410            "dominant" => Ok(Self::Dominant),
411            "half-diminished" => Ok(Self::HalfDiminished),
412            "diminished" => Ok(Self::Diminished),
413            "minor-major" => Ok(Self::MinorMajor),
414            _ => Err(ChordError::UnknownLabel),
415        }
416    }
417}
418pub type ChordExtension = u8;
419pub type ChordAlteration = String;
420
421#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
422pub struct ChordInversion(u8);
423
424impl ChordInversion {
425    pub fn new(value: u8) -> Result<Self, ChordError> {
426        if value > 7 {
427            return Err(ChordError::OutOfRange);
428        }
429        Ok(Self(value))
430    }
431
432    pub const fn value(self) -> u8 {
433        self.0
434    }
435}
436
437impl TriadKind {
438    pub const MAJOR: Self = Self::Major;
439    pub const MINOR: Self = Self::Minor;
440    pub const DIMINISHED: Self = Self::Diminished;
441    pub const AUGMENTED: Self = Self::Augmented;
442}
443
444impl SeventhChordKind {
445    pub const DOMINANT: Self = Self::Dominant;
446    pub const MAJOR_SEVENTH: Self = Self::MajorSeventh;
447    pub const MINOR_SEVENTH: Self = Self::MinorSeventh;
448}
449#[derive(Clone, Copy, Debug, Eq, PartialEq)]
450pub enum ChordError {
451    Empty,
452    InvalidFormat,
453    OutOfRange,
454    NonFinite,
455    NonPositive,
456    UnknownLabel,
457}
458
459impl fmt::Display for ChordError {
460    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
461        match self {
462            Self::Empty => formatter.write_str("chord metadata text cannot be empty"),
463            Self::InvalidFormat => formatter.write_str("chord metadata has an invalid format"),
464            Self::OutOfRange => formatter.write_str("chord metadata value is out of range"),
465            Self::NonFinite => formatter.write_str("chord metadata value must be finite"),
466            Self::NonPositive => formatter.write_str("chord metadata value must be positive"),
467            Self::UnknownLabel => formatter.write_str("unknown chord metadata label"),
468        }
469    }
470}
471
472impl Error for ChordError {}
473
474#[allow(dead_code)]
475fn non_empty_text(value: impl AsRef<str>) -> Result<String, ChordError> {
476    let trimmed = value.as_ref().trim();
477    if trimmed.is_empty() {
478        Err(ChordError::Empty)
479    } else {
480        Ok(trimmed.to_string())
481    }
482}
483
484fn normalized_label(value: &str) -> Result<String, ChordError> {
485    let trimmed = value.trim();
486    if trimmed.is_empty() {
487        Err(ChordError::Empty)
488    } else {
489        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
490    }
491}
492#[cfg(test)]
493#[allow(
494    unused_imports,
495    clippy::unnecessary_wraps,
496    clippy::assertions_on_constants
497)]
498mod tests {
499    use super::{
500        ChordAlteration, ChordError, ChordExtension, ChordInversion, ChordName, ChordQuality,
501        ChordSymbol, ChordToneRole, ChordVoicingKind, SeventhChordKind, TriadKind,
502    };
503    use core::{fmt, str::FromStr};
504
505    fn assert_enum_family<T>(variants: &[T]) -> Result<(), ChordError>
506    where
507        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = ChordError>,
508    {
509        for variant in variants {
510            let label = variant.to_string();
511            assert_eq!(label.parse::<T>()?, *variant);
512            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
513            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
514        }
515        Ok(())
516    }
517
518    #[test]
519    fn validates_text_newtypes() -> Result<(), ChordError> {
520        let value = ChordName::new(" example-value ")?;
521        assert_eq!(value.as_str(), "example-value");
522        assert_eq!(value.value(), "example-value");
523        assert_eq!(value.to_string(), "example-value");
524        assert_eq!(
525            <ChordName as TryFrom<&str>>::try_from("example-value")?,
526            value
527        );
528        let value = ChordSymbol::new(" example-value ")?;
529        assert_eq!(value.as_str(), "example-value");
530        assert_eq!(value.value(), "example-value");
531        assert_eq!(value.to_string(), "example-value");
532        assert_eq!(
533            <ChordSymbol as TryFrom<&str>>::try_from("example-value")?,
534            value
535        );
536        Ok(())
537    }
538
539    #[test]
540    fn validates_numeric_newtypes() -> Result<(), ChordError> {
541        assert!(true);
542        Ok(())
543    }
544
545    #[test]
546    fn displays_and_parses_enums() -> Result<(), ChordError> {
547        assert_enum_family(ChordQuality::ALL)?;
548        assert_enum_family(ChordToneRole::ALL)?;
549        assert_enum_family(ChordVoicingKind::ALL)?;
550        assert_enum_family(TriadKind::ALL)?;
551        assert_enum_family(SeventhChordKind::ALL)?;
552        Ok(())
553    }
554
555    #[test]
556    fn validates_chord_metadata() -> Result<(), ChordError> {
557        let symbol = ChordSymbol::new(" Cmaj7 ")?;
558        assert_eq!(symbol.as_str(), "Cmaj7");
559        assert_eq!(ChordSymbol::new(" "), Err(ChordError::Empty));
560        assert_eq!(ChordInversion::new(1)?.value(), 1);
561        assert_eq!(TriadKind::MAJOR, TriadKind::Major);
562        assert_eq!(SeventhChordKind::DOMINANT, SeventhChordKind::Dominant);
563        Ok(())
564    }
565}