use std::fmt;
use crate::chord::{Chord, ChordQuality};
use crate::interval::Interval;
use crate::pitch::PitchClass;
use crate::roman::RomanNumeral;
use crate::scale::{Scale, ScaleKind};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiatonicChord {
pub interval: Interval,
pub quality: ChordQuality,
pub roman: RomanNumeral,
}
impl DiatonicChord {
pub const fn new(
interval: Interval,
quality: ChordQuality,
roman: RomanNumeral,
) -> Self {
Self {
interval,
quality,
roman,
}
}
pub fn in_key(&self, key: Key) -> Chord {
Chord::new(key.tonic + self.interval, self.quality)
}
}
pub const MAJOR_KEY_TRIADS: &[DiatonicChord] = &[
DiatonicChord::new(Interval::UNISON, ChordQuality::Major, RomanNumeral::new(1, ChordQuality::Major)),
DiatonicChord::new(Interval::MAJOR_SECOND, ChordQuality::Minor, RomanNumeral::new(2, ChordQuality::Minor)),
DiatonicChord::new(Interval::MAJOR_THIRD, ChordQuality::Minor, RomanNumeral::new(3, ChordQuality::Minor)),
DiatonicChord::new(Interval::PERFECT_FOURTH, ChordQuality::Major, RomanNumeral::new(4, ChordQuality::Major)),
DiatonicChord::new(Interval::PERFECT_FIFTH, ChordQuality::Major, RomanNumeral::new(5, ChordQuality::Major)),
DiatonicChord::new(Interval::MAJOR_SIXTH, ChordQuality::Minor, RomanNumeral::new(6, ChordQuality::Minor)),
DiatonicChord::new(Interval::MAJOR_SEVENTH, ChordQuality::Diminished, RomanNumeral::new(7, ChordQuality::Diminished)),
];
pub const MAJOR_KEY_SEVENTHS: &[DiatonicChord] = &[
DiatonicChord::new(Interval::UNISON, ChordQuality::Major7, RomanNumeral::new(1, ChordQuality::Major7)),
DiatonicChord::new(Interval::MAJOR_SECOND, ChordQuality::Minor7, RomanNumeral::new(2, ChordQuality::Minor7)),
DiatonicChord::new(Interval::MAJOR_THIRD, ChordQuality::Minor7, RomanNumeral::new(3, ChordQuality::Minor7)),
DiatonicChord::new(Interval::PERFECT_FOURTH, ChordQuality::Major7, RomanNumeral::new(4, ChordQuality::Major7)),
DiatonicChord::new(Interval::PERFECT_FIFTH, ChordQuality::Dominant7, RomanNumeral::new(5, ChordQuality::Dominant7)),
DiatonicChord::new(Interval::MAJOR_SIXTH, ChordQuality::Minor7, RomanNumeral::new(6, ChordQuality::Minor7)),
DiatonicChord::new(Interval::MAJOR_SEVENTH, ChordQuality::HalfDiminished7, RomanNumeral::new(7, ChordQuality::HalfDiminished7)),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Key {
pub tonic: PitchClass,
}
impl Key {
pub const fn new(tonic: PitchClass) -> Self {
Self { tonic }
}
pub const fn scale(self) -> Scale {
Scale::new(self.tonic, ScaleKind::Ionian)
}
pub const fn diatonic_triads(self) -> &'static [DiatonicChord] {
MAJOR_KEY_TRIADS
}
pub const fn diatonic_sevenths(self) -> &'static [DiatonicChord] {
MAJOR_KEY_SEVENTHS
}
pub fn contains(self, chord: Chord) -> bool {
let interval = chord.root - self.tonic;
self.diatonic_triads()
.iter()
.chain(self.diatonic_sevenths().iter())
.any(|d| d.interval == interval && d.quality == chord.quality)
}
pub fn roman_for(self, chord: Chord) -> Option<RomanNumeral> {
let interval = chord.root - self.tonic;
for d in self.diatonic_triads() {
if d.interval == interval && d.quality == chord.quality {
return Some(d.roman.clone());
}
}
for d in self.diatonic_sevenths() {
if d.interval == interval && d.quality == chord.quality {
return Some(d.roman.clone());
}
}
for d in self.diatonic_triads() {
if d.interval != interval {
continue;
}
match (d.quality, chord.quality) {
(ChordQuality::Major, ChordQuality::Dominant7) => {
return Some(d.roman.clone().with_quality(ChordQuality::Dominant7));
}
(ChordQuality::Minor, ChordQuality::Minor7) => {
return Some(d.roman.clone().with_quality(ChordQuality::Minor7));
}
_ => {}
}
}
None
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} major", self.tonic)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn c_major_diatonic_triads_are_c_dm_em_f_g_am_bdim() {
let key = Key::new(PitchClass::C);
let chords: Vec<String> = key
.diatonic_triads()
.iter()
.map(|d| d.in_key(key).to_string())
.collect();
assert_eq!(
chords,
vec!["C", "Dm", "Em", "F", "G", "Am", "Bdim"]
);
}
#[test]
fn c_major_diatonic_sevenths() {
let key = Key::new(PitchClass::C);
let chords: Vec<String> = key
.diatonic_sevenths()
.iter()
.map(|d| d.in_key(key).to_string())
.collect();
assert_eq!(
chords,
vec![
"Cmaj7", "Dm7", "Em7", "Fmaj7", "G7", "Am7", "Bm7♭5"
]
);
}
#[test]
fn d_major_v7_is_a7() {
let key = Key::new(PitchClass::D);
let v7 = key.diatonic_sevenths()[4].in_key(key);
assert_eq!(v7.to_string(), "A7");
}
fn roman_str(key: Key, chord: Chord) -> Option<String> {
key.roman_for(chord).map(|r| r.to_string())
}
#[test]
fn roman_for_diatonic_triads_in_c_major() {
let key = Key::new(PitchClass::C);
let cases = [
(PitchClass::C, ChordQuality::Major, "I"),
(PitchClass::D, ChordQuality::Minor, "ii"),
(PitchClass::E, ChordQuality::Minor, "iii"),
(PitchClass::F, ChordQuality::Major, "IV"),
(PitchClass::G, ChordQuality::Major, "V"),
(PitchClass::A, ChordQuality::Minor, "vi"),
(PitchClass::B, ChordQuality::Diminished, "vii°"),
];
for (root, q, expected) in cases {
assert_eq!(
roman_str(key, Chord::new(root, q)).as_deref(),
Some(expected),
"{root:?} {q:?}"
);
}
}
#[test]
fn roman_for_seventh_chords_in_c_major() {
let key = Key::new(PitchClass::C);
assert_eq!(
roman_str(key, Chord::new(PitchClass::G, ChordQuality::Dominant7)).as_deref(),
Some("V7")
);
assert_eq!(
roman_str(key, Chord::new(PitchClass::B, ChordQuality::HalfDiminished7))
.as_deref(),
Some("viiø7")
);
assert_eq!(
roman_str(key, Chord::new(PitchClass::C, ChordQuality::Major7)).as_deref(),
Some("Imaj7")
);
}
#[test]
fn roman_for_non_diatonic_dom7_on_major_degree() {
let key = Key::new(PitchClass::C);
assert_eq!(
roman_str(key, Chord::new(PitchClass::C, ChordQuality::Dominant7)).as_deref(),
Some("I7")
);
assert_eq!(
roman_str(key, Chord::new(PitchClass::F, ChordQuality::Dominant7)).as_deref(),
Some("IV7")
);
}
#[test]
fn roman_for_returns_none_for_unrelated_chord() {
let key = Key::new(PitchClass::C);
assert!(
key.roman_for(Chord::new(PitchClass::A, ChordQuality::Major))
.is_none()
);
assert!(
key.roman_for(Chord::new(PitchClass::F_SHARP, ChordQuality::Major))
.is_none()
);
}
#[test]
fn contains_recognises_diatonic_chords() {
let key = Key::new(PitchClass::C);
assert!(key.contains(Chord::new(PitchClass::C, ChordQuality::Major)));
assert!(key.contains(Chord::new(PitchClass::A, ChordQuality::Minor)));
assert!(key.contains(Chord::new(PitchClass::G, ChordQuality::Dominant7)));
assert!(!key.contains(Chord::new(PitchClass::F_SHARP, ChordQuality::Major)));
assert!(!key.contains(Chord::new(PitchClass::C, ChordQuality::Dominant7)));
}
#[test]
fn key_scale_is_ionian_at_tonic() {
let key = Key::new(PitchClass::G);
assert_eq!(key.scale(), Scale::new(PitchClass::G, ScaleKind::Ionian));
}
#[test]
fn display_includes_mode() {
assert_eq!(Key::new(PitchClass::C).to_string(), "C major");
assert_eq!(Key::new(PitchClass::F_SHARP).to_string(), "F♯ major");
}
#[test]
fn template_lengths_are_seven_each() {
assert_eq!(MAJOR_KEY_TRIADS.len(), 7);
assert_eq!(MAJOR_KEY_SEVENTHS.len(), 7);
}
}