Skip to main content

music21/key/
mod.rs

1pub use keysignature::{
2    KeySignature, mode_sharps_alter, pitch_name_to_sharps, pitch_to_sharps, sharps_to_pitch,
3};
4
5use crate::{
6    chord::Chord,
7    defaults::IntegerType,
8    error::{Error, Result},
9    interval::Interval,
10    pitch::Pitch,
11    scale::{Scale, ScaleType},
12};
13use std::str::FromStr;
14
15/// Key-signature conversion and spelling helpers.
16pub mod keysignature;
17
18/// The modes a [`Key`] can be in, as music21 names them: the ones its
19/// diatonic scale builds.
20pub const MODES: [&str; 15] = [
21    "major",
22    "minor",
23    "ionian",
24    "dorian",
25    "phrygian",
26    "lydian",
27    "mixolydian",
28    "aeolian",
29    "locrian",
30    "hypodorian",
31    "hypophrygian",
32    "hypolydian",
33    "hypomixolydian",
34    "hypoaeolian",
35    "hypolocrian",
36];
37
38#[derive(Clone, Debug)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40/// A tonal key with a tonic pitch and mode.
41#[must_use]
42pub struct Key {
43    tonic_pitch: Pitch,
44    mode: String,
45    sharps: IntegerType,
46    #[cfg_attr(feature = "serde", serde(default))]
47    color: Option<String>,
48}
49
50impl Key {
51    pub(crate) fn new(tonic_pitch: Pitch, mode: &str, sharps: IntegerType) -> Self {
52        Self {
53            tonic_pitch,
54            mode: mode.to_string(),
55            sharps,
56            color: None,
57        }
58    }
59
60    /// The colour the key's signature is drawn in, if one was said.
61    pub fn color(&self) -> Option<&str> {
62        self.color.as_deref()
63    }
64
65    /// Sets the colour the key's signature is drawn in.
66    pub fn set_color(&mut self, color: Option<String>) {
67        self.color = color;
68    }
69
70    /// Builds a key from a tonic name and a mode. Without a mode the name
71    /// decides, as music21's `Key` reads it: a trailing `m` is minor and a
72    /// trailing `M` major (`F#m`, `EM`), otherwise a lower-case name is minor
73    /// and an upper-case one major.
74    ///
75    /// The mode is one of [`MODES`], in any case. Any other mode is an
76    /// error, as music21's `Key` raises for a mode it builds no scale for.
77    pub fn from_tonic_mode<'a, M>(tonic: &str, mode: M) -> Result<Self>
78    where
79        M: Into<Option<&'a str>>,
80    {
81        let mode = mode.into();
82        let (tonic, resolved_mode) = match mode {
83            Some(mode) => (tonic.to_string(), mode.to_lowercase()),
84            None if tonic.contains('m') => (tonic.replace('m', ""), "minor".to_string()),
85            None if tonic.contains('M') => (tonic.replace('M', ""), "major".to_string()),
86            None if tonic.chars().all(|ch| !ch.is_ascii_uppercase()) => {
87                (tonic.to_string(), "minor".to_string())
88            }
89            None => (tonic.to_string(), "major".to_string()),
90        };
91        if !MODES.contains(&resolved_mode.as_str()) {
92            return Err(Error::Key(format!(
93                "Cannot create a scale of the following mode: {resolved_mode:?}"
94            )));
95        }
96        let tonic_pitch = Pitch::from_name(tonic.as_str())?;
97
98        let sharps = pitch_to_sharps(&tonic_pitch, Some(&resolved_mode))?;
99        Ok(Self::new(tonic_pitch, &resolved_mode, sharps))
100    }
101
102    /// Builds a key and infers major/minor from tonic case.
103    pub fn from_tonic(tonic: &str) -> Result<Self> {
104        Self::from_tonic_mode(tonic, None::<&str>)
105    }
106
107    /// Returns a cloned tonic pitch.
108    pub fn tonic(&self) -> Pitch {
109        self.tonic_pitch.clone()
110    }
111
112    /// Returns a borrowed tonic pitch.
113    pub fn tonic_pitch(&self) -> &Pitch {
114        &self.tonic_pitch
115    }
116
117    /// Returns the key mode.
118    pub fn mode(&self) -> &str {
119        &self.mode
120    }
121
122    /// Returns this key moved by the interval, keeping its mode and the
123    /// colour it is drawn in.
124    pub fn transpose(&self, interval: &Interval) -> Result<Self> {
125        let mut moved = self
126            .key_signature()
127            .transpose(interval)?
128            .try_as_key(Some(&self.mode), None)?;
129        moved.color = self.color.clone();
130        Ok(moved)
131    }
132
133    /// The scale this key is written in: its tonic, and the diatonic mode
134    /// that tonic stands on within the key's own signature.
135    ///
136    /// The mode is read off the signature rather than the mode's name, so
137    /// every key has one. The church modes come out as themselves, D dorian
138    /// as `ScaleType::Dorian` on D, and a mode music21 keeps no signature for
139    /// takes the major signature on its tonic, as music21 gives it, and so
140    /// reads as major.
141    ///
142    /// music21's own `Key.getScale` answers the relative major for a church
143    /// mode, a C major scale for D dorian. The crate answers the mode.
144    pub fn scale(&self) -> Scale {
145        Scale::new(self.scale_type(), self.tonic_pitch.clone())
146    }
147
148    /// Which of the seven diatonic modes the tonic stands on in the key's
149    /// signature: how many letters it sits above the signature's major tonic,
150    /// which is four letters on for every sharp.
151    fn scale_type(&self) -> ScaleType {
152        const MODES: [ScaleType; 7] = [
153            ScaleType::Major,
154            ScaleType::Dorian,
155            ScaleType::Phrygian,
156            ScaleType::Lydian,
157            ScaleType::Mixolydian,
158            ScaleType::Minor,
159            ScaleType::Locrian,
160        ];
161        let major_step = (4 * self.sharps).rem_euclid(7);
162        let tonic_step = self.tonic_pitch.step_index();
163        MODES[(tonic_step - major_step).rem_euclid(7) as usize]
164    }
165
166    /// Returns the tonic name in music21's case convention: upper case for
167    /// major, lower case for minor, unchanged for other modes.
168    pub fn tonic_pitch_name_with_case(&self) -> String {
169        // Only the step takes the case: B-flat minor is `bb`, not `BB`.
170        let name = self.tonic_pitch.name();
171        let mut letters = name.chars();
172        let step = letters.next().unwrap_or_default();
173        let step = match self.mode.as_str() {
174            "major" => step.to_ascii_uppercase(),
175            "minor" => step.to_ascii_lowercase(),
176            _ => step,
177        };
178        format!("{step}{}", letters.as_str())
179    }
180
181    /// Returns the number of sharps in the key signature.
182    pub fn sharps(&self) -> IntegerType {
183        self.sharps
184    }
185
186    /// Returns the matching key signature.
187    pub fn key_signature(&self) -> KeySignature {
188        KeySignature::new(self.sharps)
189    }
190
191    /// This key written under another signature, its tonic and mode kept.
192    ///
193    /// music21 keeps a key's tonic and its sharps apart, so a tonic moved by
194    /// hand leaves the signature behind; this is such a key.
195    ///
196    /// ```
197    /// use music21::key::Key;
198    ///
199    /// let key = Key::from_tonic("B").unwrap().with_sharps(0);
200    /// assert_eq!(key.tonic().name(), "B");
201    /// assert_eq!(key.sharps(), 0);
202    /// ```
203    pub fn with_sharps(mut self, sharps: IntegerType) -> Self {
204        self.sharps = sharps;
205        self
206    }
207
208    /// The pitch on a one-based scale degree, counting on up past the
209    /// octave: in C major the ninth degree is `D5`, which is what stacking
210    /// thirds on the upper degrees needs. A tonic given with no octave is
211    /// read in octave 4.
212    ///
213    /// [`Scale::pitch_at_degree`] is music21's `pitchFromDegree`, which reads
214    /// the eighth degree as the tonic again.
215    pub fn pitch_from_degree(&self, degree: usize) -> Result<Pitch> {
216        if degree == 0 {
217            return Err(Error::Key("scale degrees start at 1".to_string()));
218        }
219        let within = (degree - 1) % 7;
220        let octaves_up = IntegerType::try_from((degree - 1) / 7)
221            .map_err(|_| Error::Key(format!("degree {degree} is out of range")))?;
222        let mut pitch = self.scale().pitch_at_degree(within as IntegerType + 1)?;
223        let octave = pitch.octave().unwrap_or(4) + octaves_up;
224        pitch.set_octave(Some(octave));
225        Ok(pitch)
226    }
227
228    /// The scale's pitches from the tonic up to its octave, eight in all.
229    pub fn pitches(&self) -> Result<Vec<Pitch>> {
230        (1..=8)
231            .map(|degree| self.pitch_from_degree(degree))
232            .collect()
233    }
234
235    /// The diatonic triad on a one-based degree.
236    pub fn triad_from_degree(&self, degree: usize) -> Result<Chord> {
237        self.chord_of_thirds(degree, 3)
238    }
239
240    /// The diatonic seventh chord on a one-based degree.
241    pub fn seventh_chord_from_degree(&self, degree: usize) -> Result<Chord> {
242        self.chord_of_thirds(degree, 4)
243    }
244
245    fn chord_of_thirds(&self, degree: usize, notes: usize) -> Result<Chord> {
246        let pitches = (0..notes)
247            .map(|third| self.pitch_from_degree(degree + 2 * third))
248            .collect::<Result<Vec<_>>>()?;
249        Chord::new(pitches.as_slice())
250    }
251
252    /// Returns all seven diatonic triads.
253    pub fn harmonized_triads(&self) -> Result<Vec<Chord>> {
254        (1..=7)
255            .map(|degree| self.triad_from_degree(degree))
256            .collect()
257    }
258
259    /// Returns all seven diatonic seventh chords.
260    pub fn harmonized_sevenths(&self) -> Result<Vec<Chord>> {
261        (1..=7)
262            .map(|degree| self.seventh_chord_from_degree(degree))
263            .collect()
264    }
265
266    /// Returns the relative major or minor key when applicable.
267    /// Returns the key of the same mode in which `pitch` is the given scale
268    /// degree: music21's `deriveByDegree`, so C major with `E` as degree 5 is
269    /// A major and A minor with `C` as degree 3 is A minor again.
270    pub fn derive_by_degree(&self, degree: usize, pitch: &Pitch) -> Result<Self> {
271        self.derive_by_degree_of(self.scale_type(), degree, pitch)
272    }
273
274    /// The same, reading the degree off a scale pattern the caller chooses
275    /// rather than the key's own.
276    ///
277    /// music21 spells this as a settable `.abstract` on the key: the seventh
278    /// degree of a minor key is a whole tone below the tonic in the natural
279    /// form and a semitone below it in the harmonic form, so the key that has
280    /// `E` as its seventh is F-sharp minor by one reading and F minor by the
281    /// other. The key that comes back keeps this one's mode either way.
282    pub fn derive_by_degree_of(
283        &self,
284        scale_type: ScaleType,
285        degree: usize,
286        pitch: &Pitch,
287    ) -> Result<Self> {
288        let scale =
289            Scale::new(scale_type, self.tonic_pitch.clone()).derive_by_degree(degree, pitch)?;
290        let tonic = scale.tonic().clone();
291        let sharps = pitch_to_sharps(&tonic, Some(&self.mode))?;
292        Ok(Self::new(tonic, &self.mode, sharps))
293    }
294
295    /// Returns the relative major or minor key -- the one sharing this key's
296    /// signature. A mode that is neither answers with itself.
297    pub fn relative(&self) -> Result<Self> {
298        match self.mode.as_str() {
299            "major" => self.key_signature().try_as_key(Some("minor"), None),
300            "minor" => self.key_signature().try_as_key(Some("major"), None),
301            _ => Ok(self.clone()),
302        }
303    }
304
305    /// Returns the parallel major or minor key when applicable.
306    pub fn parallel(&self) -> Result<Self> {
307        match self.mode.as_str() {
308            "major" => Self::from_tonic_mode(&self.tonic_pitch.name(), "minor"),
309            "minor" => Self::from_tonic_mode(&self.tonic_pitch.name(), "major"),
310            _ => Ok(self.clone()),
311        }
312    }
313}
314
315impl FromStr for Key {
316    type Err = Error;
317
318    fn from_str(value: &str) -> Result<Self> {
319        parse_key(value)
320    }
321}
322
323impl TryFrom<&str> for Key {
324    type Error = Error;
325
326    fn try_from(value: &str) -> Result<Self> {
327        value.parse()
328    }
329}
330
331impl TryFrom<String> for Key {
332    type Error = Error;
333
334    fn try_from(value: String) -> Result<Self> {
335        value.parse()
336    }
337}
338
339fn parse_key(value: &str) -> Result<Key> {
340    let trimmed = value.trim();
341    if trimmed.is_empty() {
342        return Err(Error::Analysis("key cannot be empty".to_string()));
343    }
344
345    let parts = trimmed.split_whitespace().collect::<Vec<_>>();
346    match parts.as_slice() {
347        [tonic] => {
348            if let Some((tonic, mode)) = split_compact_key_token(tonic) {
349                Key::from_tonic_mode(tonic, Some(mode.as_str()))
350            } else {
351                Key::from_tonic(tonic)
352            }
353        }
354        [tonic, mode] => {
355            let mode = canonical_key_mode(mode);
356            Key::from_tonic_mode(tonic, Some(mode.as_str()))
357        }
358        _ => Err(Error::Analysis(format!(
359            "invalid key {value:?}; use a tonic and optional mode, such as \"C\", \"C major\", or \"Am\""
360        ))),
361    }
362}
363
364fn split_compact_key_token(token: &str) -> Option<(&str, String)> {
365    let lower = token.to_ascii_lowercase();
366    for suffix in ["major", "minor", "maj", "min", "m"] {
367        if lower.ends_with(suffix) && lower.len() > suffix.len() {
368            let tonic_end = token.len() - suffix.len();
369            return Some((&token[..tonic_end], canonical_key_mode(suffix)));
370        }
371    }
372    None
373}
374
375fn canonical_key_mode(mode: &str) -> String {
376    match mode.to_ascii_lowercase().as_str() {
377        "maj" | "major" => "major".to_string(),
378        "m" | "min" | "minor" => "minor".to_string(),
379        other => other.to_string(),
380    }
381}
382
383/// music21's `convertKeyStringToMusic21KeyString`: a key written with `b`
384/// for flat, as in `Bb` or `eb`, rewritten with music21's `-`, so `Bb` is
385/// `B-` and `bb` is `b-`. A lone `b` is B, and anything without a trailing
386/// `b` is returned as it is.
387pub fn convert_key_string_to_music21_key_string(text: &str) -> String {
388    if !text.ends_with('b') || text == "b" {
389        return text.to_string();
390    }
391    if text == "bb" {
392        return "b-".to_string();
393    }
394    if text == "Bb" {
395        return "B-".to_string();
396    }
397    let mut chars = text.chars();
398    let Some(first) = chars.next() else {
399        return text.to_string();
400    };
401    let rest: Vec<char> = chars.collect();
402    if rest.iter().all(|c| *c == 'b') {
403        return format!("{first}{}", "-".repeat(rest.len()));
404    }
405    text.to_string()
406}
407
408#[cfg(test)]
409mod tests {
410
411    #[test]
412    fn a_key_is_read_from_an_owned_name_and_harmonizes_its_degrees() {
413        use super::Key;
414
415        let key = Key::try_from("a".to_string()).unwrap();
416        assert_eq!((key.tonic().name(), key.mode()), ("A".to_string(), "minor"));
417        let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
418        assert_eq!(dorian.mode(), "dorian");
419        let major = Key::try_from("Eb").unwrap();
420        let pitches: Vec<String> = major.pitches().unwrap().iter().map(|p| p.name()).collect();
421        assert_eq!(pitches, ["Eb", "F", "G", "Ab", "Bb", "C", "D", "Eb"]);
422        let sevenths = major.harmonized_sevenths().unwrap();
423        assert_eq!(sevenths.len(), 7);
424        assert_eq!(sevenths[4].pitch_names(), ["Bb", "D", "F", "Ab"]);
425        assert!(Key::try_from("H").is_err());
426    }
427
428    #[test]
429    fn the_pattern_read_by_decides_which_key_has_a_pitch_at_a_degree() {
430        // music21's own example: the minor key whose seventh degree is E is
431        // F-sharp minor in the natural form and F minor in the harmonic one,
432        // where the seventh is only a semitone below the tonic.
433        let minor = Key::from_tonic_mode("C", "minor").unwrap();
434        let e = Pitch::from_name("E").unwrap();
435        assert_eq!(minor.derive_by_degree(7, &e).unwrap().tonic().name(), "F#");
436        assert_eq!(
437            minor
438                .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
439                .unwrap()
440                .tonic()
441                .name(),
442            "F"
443        );
444        // The mode of the key that comes back is this key's, either way.
445        assert_eq!(
446            minor
447                .derive_by_degree_of(ScaleType::HarmonicMinor, 7, &e)
448                .unwrap()
449                .mode(),
450            "minor"
451        );
452    }
453
454    #[test]
455    fn mode_suffixes_and_semitone_transposition_match_music21() {
456        let e_major = Key::from_tonic("EM").unwrap();
457        assert_eq!(
458            (e_major.tonic().name(), e_major.mode()),
459            ("E".to_string(), "major")
460        );
461        let f_sharp_minor = Key::from_tonic("F#m").unwrap();
462        assert_eq!(
463            (f_sharp_minor.tonic().name(), f_sharp_minor.mode()),
464            ("F#".to_string(), "minor")
465        );
466        let up_a_semitone = Key::from_tonic("e")
467            .unwrap()
468            .transpose(&Interval::from_semitones(1).unwrap())
469            .unwrap();
470        assert_eq!(up_a_semitone.tonic_pitch_name_with_case(), "f");
471        assert_eq!(up_a_semitone.sharps(), -4);
472        let by_name = Key::from_tonic("e")
473            .unwrap()
474            .transpose(&Interval::from_name("m2").unwrap())
475            .unwrap();
476        assert_eq!(by_name.tonic_pitch_name_with_case(), "f");
477        assert!(matches!(
478            pitch_to_sharps(&Pitch::from_name("C~").unwrap(), None),
479            Err(crate::Error::Key(_))
480        ));
481    }
482
483    #[test]
484    fn key_strings_convert_like_music21() {
485        let cases = [
486            ("bb", "b-"),
487            ("b", "b"),
488            ("B", "B"),
489            ("Bb", "B-"),
490            ("a", "a"),
491            ("f#", "f#"),
492            ("F#", "F#"),
493            ("eb", "e-"),
494            ("e-", "e-"),
495            ("Ebb", "E--"),
496            ("Abb", "A--"),
497        ];
498        for (text, expected) in cases {
499            assert_eq!(
500                convert_key_string_to_music21_key_string(text),
501                expected,
502                "{text}"
503            );
504        }
505    }
506
507    #[test]
508    fn derive_by_degree_matches_music21() {
509        let cases: [(&str, usize, &str, &str, &str); 8] = [
510            ("C", 5, "E", "A3", "major"),
511            ("C", 1, "F#4", "F#4", "major"),
512            ("a", 3, "C", "A3", "minor"),
513            ("C", 7, "B-", "Cb4", "major"),
514            ("C", 4, "B", "F#4", "major"),
515            ("D", 2, "C#3", "B2", "major"),
516            ("c", 6, "A-4", "C4", "minor"),
517            ("C", 5, "G4", "C4", "major"),
518        ];
519        for (key, degree, pitch, tonic, mode) in cases {
520            let derived = Key::from_tonic(key)
521                .unwrap()
522                .derive_by_degree(degree, &Pitch::from_name(pitch).unwrap())
523                .unwrap();
524            assert_eq!(
525                derived.tonic_pitch().name_with_octave(),
526                tonic,
527                "{key} {degree} {pitch}"
528            );
529            assert_eq!(derived.mode(), mode, "{key} {degree} {pitch}");
530        }
531        assert_eq!(
532            Key::from_tonic("C")
533                .unwrap()
534                .derive_by_degree(5, &Pitch::from_name("E").unwrap())
535                .unwrap()
536                .sharps(),
537            3
538        );
539    }
540    use super::*;
541    use crate::key::keysignature::pitch_name_to_sharps;
542
543    #[test]
544    fn transposing_a_key_matches_music21() {
545        let cases = [
546            ("C", "M2", "D", 2, "major"),
547            ("c", "P5", "g", -2, "minor"),
548            ("F#", "m2", "G", 1, "major"),
549            ("Bb", "-M3", "Gb", -6, "major"),
550            ("D", "P8", "D", 2, "major"),
551            ("e", "M6", "c#", 4, "minor"),
552            ("C", "-m2", "B", 5, "major"),
553        ];
554        for (key, interval, tonic, sharps, mode) in cases {
555            let moved = Key::from_tonic(key)
556                .unwrap()
557                .transpose(&Interval::from_name(interval).unwrap())
558                .unwrap();
559            assert_eq!(
560                moved.tonic_pitch_name_with_case(),
561                tonic,
562                "{key} {interval}"
563            );
564            assert_eq!(moved.sharps(), sharps, "{key} {interval}");
565            assert_eq!(moved.mode(), mode, "{key} {interval}");
566        }
567        let dorian = Key::from_tonic_mode("D", "dorian").unwrap().scale();
568        assert_eq!(dorian.scale_type(), ScaleType::Dorian);
569        // music21 gives a mode it keeps no signature for the major one on
570        // its tonic, so D hypodorian is written with D major's two sharps.
571        let hypodorian = Key::from_tonic_mode("D", "hypodorian").unwrap();
572        assert_eq!(hypodorian.sharps(), 2);
573        assert_eq!(hypodorian.scale().scale_type(), ScaleType::Major);
574    }
575
576    #[test]
577    fn tonic_names_carry_mode_case() {
578        let cases = [
579            ("C", "C", "major"),
580            ("c", "c", "minor"),
581            ("F#", "F#", "major"),
582            ("f#", "f#", "minor"),
583            ("Bb", "Bb", "major"),
584            ("eb", "eb", "minor"),
585        ];
586        for (input, cased, mode) in cases {
587            let key: Key = input.parse().unwrap();
588            assert_eq!(key.tonic_pitch_name_with_case(), cased, "{input}");
589            assert_eq!(key.mode(), mode, "{input}");
590        }
591        let dorian = Key::from_tonic_mode("D", "dorian").unwrap();
592        assert_eq!(dorian.tonic_pitch_name_with_case(), "D");
593    }
594
595    #[test]
596    fn key_from_tonic_mode() {
597        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
598        assert_eq!(c_major.sharps(), 0);
599        let g_major = Key::from_tonic_mode("G", Some("major")).unwrap();
600        assert_eq!(g_major.sharps(), 1);
601        let a_minor = Key::from_tonic_mode("A", Some("minor")).unwrap();
602        assert_eq!(a_minor.sharps(), 0);
603        let e_phrygian = Key::from_tonic_mode("E", Some("phrygian")).unwrap();
604        assert_eq!(e_phrygian.sharps(), 0);
605    }
606
607    #[test]
608    fn a_mode_music21_builds_no_scale_for_is_refused() {
609        for mode in [
610            "major",
611            "Minor",
612            "ionian",
613            "aeolian",
614            "dorian",
615            "phrygian",
616            "lydian",
617            "mixolydian",
618            "locrian",
619            "hypodorian",
620            "hypophrygian",
621            "hypolydian",
622            "hypomixolydian",
623            "hypoaeolian",
624            "hypolocrian",
625        ] {
626            assert!(Key::from_tonic_mode("c", mode).is_ok(), "{mode}");
627        }
628        for mode in ["blues-like", "", "major #(system \"touch x\") %"] {
629            assert!(Key::from_tonic_mode("c", mode).is_err(), "{mode}");
630        }
631    }
632
633    #[test]
634    fn key_from_string_accepts_common_notation() {
635        let c_major: Key = "C major".parse().unwrap();
636        assert_eq!(c_major.tonic().name(), "C");
637        assert_eq!(c_major.mode(), "major");
638
639        let a_minor: Key = "Am".parse().unwrap();
640        assert_eq!(a_minor.tonic().name(), "A");
641        assert_eq!(a_minor.mode(), "minor");
642
643        let b_flat_minor: Key = "Bb minor".parse().unwrap();
644        assert_eq!(b_flat_minor.tonic().name(), "Bb");
645        assert_eq!(b_flat_minor.mode(), "minor");
646    }
647
648    #[test]
649    fn key_scale_degree_and_chords() {
650        let d_major = Key::from_tonic_mode("D", Some("major")).unwrap();
651        assert_eq!(
652            d_major.pitch_from_degree(7).unwrap().name_with_octave(),
653            "C#5"
654        );
655        assert_eq!(
656            d_major.triad_from_degree(1).unwrap().pitched_common_name(),
657            "D-major triad"
658        );
659        assert_eq!(
660            d_major
661                .seventh_chord_from_degree(5)
662                .unwrap()
663                .pitched_common_name(),
664            "A-dominant seventh chord"
665        );
666    }
667
668    #[test]
669    fn key_harmonized_triads() {
670        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
671        let triads = c_major.harmonized_triads().unwrap();
672        assert_eq!(triads.len(), 7);
673        assert_eq!(triads[0].pitched_common_name(), "C-major triad");
674        assert_eq!(triads[4].pitched_common_name(), "G-major triad");
675    }
676
677    #[test]
678    fn key_relative_and_parallel() {
679        let c_major = Key::from_tonic_mode("C", Some("major")).unwrap();
680        let relative = c_major.relative().unwrap();
681        assert_eq!(relative.mode(), "minor");
682        assert_eq!(relative.tonic().name(), "A");
683
684        let parallel = c_major.parallel().unwrap();
685        assert_eq!(parallel.mode(), "minor");
686        assert_eq!(parallel.tonic().name(), "C");
687    }
688
689    #[test]
690    fn pitch_name_to_sharps_modes() {
691        assert_eq!(pitch_name_to_sharps("C", Some("major")).unwrap(), 0);
692        assert_eq!(pitch_name_to_sharps("E", Some("minor")).unwrap(), 1);
693        assert_eq!(pitch_name_to_sharps("D", Some("dorian")).unwrap(), 0);
694        assert_eq!(pitch_name_to_sharps("A", Some("mixolydian")).unwrap(), 2);
695    }
696}