#[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(())
}
}
const SHARP_NAMES_TABLE: [&str; 12] = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
const FLAT_NAMES_TABLE: [&str; 12] = [
"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
];
fn root_semitone(note: Note, accidental: Option<Accidental>) -> u8 {
let base = match note {
Note::C => 0,
Note::D => 2,
Note::E => 4,
Note::F => 5,
Note::G => 7,
Note::A => 9,
Note::B => 11,
};
match accidental {
Some(Accidental::Sharp) => (base + 1) % 12,
Some(Accidental::Flat) => (base + 11) % 12,
None => base,
}
}
#[must_use]
pub fn canonicalize_detail(detail: &ChordDetail, force_common: bool, flats: bool) -> String {
if !force_common {
return format!("{detail}");
}
let root_semi = root_semitone(detail.root, detail.root_accidental);
let key_semi = if detail.quality == ChordQuality::Minor {
(root_semi + 12 - 3) % 12
} else {
root_semi
};
let toosharp = matches!(key_semi, 1 | 3 | 8 | 10) || (key_semi == 6 && flats);
let root_name = if toosharp {
FLAT_NAMES_TABLE[root_semi as usize]
} else {
SHARP_NAMES_TABLE[root_semi as usize]
};
let mut out = String::with_capacity(detail.extension.as_deref().unwrap_or("").len() + 8);
out.push_str(root_name);
use core::fmt::Write as _;
let _ = write!(&mut out, "{}", detail.quality);
if let Some(ext) = detail.extension.as_deref() {
out.push_str(ext);
}
if let Some((bass, bass_acc)) = detail.bass_note {
out.push('/');
let _ = write!(&mut out, "{bass}");
if let Some(acc) = bass_acc {
let _ = write!(&mut out, "{acc}");
}
}
out
}
#[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());
}
fn canon(input: &str, force_common: bool, flats: bool) -> String {
let detail = parse_chord(input).expect("input must parse");
canonicalize_detail(&detail, force_common, flats)
}
#[test]
fn force_common_off_preserves_input_spelling() {
assert_eq!(canon("A#", false, false), "A#");
assert_eq!(canon("Bb", false, false), "Bb");
assert_eq!(canon("F#m7", false, false), "F#m7");
}
#[test]
fn force_common_rewrites_toosharp_majors() {
assert_eq!(canon("C#", true, false), "Db");
assert_eq!(canon("D#", true, false), "Eb");
assert_eq!(canon("G#", true, false), "Ab");
assert_eq!(canon("A#", true, false), "Bb");
}
#[test]
fn force_common_preserves_normal_majors() {
for root in ["C", "D", "E", "F", "G", "A", "B"] {
assert_eq!(canon(root, true, false), root, "{root}");
}
}
#[test]
fn f_sharp_default_stays_sharp() {
assert_eq!(canon("F#", true, false), "F#");
assert_eq!(canon("Gb", true, false), "F#");
}
#[test]
fn f_sharp_under_flats_becomes_gb() {
assert_eq!(canon("F#", true, true), "Gb");
assert_eq!(canon("Gb", true, true), "Gb");
}
#[test]
fn force_common_preserves_extensions() {
assert_eq!(canon("A#7", true, false), "Bb7");
assert_eq!(canon("D#maj7", true, false), "Ebmaj7");
assert_eq!(canon("G#sus4", true, false), "Absus4");
assert_eq!(canon("C#9", true, false), "Db9");
}
#[test]
fn force_common_preserves_bass_slash_chord() {
assert_eq!(canon("G/B", true, false), "G/B");
assert_eq!(canon("A#/E", true, false), "Bb/E");
}
#[test]
fn force_common_minor_shift_matches_upstream() {
assert_eq!(canon("F#m", true, false), "Gbm");
assert_eq!(canon("A#m", true, false), "A#m");
assert_eq!(canon("C#m", true, false), "Dbm");
assert_eq!(canon("Em", true, false), "Em");
}
}