Skip to main content

music21_rs/
chordsymbol.rs

1use std::str::FromStr;
2
3use crate::{
4    chord::Chord,
5    defaults::{FloatType, IntegerType},
6    error::{Error, Result},
7    interval::Interval,
8    pitch::Pitch,
9};
10use std::collections::{BTreeMap, BTreeSet};
11
12/// Tertian quality parsed from a chord symbol.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum ChordQuality {
16    /// Major triad or major-family sonority.
17    Major,
18    /// Minor triad or minor-family sonority.
19    Minor,
20    /// Dominant seventh-family sonority.
21    Dominant,
22    /// Diminished triad or diminished-family sonority.
23    Diminished,
24    /// Augmented triad sonority.
25    Augmented,
26    /// Half-diminished seventh-family sonority.
27    HalfDiminished,
28    /// Suspended-second sonority.
29    Suspended2,
30    /// Suspended-fourth sonority.
31    Suspended4,
32    /// Power-chord sonority containing a root and fifth.
33    Power,
34}
35
36/// A chord-symbol alteration such as `b5` or `#11`.
37#[derive(Clone, Debug, Eq, PartialEq)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39pub struct ChordAlteration {
40    degree: u8,
41    semitones: IntegerType,
42}
43
44impl ChordAlteration {
45    /// Creates an alteration for a scale degree and semitone displacement.
46    pub fn new(degree: u8, semitones: IntegerType) -> Self {
47        Self { degree, semitones }
48    }
49
50    /// Returns the altered or added chord degree.
51    pub fn degree(&self) -> u8 {
52        self.degree
53    }
54
55    /// Returns the semitone displacement from the unaltered degree.
56    pub fn semitones(&self) -> IntegerType {
57        self.semitones
58    }
59}
60
61/// Parsed chord symbol.
62#[derive(Clone, Debug, PartialEq)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64pub struct ChordSymbol {
65    figure: String,
66    root: Pitch,
67    bass: Option<Pitch>,
68    quality: ChordQuality,
69    extensions: Vec<u8>,
70    alterations: Vec<ChordAlteration>,
71    #[cfg_attr(feature = "serde", serde(default))]
72    omissions: Vec<u8>,
73    #[cfg_attr(feature = "serde", serde(default))]
74    additions: Vec<ChordAlteration>,
75}
76
77/// A chord type from music21's `harmony.CHORD_TYPES` table.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct Music21ChordType {
80    /// music21's kind name, such as `"dominant-seventh"`.
81    pub kind: &'static str,
82    /// Scale-degree notation, such as `"1,3,5,-7"`.
83    pub notation: &'static str,
84    /// music21's first abbreviation for this kind, such as `"7"`.
85    ///
86    /// music21 lists several per kind; this is the first, which is the one it
87    /// uses when writing a figure.
88    pub abbreviation: &'static str,
89}
90
91/// Returns every chord type this crate knows from music21's harmony tables.
92///
93/// The table mirrors music21's `harmony.CHORD_TYPES` and is verified against it
94/// by `python-parity`'s `chord_type_parity` test.
95pub fn known_chord_symbol_types() -> &'static [Music21ChordType] {
96    MUSIC21_CHORD_TYPES
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100struct Music21Degree {
101    degree: u8,
102    semitone: u8,
103}
104
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106struct Music21FigureMatch {
107    kind: &'static str,
108    notation: &'static str,
109    abbreviation: &'static str,
110}
111
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113struct Music21ChordAnalysis {
114    d3: Option<u8>,
115    d5: Option<u8>,
116    d7: Option<u8>,
117    d9: Option<u8>,
118    d11: Option<u8>,
119    d13: Option<u8>,
120    is_triad: bool,
121    is_seventh: bool,
122}
123
124const MUSIC21_CHORD_TYPES: &[Music21ChordType] = &[
125    Music21ChordType {
126        kind: "major",
127        notation: "1,3,5",
128        abbreviation: "",
129    },
130    Music21ChordType {
131        kind: "minor",
132        notation: "1,-3,5",
133        abbreviation: "m",
134    },
135    Music21ChordType {
136        kind: "augmented",
137        notation: "1,3,#5",
138        abbreviation: "+",
139    },
140    Music21ChordType {
141        kind: "diminished",
142        notation: "1,-3,-5",
143        abbreviation: "dim",
144    },
145    Music21ChordType {
146        kind: "dominant-seventh",
147        notation: "1,3,5,-7",
148        abbreviation: "7",
149    },
150    Music21ChordType {
151        kind: "major-seventh",
152        notation: "1,3,5,7",
153        abbreviation: "maj7",
154    },
155    Music21ChordType {
156        kind: "minor-major-seventh",
157        notation: "1,-3,5,7",
158        abbreviation: "mM7",
159    },
160    Music21ChordType {
161        kind: "minor-seventh",
162        notation: "1,-3,5,-7",
163        abbreviation: "m7",
164    },
165    Music21ChordType {
166        kind: "augmented-major-seventh",
167        notation: "1,3,#5,7",
168        abbreviation: "+M7",
169    },
170    Music21ChordType {
171        kind: "augmented-seventh",
172        notation: "1,3,#5,-7",
173        abbreviation: "7+",
174    },
175    Music21ChordType {
176        kind: "half-diminished-seventh",
177        notation: "1,-3,-5,-7",
178        abbreviation: "\u{00f8}7",
179    },
180    Music21ChordType {
181        kind: "diminished-seventh",
182        notation: "1,-3,-5,--7",
183        abbreviation: "o7",
184    },
185    Music21ChordType {
186        kind: "seventh-flat-five",
187        notation: "1,3,-5,-7",
188        abbreviation: "dom7dim5",
189    },
190    Music21ChordType {
191        kind: "major-sixth",
192        notation: "1,3,5,6",
193        abbreviation: "6",
194    },
195    Music21ChordType {
196        kind: "minor-sixth",
197        notation: "1,-3,5,6",
198        abbreviation: "m6",
199    },
200    Music21ChordType {
201        kind: "major-ninth",
202        notation: "1,3,5,7,9",
203        abbreviation: "M9",
204    },
205    Music21ChordType {
206        kind: "dominant-ninth",
207        notation: "1,3,5,-7,9",
208        abbreviation: "9",
209    },
210    Music21ChordType {
211        kind: "minor-major-ninth",
212        notation: "1,-3,5,7,9",
213        abbreviation: "mM9",
214    },
215    Music21ChordType {
216        kind: "minor-ninth",
217        notation: "1,-3,5,-7,9",
218        abbreviation: "m9",
219    },
220    Music21ChordType {
221        kind: "augmented-major-ninth",
222        notation: "1,3,#5,7,9",
223        abbreviation: "+M9",
224    },
225    Music21ChordType {
226        kind: "augmented-dominant-ninth",
227        notation: "1,3,#5,-7,9",
228        abbreviation: "9#5",
229    },
230    Music21ChordType {
231        kind: "half-diminished-ninth",
232        notation: "1,-3,-5,-7,9",
233        abbreviation: "\u{00f8}9",
234    },
235    Music21ChordType {
236        kind: "half-diminished-minor-ninth",
237        notation: "1,-3,-5,-7,-9",
238        abbreviation: "\u{00f8}b9",
239    },
240    Music21ChordType {
241        kind: "diminished-ninth",
242        notation: "1,-3,-5,--7,9",
243        abbreviation: "o9",
244    },
245    Music21ChordType {
246        kind: "diminished-minor-ninth",
247        notation: "1,-3,-5,--7,-9",
248        abbreviation: "ob9",
249    },
250    Music21ChordType {
251        kind: "dominant-11th",
252        notation: "1,3,5,-7,9,11",
253        abbreviation: "11",
254    },
255    Music21ChordType {
256        kind: "major-11th",
257        notation: "1,3,5,7,9,11",
258        abbreviation: "M11",
259    },
260    Music21ChordType {
261        kind: "minor-major-11th",
262        notation: "1,-3,5,7,9,11",
263        abbreviation: "mM11",
264    },
265    Music21ChordType {
266        kind: "minor-11th",
267        notation: "1,-3,5,-7,9,11",
268        abbreviation: "m11",
269    },
270    Music21ChordType {
271        kind: "augmented-major-11th",
272        notation: "1,3,#5,7,9,11",
273        abbreviation: "+M11",
274    },
275    Music21ChordType {
276        kind: "augmented-11th",
277        notation: "1,3,#5,-7,9,11",
278        abbreviation: "+11",
279    },
280    Music21ChordType {
281        kind: "half-diminished-11th",
282        notation: "1,-3,-5,-7,9,11",
283        abbreviation: "\u{00f8}11",
284    },
285    Music21ChordType {
286        kind: "diminished-11th",
287        notation: "1,-3,-5,--7,9,11",
288        abbreviation: "o11",
289    },
290    Music21ChordType {
291        kind: "major-13th",
292        notation: "1,3,5,7,9,11,13",
293        abbreviation: "M13",
294    },
295    Music21ChordType {
296        kind: "dominant-13th",
297        notation: "1,3,5,-7,9,11,13",
298        abbreviation: "13",
299    },
300    Music21ChordType {
301        kind: "minor-major-13th",
302        notation: "1,-3,5,7,9,11,13",
303        abbreviation: "mM13",
304    },
305    Music21ChordType {
306        kind: "minor-13th",
307        notation: "1,-3,5,-7,9,11,13",
308        abbreviation: "m13",
309    },
310    Music21ChordType {
311        kind: "augmented-major-13th",
312        notation: "1,3,#5,7,9,11,13",
313        abbreviation: "+M13",
314    },
315    Music21ChordType {
316        kind: "augmented-dominant-13th",
317        notation: "1,3,#5,-7,9,11,13",
318        abbreviation: "+13",
319    },
320    Music21ChordType {
321        kind: "half-diminished-13th",
322        notation: "1,-3,-5,-7,9,11,13",
323        abbreviation: "\u{00f8}13",
324    },
325    Music21ChordType {
326        kind: "suspended-second",
327        notation: "1,2,5",
328        abbreviation: "sus2",
329    },
330    Music21ChordType {
331        kind: "suspended-fourth",
332        notation: "1,4,5",
333        abbreviation: "sus",
334    },
335    Music21ChordType {
336        kind: "suspended-fourth-seventh",
337        notation: "1,4,5,-7",
338        abbreviation: "7sus",
339    },
340    Music21ChordType {
341        kind: "Neapolitan",
342        notation: "1,-2,3,-5",
343        abbreviation: "N6",
344    },
345    Music21ChordType {
346        kind: "Italian",
347        notation: "1,#4,-6",
348        abbreviation: "It+6",
349    },
350    Music21ChordType {
351        kind: "French",
352        notation: "1,2,#4,-6",
353        abbreviation: "Fr+6",
354    },
355    Music21ChordType {
356        kind: "German",
357        notation: "1,-3,#4,-6",
358        abbreviation: "Gr+6",
359    },
360    Music21ChordType {
361        kind: "pedal",
362        notation: "1",
363        abbreviation: "pedal",
364    },
365    Music21ChordType {
366        kind: "power",
367        notation: "1,5",
368        abbreviation: "power",
369    },
370    Music21ChordType {
371        kind: "Tristan",
372        notation: "1,#4,#6,#9",
373        abbreviation: "tristan",
374    },
375];
376
377impl ChordSymbol {
378    /// Parses a chord symbol such as `"Cmaj7"`, `"F#m7b5"`, or `"Bb7#11"`.
379    pub fn parse(figure: impl Into<String>) -> Result<Self> {
380        let figure = figure.into();
381        let trimmed = figure.trim();
382        if trimmed.is_empty() {
383            return Err(Error::Chord("chord symbol cannot be empty".to_string()));
384        }
385
386        let (body, bass_segment) = match trimmed.split_once('/') {
387            Some((body, bass)) => (body, Some(bass)),
388            None => (trimmed, None),
389        };
390        let body_parts = split_music21_pitch_modifiers(body);
391        let bass_parts = bass_segment.map(split_music21_pitch_modifiers);
392        let bass = bass_parts
393            .as_ref()
394            .map(|parts| parse_pitch_only(&parts.base))
395            .transpose()?;
396
397        let (root_name, suffix) = parse_pitch_prefix(&body_parts.base)?;
398        let root = Pitch::from_name(root_name)?;
399        let suffix_without_additions = strip_addition_groups(suffix);
400        let mut additions = parse_additions(suffix);
401        let mut omissions = parse_omissions(suffix);
402        for pitch_name in body_parts
403            .additions
404            .iter()
405            .chain(bass_parts.iter().flat_map(|parts| parts.additions.iter()))
406        {
407            if let Some(addition) = pitch_name_addition(&root, pitch_name) {
408                additions.push(addition);
409            }
410        }
411        for pitch_name in body_parts
412            .omissions
413            .iter()
414            .chain(bass_parts.iter().flat_map(|parts| parts.omissions.iter()))
415        {
416            if let Some(omission) = pitch_name_degree(&root, pitch_name)
417                && !omissions.contains(&omission)
418            {
419                omissions.push(omission);
420            }
421        }
422
423        let mut alterations = parse_alterations(&suffix_without_additions);
424        add_implicit_music21_alterations(&suffix_without_additions, &mut alterations);
425        let extensions = parse_extensions(&suffix_without_additions, &alterations);
426        let quality = parse_quality(&suffix_without_additions, &alterations);
427
428        Ok(Self {
429            figure: trimmed.to_string(),
430            root,
431            bass,
432            quality,
433            extensions,
434            alterations,
435            omissions,
436            additions,
437        })
438    }
439
440    /// Returns the original chord-symbol figure.
441    pub fn figure(&self) -> &str {
442        &self.figure
443    }
444
445    /// Returns the root pitch.
446    pub fn root(&self) -> &Pitch {
447        &self.root
448    }
449
450    /// Returns the slash bass pitch, if one was supplied.
451    pub fn bass(&self) -> Option<&Pitch> {
452        self.bass.as_ref()
453    }
454
455    /// Returns the parsed chord quality.
456    pub fn quality(&self) -> ChordQuality {
457        self.quality
458    }
459
460    /// Returns parsed extension degrees.
461    pub fn extensions(&self) -> &[u8] {
462        &self.extensions
463    }
464
465    /// Returns parsed alterations.
466    pub fn alterations(&self) -> &[ChordAlteration] {
467        &self.alterations
468    }
469
470    /// Returns degrees omitted with `no...` or `omit...` markers.
471    pub fn omissions(&self) -> &[u8] {
472        &self.omissions
473    }
474
475    /// Returns parsed added tones from `add(...)` groups.
476    pub fn additions(&self) -> &[ChordAlteration] {
477        &self.additions
478    }
479
480    /// Realizes the chord symbol as a [`Chord`].
481    pub fn to_chord(&self) -> Result<Chord> {
482        let mut intervals = self.base_intervals();
483
484        for extension in [6, 9, 11, 13] {
485            if self.extensions.contains(&extension)
486                && !self.alterations.iter().any(|alt| alt.degree == extension)
487            {
488                intervals.push((extension, default_extension_interval(extension)));
489            }
490        }
491
492        for alteration in &self.alterations {
493            if alteration.degree == 5 {
494                continue;
495            }
496            intervals.push(altered_interval(alteration)?);
497        }
498
499        for addition in &self.additions {
500            intervals.push(added_interval(addition)?);
501        }
502
503        intervals.sort_unstable_by_key(|(degree, _)| *degree);
504        intervals.dedup();
505
506        let mut pitches = intervals
507            .into_iter()
508            .map(|(_, name)| Interval::from_name(name)?.transpose_pitch(&self.root))
509            .collect::<Result<Vec<_>>>()?;
510
511        if let Some(bass) = &self.bass {
512            if let Some(index) = pitches.iter().position(|pitch| pitch.name() == bass.name()) {
513                let bass = pitches.remove(index);
514                pitches.insert(0, bass);
515            } else {
516                pitches.insert(0, bass.clone());
517            }
518        }
519
520        Chord::new(pitches.as_slice())
521    }
522
523    fn base_intervals(&self) -> Vec<(u8, &'static str)> {
524        let altered_fifth = self
525            .alterations
526            .iter()
527            .find(|alteration| alteration.degree == 5)
528            .and_then(|alteration| match alteration.semitones {
529                -1 => Some("d5"),
530                1 => Some("a5"),
531                _ => None,
532            });
533
534        let fifth = altered_fifth.unwrap_or("P5");
535        let has_seventh = self
536            .extensions
537            .iter()
538            .any(|degree| matches!(degree, 7 | 9 | 11 | 13));
539
540        let intervals = match self.quality {
541            ChordQuality::Major => {
542                if has_seventh {
543                    vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "M7")]
544                } else {
545                    vec![(1, "P1"), (3, "M3"), (5, fifth)]
546                }
547            }
548            ChordQuality::Minor => {
549                if has_seventh {
550                    vec![(1, "P1"), (3, "m3"), (5, fifth), (7, "m7")]
551                } else {
552                    vec![(1, "P1"), (3, "m3"), (5, fifth)]
553                }
554            }
555            ChordQuality::Dominant => vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "m7")],
556            ChordQuality::Diminished => {
557                if has_seventh {
558                    vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "d7")]
559                } else {
560                    vec![(1, "P1"), (3, "m3"), (5, "d5")]
561                }
562            }
563            ChordQuality::Augmented => vec![(1, "P1"), (3, "M3"), (5, "a5")],
564            ChordQuality::HalfDiminished => vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "m7")],
565            ChordQuality::Suspended2 => vec![(1, "P1"), (2, "M2"), (5, fifth)],
566            ChordQuality::Suspended4 => vec![(1, "P1"), (4, "P4"), (5, fifth)],
567            ChordQuality::Power => vec![(1, "P1"), (5, fifth)],
568        };
569
570        intervals
571            .into_iter()
572            .filter(|(degree, _)| !self.omissions.contains(degree))
573            .collect()
574    }
575}
576
577/// Returns the music21 chord-symbol figure for a chord, when identified.
578///
579/// This ports music21's `harmony.chordSymbolFigureFromChord` matching order and
580/// spelling conventions. Music21's "Chord Symbol Cannot Be Identified" result
581/// is represented by an empty list so callers can keep using `Option<String>`.
582pub(crate) fn chord_symbol_spellings(chord: &Chord) -> Vec<String> {
583    chord_symbol_spellings_for_root(chord, None)
584}
585
586pub(crate) fn chord_symbol_spellings_with_root(chord: &Chord, root: u8) -> Vec<String> {
587    chord_symbol_spellings_for_root(chord, Some(root % 12))
588}
589
590fn chord_symbol_spellings_for_root(chord: &Chord, explicit_root: Option<u8>) -> Vec<String> {
591    music21_chord_symbol_figure(chord, explicit_root)
592        .into_iter()
593        .collect()
594}
595
596fn music21_chord_symbol_figure(chord: &Chord, explicit_root: Option<u8>) -> Option<String> {
597    let pitches = chord.pitches();
598    if pitches.iter().any(|pitch| {
599        let ps = pitch.ps();
600        (ps - ps.round()).abs() > FloatType::EPSILON
601    }) {
602        return None;
603    }
604
605    if pitches.is_empty() {
606        return None;
607    }
608
609    let mut root_pitch = if let Some(root) = explicit_root {
610        pitches
611            .iter()
612            .find(|pitch| pitch_class(pitch) == root)
613            .cloned()?
614    } else {
615        find_root_pitch(&pitches).cloned()?
616    };
617
618    if pitches.len() == 1 {
619        return Some(format!("{}pedal", root_pitch.name()));
620    }
621
622    let analysis = Music21ChordAnalysis::new(&pitches, &root_pitch);
623    let matched = identify_music21_chord_type(&analysis)?;
624    let bass_pitch = bass_pitch(&pitches)?;
625    let mut notation = matched.notation;
626    let mut abbreviation = matched.abbreviation;
627
628    if pitch_class(bass_pitch) != pitch_class(&root_pitch)
629        && matched.kind == "suspended-second"
630        && matched.abbreviation == "sus2"
631    {
632        root_pitch = bass_pitch.clone();
633        notation = "1,4,5";
634        abbreviation = "sus";
635    }
636
637    let mut figure = format!("{}{}", root_pitch.name(), abbreviation);
638    if pitch_class(bass_pitch) != pitch_class(&root_pitch) {
639        figure.push('/');
640        figure.push_str(&bass_pitch.name());
641    }
642
643    let perfect = perfect_pitch_names(&root_pitch, notation)?;
644    let in_pitches = pitches
645        .iter()
646        .map(Pitch::name)
647        .collect::<BTreeSet<String>>();
648
649    if !perfect.is_superset(&in_pitches) {
650        let additions = in_pitches.difference(&perfect).cloned().collect::<Vec<_>>();
651        let subtractions = perfect.difference(&in_pitches).cloned().collect::<Vec<_>>();
652
653        if !additions.is_empty() {
654            figure.push_str("add");
655            figure.push_str(&additions.join(","));
656        }
657        if !subtractions.is_empty() {
658            figure.push_str("omit");
659            figure.push_str(&subtractions.join(","));
660        }
661    }
662
663    Some(figure)
664}
665
666impl Music21ChordAnalysis {
667    fn new(pitches: &[Pitch], root_pitch: &Pitch) -> Self {
668        let d3 = semitones_from_chord_step(pitches, root_pitch, 3);
669        let d5 = semitones_from_chord_step(pitches, root_pitch, 5);
670        let d7 = semitones_from_chord_step(pitches, root_pitch, 7);
671        let d9 = semitones_from_chord_step(pitches, root_pitch, 2);
672        let d11 = semitones_from_chord_step(pitches, root_pitch, 4);
673        let d13 = semitones_from_chord_step(pitches, root_pitch, 6);
674        let unique_pitch_names = pitches
675            .iter()
676            .map(Pitch::name)
677            .collect::<BTreeSet<String>>();
678
679        Self {
680            d3,
681            d5,
682            d7,
683            d9,
684            d11,
685            d13,
686            is_triad: unique_pitch_names.len() == 3 && d3.is_some() && d5.is_some(),
687            is_seventh: unique_pitch_names.len() == 4
688                && d3.is_some()
689                && d5.is_some()
690                && d7.is_some(),
691        }
692    }
693}
694
695fn identify_music21_chord_type(analysis: &Music21ChordAnalysis) -> Option<Music21FigureMatch> {
696    let mut matched = None;
697
698    for chord_type in MUSIC21_CHORD_TYPES {
699        let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
700        let is_match = match chord_degrees.len() {
701            2 if analysis.is_triad => {
702                compare_music21_degrees(&[analysis.d3, analysis.d5], &chord_degrees, &[])
703            }
704            3 if analysis.is_seventh => compare_music21_degrees(
705                &[analysis.d3, analysis.d5, analysis.d7],
706                &chord_degrees,
707                &[],
708            ),
709            4 if music21_truthy(analysis.d9)
710                && !music21_truthy(analysis.d11)
711                && !music21_truthy(analysis.d13) =>
712            {
713                compare_music21_degrees(
714                    &[analysis.d3, analysis.d5, analysis.d7, analysis.d9],
715                    &chord_degrees,
716                    &[5],
717                )
718            }
719            5 if music21_truthy(analysis.d11) && !music21_truthy(analysis.d13) => {
720                compare_music21_degrees(
721                    &[
722                        analysis.d3,
723                        analysis.d5,
724                        analysis.d7,
725                        analysis.d9,
726                        analysis.d11,
727                    ],
728                    &chord_degrees,
729                    &[3, 5],
730                )
731            }
732            6 if music21_truthy(analysis.d13) => compare_music21_degrees(
733                &[
734                    analysis.d3,
735                    analysis.d5,
736                    analysis.d7,
737                    analysis.d9,
738                    analysis.d11,
739                    analysis.d13,
740                ],
741                &chord_degrees,
742                &[5, 11, 9],
743            ),
744            _ => false,
745        };
746
747        if is_match {
748            matched = Some(Music21FigureMatch {
749                kind: chord_type.kind,
750                notation: chord_type.notation,
751                abbreviation: chord_type.abbreviation,
752            });
753        }
754    }
755
756    if matched.is_some() {
757        return matched;
758    }
759
760    let mut number_of_matched_degrees = 0;
761    for chord_type in MUSIC21_CHORD_TYPES {
762        let chord_degrees = chord_degrees_for_notation(chord_type.notation)?;
763        let mut degrees = degree_numbers_for_notation(chord_type.notation)?;
764        degrees.sort_unstable();
765        let to_compare = degrees
766            .into_iter()
767            .filter(|degree| *degree != 1)
768            .map(|degree| analysis_value_for_degree(analysis, degree))
769            .collect::<Vec<_>>();
770
771        if compare_music21_degrees(&to_compare, &chord_degrees, &[])
772            && number_of_matched_degrees < chord_degrees.len()
773        {
774            number_of_matched_degrees = chord_degrees.len();
775            matched = Some(Music21FigureMatch {
776                kind: chord_type.kind,
777                notation: chord_type.notation,
778                abbreviation: chord_type.abbreviation,
779            });
780        }
781    }
782
783    matched
784}
785
786fn compare_music21_degrees(
787    in_chord_nums: &[Option<u8>],
788    given_chord_nums: &[u8],
789    permitted_omissions: &[u8],
790) -> bool {
791    if given_chord_nums.len() > in_chord_nums.len() {
792        return false;
793    }
794
795    for (index, expected) in given_chord_nums.iter().enumerate() {
796        if in_chord_nums[index] == Some(*expected) {
797            continue;
798        }
799
800        let (degree, natural) = match index {
801            0 => (3, 4),
802            1 => (5, 7),
803            2 => (7, 11),
804            3 => (9, 2),
805            4 => (11, 5),
806            5 => (13, 9),
807            _ => return false,
808        };
809
810        if !(permitted_omissions.contains(&degree)
811            && *expected == natural
812            && in_chord_nums[index].is_none())
813        {
814            return false;
815        }
816    }
817
818    true
819}
820
821fn music21_truthy(value: Option<u8>) -> bool {
822    value.is_some_and(|value| value != 0)
823}
824
825fn chord_degrees_for_notation(notation: &str) -> Option<Vec<u8>> {
826    notation
827        .split(',')
828        .filter(|token| *token != "1")
829        .map(|token| parse_music21_degree(token).map(|degree| degree.semitone))
830        .collect()
831}
832
833fn degree_numbers_for_notation(notation: &str) -> Option<Vec<u8>> {
834    notation
835        .split(',')
836        .map(|token| parse_music21_degree(token).map(|degree| degree.degree))
837        .collect()
838}
839
840fn parse_music21_degree(token: &str) -> Option<Music21Degree> {
841    let alteration = token.chars().fold(0_i32, |sum, ch| match ch {
842        '#' => sum + 1,
843        '-' => sum - 1,
844        _ => sum,
845    });
846    let degree = token
847        .chars()
848        .filter(char::is_ascii_digit)
849        .collect::<String>()
850        .parse::<u8>()
851        .ok()?;
852    let semitone = (base_semitone_for_degree(degree)? + alteration).rem_euclid(12) as u8;
853
854    Some(Music21Degree { degree, semitone })
855}
856
857fn base_semitone_for_degree(degree: u8) -> Option<IntegerType> {
858    match degree {
859        1 => Some(0),
860        2 | 9 => Some(2),
861        3 => Some(4),
862        4 | 11 => Some(5),
863        5 => Some(7),
864        6 | 13 => Some(9),
865        7 => Some(11),
866        _ => None,
867    }
868}
869
870fn analysis_value_for_degree(analysis: &Music21ChordAnalysis, degree: u8) -> Option<u8> {
871    match degree {
872        2 | 9 => analysis.d9,
873        3 => analysis.d3,
874        4 | 11 => analysis.d11,
875        5 => analysis.d5,
876        6 | 13 => analysis.d13,
877        7 => analysis.d7,
878        _ => None,
879    }
880}
881
882fn semitones_from_chord_step(pitches: &[Pitch], root_pitch: &Pitch, chord_step: u8) -> Option<u8> {
883    let root_step = step_num(root_pitch);
884    let root_pc = pitch_class(root_pitch);
885
886    pitches.iter().find_map(|pitch| {
887        let generic_interval = (step_num(pitch) - root_step).rem_euclid(7) + 1;
888        if generic_interval == chord_step as IntegerType {
889            Some((pitch_class(pitch) + 12 - root_pc) % 12)
890        } else {
891            None
892        }
893    })
894}
895
896fn perfect_pitch_names(root_pitch: &Pitch, notation: &str) -> Option<BTreeSet<String>> {
897    let mut pitch_names = BTreeSet::new();
898    pitch_names.insert(root_pitch.name());
899    for token in notation.split(',').filter(|token| *token != "1") {
900        let degree = parse_music21_degree(token)?;
901        pitch_names.insert(pitch_name_for_music21_degree(
902            root_pitch,
903            degree.degree,
904            degree.semitone,
905        )?);
906    }
907    Some(pitch_names)
908}
909
910fn pitch_name_for_music21_degree(root_pitch: &Pitch, degree: u8, semitone: u8) -> Option<String> {
911    const LETTERS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
912    const NATURAL_PCS: [IntegerType; 7] = [0, 2, 4, 5, 7, 9, 11];
913
914    let root_letter = root_pitch.name().chars().next()?.to_ascii_uppercase();
915    let root_index = LETTERS.iter().position(|letter| *letter == root_letter)?;
916    let target_index = (root_index + (degree.saturating_sub(1) as usize % 7)) % 7;
917    let desired_pc =
918        ((pitch_class(root_pitch) as IntegerType) + semitone as IntegerType).rem_euclid(12);
919    let mut accidental = desired_pc - NATURAL_PCS[target_index];
920    while accidental > 6 {
921        accidental -= 12;
922    }
923    while accidental < -6 {
924        accidental += 12;
925    }
926
927    let mut name = LETTERS[target_index].to_string();
928    if accidental > 0 {
929        name.push_str(&"#".repeat(accidental as usize));
930    } else if accidental < 0 {
931        name.push_str(&"-".repeat((-accidental) as usize));
932    }
933    Some(name)
934}
935
936fn find_root_pitch(pitches: &[Pitch]) -> Option<&Pitch> {
937    let mut non_duplicating_pitches = Vec::new();
938    let mut seen_steps = BTreeSet::new();
939    for pitch in pitches {
940        if seen_steps.insert(step_num(pitch)) {
941            non_duplicating_pitches.push(pitch);
942        }
943    }
944
945    match non_duplicating_pitches.len() {
946        0 => return None,
947        1 => return pitches.first(),
948        7 => return bass_pitch(pitches),
949        _ => {}
950    }
951
952    let mut step_nums_to_pitches = BTreeMap::new();
953    for pitch in &non_duplicating_pitches {
954        step_nums_to_pitches.insert(step_num(pitch), *pitch);
955    }
956    let step_nums = step_nums_to_pitches.keys().copied().collect::<Vec<_>>();
957
958    for start_index in 0..step_nums.len() {
959        let mut all_are_thirds = true;
960        let this_step_num = step_nums[start_index];
961        let mut last_step_num = this_step_num;
962        for end_index in (start_index + 1)..(start_index + step_nums.len()) {
963            let end_step_num = step_nums[end_index % step_nums.len()];
964            if !matches!(end_step_num - last_step_num, 2 | -5) {
965                all_are_thirds = false;
966                break;
967            }
968            last_step_num = end_step_num;
969        }
970        if all_are_thirds {
971            return step_nums_to_pitches.get(&this_step_num).copied();
972        }
973    }
974
975    let ordered_chord_steps = [3, 5, 7, 2, 4, 6];
976    let mut best_pitch = non_duplicating_pitches[0];
977    let mut best_score = FloatType::NEG_INFINITY;
978
979    for pitch in non_duplicating_pitches {
980        let this_step_num = step_num(pitch);
981        let mut score = 0.0;
982        for (root_index, chord_step_test) in ordered_chord_steps.iter().enumerate() {
983            let target = (this_step_num + chord_step_test - 1).rem_euclid(7);
984            if step_nums_to_pitches.contains_key(&target) {
985                score += 1.0 / (root_index as FloatType + 6.0);
986            }
987        }
988        if score > best_score {
989            best_score = score;
990            best_pitch = pitch;
991        }
992    }
993
994    Some(best_pitch)
995}
996
997fn bass_pitch(pitches: &[Pitch]) -> Option<&Pitch> {
998    pitches.iter().min_by(|left, right| {
999        left.ps()
1000            .partial_cmp(&right.ps())
1001            .unwrap_or(std::cmp::Ordering::Equal)
1002    })
1003}
1004
1005fn step_num(pitch: &Pitch) -> IntegerType {
1006    pitch.step().step_to_dnn_offset() - 1
1007}
1008
1009fn pitch_class(pitch: &Pitch) -> u8 {
1010    (pitch.ps().round() as IntegerType).rem_euclid(12) as u8
1011}
1012
1013impl FromStr for ChordSymbol {
1014    type Err = Error;
1015
1016    fn from_str(value: &str) -> Result<Self> {
1017        Self::parse(value)
1018    }
1019}
1020
1021impl TryFrom<&str> for ChordSymbol {
1022    type Error = Error;
1023
1024    fn try_from(value: &str) -> Result<Self> {
1025        Self::parse(value)
1026    }
1027}
1028
1029impl TryFrom<String> for ChordSymbol {
1030    type Error = Error;
1031
1032    fn try_from(value: String) -> Result<Self> {
1033        Self::parse(value)
1034    }
1035}
1036
1037#[derive(Clone, Debug, Default, Eq, PartialEq)]
1038struct Music21PitchModifiers {
1039    base: String,
1040    additions: Vec<String>,
1041    omissions: Vec<String>,
1042}
1043
1044fn split_music21_pitch_modifiers(value: &str) -> Music21PitchModifiers {
1045    let Some(start) = find_music21_modifier_start(value) else {
1046        return Music21PitchModifiers {
1047            base: value.to_string(),
1048            ..Music21PitchModifiers::default()
1049        };
1050    };
1051
1052    let mut parts = Music21PitchModifiers {
1053        base: value[..start].trim_end().to_string(),
1054        ..Music21PitchModifiers::default()
1055    };
1056    let mut cursor = start;
1057    while cursor < value.len() {
1058        let Some(marker) = music21_modifier_at(value, cursor) else {
1059            cursor += value[cursor..]
1060                .chars()
1061                .next()
1062                .map(char::len_utf8)
1063                .unwrap_or(1);
1064            continue;
1065        };
1066        let content_start = cursor + marker.len();
1067        let content_end = find_music21_modifier_start(&value[content_start..])
1068            .map(|relative| content_start + relative)
1069            .unwrap_or(value.len());
1070        let tokens = value[content_start..content_end]
1071            .split(|ch: char| ch == ',' || ch.is_whitespace())
1072            .filter(|token| !token.trim().is_empty())
1073            .map(|token| token.trim().to_string());
1074
1075        match marker {
1076            "add" => parts.additions.extend(tokens),
1077            "omit" => parts.omissions.extend(tokens),
1078            _ => {}
1079        }
1080        cursor = content_end;
1081    }
1082
1083    parts
1084}
1085
1086fn find_music21_modifier_start(value: &str) -> Option<usize> {
1087    value
1088        .char_indices()
1089        .find_map(|(idx, _)| music21_modifier_at(value, idx).map(|_| idx))
1090}
1091
1092fn music21_modifier_at(value: &str, idx: usize) -> Option<&'static str> {
1093    let rest = value.get(idx..)?;
1094    let lower = rest.to_ascii_lowercase();
1095    if lower.starts_with("add") && !matches!(rest.as_bytes().get(3), Some(b'(')) {
1096        Some("add")
1097    } else if lower.starts_with("omit") && !matches!(rest.as_bytes().get(4), Some(b'(')) {
1098        Some("omit")
1099    } else {
1100        None
1101    }
1102}
1103
1104fn pitch_name_addition(root: &Pitch, pitch_name: &str) -> Option<ChordAlteration> {
1105    let pitch = Pitch::from_name(pitch_name).ok()?;
1106    let degree = pitch_name_degree(root, pitch_name)?;
1107    let actual = ((pitch_class(&pitch) + 12 - pitch_class(root)) % 12) as IntegerType;
1108    let base = base_semitone_for_degree(degree)?.rem_euclid(12);
1109    let mut semitones = actual - base;
1110    while semitones > 6 {
1111        semitones -= 12;
1112    }
1113    while semitones < -6 {
1114        semitones += 12;
1115    }
1116
1117    Some(ChordAlteration::new(degree, semitones))
1118}
1119
1120fn pitch_name_degree(root: &Pitch, pitch_name: &str) -> Option<u8> {
1121    let pitch = Pitch::from_name(pitch_name).ok()?;
1122    let generic = (step_num(&pitch) - step_num(root)).rem_euclid(7) + 1;
1123    Some(match generic as u8 {
1124        2 => 9,
1125        4 => 11,
1126        6 => 13,
1127        degree => degree,
1128    })
1129}
1130
1131fn add_implicit_music21_alterations(suffix: &str, alterations: &mut Vec<ChordAlteration>) {
1132    let lower = suffix.to_ascii_lowercase();
1133    if lower.contains("dim5")
1134        && !alterations
1135            .iter()
1136            .any(|alteration| alteration.degree == 5 && alteration.semitones == -1)
1137    {
1138        alterations.push(ChordAlteration::new(5, -1));
1139    }
1140    if lower.ends_with("7+")
1141        && !alterations
1142            .iter()
1143            .any(|alteration| alteration.degree == 5 && alteration.semitones == 1)
1144    {
1145        alterations.push(ChordAlteration::new(5, 1));
1146    }
1147}
1148
1149fn parse_quality(suffix: &str, alterations: &[ChordAlteration]) -> ChordQuality {
1150    let lower = suffix.to_ascii_lowercase();
1151    let has_flat_five = alterations
1152        .iter()
1153        .any(|alteration| alteration.degree == 5 && alteration.semitones == -1);
1154
1155    if suffix.starts_with('\u{00f8}') {
1156        ChordQuality::HalfDiminished
1157    } else if lower.contains("sus2") {
1158        ChordQuality::Suspended2
1159    } else if lower.contains("sus") {
1160        ChordQuality::Suspended4
1161    } else if lower.starts_with("maj") || suffix.starts_with('M') {
1162        ChordQuality::Major
1163    } else if lower.starts_with("min") || lower.starts_with('m') {
1164        if has_flat_five && lower.contains('7') {
1165            ChordQuality::HalfDiminished
1166        } else {
1167            ChordQuality::Minor
1168        }
1169    } else if lower.starts_with("dim") || lower.starts_with('o') {
1170        ChordQuality::Diminished
1171    } else if lower.starts_with("aug") || lower.starts_with('+') {
1172        ChordQuality::Augmented
1173    } else if lower.starts_with('5') {
1174        ChordQuality::Power
1175    } else if lower.starts_with("dom")
1176        || lower.starts_with('7')
1177        || lower.starts_with('9')
1178        || lower.starts_with("11")
1179        || lower.starts_with("13")
1180    {
1181        ChordQuality::Dominant
1182    } else {
1183        ChordQuality::Major
1184    }
1185}
1186
1187fn parse_extensions(suffix: &str, alterations: &[ChordAlteration]) -> Vec<u8> {
1188    let mut extensions = Vec::new();
1189    let bytes = suffix.as_bytes();
1190    let mut idx = 0;
1191    while idx < bytes.len() {
1192        let byte = bytes[idx];
1193        if byte.is_ascii_digit()
1194            && idx
1195                .checked_sub(1)
1196                .is_none_or(|prev| !matches!(bytes[prev] as char, '#' | 'b' | '-'))
1197        {
1198            let start = idx;
1199            while idx < bytes.len() && bytes[idx].is_ascii_digit() {
1200                idx += 1;
1201            }
1202            if let Ok(degree) = suffix[start..idx].parse::<u8>()
1203                && matches!(degree, 6 | 7 | 9 | 11 | 13)
1204                && !extensions.contains(&degree)
1205            {
1206                extensions.push(degree);
1207            }
1208        } else {
1209            idx += 1;
1210        }
1211    }
1212
1213    for alteration in alterations {
1214        if alteration.degree > 5 && !extensions.contains(&alteration.degree) {
1215            extensions.push(alteration.degree);
1216        }
1217    }
1218
1219    extensions.sort_unstable();
1220    extensions
1221}
1222
1223fn strip_addition_groups(suffix: &str) -> String {
1224    let lower = suffix.to_ascii_lowercase();
1225    let mut stripped = String::with_capacity(suffix.len());
1226    let mut cursor = 0;
1227
1228    while let Some(relative_start) = lower[cursor..].find("add(") {
1229        let start = cursor + relative_start;
1230        let content_start = start + "add(".len();
1231        let Some(relative_end) = suffix[content_start..].find(')') else {
1232            break;
1233        };
1234
1235        stripped.push_str(&suffix[cursor..start]);
1236        cursor = content_start + relative_end + 1;
1237    }
1238
1239    stripped.push_str(&suffix[cursor..]);
1240    stripped
1241}
1242
1243fn parse_additions(suffix: &str) -> Vec<ChordAlteration> {
1244    let lower = suffix.to_ascii_lowercase();
1245    let mut additions = Vec::new();
1246    let mut cursor = 0;
1247
1248    while let Some(relative_start) = lower[cursor..].find("add(") {
1249        let content_start = cursor + relative_start + "add(".len();
1250        let Some(relative_end) = suffix[content_start..].find(')') else {
1251            break;
1252        };
1253        let content_end = content_start + relative_end;
1254
1255        for token in
1256            suffix[content_start..content_end].split(|ch: char| ch == ',' || ch.is_whitespace())
1257        {
1258            if let Some(addition) = parse_addition_token(token) {
1259                additions.push(addition);
1260            }
1261        }
1262
1263        cursor = content_end + 1;
1264    }
1265
1266    additions
1267}
1268
1269fn parse_omissions(suffix: &str) -> Vec<u8> {
1270    let lower = suffix.to_ascii_lowercase();
1271    let bytes = lower.as_bytes();
1272    let mut omissions = Vec::new();
1273    let mut cursor = 0;
1274
1275    while cursor < bytes.len() {
1276        let marker_len = if bytes[cursor..].starts_with(b"omit") {
1277            4
1278        } else if bytes[cursor..].starts_with(b"no") {
1279            2
1280        } else {
1281            cursor += 1;
1282            continue;
1283        };
1284
1285        cursor += marker_len;
1286        while cursor < bytes.len() && (bytes[cursor].is_ascii_whitespace() || bytes[cursor] == b'(')
1287        {
1288            cursor += 1;
1289        }
1290
1291        let degree_start = cursor;
1292        while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
1293            cursor += 1;
1294        }
1295        if degree_start == cursor {
1296            continue;
1297        }
1298
1299        if let Ok(degree) = std::str::from_utf8(&bytes[degree_start..cursor])
1300            .unwrap_or_default()
1301            .parse::<u8>()
1302            && !omissions.contains(&degree)
1303        {
1304            omissions.push(degree);
1305        }
1306    }
1307
1308    omissions
1309}
1310
1311fn parse_addition_token(token: &str) -> Option<ChordAlteration> {
1312    let token = token.trim();
1313    if token.is_empty() {
1314        return None;
1315    }
1316
1317    let (semitones, degree) = match token.as_bytes()[0] as char {
1318        '#' => (1, &token[1..]),
1319        'b' | '-' => (-1, &token[1..]),
1320        _ => (0, token),
1321    };
1322
1323    degree
1324        .parse::<u8>()
1325        .ok()
1326        .map(|degree| ChordAlteration::new(degree, semitones))
1327}
1328
1329fn parse_alterations(suffix: &str) -> Vec<ChordAlteration> {
1330    let bytes = suffix.as_bytes();
1331    let mut alterations = Vec::new();
1332    let mut idx = 0;
1333    while idx < bytes.len() {
1334        let semitones = match bytes[idx] as char {
1335            '#' => 1,
1336            'b' | '-' => -1,
1337            _ => {
1338                idx += 1;
1339                continue;
1340            }
1341        };
1342        idx += 1;
1343        let start = idx;
1344        while idx < bytes.len() && bytes[idx].is_ascii_digit() {
1345            idx += 1;
1346        }
1347        if start == idx {
1348            continue;
1349        }
1350        if let Ok(degree) = suffix[start..idx].parse::<u8>() {
1351            alterations.push(ChordAlteration::new(degree, semitones));
1352        }
1353    }
1354    alterations
1355}
1356
1357fn parse_pitch_only(value: &str) -> Result<Pitch> {
1358    let (name, rest) = parse_pitch_prefix(value)?;
1359    if !rest.is_empty() {
1360        return Err(Error::Chord(format!("invalid slash bass {value:?}")));
1361    }
1362    Pitch::from_name(name)
1363}
1364
1365fn parse_pitch_prefix(value: &str) -> Result<(String, &str)> {
1366    let mut chars = value.char_indices();
1367    let Some((_, first)) = chars.next() else {
1368        return Err(Error::Chord("missing pitch name".to_string()));
1369    };
1370
1371    if !matches!(first.to_ascii_uppercase(), 'A'..='G') {
1372        return Err(Error::Chord(format!("invalid pitch name in {value:?}")));
1373    }
1374
1375    let mut end = first.len_utf8();
1376    let mut name = first.to_ascii_uppercase().to_string();
1377    for (idx, ch) in chars {
1378        match ch {
1379            '#' => {
1380                name.push('#');
1381                end = idx + ch.len_utf8();
1382            }
1383            'b' | '-' => {
1384                name.push('-');
1385                end = idx + ch.len_utf8();
1386            }
1387            _ => break,
1388        }
1389    }
1390
1391    Ok((name, &value[end..]))
1392}
1393
1394fn default_extension_interval(degree: u8) -> &'static str {
1395    match degree {
1396        6 => "M6",
1397        9 => "M9",
1398        11 => "P11",
1399        13 => "M13",
1400        _ => "P1",
1401    }
1402}
1403
1404fn altered_interval(alteration: &ChordAlteration) -> Result<(u8, &'static str)> {
1405    match (alteration.degree, alteration.semitones) {
1406        (5, -1) => Ok((5, "d5")),
1407        (5, 1) => Ok((5, "a5")),
1408        (9, -1) => Ok((9, "m9")),
1409        (9, 1) => Ok((9, "a9")),
1410        (11, 1) => Ok((11, "a11")),
1411        (13, -1) => Ok((13, "m13")),
1412        (13, 1) => Ok((13, "a13")),
1413        _ => Err(Error::Chord(format!(
1414            "unsupported chord-symbol alteration {alteration:?}"
1415        ))),
1416    }
1417}
1418
1419fn added_interval(addition: &ChordAlteration) -> Result<(u8, &'static str)> {
1420    let degree = match addition.degree {
1421        2 => 9,
1422        4 => 11,
1423        6 => 13,
1424        degree => degree,
1425    };
1426
1427    match (degree, addition.semitones) {
1428        (3, -1) => Ok((degree, "m3")),
1429        (3, 0) => Ok((degree, "M3")),
1430        (3, 1) => Ok((degree, "a3")),
1431        (5, -1) => Ok((degree, "d5")),
1432        (5, 0) => Ok((degree, "P5")),
1433        (5, 1) => Ok((degree, "a5")),
1434        (7, -1) => Ok((degree, "m7")),
1435        (7, 0) => Ok((degree, "M7")),
1436        (9, -1) => Ok((degree, "m9")),
1437        (9, 0) => Ok((degree, "M9")),
1438        (9, 1) => Ok((degree, "a9")),
1439        (11, -1) => Ok((degree, "d11")),
1440        (11, 0) => Ok((degree, "P11")),
1441        (11, 1) => Ok((degree, "a11")),
1442        (13, -1) => Ok((degree, "m13")),
1443        (13, 0) => Ok((degree, "M13")),
1444        (13, 1) => Ok((degree, "a13")),
1445        _ => Err(Error::Chord(format!(
1446            "unsupported chord-symbol added tone {addition:?}"
1447        ))),
1448    }
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453    use super::*;
1454
1455    #[test]
1456    fn parses_major_seventh_symbol() {
1457        let symbol: ChordSymbol = "Cmaj7".parse().unwrap();
1458        assert_eq!(symbol.root().name(), "C");
1459        assert_eq!(symbol.quality(), ChordQuality::Major);
1460        assert_eq!(symbol.extensions(), &[7]);
1461        assert_eq!(
1462            symbol.to_chord().unwrap().pitched_common_name(),
1463            "C-major seventh chord"
1464        );
1465    }
1466
1467    #[test]
1468    fn parses_half_diminished_symbol() {
1469        let symbol = ChordSymbol::parse("F#m7b5").unwrap();
1470        assert_eq!(symbol.root().name(), "F#");
1471        assert_eq!(symbol.quality(), ChordQuality::HalfDiminished);
1472        assert_eq!(
1473            symbol.to_chord().unwrap().pitched_common_name(),
1474            "F#-half-diminished seventh chord"
1475        );
1476    }
1477
1478    #[test]
1479    fn parses_dominant_altered_symbol() {
1480        let symbol = ChordSymbol::parse("Bb7#11").unwrap();
1481        assert_eq!(symbol.root().name(), "B-");
1482        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1483        assert_eq!(symbol.extensions(), &[7, 11]);
1484        assert_eq!(symbol.alterations()[0], ChordAlteration::new(11, 1));
1485        let names = symbol
1486            .to_chord()
1487            .unwrap()
1488            .pitches()
1489            .iter()
1490            .map(Pitch::name)
1491            .collect::<Vec<_>>();
1492        assert_eq!(names, vec!["B-", "D", "F", "A-", "E"]);
1493    }
1494
1495    #[test]
1496    fn parses_added_tones_without_changing_the_base_chord() {
1497        let symbol = ChordSymbol::parse("Cdim9 add(#5)").unwrap();
1498        assert_eq!(symbol.extensions(), &[9]);
1499        assert_eq!(symbol.additions(), &[ChordAlteration::new(5, 1)]);
1500        assert_eq!(
1501            symbol.to_chord().unwrap().pitch_classes(),
1502            vec![0, 2, 3, 6, 8, 9]
1503        );
1504    }
1505
1506    #[test]
1507    fn parses_altered_dominant_with_slash_bass() {
1508        let symbol = ChordSymbol::parse("D7b9#11/C").unwrap();
1509        assert_eq!(symbol.root().name(), "D");
1510        assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1511        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1512        assert_eq!(symbol.extensions(), &[7, 9, 11]);
1513        assert_eq!(
1514            symbol.alterations(),
1515            &[ChordAlteration::new(9, -1), ChordAlteration::new(11, 1)]
1516        );
1517        assert_eq!(
1518            symbol.to_chord().unwrap().pitch_classes(),
1519            vec![0, 2, 3, 6, 8, 9]
1520        );
1521    }
1522
1523    #[test]
1524    fn parses_music21_pitch_name_additions() {
1525        let symbol = ChordSymbol::parse("Ddom7dim5/CaddA,E-").unwrap();
1526
1527        assert_eq!(symbol.root().name(), "D");
1528        assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1529        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1530        assert_eq!(symbol.alterations(), &[ChordAlteration::new(5, -1)]);
1531        assert_eq!(
1532            symbol.additions(),
1533            &[ChordAlteration::new(5, 0), ChordAlteration::new(9, -1)]
1534        );
1535        assert_eq!(
1536            symbol.to_chord().unwrap().pitch_classes(),
1537            vec![0, 2, 3, 6, 8, 9]
1538        );
1539    }
1540
1541    #[test]
1542    fn generates_petrushka_chord_symbol_name() {
1543        let chord = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1544        let names = chord_symbol_spellings(&chord);
1545
1546        assert_eq!(
1547            names.first().map(String::as_str),
1548            Some("Ddom7dim5/CaddA,E-")
1549        );
1550        assert!(names.iter().any(|name| name == "Ddom7dim5/CaddA,E-"));
1551    }
1552
1553    #[test]
1554    fn generates_common_chord_symbols() {
1555        let major_seventh = Chord::new("C E G B").unwrap();
1556        let dominant_ninth = Chord::new("C E G B- D").unwrap();
1557
1558        assert_eq!(
1559            chord_symbol_spellings(&major_seventh)
1560                .first()
1561                .map(String::as_str),
1562            Some("Cmaj7")
1563        );
1564        assert_eq!(
1565            chord_symbol_spellings(&dominant_ninth)
1566                .first()
1567                .map(String::as_str),
1568            Some("C9")
1569        );
1570    }
1571
1572    #[test]
1573    fn split_third_triads_do_not_spell_lower_third_as_sharp_nine() {
1574        let split_third = Chord::new("D4 A4 F#4 F4").unwrap();
1575        let names = chord_symbol_spellings(&split_third);
1576
1577        assert_eq!(names.first().map(String::as_str), Some("DaddF"));
1578        assert!(!names.iter().any(|name| name == "D add(#9)"));
1579    }
1580
1581    #[test]
1582    fn altered_dominants_use_music21_pitch_name_additions() {
1583        let altered_dominant = Chord::new("C4 E4 G4 Bb4 Eb5").unwrap();
1584        let names = chord_symbol_spellings(&altered_dominant);
1585
1586        assert_eq!(names.first().map(String::as_str), Some("C7addE-"));
1587    }
1588
1589    #[test]
1590    fn unrecognized_music21_figures_return_no_symbol() {
1591        let chord = Chord::new("F4 C5 D5 E-5").unwrap();
1592        let names = chord_symbol_spellings(&chord);
1593
1594        assert!(names.is_empty());
1595    }
1596
1597    #[test]
1598    fn generates_music21_figures_with_explicit_root() {
1599        let major_triad = Chord::new("G3 C4 E4").unwrap();
1600        let dominant_seventh = Chord::new("G3 B-3 C4 E4").unwrap();
1601        let power_chord = Chord::new("C4 G4").unwrap();
1602        let unsupported_dyad = Chord::new("C4 A4").unwrap();
1603
1604        assert_eq!(
1605            chord_symbol_spellings_with_root(&major_triad, 0)
1606                .first()
1607                .map(String::as_str),
1608            Some("C/G")
1609        );
1610        assert_eq!(
1611            chord_symbol_spellings_with_root(&dominant_seventh, 0)
1612                .first()
1613                .map(String::as_str),
1614            Some("C7/G")
1615        );
1616        assert_eq!(
1617            chord_symbol_spellings_with_root(&power_chord, 0)
1618                .first()
1619                .map(String::as_str),
1620            Some("Cpower")
1621        );
1622        assert!(chord_symbol_spellings_with_root(&unsupported_dyad, 0).is_empty());
1623    }
1624
1625    #[test]
1626    fn dense_sets_follow_music21_fallback_matching() {
1627        let chord = Chord::new("C4 D-4 E-4 E4 F#4 G4 A-4 A4").unwrap();
1628
1629        assert_eq!(
1630            chord_symbol_spellings(&chord).first().map(String::as_str),
1631            Some("CsusaddA,A-,D-,E,E-,F#omitF")
1632        );
1633    }
1634}