#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Note {
C,
D,
E,
F,
G,
A,
B,
}
impl Note {
fn from_char(c: char) -> Option<Self> {
match c {
'C' => Some(Self::C),
'D' => Some(Self::D),
'E' => Some(Self::E),
'F' => Some(Self::F),
'G' => Some(Self::G),
'A' => Some(Self::A),
'B' => Some(Self::B),
_ => None,
}
}
}
impl core::fmt::Display for Note {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s = match self {
Self::C => "C",
Self::D => "D",
Self::E => "E",
Self::F => "F",
Self::G => "G",
Self::A => "A",
Self::B => "B",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Accidental {
Sharp,
Flat,
}
impl core::fmt::Display for Accidental {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Sharp => f.write_str("#"),
Self::Flat => f.write_str("b"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChordQuality {
Major,
Minor,
Diminished,
Augmented,
}
impl core::fmt::Display for ChordQuality {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Major => Ok(()),
Self::Minor => f.write_str("m"),
Self::Diminished => f.write_str("dim"),
Self::Augmented => f.write_str("aug"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ChordDetail {
pub root: Note,
pub root_accidental: Option<Accidental>,
pub quality: ChordQuality,
pub extension: Option<String>,
pub bass_note: Option<(Note, Option<Accidental>)>,
}
impl core::fmt::Display for ChordDetail {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.root)?;
if let Some(ref acc) = self.root_accidental {
write!(f, "{acc}")?;
}
write!(f, "{}", self.quality)?;
if let Some(ref ext) = self.extension {
f.write_str(ext)?;
}
if let Some((ref bass, ref bass_acc)) = self.bass_note {
write!(f, "/{bass}")?;
if let Some(acc) = bass_acc {
write!(f, "{acc}")?;
}
}
Ok(())
}
}
#[must_use]
pub fn parse_chord(input: &str) -> Option<ChordDetail> {
let mut chars = input.chars().peekable();
let root = Note::from_char(*chars.peek()?)?;
chars.next();
let root_accidental = match chars.peek() {
Some('#') => {
chars.next();
Some(Accidental::Sharp)
}
Some('b') => {
chars.next();
Some(Accidental::Flat)
}
_ => None,
};
let rest: String = chars.collect();
let (quality_ext_str, bass_str) = if let Some(slash_pos) = rest.find('/') {
let (before, after) = rest.split_at(slash_pos);
(before, Some(&after[1..]))
} else {
(rest.as_str(), None)
};
let bass_note = if let Some(bass) = bass_str {
parse_note_with_accidental(bass)
} else {
None
};
if bass_str.is_some() && bass_note.is_none() {
return None;
}
let (quality, extension) = parse_quality_and_extension(quality_ext_str);
Some(ChordDetail {
root,
root_accidental,
quality,
extension,
bass_note,
})
}
fn parse_note_with_accidental(s: &str) -> Option<(Note, Option<Accidental>)> {
let mut chars = s.chars();
let note = Note::from_char(chars.next()?)?;
let accidental = match chars.next() {
Some('#') => Some(Accidental::Sharp),
Some('b') => Some(Accidental::Flat),
Some(_) => return None, None => None,
};
if chars.next().is_some() {
return None;
}
Some((note, accidental))
}
fn parse_quality_and_extension(s: &str) -> (ChordQuality, Option<String>) {
if s.is_empty() {
return (ChordQuality::Major, None);
}
if let Some(rest) = s.strip_prefix("dim") {
let ext = non_empty_string(rest);
return (ChordQuality::Diminished, ext);
}
if let Some(rest) = s.strip_prefix("aug") {
let ext = non_empty_string(rest);
return (ChordQuality::Augmented, ext);
}
if let Some(rest) = s.strip_prefix('+') {
let ext = non_empty_string(rest);
return (ChordQuality::Augmented, ext);
}
if let Some(rest) = s.strip_prefix("min") {
let ext = non_empty_string(rest);
return (ChordQuality::Minor, ext);
}
if let Some(rest) = s.strip_prefix("maj") {
let ext = if rest.is_empty() {
Some("maj".to_string())
} else {
Some(format!("maj{rest}"))
};
return (ChordQuality::Major, ext);
}
if let Some(rest) = s.strip_prefix('m') {
let ext = non_empty_string(rest);
return (ChordQuality::Minor, ext);
}
if s.starts_with("sus") {
return (ChordQuality::Major, Some(s.to_string()));
}
if s.starts_with("add") {
return (ChordQuality::Major, Some(s.to_string()));
}
if s.starts_with(|c: char| c.is_ascii_digit()) {
return (ChordQuality::Major, Some(s.to_string()));
}
if let Some(rest) = s.strip_prefix('°') {
let ext = non_empty_string(rest);
return (ChordQuality::Diminished, ext);
}
(ChordQuality::Major, Some(s.to_string()))
}
fn non_empty_string(s: &str) -> Option<String> {
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pd(input: &str) -> ChordDetail {
parse_chord(input).unwrap_or_else(|| panic!("expected Some for chord '{input}'"))
}
#[test]
fn basic_major_chords() {
for (input, expected_root) in [
("C", Note::C),
("D", Note::D),
("E", Note::E),
("F", Note::F),
("G", Note::G),
("A", Note::A),
("B", Note::B),
] {
let detail = pd(input);
assert_eq!(detail.root, expected_root, "root for '{input}'");
assert_eq!(detail.root_accidental, None, "accidental for '{input}'");
assert_eq!(detail.quality, ChordQuality::Major, "quality for '{input}'");
assert_eq!(detail.extension, None, "extension for '{input}'");
assert_eq!(detail.bass_note, None, "bass for '{input}'");
}
}
#[test]
fn minor_chords() {
let detail = pd("Am");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension, None);
let detail = pd("Em");
assert_eq!(detail.root, Note::E);
assert_eq!(detail.quality, ChordQuality::Minor);
let detail = pd("Dm");
assert_eq!(detail.root, Note::D);
assert_eq!(detail.quality, ChordQuality::Minor);
}
#[test]
fn minor_with_min_suffix() {
let detail = pd("Amin");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension, None);
}
#[test]
fn sharp_major() {
let detail = pd("C#");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
assert_eq!(detail.quality, ChordQuality::Major);
}
#[test]
fn flat_major() {
let detail = pd("Db");
assert_eq!(detail.root, Note::D);
assert_eq!(detail.root_accidental, Some(Accidental::Flat));
assert_eq!(detail.quality, ChordQuality::Major);
}
#[test]
fn sharp_minor() {
let detail = pd("F#m");
assert_eq!(detail.root, Note::F);
assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
assert_eq!(detail.quality, ChordQuality::Minor);
}
#[test]
fn flat_minor() {
let detail = pd("Bbm");
assert_eq!(detail.root, Note::B);
assert_eq!(detail.root_accidental, Some(Accidental::Flat));
assert_eq!(detail.quality, ChordQuality::Minor);
}
#[test]
fn bb_flat() {
let detail = pd("Bb");
assert_eq!(detail.root, Note::B);
assert_eq!(detail.root_accidental, Some(Accidental::Flat));
assert_eq!(detail.quality, ChordQuality::Major);
}
#[test]
fn major_seventh() {
let detail = pd("Cmaj7");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("maj7"));
}
#[test]
fn minor_seventh() {
let detail = pd("Am7");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension.as_deref(), Some("7"));
}
#[test]
fn dominant_seventh() {
let detail = pd("G7");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("7"));
}
#[test]
fn ninth_chord() {
let detail = pd("G9");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("9"));
}
#[test]
fn sus4() {
let detail = pd("Dsus4");
assert_eq!(detail.root, Note::D);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("sus4"));
}
#[test]
fn sus2() {
let detail = pd("Asus2");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("sus2"));
}
#[test]
fn add9() {
let detail = pd("Cadd9");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("add9"));
}
#[test]
fn minor_major_seventh() {
let detail = pd("Cmmaj7");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension.as_deref(), Some("maj7"));
}
#[test]
fn seventh_sus4() {
let detail = pd("G7sus4");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("7sus4"));
}
#[test]
fn sixth_chord() {
let detail = pd("C6");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("6"));
}
#[test]
fn minor_sixth() {
let detail = pd("Am6");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension.as_deref(), Some("6"));
}
#[test]
fn eleventh_chord() {
let detail = pd("G11");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("11"));
}
#[test]
fn thirteenth_chord() {
let detail = pd("C13");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("13"));
}
#[test]
fn diminished() {
let detail = pd("Bdim");
assert_eq!(detail.root, Note::B);
assert_eq!(detail.quality, ChordQuality::Diminished);
assert_eq!(detail.extension, None);
}
#[test]
fn diminished_seventh() {
let detail = pd("Cdim7");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Diminished);
assert_eq!(detail.extension.as_deref(), Some("7"));
}
#[test]
fn diminished_symbol() {
let detail = pd("B°");
assert_eq!(detail.root, Note::B);
assert_eq!(detail.quality, ChordQuality::Diminished);
assert_eq!(detail.extension, None);
}
#[test]
fn augmented() {
let detail = pd("Faug");
assert_eq!(detail.root, Note::F);
assert_eq!(detail.quality, ChordQuality::Augmented);
assert_eq!(detail.extension, None);
}
#[test]
fn augmented_plus_symbol() {
let detail = pd("C+");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Augmented);
assert_eq!(detail.extension, None);
}
#[test]
fn augmented_seventh() {
let detail = pd("Caug7");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Augmented);
assert_eq!(detail.extension.as_deref(), Some("7"));
}
#[test]
fn slash_chord_simple() {
let detail = pd("G/B");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.bass_note, Some((Note::B, None)));
}
#[test]
fn slash_chord_minor() {
let detail = pd("Am/E");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.bass_note, Some((Note::E, None)));
}
#[test]
fn slash_chord_with_accidental_bass() {
let detail = pd("C/Bb");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.bass_note, Some((Note::B, Some(Accidental::Flat))));
}
#[test]
fn slash_chord_with_sharp_bass() {
let detail = pd("Am/G#");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.bass_note, Some((Note::G, Some(Accidental::Sharp))));
}
#[test]
fn slash_chord_extended() {
let detail = pd("Am7/G");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension.as_deref(), Some("7"));
assert_eq!(detail.bass_note, Some((Note::G, None)));
}
#[test]
fn slash_chord_sharp_root() {
let detail = pd("F#m/E");
assert_eq!(detail.root, Note::F);
assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.bass_note, Some((Note::E, None)));
}
#[test]
fn empty_string() {
assert!(parse_chord("").is_none());
}
#[test]
fn lowercase_root() {
assert!(parse_chord("am").is_none());
}
#[test]
fn non_note_root() {
assert!(parse_chord("Hm").is_none());
assert!(parse_chord("X").is_none());
}
#[test]
fn numeric_only() {
assert!(parse_chord("7").is_none());
}
#[test]
fn slash_with_invalid_bass() {
assert!(parse_chord("G/X").is_none());
assert!(parse_chord("G/").is_none());
}
#[test]
fn slash_bass_too_long() {
assert!(parse_chord("G/Bm").is_none());
}
#[test]
fn multi_slash_is_invalid() {
assert!(parse_chord("C/D/E").is_none());
}
#[test]
fn display_basic_major() {
assert_eq!(pd("C").to_string(), "C");
}
#[test]
fn display_minor() {
assert_eq!(pd("Am").to_string(), "Am");
}
#[test]
fn display_sharp_minor_seventh() {
assert_eq!(pd("C#m7").to_string(), "C#m7");
}
#[test]
fn display_slash_chord() {
assert_eq!(pd("G/B").to_string(), "G/B");
}
#[test]
fn display_flat_chord() {
assert_eq!(pd("Bb").to_string(), "Bb");
}
#[test]
fn display_diminished() {
assert_eq!(pd("Bdim").to_string(), "Bdim");
}
#[test]
fn display_augmented() {
assert_eq!(pd("Faug").to_string(), "Faug");
}
#[test]
fn display_sus4() {
assert_eq!(pd("Dsus4").to_string(), "Dsus4");
}
#[test]
fn display_slash_with_accidental() {
assert_eq!(pd("C/Bb").to_string(), "C/Bb");
}
#[test]
fn display_complex_chord() {
assert_eq!(pd("F#m7/E").to_string(), "F#m7/E");
}
#[test]
fn maj_alone() {
let detail = pd("Cmaj");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("maj"));
}
#[test]
fn maj9() {
let detail = pd("Cmaj9");
assert_eq!(detail.root, Note::C);
assert_eq!(detail.quality, ChordQuality::Major);
assert_eq!(detail.extension.as_deref(), Some("maj9"));
}
#[test]
fn sharp_augmented() {
let detail = pd("G#+");
assert_eq!(detail.root, Note::G);
assert_eq!(detail.root_accidental, Some(Accidental::Sharp));
assert_eq!(detail.quality, ChordQuality::Augmented);
}
#[test]
fn flat_diminished() {
let detail = pd("Ebdim");
assert_eq!(detail.root, Note::E);
assert_eq!(detail.root_accidental, Some(Accidental::Flat));
assert_eq!(detail.quality, ChordQuality::Diminished);
}
#[test]
fn minor_add9() {
let detail = pd("Amadd9");
assert_eq!(detail.root, Note::A);
assert_eq!(detail.quality, ChordQuality::Minor);
assert_eq!(detail.extension.as_deref(), Some("add9"));
}
#[test]
fn empty_bass_string_is_invalid() {
assert!(parse_chord("G/").is_none());
}
}