Skip to main content

music21/chord/
names.rs

1//! What a chord is called: the common names of the Forte tables spelled
2//! on the chord's own root, and the lead-sheet symbol it is written as.
3
4use super::*;
5
6impl Chord {
7    /// Returns the unpitched chord types known to the music21-derived table.
8    pub fn known_chord_types() -> Vec<KnownChordType> {
9        tables::known_chord_table_entries()
10            .into_iter()
11            .map(|entry| KnownChordType {
12                cardinality: entry.cardinality,
13                common_names: entry.common_names.into_iter().map(str::to_string).collect(),
14                forte_class: entry.forte_class,
15                normal_form: entry.normal_form,
16                interval_class_vector: entry.interval_class_vector,
17            })
18            .collect()
19    }
20
21    /// Returns the primary music21-style common name with a pitch prefix.
22    pub fn pitched_common_name(&self) -> String {
23        self.pitched_name_for_common_name(&self.common_name())
24    }
25
26    /// Returns every known music21-style common name with pitch prefixes.
27    ///
28    /// Most chords have a single common name, while some Forte-table entries
29    /// have aliases. This method exposes all of them in table order.
30    pub fn pitched_common_names(&self) -> Vec<String> {
31        let common_names = self.common_names();
32        if common_names.is_empty() {
33            return vec![self.pitched_common_name()];
34        }
35
36        common_names
37            .iter()
38            .map(|name| self.pitched_name_for_common_name(name))
39            .collect()
40    }
41
42    /// Returns the preferred chord symbol, when available.
43    ///
44    /// This is separate from [`Self::pitched_common_name`]: common names follow
45    /// the music21/Forte tables, while chord symbols use music21-style
46    /// figures such as `Cmaj7`, `F#m7b5`, or `Ddom7dim5add5addb9/C`.
47    pub fn chord_symbol(&self) -> Option<String> {
48        self.chord_symbols().into_iter().next()
49    }
50
51    /// Returns ranked chord symbols for this pitch-class set.
52    ///
53    /// Empty and microtonal chords return no symbols because this notation layer
54    /// assumes twelve-tone equal-tempered pitch classes.
55    pub fn chord_symbols(&self) -> Vec<String> {
56        crate::chordsymbol::chord_symbol_spellings(self)
57    }
58
59    /// Returns the preferred chord symbol using an explicit root.
60    ///
61    /// This is useful for pitch-class sets and browser tables where the caller
62    /// already knows the harmonic spelling anchor and does not want an
63    /// inversion/root inference pass to choose another chord member. String
64    /// roots are parsed as pitch names; numeric roots are parsed as pitch
65    /// classes, so use numbers for pitch-class-only values such as 10 or 11.
66    pub fn chord_symbol_with_root(
67        &self,
68        root: impl Into<PitchClassSpecifier>,
69    ) -> Result<Option<String>> {
70        Ok(self.chord_symbols_with_root(root)?.into_iter().next())
71    }
72
73    /// Returns ranked chord symbols using an explicit root.
74    ///
75    /// Empty, microtonal, and rootless-with-respect-to-the-given-root chords
76    /// return no symbols. Non-integer roots are rejected because chord symbols
77    /// are generated in twelve-tone pitch-class space.
78    pub fn chord_symbols_with_root(
79        &self,
80        root: impl Into<PitchClassSpecifier>,
81    ) -> Result<Vec<String>> {
82        let root = Self::chord_symbol_root_pitch_class(root.into())?;
83
84        Ok(crate::chordsymbol::chord_symbol_spellings_with_root(
85            self, root,
86        ))
87    }
88
89    pub(super) fn pitched_name_for_common_name(&self, name_str: &str) -> String {
90        if name_str == "empty chord" {
91            return name_str.to_string();
92        }
93
94        if matches!(name_str, "note" | "unison") {
95            return self
96                .notes
97                .first()
98                .map(|n| n.pitch.name())
99                .unwrap_or_else(|| name_str.to_string());
100        }
101
102        let pitch_class_cardinality = self.ordered_pitch_classes().len();
103        if pitch_class_cardinality <= 2
104            || name_str.contains("enharmonic")
105            || name_str.contains("forte class")
106            || name_str.contains(" semitone")
107        {
108            // music21's `bass()`, which a bass set by hand answers.
109            if let Some(bass) = self.bass() {
110                return format!("{name_str} above {}", bass.name());
111            }
112            return name_str.to_string();
113        }
114
115        // music21's `root()`, which a root set by hand answers, or the first
116        // note where no root is found.
117        let root_name = self
118            .root()
119            .or_else(|| self.notes.first().map(|n| &n.pitch))
120            .map(Pitch::name);
121
122        match root_name {
123            Some(root_name) => format!("{root_name}-{name_str}"),
124            None => name_str.to_string(),
125        }
126    }
127
128    pub(super) fn chord_symbol_root_pitch_class(root: PitchClassSpecifier) -> Result<u8> {
129        match root {
130            PitchClassSpecifier::String(value) => match Pitch::from_name(value.as_str()) {
131                Ok(pitch) => Self::integer_pitch_class_for_chord_symbol_root(pitch.ps()),
132                Err(pitch_error) => {
133                    let pitch_class = PitchClass::new(value.as_str()).map_err(|pitch_class_error| {
134                        Error::Chord(format!(
135                            "cannot parse chord-symbol root {value:?} as a pitch name ({pitch_error}) or pitch class ({pitch_class_error})"
136                        ))
137                    })?;
138                    Self::integer_pitch_class_from_value(pitch_class)
139                }
140            },
141            specifier => {
142                let pitch_class = PitchClass::new(specifier)?;
143                Self::integer_pitch_class_from_value(pitch_class)
144            }
145        }
146    }
147
148    pub(super) fn integer_pitch_class_from_value(pitch_class: PitchClass) -> Result<u8> {
149        let Some(root) = pitch_class.integer() else {
150            return Err(Error::Chord(
151                "chord symbols require an integer pitch-class root".to_string(),
152            ));
153        };
154        Ok(root as u8)
155    }
156
157    pub(super) fn integer_pitch_class_for_chord_symbol_root(ps: FloatType) -> Result<u8> {
158        if (ps - ps.round()).abs() > FloatType::EPSILON {
159            return Err(Error::Chord(
160                "chord symbols require an integer pitch-class root".to_string(),
161            ));
162        }
163
164        Ok((ps.round() as IntegerType).rem_euclid(12) as u8)
165    }
166
167    /// Returns the primary unpitched music21-style common name.
168    ///
169    /// For chords with multiple table aliases, this is the first common name in
170    /// table order. Use [`Self::common_names`] to get every unpitched alias.
171    pub fn common_name(&self) -> String {
172        if self
173            .notes
174            .iter()
175            .any(|n| (n.pitch.alter() - n.pitch.alter().round()).abs() > FloatType::EPSILON)
176        {
177            return "microtonal chord".to_string();
178        }
179
180        if self.notes.is_empty() {
181            return "empty chord".to_string();
182        }
183
184        let ordered_pcs = self.ordered_pitch_classes();
185        if ordered_pcs.is_empty() {
186            return "empty chord".to_string();
187        }
188
189        if ordered_pcs.len() == 1 {
190            if self.notes.len() == 1 {
191                return "note".to_string();
192            }
193
194            let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
195
196            if pitch_names.len() == 1 {
197                if pitch_pses.len() == 1 {
198                    return "unison".to_string();
199                }
200                if pitch_pses.len() == 2 {
201                    return Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
202                        .unwrap_or_else(|| "multiple octaves".to_string());
203                }
204                return "multiple octaves".to_string();
205            }
206            if pitch_pses.len() == 1 {
207                return "enharmonic unison".to_string();
208            }
209            return "enharmonic octaves".to_string();
210        }
211
212        if ordered_pcs.len() == 2 {
213            return self.dyad_common_name();
214        }
215
216        let address = match tables::seek_chord_tables_address(&ordered_pcs) {
217            Ok(address) => address,
218            Err(_) => return "unknown chord".to_string(),
219        };
220
221        // The names stay borrowed from the table until one of them is the
222        // answer: a chord is named constantly, and writing every name a set
223        // class carries out to hand back one of them is three or four
224        // allocations for nothing.
225        let common_names: Vec<&'static str> = match tables::address_to_common_names(address) {
226            Ok(Some(names)) => names,
227            _ => Vec::new(),
228        };
229        let forte_name = tables::address_to_forte_name(address, "tn").ok();
230
231        if let Some(forte_name) = forte_name.as_deref() {
232            // The address is already in hand, so the augmented-sixth tests
233            // below need not look it up again.
234            let found = Some((address.0, address.1, address.2));
235            if let Some(name) = self.augmented_sixth_common_name(forte_name, found, &common_names) {
236                return name;
237            }
238            if let Some(first) = common_names.first() {
239                if matches!(forte_name, "4-20" | "4-26") {
240                    return if self.is_seventh_with_perfect_fifths_above_root_and_third() {
241                        (*first).to_string()
242                    } else {
243                        format!("enharmonic equivalent to {first}")
244                    };
245                }
246                if let Some(spelled) = self.spelled_as_named(forte_name) {
247                    return if spelled {
248                        (*first).to_string()
249                    } else {
250                        format!("enharmonic equivalent to {first}")
251                    };
252                }
253            }
254        }
255
256        match common_names.first() {
257            Some(name) => (*name).to_string(),
258            None => match forte_name {
259                Some(forte_name) => format!("forte class {forte_name}"),
260                None => "unknown chord".to_string(),
261            },
262        }
263    }
264
265    /// music21 names the set classes that carry an augmented sixth by which
266    /// augmented sixth they are actually spelled as, and falls back to
267    /// `enharmonic to` the plain name when the spelling does not match.
268    pub(super) fn augmented_sixth_common_name(
269        &self,
270        forte_name: &str,
271        found: Option<(u8, u8, i8)>,
272        common_names: &[&str],
273    ) -> Option<String> {
274        let named = |index: usize| common_names.get(index).map(|name| (*name).to_string());
275        let in_inversion = |index: usize| {
276            named(index).map(|name| format!("{name} in {}", self.inversion_text().to_lowercase()))
277        };
278        match forte_name {
279            "4-27B" => {
280                if self.is_dominant_seventh() {
281                    named(0)
282                } else if self.is_german_from(found, false) {
283                    named(2)
284                } else if self.is_german_from(found, true) {
285                    in_inversion(2)
286                } else if self.is_swiss_from(found, false) {
287                    named(3)
288                } else if self.is_swiss_from(found, true) {
289                    in_inversion(3)
290                } else {
291                    named(0).map(|name| format!("enharmonic to {name}"))
292                }
293            }
294            "4-25" => {
295                if self.is_french_from(found, false) {
296                    named(1)
297                } else if self.is_french_from(found, true) {
298                    in_inversion(1)
299                } else {
300                    named(0)
301                }
302            }
303            "3-8A" => {
304                if self.is_italian_augmented_sixth(false, false) {
305                    named(1)
306                } else if self.is_italian_augmented_sixth(true, false) {
307                    in_inversion(1)
308                } else {
309                    named(0)
310                }
311            }
312            _ => None,
313        }
314    }
315
316    /// Whether the chord is spelled as the set class's common name says, for
317    /// the classes where music21 checks: a `3-11B` that is not a major triad
318    /// is only enharmonically one.
319    pub(super) fn spelled_as_named(&self, forte_name: &str) -> Option<bool> {
320        match forte_name {
321            "3-11A" => Some(self.is_minor_triad()),
322            "3-11B" => Some(self.is_major_triad()),
323            "3-10" => Some(self.is_diminished_triad()),
324            "3-12" => Some(self.is_augmented_triad()),
325            "4-27A" => Some(self.is_half_diminished_seventh()),
326            "4-28" => Some(self.is_diminished_seventh()),
327            "5-27A" | "5-27B" | "5-34" => Some(self.is_ninth()),
328            _ => None,
329        }
330    }
331
332    /// music21's check for the two seventh-chord set classes that can be
333    /// spelled several ways: a real major or minor seventh has a perfect
334    /// fifth above both its root and its third.
335    pub(super) fn is_seventh_with_perfect_fifths_above_root_and_third(&self) -> bool {
336        // The root is found once, for the seventh test and the fifths alike.
337        let Some(root) = self.root() else {
338            return false;
339        };
340        if !self.is_seventh_from(root) {
341            return false;
342        }
343        let names = self.pitch_names();
344        // Through the pitch's own transpose, as music21 does: a pitch whose
345        // spelling was inferred is respelled on the way, so the fifth above
346        // an integer-built D# is B-, which the chord does not hold.
347        let has_fifth_above = |pitch: &Pitch| {
348            pitch
349                .transpose(&PERFECT_FIFTH)
350                .is_ok_and(|above| names.contains(&above.name()))
351        };
352        let Some(third) = self.chord_step_from(3, root) else {
353            return false;
354        };
355        has_fifth_above(root) && has_fifth_above(third)
356    }
357
358    /// The distinct pitch names and the distinct rounded pitch spaces among
359    /// the notes: `C4 C5` is one name and two spaces.
360    fn distinct_names_and_spaces(
361        &self,
362    ) -> (
363        std::collections::BTreeSet<String>,
364        std::collections::BTreeSet<IntegerType>,
365    ) {
366        let names = self.notes.iter().map(|n| n.pitch.name()).collect();
367        let spaces = self
368            .notes
369            .iter()
370            .map(|n| n.pitch.ps().round() as IntegerType)
371            .collect();
372        (names, spaces)
373    }
374
375    pub(super) fn dyad_common_name(&self) -> String {
376        let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
377
378        let Some(p0) = self.notes.first().map(|n| &n.pitch) else {
379            return "empty chord".to_string();
380        };
381        let p0_pitch_class = root::pitch_class(p0);
382
383        let Some(p1) = self
384            .notes
385            .iter()
386            .skip(1)
387            .find(|n| root::pitch_class(&n.pitch) != p0_pitch_class)
388            .map(|n| &n.pitch)
389        else {
390            return "unknown chord".to_string();
391        };
392
393        let relevant_interval = Interval::between(p0.clone(), p1.clone());
394
395        if pitch_names.len() > 2 {
396            let Ok(interval) = relevant_interval else {
397                return "unknown chord".to_string();
398            };
399            let semitones = interval.chromatic.simple_undirected();
400            let plural = if semitones == 1 { "" } else { "s" };
401            return format!("{semitones} semitone{plural}");
402        }
403
404        if pitch_pses.len() > 2 {
405            return relevant_interval
406                .map(|interval| {
407                    format!("{} with octave doublings", interval.semi_simple_nice_name())
408                })
409                .unwrap_or_else(|_| "unknown chord".to_string());
410        }
411
412        Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
413            .unwrap_or_else(|| "unknown chord".to_string())
414    }
415
416    /// Returns all unpitched common-name aliases known for this chord.
417    pub fn common_names(&self) -> Vec<String> {
418        let ordered_pcs = self.ordered_pitch_classes();
419        let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
420            return Vec::new();
421        };
422        tables::address_to_common_names(address)
423            .ok()
424            .flatten()
425            .unwrap_or_default()
426            .into_iter()
427            .map(str::to_string)
428            .collect()
429    }
430
431    /// Returns the inferred root pitch name when the chord has one.
432    ///
433    /// Returns `None` for empty chords, where there is no pitch from which a
434    /// root can be inferred.
435    pub fn root_pitch_name(&self) -> Option<String> {
436        self.root_pitch_name_from_tables()
437    }
438
439    /// Returns the lowest pitch name in the chord.
440    ///
441    /// Returns `None` for empty chords, where there is no bass pitch.
442    pub fn bass_pitch_name(&self) -> Option<String> {
443        self.bass_pitch().map(Pitch::name)
444    }
445
446    pub(super) fn root_pitch_name_from_tables(&self) -> Option<String> {
447        self.find_root_pitch().map(Pitch::name)
448    }
449
450    pub(super) fn common_names_with_primary(&self) -> Vec<String> {
451        let mut names = vec![self.common_name()];
452        names.extend(self.common_names());
453        names.sort();
454        names.dedup();
455        names
456    }
457
458    pub(super) fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
459        Interval::between(start.clone(), end.clone())
460            .ok()
461            .map(|interval| interval.nice_name())
462    }
463
464    pub(super) fn display_key_name(key: &Key) -> String {
465        format!("{} {}", key.tonic().name(), key.mode())
466    }
467
468    /// Returns music21's `fullName`: the pitches' full names between braces,
469    /// followed by the duration's full name when the chord has a duration
470    /// that music21 can name.
471    pub fn full_name(&self) -> String {
472        let pitches = self
473            .notes
474            .iter()
475            .map(|note| note.pitch.full_name())
476            .collect::<Vec<_>>()
477            .join(" | ");
478        let duration = self
479            .duration
480            .clone()
481            .unwrap_or_else(Duration::quarter)
482            .full_name();
483        format!("Chord {{{pitches}}} {duration}")
484    }
485}
486
487#[derive(Debug, Clone, PartialEq)]
488#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
489/// An unpitched chord type known to the music21-derived chord table.
490pub struct KnownChordType {
491    /// Number of distinct pitch classes in the chord type.
492    pub cardinality: u8,
493    /// Unpitched common-name aliases in music21 table order.
494    pub common_names: Vec<String>,
495    /// Forte class for this transposition-normal entry, such as `"3-11B"`.
496    pub forte_class: String,
497    /// Transposed normal form pitch classes.
498    pub normal_form: Vec<u8>,
499    /// Six-entry interval-class vector.
500    pub interval_class_vector: Vec<u8>,
501}
502
503#[cfg(test)]
504mod augmented_sixth_voicings {
505    use crate::{Chord, defaults::IntegerType};
506
507    /// music21's `commonName` for every voicing of the augmented-sixth
508    /// spellings and their enharmonic neighbours, bass by bass, and for the
509    /// same sets built from pitch-class numbers. Generated from music21.
510    const EXPECTED: &str = "\
511A#3 C#4 D#4 F#4	minor seventh chord
512A#3 C#4 E#4 F#4	major seventh chord
513A#3 C#4 E#4 G#4	minor seventh chord
514A#3 C#4 G4	enharmonic equivalent to diminished triad
515A#3 C4 E4 F#4	French augmented sixth chord in first inversion
516A- C D F#	French augmented sixth chord in third inversion
517A- C E- F#	German augmented sixth chord in second inversion
518A- C F#	Italian augmented sixth chord in second inversion
519A-3 C-4 E-4 F-4	major seventh chord
520A-3 C-4 E-4 G-4	minor seventh chord
521A-3 C4 D4 F#4	French augmented sixth chord
522A-3 C4 E-4 F#4	German augmented sixth chord
523A-3 C4 F#4	Italian augmented sixth chord
524A3 B3 E-4 F#4	enharmonic to dominant seventh chord
525A3 C#4 E-4 G4	French augmented sixth chord in root position
526A3 E-4 F#4	enharmonic equivalent to diminished triad
527B-3 C-4 E-4 G-4	major seventh chord
528B-3 D4 E4 G#4	French augmented sixth chord
529B3 C#4 E#4 G4	French augmented sixth chord in third inversion
530B3 E-4 F#4 A4	enharmonic to dominant seventh chord
531C D F# A-	French augmented sixth chord in third inversion
532C E F# A#	French augmented sixth chord
533C# D# F# A#	minor seventh chord
534C# E# F# A#	major seventh chord
535C# E# G B	French augmented sixth chord in root position
536C# E# G# A#	minor seventh chord
537C# E- G	Italian augmented sixth chord in root position
538C# E- G A	French augmented sixth chord in first inversion
539C# G A#	enharmonic equivalent to diminished triad
540C#3 D#3 F#3 A#3	minor seventh chord
541C#3 E#3 F#3 A#3	major seventh chord
542C#3 E#3 G#3 A#3	minor seventh chord
543C#3 E#3 G3 B3	French augmented sixth chord in root position
544C#3 E-3 G3	Italian augmented sixth chord in root position
545C#3 E-3 G3 A3	French augmented sixth chord in first inversion
546C#3 G3 A#3	enharmonic equivalent to diminished triad
547C-3 E-3 F-3 A-3	major seventh chord
548C-3 E-3 G-3 A-3	minor seventh chord
549C-3 E-3 G-3 B-3	major seventh chord
550C3 D3 F#3 A-3	French augmented sixth chord in third inversion
551C3 E-3 F#3 A-3	German augmented sixth chord in second inversion
552C3 E3 F#3 A#3	French augmented sixth chord
553C3 F#3 A-3	Italian augmented sixth chord in second inversion
554D E G# B-	French augmented sixth chord in third inversion
555D#3 F#3 A#3 C#4	minor seventh chord
556D3 E3 G#3 B-3	French augmented sixth chord in third inversion
557D3 F#3 A-3 C4	French augmented sixth chord in root position
558E#3 F#3 A#3 C#4	major seventh chord
559E#3 G#3 A#3 C#4	minor seventh chord
560E#3 G3 B3 C#4	French augmented sixth chord in first inversion
561E- F# A	enharmonic equivalent to diminished triad
562E- F# A B	enharmonic to dominant seventh chord
563E- F- A- C-	major seventh chord
564E- G- A- C-	minor seventh chord
565E- G- B- C-	major seventh chord
566E-3 F#3 A-3 C4	German augmented sixth chord in third inversion
567E-3 F#3 A3	enharmonic equivalent to diminished triad
568E-3 F#3 A3 B3	enharmonic to dominant seventh chord
569E-3 F-3 A-3 C-4	major seventh chord
570E-3 G-3 A-3 C-4	minor seventh chord
571E-3 G-3 B-3 C-4	major seventh chord
572E-3 G3 A3 C#4	French augmented sixth chord
573E-3 G3 C#4	Italian augmented sixth chord
574E3 F#3 A#3 C4	French augmented sixth chord in third inversion
575E3 G#3 B-3 D4	French augmented sixth chord in root position
576F#3 A#3 C#4 D#4	minor seventh chord
577F#3 A#3 C#4 E#4	major seventh chord
578F#3 A#3 C4 E4	French augmented sixth chord in root position
579F#3 A-3 C4	Italian augmented sixth chord in root position
580F#3 A-3 C4 D4	French augmented sixth chord in first inversion
581F#3 A-3 C4 E-4	German augmented sixth chord in root position
582F#3 A3 B3 E-4	enharmonic to dominant seventh chord
583F#3 A3 E-4	enharmonic equivalent to diminished triad
584F-3 A-3 C-4 E-4	major seventh chord
585G#3 A#3 C#4 E#4	minor seventh chord
586G#3 B-3 D4 E4	French augmented sixth chord in first inversion
587G-3 A-3 C-4 E-4	minor seventh chord
588G-3 B-3 C-4 E-4	major seventh chord
589G3 A#3 C#4	enharmonic equivalent to diminished triad
590G3 A3 C#4 E-4	French augmented sixth chord in third inversion
591G3 B3 C#4 E#4	French augmented sixth chord
592G3 C#4 E-4	Italian augmented sixth chord in second inversion
593[0, 2, 6, 8]	French augmented sixth chord in third inversion
594[1, 3, 6, 10]	enharmonic equivalent to minor seventh chord
595[1, 3, 7]	Italian augmented sixth chord in root position
596[1, 5, 6, 10]	enharmonic equivalent to major seventh chord
597[1, 5, 8, 10]	enharmonic equivalent to minor seventh chord
598[1, 7, 10]	enharmonic equivalent to diminished triad
599[3, 4, 8, 11]	enharmonic equivalent to major seventh chord
600[3, 6, 10, 11]	enharmonic equivalent to major seventh chord
601[3, 6, 8, 11]	enharmonic equivalent to minor seventh chord
602[3, 6, 9]	enharmonic equivalent to diminished triad
603";
604
605    /// music21 puts the root it asks `root()` for in front of the name, and
606    /// the bass `bass()` gives after `above`: overrides included, and for
607    /// every voicing.
608    #[test]
609    fn pitched_names_read_the_root_music21_reads() {
610        let cases = [
611            (
612                "C# E- G",
613                "C#-Italian augmented sixth chord in root position",
614            ),
615            (
616                "G4 C#5 E-5",
617                "C#-Italian augmented sixth chord in second inversion",
618            ),
619            ("E-4 G4 C#5", "C#-Italian augmented sixth chord"),
620            (
621                "C D F# A-",
622                "D-French augmented sixth chord in third inversion",
623            ),
624            ("A-4 C5 D5 F#5", "D-French augmented sixth chord"),
625            (
626                "F#4 A-4 C5 D5",
627                "D-French augmented sixth chord in first inversion",
628            ),
629            (
630                "D4 F#4 A-4 C5",
631                "D-French augmented sixth chord in root position",
632            ),
633            (
634                "C# E- G A",
635                "A-French augmented sixth chord in first inversion",
636            ),
637            (
638                "A4 C#5 E-5 G5",
639                "A-French augmented sixth chord in root position",
640            ),
641            ("C E F# A#", "F#-French augmented sixth chord"),
642            (
643                "A#3 C4 E4 F#4",
644                "F#-French augmented sixth chord in first inversion",
645            ),
646            (
647                "C# E# G B",
648                "C#-French augmented sixth chord in root position",
649            ),
650            (
651                "B4 C#5 E#5 G5",
652                "C#-French augmented sixth chord in third inversion",
653            ),
654            (
655                "D E G# B-",
656                "E-French augmented sixth chord in third inversion",
657            ),
658        ];
659        for (notes, expected) in cases {
660            assert_eq!(
661                Chord::new(notes).unwrap().pitched_common_name(),
662                expected,
663                "{notes}"
664            );
665        }
666
667        let mut rooted = Chord::new("F# A C# E#").unwrap();
668        rooted.set_root(Some("A".parse().unwrap()));
669        assert_eq!(rooted.pitched_common_name(), "A-minor-augmented tetrachord");
670
671        let mut added = Chord::new("C E G B#").unwrap();
672        added.set_bass(Some("E".parse().unwrap()));
673        assert_eq!(
674            added.pitched_common_name(),
675            "enharmonic equivalent to major triad above E"
676        );
677    }
678
679    #[test]
680    fn every_voicing_is_named_as_music21_names_it() {
681        let mut wrong = Vec::new();
682        for line in EXPECTED.lines() {
683            let (notes, expected) = line.split_once('\t').unwrap();
684            let chord = if let Some(numbers) = notes.strip_prefix('[') {
685                let classes: Vec<IntegerType> = numbers
686                    .trim_end_matches(']')
687                    .split(", ")
688                    .map(|number| number.parse().unwrap())
689                    .collect();
690                Chord::new(classes.as_slice()).unwrap()
691            } else {
692                Chord::new(notes).unwrap()
693            };
694            let named = chord.common_name();
695            if named != expected {
696                wrong.push(format!("{notes}: {named}, music21 {expected}"));
697            }
698        }
699        assert!(
700            wrong.is_empty(),
701            "{} of {}:\n{}",
702            wrong.len(),
703            EXPECTED.lines().count(),
704            wrong.join("\n")
705        );
706    }
707}