Skip to main content

music21_rs/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            if let Some(name) = self.augmented_sixth_common_name(forte_name, &common_names) {
233                return name;
234            }
235            if let Some(first) = common_names.first() {
236                if matches!(forte_name, "4-20" | "4-26") {
237                    return if self.is_seventh_with_perfect_fifths_above_root_and_third() {
238                        (*first).to_string()
239                    } else {
240                        format!("enharmonic equivalent to {first}")
241                    };
242                }
243                if let Some(spelled) = self.spelled_as_named(forte_name) {
244                    return if spelled {
245                        (*first).to_string()
246                    } else {
247                        format!("enharmonic equivalent to {first}")
248                    };
249                }
250            }
251        }
252
253        match common_names.first() {
254            Some(name) => (*name).to_string(),
255            None => match forte_name {
256                Some(forte_name) => format!("forte class {forte_name}"),
257                None => "unknown chord".to_string(),
258            },
259        }
260    }
261
262    /// music21 names the set classes that carry an augmented sixth by which
263    /// augmented sixth they are actually spelled as, and falls back to
264    /// `enharmonic to` the plain name when the spelling does not match.
265    pub(super) fn augmented_sixth_common_name(
266        &self,
267        forte_name: &str,
268        common_names: &[&str],
269    ) -> Option<String> {
270        let named = |index: usize| common_names.get(index).map(|name| (*name).to_string());
271        let in_inversion = |index: usize| {
272            named(index).map(|name| format!("{name} in {}", self.inversion_text().to_lowercase()))
273        };
274        match forte_name {
275            "4-27B" => {
276                if self.is_dominant_seventh() {
277                    named(0)
278                } else if self.is_german_augmented_sixth(false) {
279                    named(2)
280                } else if self.is_german_augmented_sixth(true) {
281                    in_inversion(2)
282                } else if self.is_swiss_augmented_sixth(false) {
283                    named(3)
284                } else if self.is_swiss_augmented_sixth(true) {
285                    in_inversion(3)
286                } else {
287                    named(0).map(|name| format!("enharmonic to {name}"))
288                }
289            }
290            "4-25" => {
291                if self.is_french_augmented_sixth(false) {
292                    named(1)
293                } else if self.is_french_augmented_sixth(true) {
294                    in_inversion(1)
295                } else {
296                    named(0)
297                }
298            }
299            "3-8A" => {
300                if self.is_italian_augmented_sixth(false, false) {
301                    named(1)
302                } else if self.is_italian_augmented_sixth(true, false) {
303                    in_inversion(1)
304                } else {
305                    named(0)
306                }
307            }
308            _ => None,
309        }
310    }
311
312    /// Whether the chord is spelled as the set class's common name says, for
313    /// the classes where music21 checks: a `3-11B` that is not a major triad
314    /// is only enharmonically one.
315    pub(super) fn spelled_as_named(&self, forte_name: &str) -> Option<bool> {
316        match forte_name {
317            "3-11A" => Some(self.is_minor_triad()),
318            "3-11B" => Some(self.is_major_triad()),
319            "3-10" => Some(self.is_diminished_triad()),
320            "3-12" => Some(self.is_augmented_triad()),
321            "4-27A" => Some(self.is_half_diminished_seventh()),
322            "4-28" => Some(self.is_diminished_seventh()),
323            "5-27A" | "5-27B" | "5-34" => Some(self.is_ninth()),
324            _ => None,
325        }
326    }
327
328    /// music21's check for the two seventh-chord set classes that can be
329    /// spelled several ways: a real major or minor seventh has a perfect
330    /// fifth above both its root and its third.
331    pub(super) fn is_seventh_with_perfect_fifths_above_root_and_third(&self) -> bool {
332        if !self.is_seventh() {
333            return false;
334        }
335        let names = self.pitch_names();
336        // Through the pitch's own transpose, as music21 does: a pitch whose
337        // spelling was inferred is respelled on the way, so the fifth above
338        // an integer-built D# is B-, which the chord does not hold.
339        let has_fifth_above = |pitch: &Pitch| {
340            pitch
341                .transpose(&PERFECT_FIFTH)
342                .is_ok_and(|above| names.contains(&above.name()))
343        };
344        let (Some(root), Some(third)) = (self.root(), self.third()) else {
345            return false;
346        };
347        has_fifth_above(root) && has_fifth_above(third)
348    }
349
350    /// The distinct pitch names and the distinct rounded pitch spaces among
351    /// the notes: `C4 C5` is one name and two spaces.
352    fn distinct_names_and_spaces(
353        &self,
354    ) -> (
355        std::collections::BTreeSet<String>,
356        std::collections::BTreeSet<IntegerType>,
357    ) {
358        let names = self.notes.iter().map(|n| n.pitch.name()).collect();
359        let spaces = self
360            .notes
361            .iter()
362            .map(|n| n.pitch.ps().round() as IntegerType)
363            .collect();
364        (names, spaces)
365    }
366
367    pub(super) fn dyad_common_name(&self) -> String {
368        let (pitch_names, pitch_pses) = self.distinct_names_and_spaces();
369
370        let Some(p0) = self.notes.first().map(|n| &n.pitch) else {
371            return "empty chord".to_string();
372        };
373        let p0_pitch_class = root::pitch_class(p0);
374
375        let Some(p1) = self
376            .notes
377            .iter()
378            .skip(1)
379            .find(|n| root::pitch_class(&n.pitch) != p0_pitch_class)
380            .map(|n| &n.pitch)
381        else {
382            return "unknown chord".to_string();
383        };
384
385        let relevant_interval = Interval::between(p0.clone(), p1.clone());
386
387        if pitch_names.len() > 2 {
388            let Ok(interval) = relevant_interval else {
389                return "unknown chord".to_string();
390            };
391            let semitones = interval.chromatic.simple_undirected();
392            let plural = if semitones == 1 { "" } else { "s" };
393            return format!("{semitones} semitone{plural}");
394        }
395
396        if pitch_pses.len() > 2 {
397            return relevant_interval
398                .map(|interval| {
399                    format!("{} with octave doublings", interval.semi_simple_nice_name())
400                })
401                .unwrap_or_else(|_| "unknown chord".to_string());
402        }
403
404        Self::interval_nice_name(&self.notes[0].pitch, &self.notes[1].pitch)
405            .unwrap_or_else(|| "unknown chord".to_string())
406    }
407
408    /// Returns all unpitched common-name aliases known for this chord.
409    pub fn common_names(&self) -> Vec<String> {
410        let ordered_pcs = self.ordered_pitch_classes();
411        let Ok(address) = tables::seek_chord_tables_address(&ordered_pcs) else {
412            return Vec::new();
413        };
414        tables::address_to_common_names(address)
415            .ok()
416            .flatten()
417            .unwrap_or_default()
418            .into_iter()
419            .map(str::to_string)
420            .collect()
421    }
422
423    /// Returns the inferred root pitch name when the chord has one.
424    ///
425    /// Returns `None` for empty chords, where there is no pitch from which a
426    /// root can be inferred.
427    pub fn root_pitch_name(&self) -> Option<String> {
428        self.root_pitch_name_from_tables()
429    }
430
431    /// Returns the lowest pitch name in the chord.
432    ///
433    /// Returns `None` for empty chords, where there is no bass pitch.
434    pub fn bass_pitch_name(&self) -> Option<String> {
435        self.bass_pitch().map(Pitch::name)
436    }
437
438    pub(super) fn root_pitch_name_from_tables(&self) -> Option<String> {
439        self.find_root_pitch().map(Pitch::name)
440    }
441
442    pub(super) fn common_names_with_primary(&self) -> Vec<String> {
443        let mut names = vec![self.common_name()];
444        names.extend(self.common_names());
445        names.sort();
446        names.dedup();
447        names
448    }
449
450    pub(super) fn interval_nice_name(start: &Pitch, end: &Pitch) -> Option<String> {
451        Interval::between(start.clone(), end.clone())
452            .ok()
453            .map(|interval| interval.nice_name())
454    }
455
456    pub(super) fn display_key_name(key: &Key) -> String {
457        format!("{} {}", key.tonic().name(), key.mode())
458    }
459
460    /// Returns music21's `fullName`: the pitches' full names between braces,
461    /// followed by the duration's full name when the chord has a duration
462    /// that music21 can name.
463    pub fn full_name(&self) -> String {
464        let pitches = self
465            .notes
466            .iter()
467            .map(|note| note.pitch.full_name())
468            .collect::<Vec<_>>()
469            .join(" | ");
470        let duration = self
471            .duration
472            .clone()
473            .unwrap_or_else(Duration::quarter)
474            .full_name();
475        format!("Chord {{{pitches}}} {duration}")
476    }
477}
478
479#[derive(Debug, Clone, PartialEq)]
480#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
481/// An unpitched chord type known to the music21-derived chord table.
482pub struct KnownChordType {
483    /// Number of distinct pitch classes in the chord type.
484    pub cardinality: u8,
485    /// Unpitched common-name aliases in music21 table order.
486    pub common_names: Vec<String>,
487    /// Forte class for this transposition-normal entry, such as `"3-11B"`.
488    pub forte_class: String,
489    /// Transposed normal form pitch classes.
490    pub normal_form: Vec<u8>,
491    /// Six-entry interval-class vector.
492    pub interval_class_vector: Vec<u8>,
493}
494
495#[cfg(test)]
496mod augmented_sixth_voicings {
497    use crate::{Chord, defaults::IntegerType};
498
499    /// music21's `commonName` for every voicing of the augmented-sixth
500    /// spellings and their enharmonic neighbours, bass by bass, and for the
501    /// same sets built from pitch-class numbers. Generated from music21.
502    const EXPECTED: &str = "\
503A#3 C#4 D#4 F#4	minor seventh chord
504A#3 C#4 E#4 F#4	major seventh chord
505A#3 C#4 E#4 G#4	minor seventh chord
506A#3 C#4 G4	enharmonic equivalent to diminished triad
507A#3 C4 E4 F#4	French augmented sixth chord in first inversion
508A- C D F#	French augmented sixth chord in third inversion
509A- C E- F#	German augmented sixth chord in second inversion
510A- C F#	Italian augmented sixth chord in second inversion
511A-3 C-4 E-4 F-4	major seventh chord
512A-3 C-4 E-4 G-4	minor seventh chord
513A-3 C4 D4 F#4	French augmented sixth chord
514A-3 C4 E-4 F#4	German augmented sixth chord
515A-3 C4 F#4	Italian augmented sixth chord
516A3 B3 E-4 F#4	enharmonic to dominant seventh chord
517A3 C#4 E-4 G4	French augmented sixth chord in root position
518A3 E-4 F#4	enharmonic equivalent to diminished triad
519B-3 C-4 E-4 G-4	major seventh chord
520B-3 D4 E4 G#4	French augmented sixth chord
521B3 C#4 E#4 G4	French augmented sixth chord in third inversion
522B3 E-4 F#4 A4	enharmonic to dominant seventh chord
523C D F# A-	French augmented sixth chord in third inversion
524C E F# A#	French augmented sixth chord
525C# D# F# A#	minor seventh chord
526C# E# F# A#	major seventh chord
527C# E# G B	French augmented sixth chord in root position
528C# E# G# A#	minor seventh chord
529C# E- G	Italian augmented sixth chord in root position
530C# E- G A	French augmented sixth chord in first inversion
531C# G A#	enharmonic equivalent to diminished triad
532C#3 D#3 F#3 A#3	minor seventh chord
533C#3 E#3 F#3 A#3	major seventh chord
534C#3 E#3 G#3 A#3	minor seventh chord
535C#3 E#3 G3 B3	French augmented sixth chord in root position
536C#3 E-3 G3	Italian augmented sixth chord in root position
537C#3 E-3 G3 A3	French augmented sixth chord in first inversion
538C#3 G3 A#3	enharmonic equivalent to diminished triad
539C-3 E-3 F-3 A-3	major seventh chord
540C-3 E-3 G-3 A-3	minor seventh chord
541C-3 E-3 G-3 B-3	major seventh chord
542C3 D3 F#3 A-3	French augmented sixth chord in third inversion
543C3 E-3 F#3 A-3	German augmented sixth chord in second inversion
544C3 E3 F#3 A#3	French augmented sixth chord
545C3 F#3 A-3	Italian augmented sixth chord in second inversion
546D E G# B-	French augmented sixth chord in third inversion
547D#3 F#3 A#3 C#4	minor seventh chord
548D3 E3 G#3 B-3	French augmented sixth chord in third inversion
549D3 F#3 A-3 C4	French augmented sixth chord in root position
550E#3 F#3 A#3 C#4	major seventh chord
551E#3 G#3 A#3 C#4	minor seventh chord
552E#3 G3 B3 C#4	French augmented sixth chord in first inversion
553E- F# A	enharmonic equivalent to diminished triad
554E- F# A B	enharmonic to dominant seventh chord
555E- F- A- C-	major seventh chord
556E- G- A- C-	minor seventh chord
557E- G- B- C-	major seventh chord
558E-3 F#3 A-3 C4	German augmented sixth chord in third inversion
559E-3 F#3 A3	enharmonic equivalent to diminished triad
560E-3 F#3 A3 B3	enharmonic to dominant seventh chord
561E-3 F-3 A-3 C-4	major seventh chord
562E-3 G-3 A-3 C-4	minor seventh chord
563E-3 G-3 B-3 C-4	major seventh chord
564E-3 G3 A3 C#4	French augmented sixth chord
565E-3 G3 C#4	Italian augmented sixth chord
566E3 F#3 A#3 C4	French augmented sixth chord in third inversion
567E3 G#3 B-3 D4	French augmented sixth chord in root position
568F#3 A#3 C#4 D#4	minor seventh chord
569F#3 A#3 C#4 E#4	major seventh chord
570F#3 A#3 C4 E4	French augmented sixth chord in root position
571F#3 A-3 C4	Italian augmented sixth chord in root position
572F#3 A-3 C4 D4	French augmented sixth chord in first inversion
573F#3 A-3 C4 E-4	German augmented sixth chord in root position
574F#3 A3 B3 E-4	enharmonic to dominant seventh chord
575F#3 A3 E-4	enharmonic equivalent to diminished triad
576F-3 A-3 C-4 E-4	major seventh chord
577G#3 A#3 C#4 E#4	minor seventh chord
578G#3 B-3 D4 E4	French augmented sixth chord in first inversion
579G-3 A-3 C-4 E-4	minor seventh chord
580G-3 B-3 C-4 E-4	major seventh chord
581G3 A#3 C#4	enharmonic equivalent to diminished triad
582G3 A3 C#4 E-4	French augmented sixth chord in third inversion
583G3 B3 C#4 E#4	French augmented sixth chord
584G3 C#4 E-4	Italian augmented sixth chord in second inversion
585[0, 2, 6, 8]	French augmented sixth chord in third inversion
586[1, 3, 6, 10]	enharmonic equivalent to minor seventh chord
587[1, 3, 7]	Italian augmented sixth chord in root position
588[1, 5, 6, 10]	enharmonic equivalent to major seventh chord
589[1, 5, 8, 10]	enharmonic equivalent to minor seventh chord
590[1, 7, 10]	enharmonic equivalent to diminished triad
591[3, 4, 8, 11]	enharmonic equivalent to major seventh chord
592[3, 6, 10, 11]	enharmonic equivalent to major seventh chord
593[3, 6, 8, 11]	enharmonic equivalent to minor seventh chord
594[3, 6, 9]	enharmonic equivalent to diminished triad
595";
596
597    /// music21 puts the root it asks `root()` for in front of the name, and
598    /// the bass `bass()` gives after `above`: overrides included, and for
599    /// every voicing.
600    #[test]
601    fn pitched_names_read_the_root_music21_reads() {
602        let cases = [
603            (
604                "C# E- G",
605                "C#-Italian augmented sixth chord in root position",
606            ),
607            (
608                "G4 C#5 E-5",
609                "C#-Italian augmented sixth chord in second inversion",
610            ),
611            ("E-4 G4 C#5", "C#-Italian augmented sixth chord"),
612            (
613                "C D F# A-",
614                "D-French augmented sixth chord in third inversion",
615            ),
616            ("A-4 C5 D5 F#5", "D-French augmented sixth chord"),
617            (
618                "F#4 A-4 C5 D5",
619                "D-French augmented sixth chord in first inversion",
620            ),
621            (
622                "D4 F#4 A-4 C5",
623                "D-French augmented sixth chord in root position",
624            ),
625            (
626                "C# E- G A",
627                "A-French augmented sixth chord in first inversion",
628            ),
629            (
630                "A4 C#5 E-5 G5",
631                "A-French augmented sixth chord in root position",
632            ),
633            ("C E F# A#", "F#-French augmented sixth chord"),
634            (
635                "A#3 C4 E4 F#4",
636                "F#-French augmented sixth chord in first inversion",
637            ),
638            (
639                "C# E# G B",
640                "C#-French augmented sixth chord in root position",
641            ),
642            (
643                "B4 C#5 E#5 G5",
644                "C#-French augmented sixth chord in third inversion",
645            ),
646            (
647                "D E G# B-",
648                "E-French augmented sixth chord in third inversion",
649            ),
650        ];
651        for (notes, expected) in cases {
652            assert_eq!(
653                Chord::new(notes).unwrap().pitched_common_name(),
654                expected,
655                "{notes}"
656            );
657        }
658
659        let mut rooted = Chord::new("F# A C# E#").unwrap();
660        rooted.set_root(Some("A".parse().unwrap()));
661        assert_eq!(rooted.pitched_common_name(), "A-minor-augmented tetrachord");
662
663        let mut added = Chord::new("C E G B#").unwrap();
664        added.set_bass(Some("E".parse().unwrap()));
665        assert_eq!(
666            added.pitched_common_name(),
667            "enharmonic equivalent to major triad above E"
668        );
669    }
670
671    #[test]
672    fn every_voicing_is_named_as_music21_names_it() {
673        let mut wrong = Vec::new();
674        for line in EXPECTED.lines() {
675            let (notes, expected) = line.split_once('\t').unwrap();
676            let chord = if let Some(numbers) = notes.strip_prefix('[') {
677                let classes: Vec<IntegerType> = numbers
678                    .trim_end_matches(']')
679                    .split(", ")
680                    .map(|number| number.parse().unwrap())
681                    .collect();
682                Chord::new(classes.as_slice()).unwrap()
683            } else {
684                Chord::new(notes).unwrap()
685            };
686            let named = chord.common_name();
687            if named != expected {
688                wrong.push(format!("{notes}: {named}, music21 {expected}"));
689            }
690        }
691        assert!(
692            wrong.is_empty(),
693            "{} of {}:\n{}",
694            wrong.len(),
695            EXPECTED.lines().count(),
696            wrong.join("\n")
697        );
698    }
699}