Skip to main content

music21/chordsymbol/
mod.rs

1use std::str::FromStr;
2
3use crate::{
4    chord::Chord,
5    chord::root::{pitch_class, step_num},
6    defaults::{FloatType, IntegerType},
7    duration::{Duration, DurationType},
8    error::{Error, Result},
9    interval::Interval,
10    pitch::Pitch,
11    stream::{Stream, StreamElement},
12};
13use std::collections::BTreeSet;
14
15mod figure;
16mod parse;
17mod realize;
18mod tables;
19
20pub use figure::{
21    ChordSymbolFigure, alter_to_chord_symbol_string, chord_symbol_figure_from_chord,
22    chord_symbol_from_chord, chord_symbol_kind_from_chord,
23};
24pub(crate) use figure::{chord_symbol_spellings, chord_symbol_spellings_with_root};
25pub use tables::{
26    CHORD_KIND_ALIASES, Music21ChordType, abbreviations_for_kind, current_abbreviation_for_kind,
27    known_chord_symbol_types, notation_for_kind, resolve_kind_alias,
28};
29
30use parse::*;
31pub use realize::{
32    ChordStepModification, ChordStepModificationType, ChordVoicing, inversion_is_valid_for_kind,
33    sound_chord_kind, sound_chord_notation, voice_chord_notation,
34};
35use tables::*;
36
37/// Tertian quality parsed from a chord symbol.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub enum ChordQuality {
41    /// Major triad or major-family sonority.
42    Major,
43    /// Minor triad or minor-family sonority.
44    Minor,
45    /// Dominant seventh-family sonority.
46    Dominant,
47    /// Diminished triad or diminished-family sonority.
48    Diminished,
49    /// Augmented triad sonority.
50    Augmented,
51    /// Half-diminished seventh-family sonority.
52    HalfDiminished,
53    /// Suspended-second sonority.
54    Suspended2,
55    /// Suspended-fourth sonority.
56    Suspended4,
57    /// Power-chord sonority containing a root and fifth.
58    Power,
59    /// A single pitch: music21's `pedal` kind.
60    Pedal,
61}
62
63/// A chord-symbol alteration such as `b5` or `#11`.
64#[derive(Clone, Debug, Eq, PartialEq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct ChordAlteration {
67    degree: u8,
68    semitones: IntegerType,
69}
70
71impl ChordAlteration {
72    /// Creates an alteration for a scale degree and semitone displacement.
73    pub fn new(degree: u8, semitones: IntegerType) -> Self {
74        Self { degree, semitones }
75    }
76
77    /// Returns the altered or added chord degree.
78    pub fn degree(&self) -> u8 {
79        self.degree
80    }
81
82    /// Returns the semitone displacement from the unaltered degree.
83    pub fn semitones(&self) -> IntegerType {
84        self.semitones
85    }
86}
87
88/// Parsed chord symbol.
89#[derive(Clone, Debug, PartialEq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91#[must_use]
92pub struct ChordSymbol {
93    figure: String,
94    root: Pitch,
95    bass: Option<Pitch>,
96    quality: ChordQuality,
97    extensions: Vec<u8>,
98    alterations: Vec<ChordAlteration>,
99    #[cfg_attr(feature = "serde", serde(default))]
100    omissions: Vec<u8>,
101    #[cfg_attr(feature = "serde", serde(default))]
102    additions: Vec<ChordAlteration>,
103    /// music21's kind, where the shorthand is one of its abbreviations.
104    #[cfg_attr(feature = "serde", serde(default))]
105    kind: Option<String>,
106    /// What music21 reads after the kind, applied in order when the symbol
107    /// is realized.
108    #[cfg_attr(feature = "serde", serde(default))]
109    modifications: Vec<ChordStepModification>,
110    /// How long the symbol holds. A symbol as written takes no time, as
111    /// music21's takes none.
112    #[cfg_attr(feature = "serde", serde(default = "no_time"))]
113    duration: Duration,
114    /// How the kind is written where a score says, as `M` for a major
115    /// triad: music21's `chordKindStr`.
116    #[cfg_attr(feature = "serde", serde(default))]
117    kind_text: crate::display::Drawn<Option<String>>,
118    #[cfg_attr(feature = "serde", serde(default))]
119    placement: crate::display::Drawn<Option<crate::notation::Placement>>,
120    /// Syllables sung to the symbol, which music21 lets any note-like
121    /// object carry.
122    #[cfg_attr(feature = "serde", serde(default))]
123    lyrics: Vec<crate::notation::Lyric>,
124    /// The pitches a transposition left, each moved on its own as music21
125    /// moves a chord's, which stand in for the ones the figure would sound.
126    #[cfg_attr(feature = "serde", serde(default))]
127    transposed: Option<Vec<Pitch>>,
128}
129
130/// The length of a symbol nobody has given one: none at all.
131fn no_time() -> Duration {
132    Duration::from_type(DurationType::Zero)
133}
134
135/// The kind a shorthand names outright, music21's `_getKindFromShortHand`:
136/// the shorthand up to its first `add`, `alter`, `omit` or `subtract`, and
137/// up to the first sharp or flat that has a digit after it, matched whole
138/// against the abbreviations. With it, how much of the shorthand it took.
139fn music21_kind(shorthand: &str) -> Option<(&'static Music21ChordType, usize)> {
140    let mut cut = shorthand.len();
141    for marker in ["add", "alter", "omit", "subtract"] {
142        if let Some(at) = shorthand.find(marker) {
143            cut = cut.min(at);
144        }
145    }
146    let mut head = &shorthand[..cut];
147    if let Some(at) = head.find('#')
148        && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
149    {
150        head = &head[..at];
151    }
152    if let Some(at) = head.find('b')
153        && at < head.len() - 1
154        && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
155        && !head.contains("ob9")
156        && !head.contains("øb9")
157    {
158        head = &head[..at];
159    }
160    MUSIC21_CHORD_TYPES
161        .iter()
162        .find(|chord_type| chord_type.abbreviations.contains(&head))
163        .map(|chord_type| (chord_type, head.len()))
164}
165
166/// What music21's `_parseFigure` reads out of the shorthand after the kind:
167/// the `add`, `alter`, `omit` and `subtract` tokens in the order written,
168/// then every sharpened or flattened degree left over, each as an addition.
169/// `None` where the leftover is not degrees at all, which music21 refuses.
170/// What Python's `int` makes of a string of digits: an optional sign and
171/// at least one digit, and nothing for anything else.
172fn python_int(text: &str) -> Option<i64> {
173    let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
174    if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
175        return None;
176    }
177    text.parse().ok()
178}
179
180fn music21_modifications(remaining: &str) -> Option<Vec<ChordStepModification>> {
181    const MARKERS: [(&str, ChordStepModificationType); 4] = [
182        ("add", ChordStepModificationType::Add),
183        ("alter", ChordStepModificationType::Alter),
184        ("omit", ChordStepModificationType::Subtract),
185        ("subtract", ChordStepModificationType::Subtract),
186    ];
187    let mut out = Vec::new();
188    let first_marker = MARKERS
189        .iter()
190        .filter_map(|(marker, _)| remaining.find(marker))
191        .min();
192    if let Some(start) = first_marker {
193        let mut text = &remaining[start..];
194        while let Some((at, marker, kind)) = MARKERS
195            .iter()
196            .filter_map(|(marker, kind)| text.find(marker).map(|at| (at, *marker, *kind)))
197            .min_by_key(|(at, _, _)| *at)
198        {
199            let mut rest = &text[at + marker.len()..];
200            let mut alter = 0;
201            while let Some(sign) = rest.chars().next().filter(|c| matches!(c, 'b' | '-' | '#')) {
202                alter += if sign == '#' { 1 } else { -1 };
203                rest = &rest[1..];
204            }
205            let digits = rest.chars().take_while(char::is_ascii_digit).count().min(2);
206            let degree_text = &rest[..digits];
207            if let Ok(degree) = degree_text.parse::<u8>() {
208                out.push(ChordStepModification::new(kind, degree, alter).ok()?);
209            }
210            text = &rest[digits..];
211        }
212    }
213
214    // The degrees before any marker, sharpened or flattened, each an
215    // addition: `b9` in `C7b9`, or `#5b9` in `C7#5b9`.
216    let before: String = remaining[..first_marker.unwrap_or(remaining.len())].replace(',', "");
217    let mut items: Vec<String> = Vec::new();
218    if before.contains(['b', '#']) {
219        let chars: Vec<char> = before.chars().collect();
220        let mut current = String::new();
221        let mut index = 0;
222        while index < chars.len() {
223            if matches!(chars[index], 'b' | '#') {
224                if !current.is_empty() {
225                    items.push(std::mem::take(&mut current));
226                }
227                let mut group = String::new();
228                while index < chars.len() && matches!(chars[index], 'b' | '#') {
229                    group.push(chars[index]);
230                    index += 1;
231                }
232                while index < chars.len() && !matches!(chars[index], 'b' | '#') {
233                    group.push(chars[index]);
234                    index += 1;
235                }
236                items.push(group);
237            } else {
238                current.push(chars[index]);
239                index += 1;
240            }
241        }
242        if !current.is_empty() {
243            items.push(current);
244        }
245    } else if !before.is_empty() {
246        items.push(before);
247    }
248
249    // What is left is degrees and nothing else: music21 hands each to
250    // Python's `int` once its sharps and flats are off, and refuses anything
251    // `int` refuses. `int` takes a sign, so `b-7` is a flat seventh.
252    let mut tokens: Vec<String> = Vec::new();
253    for item in items {
254        let unsigned: String = item.chars().filter(|c| !matches!(c, 'b' | '#')).collect();
255        let number = python_int(&unsigned)?;
256        if number > 20 {
257            // Several degrees run together, `69` or `b913`: a `1` takes the
258            // digit after it, and an accidental waits for the digit after it.
259            let chars: Vec<char> = item.chars().collect();
260            let mut prefix = String::new();
261            let mut skip = false;
262            for (index, &ch) in chars.iter().enumerate() {
263                if skip {
264                    skip = false;
265                    continue;
266                }
267                if ch == '1' {
268                    let mut token = String::from(ch);
269                    if let Some(next) = chars.get(index + 1) {
270                        token.push(*next);
271                    }
272                    tokens.push(token);
273                    skip = true;
274                } else if matches!(ch, 'b' | '#') {
275                    prefix.push(ch);
276                } else {
277                    prefix.push(ch);
278                    tokens.push(std::mem::take(&mut prefix));
279                }
280            }
281        } else {
282            tokens.push(item);
283        }
284    }
285    for token in tokens {
286        let alter = if token.contains('b') {
287            -(token.matches('b').count() as IntegerType)
288        } else {
289            token.matches('#').count() as IntegerType
290        };
291        // music21's `[1-9]+`: the first run of digits without a nought, so
292        // `10` is a one.
293        let nonzero = |c: &char| matches!(c, '1'..='9');
294        let digits: String = token
295            .chars()
296            .skip_while(|c| !nonzero(c))
297            .take_while(nonzero)
298            .collect();
299        if let Ok(degree) = digits.parse::<u8>() {
300            out.push(
301                ChordStepModification::new(ChordStepModificationType::Add, degree, alter).ok()?,
302            );
303        }
304    }
305    Some(out)
306}
307
308impl ChordSymbol {
309    /// Parses a chord symbol such as `"Cmaj7"`, `"F#m7b5"`, or `"Bb7#11"`.
310    ///
311    /// A `b` after the root's letter is read as a flat, as a lead sheet
312    /// writes one; [`Self::parse_music21`] reads a figure as music21 does.
313    pub fn parse(figure: impl Into<String>) -> Result<Self> {
314        Self::parse_with(figure.into(), false)
315    }
316
317    /// Parses a figure exactly as music21's `ChordSymbol` reads one, where
318    /// a root or a bass is a letter with only `#` and `-` after it. A `b`
319    /// is not a flat there: `Bb7` is a B major triad with a flattened
320    /// seventh added, and `Ebmaj7` is no figure at all, since `bmaj7` names
321    /// no kind.
322    ///
323    /// The root is the letter at the front, or everything before a comma;
324    /// the bass is a `/` and a letter wherever it stands, so `C/B- add 2`
325    /// adds a second to a C triad over B flat; and what is left names one
326    /// of music21's kinds and its modifications, or is a list of degrees
327    /// over no kind at all — `C35b7` is a root with a third, a fifth and a
328    /// flat seventh added to it. Anything else is refused with music21's
329    /// words.
330    pub fn parse_music21(figure: impl Into<String>) -> Result<Self> {
331        let figure = figure.into();
332        let compact: String = figure.chars().filter(|c| !c.is_whitespace()).collect();
333        let refused_root = || {
334            Error::Chord(format!(
335                "Chord {compact} does not begin with a valid root note."
336            ))
337        };
338        let (root_text, rest) = match compact.find(',') {
339            Some(at) => {
340                let root = compact[..at].to_string();
341                let rest = compact.replace(',', "").replace(root.as_str(), "");
342                (root, rest)
343            }
344            None => {
345                let mut letters = compact.char_indices();
346                match letters.next() {
347                    Some((_, first)) if "ABCDEFGabcdefg".contains(first) => {}
348                    _ => return Err(refused_root()),
349                }
350                let end = letters
351                    .find(|(_, c)| !matches!(c, '#' | '-'))
352                    .map_or(compact.len(), |(at, _)| at);
353                let root = compact[..end].to_string();
354                let rest = compact.replacen(root.as_str(), "", 1);
355                (root, rest)
356            }
357        };
358        // A root the pattern reads but no pitch spells, `C#-`, is refused by
359        // the pitch, as music21 lets its accidental error through.
360        let root = Pitch::from_name(&root_text)?;
361        // The bass is `/` and a root-shaped name wherever it stands, and it
362        // is taken out of what is left wherever it appears there.
363        let mut bass = None;
364        let mut remaining = rest.clone();
365        if let Some(slash) = compact.find('/') {
366            let after = &compact[slash + 1..];
367            let mut letters = after.char_indices();
368            if let Some((_, first)) = letters.next()
369                && "ABCDEFGabcdefg".contains(first)
370            {
371                let end = letters
372                    .find(|(_, c)| !matches!(c, '#' | '-'))
373                    .map_or(after.len(), |(at, _)| at);
374                let written = &after[..end];
375                bass = Some(Pitch::from_name(written)?);
376                remaining = rest.replace(&format!("/{written}"), "");
377            }
378        }
379        let (kind, taken) = match music21_kind(&remaining) {
380            Some((chord_type, taken)) => (chord_type.kind.to_string(), taken),
381            None => (String::new(), 0),
382        };
383        let refused = || {
384            let mut said = remaining[taken..].replace(',', "");
385            for marker in ["add", "alter", "omit", "subtract"] {
386                if let Some(at) = said.find(marker) {
387                    said.truncate(at);
388                }
389            }
390            Error::Chord(format!(
391                "Invalid chord abbreviation '{said}'; see music21.harmony.CHORD_TYPES for valid abbreviations or specify all alterations."
392            ))
393        };
394        let modifications = music21_modifications(&remaining[taken..]).ok_or_else(refused)?;
395        // The crate's own reading of the letters, which says nothing about
396        // the root and so is read over C; music21 carries no such reading
397        // and sounds only what the kind and its modifications say.
398        let own = Self::parse_with(format!("C{remaining}"), false).ok();
399        Ok(Self {
400            figure: figure.trim().to_string(),
401            root,
402            bass,
403            quality: own.as_ref().map_or(ChordQuality::Major, |own| own.quality),
404            extensions: own
405                .as_ref()
406                .map(|own| own.extensions.clone())
407                .unwrap_or_default(),
408            alterations: own.map(|own| own.alterations).unwrap_or_default(),
409            omissions: Vec::new(),
410            additions: Vec::new(),
411            kind: Some(kind),
412            modifications,
413            duration: no_time(),
414            kind_text: crate::display::Drawn(None),
415            placement: crate::display::Drawn(None),
416            lyrics: Vec::new(),
417            transposed: None,
418        })
419    }
420
421    fn parse_with(figure: String, music21: bool) -> Result<Self> {
422        let trimmed = figure.trim();
423        if trimmed.is_empty() {
424            return Err(Error::Chord("chord symbol cannot be empty".to_string()));
425        }
426        // music21 drops every space before reading a figure, so `C7 omit 3`
427        // is `C7omit3`.
428        let compact: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
429
430        let (body, bass_segment) = match compact.split_once('/') {
431            Some((body, bass)) => (body, Some(bass)),
432            None => (compact.as_str(), None),
433        };
434        let body_parts = split_music21_pitch_modifiers(body);
435        let bass_parts = bass_segment.map(split_music21_pitch_modifiers);
436        let bass = bass_parts
437            .as_ref()
438            .map(|parts| parse_pitch_only(&parts.base, music21))
439            .transpose()?;
440
441        let (root_name, suffix) = parse_pitch_prefix(&body_parts.base, music21)?;
442        let root = Pitch::from_name(root_name)?;
443        let suffix_without_additions = strip_addition_groups(suffix);
444        let mut additions = parse_additions(suffix);
445        let mut omissions = parse_omissions(suffix);
446        for pitch_name in body_parts
447            .additions
448            .iter()
449            .chain(bass_parts.iter().flat_map(|parts| parts.additions.iter()))
450        {
451            if let Some(addition) = pitch_name_addition(&root, pitch_name) {
452                additions.push(addition);
453            }
454        }
455        for pitch_name in body_parts
456            .omissions
457            .iter()
458            .chain(bass_parts.iter().flat_map(|parts| parts.omissions.iter()))
459        {
460            if let Some(omission) = pitch_name_degree(&root, pitch_name)
461                && !omissions.contains(&omission)
462            {
463                omissions.push(omission);
464            }
465        }
466
467        let mut alterations = parse_alterations(&suffix_without_additions);
468        add_implicit_music21_alterations(&suffix_without_additions, &mut alterations);
469        let extensions = parse_extensions(&suffix_without_additions, &alterations);
470        let quality = parse_quality(&suffix_without_additions, &alterations);
471        // music21's `_getKindFromShortHand` names the kind outright, and
472        // what it leaves is read as music21 reads it; a leftover that is not
473        // degrees is a figure music21 refuses, which the crate then reads
474        // its own way.
475        // The whole of the figure after the root, `add` and `omit` included,
476        // which the pitch-modifier split above took off `suffix`.
477        let full_suffix = &body[body_parts.base.len() - suffix.len()..];
478        let (kind, modifications) = match music21_kind(full_suffix) {
479            Some((chord_type, taken)) => match music21_modifications(&full_suffix[taken..]) {
480                Some(modifications) => (Some(chord_type.kind.to_string()), modifications),
481                None => (None, Vec::new()),
482            },
483            None => (None, Vec::new()),
484        };
485
486        Ok(Self {
487            figure: trimmed.to_string(),
488            root,
489            bass,
490            quality,
491            extensions,
492            alterations,
493            omissions,
494            additions,
495            kind,
496            modifications,
497            duration: no_time(),
498            kind_text: crate::display::Drawn(None),
499            placement: crate::display::Drawn(None),
500            lyrics: Vec::new(),
501            transposed: None,
502        })
503    }
504
505    /// A symbol saying nothing sounds, written `N.C.` unless `text` says
506    /// otherwise: music21's `NoChord`, whose kind is `none`. It has a root
507    /// only because every symbol has one; it sounds no pitches at all.
508    pub fn no_chord(text: Option<String>) -> Self {
509        Self {
510            figure: "N.C.".to_string(),
511            root: Pitch::from_name("C").expect("C is a pitch name"),
512            bass: None,
513            quality: ChordQuality::Major,
514            extensions: Vec::new(),
515            alterations: Vec::new(),
516            omissions: Vec::new(),
517            additions: Vec::new(),
518            kind: Some("none".to_string()),
519            modifications: Vec::new(),
520            duration: no_time(),
521            kind_text: crate::display::Drawn(Some(text.unwrap_or_else(|| "N.C.".to_string()))),
522            placement: crate::display::Drawn(None),
523            lyrics: Vec::new(),
524            transposed: None,
525        }
526    }
527
528    /// The syllables sung to the symbol, one per verse.
529    pub fn lyrics(&self) -> &[crate::notation::Lyric] {
530        &self.lyrics
531    }
532
533    /// The same, for editing in place.
534    pub fn lyrics_mut(&mut self) -> &mut Vec<crate::notation::Lyric> {
535        &mut self.lyrics
536    }
537
538    /// Adds a syllable as a verse, as [`crate::note::Note::add_lyric`] does:
539    /// music21's `addLyric`.
540    pub fn add_lyric(&mut self, text: &str, number: Option<IntegerType>, apply_raw: bool) {
541        crate::notation::verses::add(&mut self.lyrics, text, number, apply_raw);
542    }
543
544    /// Whether this says nothing sounds: music21's `NoChord`.
545    pub fn is_no_chord(&self) -> bool {
546        self.kind.as_deref() == Some("none")
547    }
548
549    /// How the kind is written where a score says, as `M` for a major
550    /// triad: music21's `chordKindStr`.
551    pub fn kind_text(&self) -> Option<&str> {
552        self.kind_text.0.as_deref()
553    }
554
555    /// Says how the kind is written.
556    pub fn set_kind_text(&mut self, text: Option<String>) {
557        self.kind_text.0 = text.filter(|text| !text.is_empty());
558    }
559
560    /// Which side of the staff the symbol is written on, where a score says.
561    pub fn placement(&self) -> Option<crate::notation::Placement> {
562        self.placement.0
563    }
564
565    /// Says which side of the staff the symbol is written on.
566    pub fn set_placement(&mut self, placement: Option<crate::notation::Placement>) {
567        self.placement.0 = placement;
568    }
569
570    /// How long the symbol holds. A symbol as written takes no time, which
571    /// is music21's default; [`realize_chord_symbol_durations`] gives each
572    /// one in a stream the time until the next.
573    pub fn duration(&self) -> &Duration {
574        &self.duration
575    }
576
577    /// Says how long the symbol holds.
578    pub fn set_duration(&mut self, duration: Duration) {
579        self.duration = duration;
580    }
581
582    /// The same symbol holding for a given time.
583    pub fn with_duration(mut self, duration: Duration) -> Self {
584        self.duration = duration;
585        self
586    }
587
588    /// music21's `chordKind`: the kind the shorthand names, where it is one
589    /// of music21's abbreviations, so `Cmaj7` is `major-seventh` and `CN6`
590    /// the Neapolitan. `None` for a shorthand the crate reads on its own.
591    pub fn kind(&self) -> Option<&str> {
592        self.kind.as_deref()
593    }
594
595    /// Returns the original chord-symbol figure.
596    pub fn figure(&self) -> &str {
597        &self.figure
598    }
599
600    /// Returns the root pitch.
601    pub fn root(&self) -> &Pitch {
602        &self.root
603    }
604
605    /// Returns the slash bass pitch, if one was supplied.
606    pub fn bass(&self) -> Option<&Pitch> {
607        self.bass.as_ref()
608    }
609
610    /// Returns the parsed chord quality.
611    pub fn quality(&self) -> ChordQuality {
612        self.quality
613    }
614
615    /// Returns parsed extension degrees.
616    pub fn extensions(&self) -> &[u8] {
617        &self.extensions
618    }
619
620    /// Returns parsed alterations.
621    pub fn alterations(&self) -> &[ChordAlteration] {
622        &self.alterations
623    }
624
625    /// Returns degrees omitted with `no...` or `omit...` markers.
626    pub fn omissions(&self) -> &[u8] {
627        &self.omissions
628    }
629
630    /// Returns parsed added tones from `add(...)` groups.
631    pub fn additions(&self) -> &[ChordAlteration] {
632        &self.additions
633    }
634
635    /// Rebuilds the figure from the parsed parts in one canonical spelling:
636    /// music21's `findFigure`. Whatever was typed, a half-diminished seventh
637    /// comes back as `m7b5`, an augmented triad as `aug`, an added ninth as
638    /// `add(9)`, and the result parses back to the same chord.
639    pub fn find_figure(&self) -> String {
640        let mut figure = self.root.name();
641        figure.push_str(&self.quality_and_extension());
642        for alteration in &self.alterations {
643            if alteration.degree == 5 && self.fifth_is_implied() {
644                continue;
645            }
646            figure.push_str(&alteration_text(alteration));
647        }
648        for addition in &self.additions {
649            figure.push_str(&format!("add({})", alteration_text(addition)));
650        }
651        for omission in &self.omissions {
652            figure.push_str(&format!("no{omission}"));
653        }
654        if let Some(bass) = &self.bass
655            && bass.name() != self.root.name()
656        {
657            figure.push('/');
658            figure.push_str(&bass.name());
659        }
660        figure
661    }
662
663    /// The same chord on a transposed root and bass, its figure rebuilt by
664    /// [`Self::find_figure`]: music21's `transpose`, so `B-/D` up a major
665    /// second is `C/E`.
666    pub fn transpose(&self, interval: &Interval) -> Result<ChordSymbol> {
667        let mut transposed = self.clone();
668        // music21 moves a chord's pitches one by one; a symbol sounding
669        // nothing it leaves be.
670        if !self.is_no_chord() {
671            let moved = self
672                .pitches()?
673                .iter()
674                .map(|pitch| interval.transpose_pitch(pitch))
675                .collect::<Result<Vec<_>>>()?;
676            transposed.transposed = Some(moved);
677        }
678        transposed.root = self.root.transpose(interval)?;
679        transposed.bass = self
680            .bass
681            .as_ref()
682            .map(|bass| bass.transpose(interval))
683            .transpose()?;
684        transposed.figure = transposed.find_figure();
685        Ok(transposed)
686    }
687
688    /// Whether the chord can stand in the given inversion: music21's
689    /// `inversionIsValid`, which reads the kind — first and second
690    /// inversions for anything but a pedal, a third for the sevenths and
691    /// the stacked kinds above them, a fourth for the ninths and up, a fifth
692    /// for the elevenths and thirteenths. A shorthand naming no kind of
693    /// music21's is read by the extensions the crate found in it. Root
694    /// position is not an inversion.
695    pub fn inversion_is_valid(&self, inversion: u8) -> bool {
696        if let Some(kind) = &self.kind {
697            return realize::inversion_is_valid_for_kind(kind, inversion);
698        }
699        let highest = self
700            .extensions
701            .iter()
702            .copied()
703            .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
704            .max()
705            .unwrap_or(5);
706        match inversion {
707            1 | 2 => true,
708            3 => highest >= 7,
709            4 => highest >= 9,
710            5 => highest >= 11,
711            _ => false,
712        }
713    }
714
715    /// The degrees added to, taken from or altered in the kind: music21's
716    /// `chordStepModifications`, read off the figure or handed over.
717    pub fn chord_step_modifications(&self) -> &[ChordStepModification] {
718        &self.modifications
719    }
720
721    /// Adds a chord-step modification, which the next realization applies:
722    /// music21's `addChordStepModification`. One already there is not added
723    /// twice.
724    pub fn add_chord_step_modification(&mut self, modification: ChordStepModification) {
725        if !self.modifications.contains(&modification) {
726            self.modifications.push(modification);
727        }
728        self.transposed = None;
729    }
730
731    /// Replaces every chord-step modification at once.
732    pub fn set_chord_step_modifications(&mut self, modifications: Vec<ChordStepModification>) {
733        self.modifications = modifications;
734        self.transposed = None;
735    }
736
737    /// A symbol given as MusicXML gives one: a root, one of music21's chord
738    /// kinds, and a bass where it is not the root, rather than a figure.
739    /// The figure is written as music21 would write that kind.
740    pub fn from_kind(root: Pitch, kind: &str, bass: Option<Pitch>) -> Result<Self> {
741        let abbreviation = current_abbreviation_for_kind(kind)
742            .ok_or_else(|| Error::Chord(format!("no such chord kind: {kind}")))?;
743        let mut figure = format!("{}{abbreviation}", root.name());
744        if let Some(bass) = bass.as_ref().filter(|bass| bass.name() != root.name()) {
745            figure.push('/');
746            figure.push_str(&bass.name());
747        }
748        let mut symbol = Self::parse(&figure)?;
749        symbol.kind = Some(kind.to_string());
750        symbol.modifications.clear();
751        symbol.root = root;
752        if bass.is_some() {
753            symbol.bass = bass;
754        }
755        Ok(symbol)
756    }
757
758    /// The same symbol as a chord of another kind, with no modifications:
759    /// a kind music21's table has been given while a program runs, which
760    /// the crate's own table has not got.
761    pub fn with_kind(mut self, kind: &str) -> Self {
762        self.kind = Some(kind.to_string());
763        self.modifications.clear();
764        self.transposed = None;
765        self
766    }
767
768    /// Sets the root the symbol is sounded on.
769    pub fn set_root(&mut self, root: Pitch) {
770        self.root = root;
771        self.transposed = None;
772    }
773
774    /// Sets the bass, or takes it away with `None`.
775    pub fn set_bass(&mut self, bass: Option<Pitch>) {
776        self.bass = bass;
777        self.transposed = None;
778    }
779
780    fn fifth_is_implied(&self) -> bool {
781        matches!(
782            self.quality,
783            ChordQuality::Diminished | ChordQuality::Augmented | ChordQuality::HalfDiminished
784        )
785    }
786
787    /// The quality with the extension it is written with: the highest
788    /// extension that no alteration accounts for, so `E7#9` keeps its `7`.
789    fn quality_and_extension(&self) -> String {
790        let seventh_family = self
791            .extensions
792            .iter()
793            .copied()
794            .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
795            .filter(|degree| {
796                !self
797                    .alterations
798                    .iter()
799                    .any(|alteration| alteration.degree == *degree)
800            })
801            .max()
802            .or_else(|| {
803                self.extensions
804                    .iter()
805                    .any(|degree| matches!(degree, 7 | 9 | 11 | 13))
806                    .then_some(7)
807            });
808        let sixth = self.extensions.contains(&6);
809        let extension = |written: &str| -> String {
810            match seventh_family {
811                Some(degree) => format!("{written}{degree}"),
812                None if sixth => format!("{written}6"),
813                None => written.to_string(),
814            }
815        };
816        match self.quality {
817            ChordQuality::Major => match seventh_family {
818                Some(degree) => format!("maj{degree}"),
819                None if sixth => "6".to_string(),
820                None => String::new(),
821            },
822            ChordQuality::Minor => extension("m"),
823            ChordQuality::Dominant => seventh_family.unwrap_or(7).to_string(),
824            ChordQuality::Diminished => extension("dim"),
825            ChordQuality::Augmented => extension("aug"),
826            ChordQuality::HalfDiminished => format!("m{}b5", seventh_family.unwrap_or(7)),
827            ChordQuality::Suspended2 => match seventh_family {
828                Some(degree) => format!("{degree}sus2"),
829                None => "sus2".to_string(),
830            },
831            ChordQuality::Suspended4 => match seventh_family {
832                Some(degree) => format!("{degree}sus4"),
833                None => "sus4".to_string(),
834            },
835            ChordQuality::Power => "5".to_string(),
836            ChordQuality::Pedal => "pedal".to_string(),
837        }
838    }
839
840    /// Realizes the symbol as a chord: [`Self::pitches`], sounding
841    /// together.
842    pub fn to_chord(&self) -> Result<Chord> {
843        Chord::new(self.pitches()?.as_slice())
844    }
845
846    /// The pitches a transposition left the symbol sounding, each moved on
847    /// its own, where it has been transposed; [`ChordSymbol::pitches`]
848    /// answers them.
849    pub fn transposed_pitches(&self) -> Option<&[Pitch]> {
850        self.transposed.as_deref()
851    }
852
853    /// The pitches the symbol sounds, with their octaves, lowest first:
854    /// music21's `ChordSymbol._updatePitches`.
855    ///
856    /// The root is taken from octave three and the kind laid out above it; a
857    /// ninth, eleventh or thirteenth has its upper notes lifted an octave; a
858    /// bass the kind can invert onto is put under the chord by raising the
859    /// notes below it, and one it cannot is added an octave under the root;
860    /// the chord-step modifications are applied; and the whole is moved down
861    /// until nothing is above the D over middle C, or up until nothing is
862    /// below the piano's lowest A. So `C/E` is `E3 G3 C4`, `C11` is `C2 E2 G2
863    /// Bb2 D3 F3`, and `Gm/F#` is `F#2 G3 Bb3 D4`.
864    ///
865    /// A shorthand naming none of music21's kinds is laid out the same way
866    /// from the crate's own reading of it.
867    pub fn pitches(&self) -> Result<Vec<Pitch>> {
868        if self.is_no_chord() {
869            return Ok(Vec::new());
870        }
871        if let Some(moved) = &self.transposed {
872            return Ok(moved.clone());
873        }
874        // A kind music21's table has no notation for -- one a MusicXML file
875        // names, as `minor-major` -- sounds as the empty kind does: the root,
876        // and the bass where it is another note.
877        if self.kind.as_deref() == Some("")
878            || (self.kind.is_some() && self.kind_notation().is_none())
879        {
880            return realize::sound_chord_kind(
881                &self.root,
882                self.kind.as_deref().unwrap_or(""),
883                self.bass.as_ref(),
884                &self.modifications,
885            );
886        }
887        match self.kind_notation() {
888            Some(notation) => {
889                let mut realized = realize::realize(
890                    &self.root,
891                    self.bass.as_ref(),
892                    self.kind.as_deref(),
893                    Some(notation),
894                    None,
895                    &self.modifications,
896                )?;
897                self.apply_own_reading(&mut realized.pitches, notation)?;
898                Ok(realized.pitches)
899            }
900            None => {
901                let mut intervals = self.spelled_intervals()?;
902                intervals.sort_unstable_by_key(|(degree, _)| *degree);
903                intervals.dedup();
904                let names = intervals
905                    .into_iter()
906                    .map(|(_, name)| {
907                        Ok(Interval::from_name(&name)?
908                            .transpose_pitch(&self.root)?
909                            .name())
910                    })
911                    .collect::<Result<Vec<_>>>()?;
912                Ok(
913                    realize::realize(&self.root, self.bass.as_ref(), None, None, Some(names), &[])?
914                        .pitches,
915                )
916            }
917        }
918    }
919
920    /// What the crate reads beyond music21 over a kind: degrees omitted with
921    /// `no`, and degrees added in `add(...)` groups or by naming a pitch,
922    /// which its own figure writer produces.
923    fn apply_own_reading(&self, pitches: &mut Vec<Pitch>, notation: &str) -> Result<()> {
924        if !self.omissions.is_empty() {
925            for (degree, name) in notation_intervals(notation)? {
926                if !self.omissions.contains(&degree) {
927                    continue;
928                }
929                let omitted = Interval::from_name(&name)?
930                    .transpose_pitch(&self.root)?
931                    .name();
932                pitches.retain(|pitch| pitch.name() != omitted);
933            }
934        }
935        for addition in &self.additions {
936            let (_, name) = added_interval(addition)?;
937            let pitch = Interval::from_name(name)?.transpose_pitch(&self.root)?;
938            if pitches
939                .iter()
940                .any(|existing| existing.name() == pitch.name())
941            {
942                continue;
943            }
944            // Over the chord, where an added tone is written.
945            let mut placed = pitch;
946            let top = pitches.last().map_or(0.0, Pitch::ps);
947            placed.set_octave(pitches.last().and_then(Pitch::octave));
948            while placed.ps() <= top {
949                let octave = placed.octave().unwrap_or(3);
950                placed.set_octave(Some(octave + 1));
951            }
952            pitches.push(placed);
953        }
954        Ok(())
955    }
956
957    /// The notation the symbol is realized from when its shorthand names one
958    /// of music21's kinds outright: `N6` is the Neapolitan whatever the
959    /// crate's own reading of the letters would be.
960    fn kind_notation(&self) -> Option<&'static str> {
961        notation_for_kind(self.kind.as_deref()?)
962    }
963
964    /// The intervals the symbol's quality, extensions, alterations and
965    /// additions spell, for a shorthand that names no kind outright.
966    fn spelled_intervals(&self) -> Result<Vec<(u8, String)>> {
967        let mut intervals = self.base_intervals();
968
969        let highest = self
970            .extensions
971            .iter()
972            .copied()
973            .filter(|degree| !self.alterations.iter().any(|alt| alt.degree == *degree))
974            .max()
975            .unwrap_or(0);
976        for extension in [6, 9, 11, 13] {
977            let implied = match extension {
978                9 => highest >= 11,
979                11 => highest == 13,
980                _ => false,
981            };
982            if (self.extensions.contains(&extension) || implied)
983                && !self.alterations.iter().any(|alt| alt.degree == extension)
984                && !self.omissions.contains(&extension)
985            {
986                intervals.push((extension, default_extension_interval(extension)));
987            }
988        }
989
990        for alteration in &self.alterations {
991            if alteration.degree == 5 {
992                continue;
993            }
994            let altered = altered_interval(alteration)?;
995            // An altered degree stands in place of the one the chord's own
996            // quality would give, rather than sounding beside it: the seventh
997            // of `F#mM7` is `E#` alone, not `E` and `E#` together.
998            intervals.retain(|(degree, _)| *degree != altered.0);
999            intervals.push(altered);
1000        }
1001
1002        for addition in &self.additions {
1003            intervals.push(added_interval(addition)?);
1004        }
1005
1006        Ok(intervals
1007            .into_iter()
1008            .map(|(degree, name)| (degree, name.to_string()))
1009            .collect())
1010    }
1011
1012    fn base_intervals(&self) -> Vec<(u8, &'static str)> {
1013        let altered_fifth = self
1014            .alterations
1015            .iter()
1016            .find(|alteration| alteration.degree == 5)
1017            .and_then(|alteration| match alteration.semitones {
1018                -1 => Some("d5"),
1019                1 => Some("a5"),
1020                _ => None,
1021            });
1022
1023        let fifth = altered_fifth.unwrap_or("P5");
1024        let has_seventh = self
1025            .extensions
1026            .iter()
1027            .any(|degree| matches!(degree, 7 | 9 | 11 | 13));
1028
1029        let intervals = match self.quality {
1030            ChordQuality::Major => {
1031                if has_seventh {
1032                    vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "M7")]
1033                } else {
1034                    vec![(1, "P1"), (3, "M3"), (5, fifth)]
1035                }
1036            }
1037            ChordQuality::Minor => {
1038                if has_seventh {
1039                    vec![(1, "P1"), (3, "m3"), (5, fifth), (7, "m7")]
1040                } else {
1041                    vec![(1, "P1"), (3, "m3"), (5, fifth)]
1042                }
1043            }
1044            ChordQuality::Dominant => vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "m7")],
1045            ChordQuality::Diminished => {
1046                if has_seventh {
1047                    vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "d7")]
1048                } else {
1049                    vec![(1, "P1"), (3, "m3"), (5, "d5")]
1050                }
1051            }
1052            ChordQuality::Augmented => vec![(1, "P1"), (3, "M3"), (5, "a5")],
1053            ChordQuality::HalfDiminished => vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "m7")],
1054            ChordQuality::Suspended2 => vec![(1, "P1"), (2, "M2"), (5, fifth)],
1055            ChordQuality::Suspended4 => vec![(1, "P1"), (4, "P4"), (5, fifth)],
1056            ChordQuality::Power => vec![(1, "P1"), (5, fifth)],
1057            ChordQuality::Pedal => vec![(1, "P1")],
1058        };
1059
1060        intervals
1061            .into_iter()
1062            .filter(|(degree, _)| !self.omissions.contains(degree))
1063            .collect()
1064    }
1065}
1066
1067/// Gives every chord symbol in a stream the time it holds for: music21's
1068/// `realizeChordSymbolDurations`.
1069///
1070/// A symbol holds until the next one, wherever in the nesting that is, and
1071/// the last holds to the end of the stream -- so a single symbol over many
1072/// notes holds for all of them. A stream with no symbols is left alone.
1073///
1074/// music21 hands back the flattened stream, its symbols being the same
1075/// objects as the score's. A stream here owns what it holds, so the symbols
1076/// are changed where they sit and [`Stream::flatten`] is there for a caller
1077/// who wants the flat reading.
1078///
1079/// ```
1080/// use music21::{ChordSymbol, Note, Stream, realize_chord_symbol_durations};
1081///
1082/// let mut stream = Stream::new();
1083/// stream.insert(0.0, ChordSymbol::parse("C")?);
1084/// stream.insert(2.0, ChordSymbol::parse("G7")?);
1085/// for beat in 0..8 {
1086///     stream.insert(f64::from(beat), Note::from_name("C4")?);
1087/// }
1088/// realize_chord_symbol_durations(&mut stream);
1089/// let held: Vec<f64> = stream
1090///     .events()
1091///     .iter()
1092///     .filter(|event| matches!(event.element(), music21::StreamElement::ChordSymbol(_)))
1093///     .map(|event| event.element().quarter_length())
1094///     .collect();
1095/// assert_eq!(held, [2.0, 6.0]);
1096/// # Ok::<(), music21::Error>(())
1097/// ```
1098pub fn realize_chord_symbol_durations(stream: &mut Stream) {
1099    let lengths = chord_symbol_durations(stream);
1100    let mut position = 0;
1101    stream.for_each_mut(&mut |_, element| {
1102        if let StreamElement::ChordSymbol(symbol) = element
1103            && let Some((_, length)) = lengths.iter().find(|(at, _)| *at == position)
1104            && let Ok(duration) = Duration::new(*length)
1105        {
1106            symbol.set_duration(duration);
1107        }
1108        position += 1;
1109    });
1110}
1111
1112/// How long each chord symbol of a stream is held: its position in
1113/// [`Stream::leaves`] and the quarter length from it to the next symbol, the
1114/// last to the end of the stream. This is the reading
1115/// [`realize_chord_symbol_durations`] writes, for a caller that keeps
1116/// something of its own beside each element.
1117///
1118/// ```
1119/// use music21::{chordsymbol::chord_symbol_durations, ChordSymbol, Note, Stream};
1120///
1121/// let mut stream = Stream::new();
1122/// stream.insert(0.0, ChordSymbol::parse("C")?);
1123/// stream.insert(0.0, Note::from_name("C4")?);
1124/// stream.insert(3.0, ChordSymbol::parse("G7")?);
1125/// stream.insert(3.0, Note::from_name("B3")?);
1126/// assert_eq!(chord_symbol_durations(&stream), [(0, 3.0), (2, 1.0)]);
1127/// # Ok::<(), music21::Error>(())
1128/// ```
1129pub fn chord_symbol_durations(stream: &Stream) -> Vec<(usize, FloatType)> {
1130    let end = stream.end_offset();
1131    let mut symbols: Vec<(FloatType, usize)> = stream
1132        .leaves()
1133        .iter()
1134        .enumerate()
1135        .filter(|(_, (_, element))| matches!(element, StreamElement::ChordSymbol(_)))
1136        .map(|(position, (offset, _))| (*offset, position))
1137        .collect();
1138    // Stable, so symbols at one offset keep the order the stream gives them.
1139    symbols.sort_by(|left, right| left.0.total_cmp(&right.0));
1140    symbols
1141        .iter()
1142        .enumerate()
1143        .map(|(index, &(offset, position))| {
1144            let until = symbols.get(index + 1).map_or(end, |next| next.0);
1145            (position, (until - offset).max(0.0))
1146        })
1147        .collect()
1148}
1149
1150impl FromStr for ChordSymbol {
1151    type Err = Error;
1152
1153    fn from_str(value: &str) -> Result<Self> {
1154        Self::parse(value)
1155    }
1156}
1157
1158impl TryFrom<&str> for ChordSymbol {
1159    type Error = Error;
1160
1161    fn try_from(value: &str) -> Result<Self> {
1162        Self::parse(value)
1163    }
1164}
1165
1166impl TryFrom<String> for ChordSymbol {
1167    type Error = Error;
1168
1169    fn try_from(value: String) -> Result<Self> {
1170        Self::parse(value)
1171    }
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176
1177    /// music21 transposes a symbol as it does any chord, moving each of its
1178    /// pitches on its own: by a count of semitones each is spelled afresh
1179    /// where it lands, so B major down an octave sounds `B1 Eb2 F#2`.
1180    #[test]
1181    fn a_transposed_symbol_moves_each_pitch() {
1182        let symbol = ChordSymbol::parse_music21("B").unwrap();
1183        let interval = crate::interval::Interval::from_semitones(-12).unwrap();
1184        let moved = symbol.transpose(&interval).unwrap();
1185        let names: Vec<String> = moved
1186            .pitches()
1187            .unwrap()
1188            .iter()
1189            .map(Pitch::name_with_octave)
1190            .collect();
1191        assert_eq!(names, ["B1", "Eb2", "F#2"]);
1192    }
1193
1194    /// music21 puts its root and bass objects themselves in the list it
1195    /// voices, so an octave pass over the list moves them too, once for
1196    /// every place they hold: a bass added twice is one note, twice. Its
1197    /// voicings, as it prints them.
1198    #[test]
1199    fn the_voicing_keeps_music21s_root_and_bass() {
1200        let voiced = |figure: &str| {
1201            let symbol = ChordSymbol::parse_music21(figure).unwrap();
1202            let kind = symbol.kind().unwrap_or_default();
1203            voice_chord_notation(
1204                symbol.root(),
1205                kind,
1206                notation_for_kind(resolve_kind_alias(kind)),
1207                symbol.bass(),
1208                symbol.chord_step_modifications(),
1209            )
1210            .unwrap()
1211        };
1212        let a = voiced("A10/C");
1213        assert_eq!(a.root().name_with_octave(), "A3");
1214        assert_eq!(a.bass().name_with_octave(), "C2");
1215        let names: Vec<String> = a.pitches().iter().map(Pitch::name_with_octave).collect();
1216        assert_eq!(names, ["C2", "A2", "A3"]);
1217        assert_eq!(voiced("Ab10/F#").root().name_with_octave(), "A3");
1218
1219        // An added flat one takes the root out and puts a new note in; the
1220        // bass, added before, keeps its place and moves with the chord.
1221        let flat_one = voiced("Bb10/B-");
1222        assert_eq!(flat_one.bass().name_with_octave(), "Bb1");
1223        assert_eq!(voiced("Ab10/A-").bass().name_with_octave(), "Ab1");
1224    }
1225
1226    #[test]
1227    fn a_slash_bass_is_voiced_as_music21_voices_it() {
1228        let cases: [(&str, &[&str]); 20] = [
1229            ("B-5/E", &["E2", "E2", "Bb3", "F4"]),
1230            ("B-69/E", &["E2", "E2", "Bb3", "G4", "C5"]),
1231            ("C5/G", &["G3", "G3", "C4"]),
1232            ("C5/D", &["D2", "D2", "C3", "G3"]),
1233            ("C69/D", &["D2", "D2", "C3", "A3", "D4"]),
1234            ("C5/C#", &["C#2", "C#2", "C3", "G3"]),
1235            ("F5/E", &["E2", "E2", "F3", "C4"]),
1236            ("A-10/E", &["E3", "Ab3", "Ab3"]),
1237            ("A#10/E", &["E3", "A#3", "A#3"]),
1238            ("C10/G", &["C3", "G3", "C4"]),
1239            ("C35/B", &["B2", "B2", "C3", "E3", "G3"]),
1240            ("C/B-", &["Bb2", "C3", "E3", "G3"]),
1241            ("C7/D", &["D2", "C3", "E3", "G3", "Bb3"]),
1242            ("Cm/E-", &["Eb3", "G3", "C4"]),
1243            ("C9/E", &["E3", "G3", "Bb3", "C4", "D4"]),
1244            ("C13/B-", &["Bb2", "C3", "D3", "E3", "F3", "G3", "A3"]),
1245            ("Cadd9/D", &["D2", "C3", "E3", "G3", "D4"]),
1246            ("C6/D", &["D2", "C3", "E3", "G3", "A3"]),
1247            ("C69/E", &["E3", "A3", "C4", "D4"]),
1248            ("C5/E", &["E3", "G3", "C4"]),
1249        ];
1250        for (figure, voiced) in cases {
1251            let symbol = ChordSymbol::parse_music21(figure).unwrap();
1252            let names: Vec<String> = symbol
1253                .pitches()
1254                .unwrap()
1255                .iter()
1256                .map(Pitch::name_with_octave)
1257                .collect();
1258            assert_eq!(names, voiced, "{figure}");
1259        }
1260    }
1261
1262    /// music21 strips a degree's sharps and flats and hands what is left to
1263    /// Python's `int`, which takes a sign; the degree is then the first run
1264    /// of the digits one to nine. Its answers, as (degree, semitones) adds.
1265    #[test]
1266    fn degrees_are_read_as_music21_reads_them() {
1267        type Adds = &'static [(u8, IntegerType)];
1268        let cases: [(&str, &str, Adds); 8] = [
1269            ("Cb-7", "", &[(7, -1)]),
1270            ("Cb+7", "", &[(7, -1)]),
1271            ("C7b-9", "", &[(7, 0), (9, -1)]),
1272            ("C9b-13", "", &[(9, 0), (13, -1)]),
1273            ("C10", "", &[(1, 0)]),
1274            ("C#10", "", &[(1, 0)]),
1275            ("Cb10", "major", &[(1, -1)]),
1276            ("Abb7", "", &[(7, -2)]),
1277        ];
1278        for (figure, kind, adds) in cases {
1279            let symbol = ChordSymbol::parse_music21(figure).unwrap();
1280            assert_eq!(symbol.kind(), Some(kind), "{figure}");
1281            let read: Vec<(u8, IntegerType)> = symbol
1282                .chord_step_modifications()
1283                .iter()
1284                .map(|modification| {
1285                    let semitones = modification.interval().semitones() as IntegerType;
1286                    (modification.degree(), semitones)
1287                })
1288                .collect();
1289            assert_eq!(read, adds, "{figure}");
1290        }
1291    }
1292
1293    /// An `add` or `omit` takes any run of `b`, `-` and `#` before its
1294    /// degree, as music21 reads one.
1295    #[test]
1296    fn a_modification_takes_every_accidental_before_its_degree() {
1297        for (figure, semitones) in [("G7addb9", -1), ("G7add-9", -1), ("G7addbb9", -2)] {
1298            let symbol = ChordSymbol::parse_music21(figure).unwrap();
1299            let modification = &symbol.chord_step_modifications()[0];
1300            assert_eq!(modification.degree(), 9, "{figure}");
1301            assert_eq!(
1302                modification.interval().semitones() as IntegerType,
1303                semitones,
1304                "{figure}"
1305            );
1306        }
1307    }
1308
1309    /// music21's own cases for a figure with additions and omissions, each
1310    /// of which reads back as the notes it was written from.
1311    #[test]
1312    fn additions_and_omissions_are_written_as_degrees_that_read_back() {
1313        use super::chord_symbol_figure_from_chord;
1314        for (notes, expected) in [
1315            ("G3 B3 D4 F4 A-4 C5 E-5", "G7addb9add11addb13"),
1316            ("E2 B2 G#3 D4 F##4", "E7add#9"),
1317            ("B-2 F3 A3 D4 E4", "Bbmaj7add#11"),
1318            ("A-2 E-3 G-3 C4 D4 B-4", "Ab9add#11"),
1319            ("A2 E3 A3 B3 C4 E4", "Amadd9"),
1320            ("D4 E4 F#4 A4", "Dadd9"),
1321            ("D3 A3 F#4 E5", "Dadd9"),
1322            ("G B D F A C# E", "C#øb9addb11addb13"),
1323            ("C E G B- D- F#", "C7addb9add#11"),
1324            ("C E G A D", "Am7add11/C"),
1325            ("C# F# B D-", "C#susaddb7addbb9omit5"),
1326            ("D-3 F#3 A-3", "Dbsusadd#3omit4"),
1327            ("F#2 C#3 E-3 A3", "F#m6addbb7omit6"),
1328        ] {
1329            let chord = Chord::new(notes).unwrap();
1330            let figure = chord_symbol_figure_from_chord(&chord).unwrap().unwrap();
1331            assert_eq!(figure, expected, "{notes}");
1332            // music21's parser reads a flat root or bass as `-`, which is
1333            // how the wheel spells a figure before handing it over.
1334            let respell = |name: &str| {
1335                let mut chars = name.chars();
1336                let Some(first) = chars.next() else {
1337                    return String::new();
1338                };
1339                let rest = chars.as_str();
1340                let flats = rest.chars().take_while(|c| *c == 'b').count();
1341                format!("{first}{}{}", "-".repeat(flats), &rest[flats..])
1342            };
1343            let spelled = match figure.split_once('/') {
1344                Some((root, bass)) => format!("{}/{}", respell(root), respell(bass)),
1345                None => respell(&figure),
1346            };
1347            let sounded: BTreeSet<String> = ChordSymbol::parse_music21(spelled)
1348                .unwrap()
1349                .pitches()
1350                .unwrap()
1351                .iter()
1352                .map(Pitch::name)
1353                .collect();
1354            let written: BTreeSet<String> = chord.pitches().iter().map(Pitch::name).collect();
1355            assert_eq!(sounded, written, "{notes}");
1356        }
1357        assert_eq!(super::alter_to_chord_symbol_string(-1), "b");
1358        assert_eq!(super::alter_to_chord_symbol_string(2), "##");
1359        assert_eq!(super::alter_to_chord_symbol_string(0), "");
1360    }
1361
1362    /// A root music21 reads but no pitch can spell is refused by the pitch,
1363    /// as an accidental, not as a figure without a root.
1364    #[test]
1365    fn a_root_with_an_unspellable_accidental_is_an_accidental_error() {
1366        for figure in ["C#-7", "C##-", "F#-7/E"] {
1367            let refused = ChordSymbol::parse_music21(figure).unwrap_err();
1368            assert!(
1369                matches!(refused, Error::Accidental(_)),
1370                "{figure}: {refused:?}"
1371            );
1372        }
1373        assert!(ChordSymbol::parse_music21("H7").is_err());
1374    }
1375    /// Every shorthand in music21's table realizes the notes its notation
1376    /// names, read independently off the root's major scale.
1377    #[test]
1378    fn every_music21_kind_realizes_its_own_notation() {
1379        use super::{ChordSymbol, MUSIC21_CHORD_TYPES};
1380        use crate::pitch::Accidental;
1381        use crate::{Key, Pitch};
1382        use std::collections::BTreeSet;
1383
1384        let major = Key::from_tonic("C").unwrap();
1385        for chord_type in MUSIC21_CHORD_TYPES {
1386            let expected: BTreeSet<String> = chord_type
1387                .notation
1388                .split(',')
1389                .map(|token| {
1390                    let degree: usize = token.trim_matches(['#', '-']).parse().unwrap();
1391                    let alter =
1392                        token.matches('#').count() as f64 - token.matches('-').count() as f64;
1393                    let mut pitch = major.pitch_from_degree((degree - 1) % 7 + 1).unwrap();
1394                    if alter != 0.0 {
1395                        pitch.set_accidental(Accidental::new(alter).unwrap());
1396                    }
1397                    pitch.name()
1398                })
1399                .collect();
1400            for abbreviation in chord_type.abbreviations {
1401                // music21 reads a sharp or flat with a digit after it as an
1402                // alteration before it looks the kind up, so `m7b5` is its
1403                // minor seventh with a lowered fifth; those abbreviations are
1404                // checked against music21 itself in `chord_symbol_parity`.
1405                if music21_kind(abbreviation).is_none_or(|(read, _)| read.kind != chord_type.kind) {
1406                    continue;
1407                }
1408                let symbol = ChordSymbol::parse(format!("C{abbreviation}")).unwrap();
1409                assert_eq!(symbol.kind(), Some(chord_type.kind), "C{abbreviation}");
1410                let names: BTreeSet<String> = symbol
1411                    .to_chord()
1412                    .unwrap()
1413                    .pitch_names()
1414                    .into_iter()
1415                    .collect();
1416                assert_eq!(names, expected, "C{abbreviation} ({})", chord_type.kind);
1417            }
1418        }
1419        // music21's kind for `C7#11` is the dominant seventh, with the
1420        // sharpened eleventh added after.
1421        assert_eq!(
1422            ChordSymbol::parse("C7#11").unwrap().kind(),
1423            Some("dominant-seventh")
1424        );
1425        assert_eq!(
1426            ChordSymbol::parse("CN6/E")
1427                .unwrap()
1428                .to_chord()
1429                .unwrap()
1430                .pitch_names(),
1431            ["E", "Gb", "C", "Db"]
1432        );
1433        let _ = Pitch::from_name("C").unwrap();
1434    }
1435
1436    /// music21's own `chordSymbolFigureFromChord` examples, the special
1437    /// kinds and the suspended second in inversion among them.
1438    #[test]
1439    fn figures_are_written_as_music21_writes_them() {
1440        use super::{
1441            ChordSymbolFigure, chord_symbol_figure_from_chord, chord_symbol_kind_from_chord,
1442        };
1443        use crate::{Chord, Pitch};
1444
1445        let figure =
1446            |notes: &str| chord_symbol_figure_from_chord(&Chord::new(notes).unwrap()).unwrap();
1447        let kind = |notes: &str| chord_symbol_kind_from_chord(&Chord::new(notes).unwrap());
1448        assert_eq!(figure("F3 A3 C#4 E-4 G4 B-4").as_deref(), Some("F+11"));
1449        assert_eq!(kind("F3 A3 C#4 E-4 G4 B-4"), Some("augmented-11th"));
1450        assert_eq!(figure("C3 E3 B3 D4").as_deref(), Some("CM9"));
1451        assert_eq!(figure("C3 D-3 E3 G-3").as_deref(), Some("CN6"));
1452        assert_eq!(kind("C3 D-3 E3 G-3"), Some("Neapolitan"));
1453        assert_eq!(figure("C3 D3 G3").as_deref(), Some("Csus2"));
1454        assert_eq!(figure("C3 E3 G3 D-4").as_deref(), Some("Caddb9"));
1455        assert_eq!(figure("C3").as_deref(), Some("Cpedal"));
1456        assert_eq!(figure("").as_deref(), Some(""));
1457
1458        // A suspended second in inversion is a suspended fourth on the bass.
1459        let inverted = Chord::new("C3 F3 G3").unwrap();
1460        let parts = ChordSymbolFigure::from_chord(&inverted).unwrap();
1461        assert_eq!(parts.root, "C");
1462        assert_eq!(
1463            (parts.kind, parts.abbreviation, parts.bass.as_deref()),
1464            ("suspended-fourth", "sus", None)
1465        );
1466        assert_eq!(parts.to_string(), "Csus");
1467        assert_eq!(parts.written_with("sus4"), "Csus4");
1468
1469        // The augmented sixths are named from a root the caller fixes.
1470        let with_root = |notes: &str, root: &str| {
1471            ChordSymbolFigure::from_chord_with_root(
1472                &Chord::new(notes).unwrap(),
1473                &Pitch::from_name(root).unwrap(),
1474            )
1475            .unwrap()
1476        };
1477        assert_eq!(with_root("C3 F#3 A-3", "C3").to_string(), "CIt+6");
1478        assert_eq!(with_root("C3 D3 F#3 A-3", "C3").to_string(), "CFr+6");
1479        assert_eq!(with_root("C3 E-3 F#3 A-3", "C3").to_string(), "CGr+6");
1480        assert_eq!(with_root("F2 B2 D#3 G#3", "F2").to_string(), "Ftristan");
1481        assert_eq!(with_root("F2 B2 D#3 G#3", "F2").kind, "Tristan");
1482
1483        // An inversion writes the bass, and additions are written with the
1484        // notes the kind lacks beside them.
1485        let parts = ChordSymbolFigure::from_chord(&Chord::new("E3 G3 C4 B-4").unwrap()).unwrap();
1486        assert_eq!(parts.to_string(), "C7/E");
1487        // A bass the kind does not carry is written as the bass alone.
1488        assert_eq!(figure("D3 C4 E-4 G4").as_deref(), Some("Cm/D"));
1489        assert!(ChordSymbolFigure::from_chord(&Chord::new("C4 C~4").unwrap()).is_none());
1490        assert!(ChordSymbolFigure::from_chord(&Chord::new("").unwrap()).is_none());
1491    }
1492
1493    #[test]
1494    fn a_symbol_is_parsed_through_try_from_and_reports_its_alterations() {
1495        use super::{ChordSymbol, chord_symbol_kind_from_chord};
1496        use crate::chord::Chord;
1497
1498        let symbol = ChordSymbol::try_from("C7#11").unwrap();
1499        assert_eq!(symbol.alterations().len(), 1);
1500        assert_eq!(symbol.alterations()[0].degree(), 11);
1501        assert_eq!(symbol.alterations()[0].semitones(), 1);
1502        assert!(symbol.omissions().is_empty());
1503        let omitting = ChordSymbol::try_from("C[no3]".to_string()).unwrap();
1504        assert_eq!(omitting.omissions(), [3]);
1505        assert!(ChordSymbol::try_from("").is_err());
1506
1507        assert_eq!(
1508            chord_symbol_kind_from_chord(&Chord::new("C").unwrap()),
1509            Some("pedal")
1510        );
1511        assert_eq!(
1512            chord_symbol_kind_from_chord(&Chord::new("C G").unwrap()),
1513            Some("power")
1514        );
1515        assert_eq!(
1516            chord_symbol_kind_from_chord(&Chord::new("C E G").unwrap()),
1517            Some("major")
1518        );
1519        assert_eq!(chord_symbol_kind_from_chord(&Chord::new("").unwrap()), None);
1520    }
1521
1522    #[test]
1523    fn chord_symbol_figures_match_music21() {
1524        let cases: [(&str, Option<&str>); 26] = [
1525            ("C4 E4 G4", Some("C")),
1526            ("C4 E-4 G4", Some("Cm")),
1527            ("C4 E4 G#4", Some("C+")),
1528            ("C4 E-4 G-4", Some("Cdim")),
1529            ("C4 E4 G4 B-4", Some("C7")),
1530            ("C4 E4 G4 B4", Some("Cmaj7")),
1531            ("C4 E-4 G4 B-4", Some("Cm7")),
1532            ("C4 E-4 G-4 B--4", Some("Co7")),
1533            ("C4 E-4 G-4 B-4", Some("C\u{00f8}7")),
1534            ("E4 G4 C5", Some("C/E")),
1535            ("G3 C4 E4", Some("C/G")),
1536            ("E4 G4 B-4 C5", Some("C7/E")),
1537            ("C4 E4 G4 B-4 D5", Some("C9")),
1538            ("C4 E4 G4 B4 D5", Some("CM9")),
1539            ("C4 D4 G4", Some("Csus2")),
1540            ("C4 F4 G4", Some("Csus")),
1541            ("C4", Some("Cpedal")),
1542            ("C4 G4", Some("Cpower")),
1543            ("C4 E4 G4 B-4 D5 F5", Some("C11")),
1544            ("C4 E4 G4 B-4 D5 F5 A5", Some("C13")),
1545            ("C4 E4 G4 A4", Some("Am7/C")),
1546            ("C4 E-4 G4 A4", Some("A\u{00f8}7/C")),
1547            ("C4 E4 G4 D5", Some("Cadd9")),
1548            ("F4 A4 C5 D5", Some("Dm7/F")),
1549            ("B3 D4 F4", Some("Bdim")),
1550            ("C4 D-4 E4", None),
1551        ];
1552        for (notes, expected) in cases {
1553            let chord = Chord::new(notes).unwrap();
1554            assert_eq!(
1555                chord_symbol_figure_from_chord(&chord).unwrap().as_deref(),
1556                expected,
1557                "{notes}"
1558            );
1559        }
1560        assert_eq!(
1561            chord_symbol_figure_from_chord(&Chord::empty())
1562                .unwrap()
1563                .as_deref(),
1564            Some("")
1565        );
1566
1567        let symbol = chord_symbol_from_chord(&Chord::new("E4 G4 B-4 C5").unwrap())
1568            .unwrap()
1569            .unwrap();
1570        assert_eq!(symbol.figure(), "C7/E");
1571        assert_eq!(symbol.root().name(), "C");
1572        assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("E"));
1573        assert!(
1574            chord_symbol_from_chord(&Chord::new("C4 D-4 E4").unwrap())
1575                .unwrap()
1576                .is_none()
1577        );
1578        assert!(chord_symbol_from_chord(&Chord::empty()).unwrap().is_none());
1579    }
1580
1581    #[test]
1582    fn kind_lookups_match_music21() {
1583        assert_eq!(
1584            abbreviations_for_kind("dominant-seventh"),
1585            Some(&["7", "dom7"][..])
1586        );
1587        assert_eq!(notation_for_kind("dominant-seventh"), Some("1,3,5,-7"));
1588        assert_eq!(current_abbreviation_for_kind("dominant-seventh"), Some("7"));
1589        assert_eq!(abbreviations_for_kind("major"), Some(&["", "M", "maj"][..]));
1590        assert_eq!(current_abbreviation_for_kind("major"), Some(""));
1591        assert_eq!(abbreviations_for_kind("nonsense"), None);
1592        for chord_type in known_chord_symbol_types() {
1593            assert_eq!(
1594                chord_type.abbreviations.first(),
1595                Some(&chord_type.abbreviation)
1596            );
1597        }
1598    }
1599
1600    #[test]
1601    fn find_figure_writes_one_canonical_spelling_that_parses_back() {
1602        let cases = [
1603            ("C", "C", "D"),
1604            ("Cm7", "Cm7", "Dm7"),
1605            ("F#dim", "F#dim", "G#dim"),
1606            ("Bb/D", "Bb/D", "C/E"),
1607            ("G7/B", "G7/B", "A7/C#"),
1608            ("Am/C", "Am/C", "Bm/D"),
1609            ("Cmaj7", "Cmaj7", "Dmaj7"),
1610            ("Dm7b5", "Dm7b5", "Em7b5"),
1611            ("C\u{00f8}7", "Cm7b5", "Dm7b5"),
1612            ("E7#9", "E7#9", "F#7#9"),
1613            ("Csus4", "Csus4", "Dsus4"),
1614            ("G7sus4", "G7sus4", "A7sus4"),
1615            ("Cadd(9)", "Cadd(9)", "Dadd(9)"),
1616            ("C6", "C6", "D6"),
1617            ("Cm6", "Cm6", "Dm6"),
1618            ("Cdim7", "Cdim7", "Ddim7"),
1619            ("Caug", "Caug", "Daug"),
1620            ("C+", "Caug", "Daug"),
1621            ("C9", "C9", "D9"),
1622            ("Cmaj7#11", "Cmaj7#11", "Dmaj7#11"),
1623            ("C5", "C5", "D5"),
1624            ("Cno3", "Cno3", "Dno3"),
1625        ];
1626        let whole_tone = Interval::from_name("M2").unwrap();
1627        for (typed, canonical, transposed) in cases {
1628            let symbol = ChordSymbol::parse(typed).unwrap();
1629            assert_eq!(symbol.find_figure(), canonical, "{typed}");
1630            let reparsed = ChordSymbol::parse(symbol.find_figure()).unwrap();
1631            assert_eq!(reparsed.quality(), symbol.quality(), "{typed}");
1632            assert_eq!(
1633                reparsed.to_chord().unwrap().pitch_names(),
1634                symbol.to_chord().unwrap().pitch_names(),
1635                "{typed}"
1636            );
1637            let up = symbol.transpose(&whole_tone).unwrap();
1638            assert_eq!(up.figure(), transposed, "{typed}");
1639            assert_eq!(up.find_figure(), transposed, "{typed}");
1640        }
1641    }
1642
1643    #[test]
1644    fn inversion_validity_follows_the_chord_size() {
1645        let valid = |figure: &str| -> Vec<u8> {
1646            let symbol = ChordSymbol::parse(figure).unwrap();
1647            (0..=6).filter(|n| symbol.inversion_is_valid(*n)).collect()
1648        };
1649        assert_eq!(valid("C"), [1, 2]);
1650        assert_eq!(valid("C6"), [1, 2]);
1651        assert_eq!(valid("Cm7"), [1, 2, 3]);
1652        assert_eq!(valid("C9"), [1, 2, 3, 4]);
1653        assert_eq!(valid("C13"), [1, 2, 3, 4, 5]);
1654        // music21 reads the kind, and `E7#9` is a dominant seventh with a
1655        // raised ninth added: it inverts as a seventh does.
1656        assert_eq!(valid("E7#9"), [1, 2, 3]);
1657        // A figure music21 refuses is read by the extensions the crate
1658        // found in it.
1659        assert_eq!(valid("Cmaj11"), [1, 2, 3, 4, 5]);
1660    }
1661    use super::*;
1662
1663    #[test]
1664    fn parses_major_seventh_symbol() {
1665        let symbol: ChordSymbol = "Cmaj7".parse().unwrap();
1666        assert_eq!(symbol.root().name(), "C");
1667        assert_eq!(symbol.quality(), ChordQuality::Major);
1668        assert_eq!(symbol.extensions(), &[7]);
1669        assert_eq!(
1670            symbol.to_chord().unwrap().pitched_common_name(),
1671            "C-major seventh chord"
1672        );
1673    }
1674
1675    #[test]
1676    fn parses_half_diminished_symbol() {
1677        let symbol = ChordSymbol::parse("F#m7b5").unwrap();
1678        assert_eq!(symbol.root().name(), "F#");
1679        assert_eq!(symbol.quality(), ChordQuality::HalfDiminished);
1680        assert_eq!(
1681            symbol.to_chord().unwrap().pitched_common_name(),
1682            "F#-half-diminished seventh chord"
1683        );
1684    }
1685
1686    #[test]
1687    fn parses_dominant_altered_symbol() {
1688        let symbol = ChordSymbol::parse("Bb7#11").unwrap();
1689        assert_eq!(symbol.root().name(), "Bb");
1690        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1691        assert_eq!(symbol.extensions(), &[7, 11]);
1692        assert_eq!(symbol.alterations()[0], ChordAlteration::new(11, 1));
1693        let names = symbol
1694            .to_chord()
1695            .unwrap()
1696            .pitches()
1697            .iter()
1698            .map(Pitch::name)
1699            .collect::<Vec<_>>();
1700        assert_eq!(names, vec!["Bb", "D", "F", "Ab", "E"]);
1701    }
1702
1703    #[test]
1704    fn parses_added_tones_without_changing_the_base_chord() {
1705        let symbol = ChordSymbol::parse("Cdim9 add(#5)").unwrap();
1706        assert_eq!(symbol.extensions(), &[9]);
1707        assert_eq!(symbol.additions(), &[ChordAlteration::new(5, 1)]);
1708        assert_eq!(
1709            symbol.to_chord().unwrap().pitch_classes(),
1710            vec![0, 2, 3, 6, 8, 9]
1711        );
1712    }
1713
1714    #[test]
1715    fn parses_altered_dominant_with_slash_bass() {
1716        let symbol = ChordSymbol::parse("D7b9#11/C").unwrap();
1717        assert_eq!(symbol.root().name(), "D");
1718        assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1719        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1720        assert_eq!(symbol.extensions(), &[7, 9, 11]);
1721        assert_eq!(
1722            symbol.alterations(),
1723            &[ChordAlteration::new(9, -1), ChordAlteration::new(11, 1)]
1724        );
1725        assert_eq!(
1726            symbol.to_chord().unwrap().pitch_classes(),
1727            vec![0, 2, 3, 6, 8, 9]
1728        );
1729    }
1730
1731    #[test]
1732    fn parses_music21_pitch_name_additions() {
1733        let symbol = ChordSymbol::parse("Ddom7dim5/CaddA,E-").unwrap();
1734
1735        assert_eq!(symbol.root().name(), "D");
1736        assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1737        assert_eq!(symbol.quality(), ChordQuality::Dominant);
1738        assert_eq!(symbol.alterations(), &[ChordAlteration::new(5, -1)]);
1739        assert_eq!(
1740            symbol.additions(),
1741            &[ChordAlteration::new(5, 0), ChordAlteration::new(9, -1)]
1742        );
1743        assert_eq!(
1744            symbol.to_chord().unwrap().pitch_classes(),
1745            vec![0, 2, 3, 6, 8, 9]
1746        );
1747    }
1748
1749    #[test]
1750    fn generates_petrushka_chord_symbol_name() {
1751        let chord = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1752        let names = chord_symbol_spellings(&chord);
1753
1754        assert_eq!(
1755            names.first().map(String::as_str),
1756            Some("Ddom7dim5add5addb9/C")
1757        );
1758    }
1759
1760    #[test]
1761    fn generates_common_chord_symbols() {
1762        let major_seventh = Chord::new("C E G B").unwrap();
1763        let dominant_ninth = Chord::new("C E G B- D").unwrap();
1764
1765        assert_eq!(
1766            chord_symbol_spellings(&major_seventh)
1767                .first()
1768                .map(String::as_str),
1769            Some("Cmaj7")
1770        );
1771        assert_eq!(
1772            chord_symbol_spellings(&dominant_ninth)
1773                .first()
1774                .map(String::as_str),
1775            Some("C9")
1776        );
1777    }
1778
1779    #[test]
1780    fn split_third_triads_do_not_spell_lower_third_as_sharp_nine() {
1781        let split_third = Chord::new("D4 A4 F#4 F4").unwrap();
1782        let names = chord_symbol_spellings(&split_third);
1783
1784        assert_eq!(names.first().map(String::as_str), Some("Daddb3"));
1785        assert!(!names.iter().any(|name| name == "D add(#9)"));
1786    }
1787
1788    #[test]
1789    fn altered_dominants_write_additions_as_degrees() {
1790        let altered_dominant = Chord::new("C4 E4 G4 Bb4 Eb5").unwrap();
1791        let names = chord_symbol_spellings(&altered_dominant);
1792
1793        assert_eq!(names.first().map(String::as_str), Some("C7addb3"));
1794    }
1795
1796    #[test]
1797    fn unrecognized_music21_figures_return_no_symbol() {
1798        let chord = Chord::new("F4 C5 D5 E-5").unwrap();
1799        let names = chord_symbol_spellings(&chord);
1800
1801        assert!(names.is_empty());
1802    }
1803
1804    #[test]
1805    fn generates_music21_figures_with_explicit_root() {
1806        let major_triad = Chord::new("G3 C4 E4").unwrap();
1807        let dominant_seventh = Chord::new("G3 B-3 C4 E4").unwrap();
1808        let power_chord = Chord::new("C4 G4").unwrap();
1809        let unsupported_dyad = Chord::new("C4 A4").unwrap();
1810
1811        assert_eq!(
1812            chord_symbol_spellings_with_root(&major_triad, 0)
1813                .first()
1814                .map(String::as_str),
1815            Some("C/G")
1816        );
1817        assert_eq!(
1818            chord_symbol_spellings_with_root(&dominant_seventh, 0)
1819                .first()
1820                .map(String::as_str),
1821            Some("C7/G")
1822        );
1823        assert_eq!(
1824            chord_symbol_spellings_with_root(&power_chord, 0)
1825                .first()
1826                .map(String::as_str),
1827            Some("Cpower")
1828        );
1829        assert!(chord_symbol_spellings_with_root(&unsupported_dyad, 0).is_empty());
1830    }
1831
1832    #[test]
1833    fn dense_sets_follow_music21_fallback_matching() {
1834        let chord = Chord::new("C4 D-4 E-4 E4 F#4 G4 A-4 A4").unwrap();
1835
1836        assert_eq!(
1837            chord_symbol_spellings(&chord).first().map(String::as_str),
1838            Some("Csusaddb3add3add#4addb9addb13add13omit4")
1839        );
1840    }
1841
1842    /// The lengths are music21's, read off `realizeChordSymbolDurations`
1843    /// over the same three bars.
1844    #[test]
1845    fn chord_symbols_hold_until_the_next_one_across_bars() {
1846        use crate::{Note, Stream, StreamElement, StreamKind};
1847
1848        let bar = |figure: &str, at: FloatType| {
1849            let mut measure = Stream::with_kind(StreamKind::Measure);
1850            measure.insert(at, ChordSymbol::parse(figure).unwrap());
1851            // Appended, so the four notes start where the symbol stands, as
1852            // music21's `repeatAppend` starts them.
1853            for _ in 0..4 {
1854                measure.push(Note::from_name("C4").unwrap());
1855            }
1856            measure
1857        };
1858        let held = |stream: &Stream| -> Vec<FloatType> {
1859            stream
1860                .flatten()
1861                .events()
1862                .iter()
1863                .filter(|event| matches!(event.element(), StreamElement::ChordSymbol(_)))
1864                .map(|event| event.element().quarter_length())
1865                .collect()
1866        };
1867
1868        let mut part = Stream::with_kind(StreamKind::Part);
1869        part.insert(0.0, bar("C", 0.0));
1870        part.insert(4.0, bar("G7", 1.5));
1871        part.insert(8.0, bar("Am", 2.0));
1872        assert_eq!(held(&part), [0.0, 0.0, 0.0]);
1873        realize_chord_symbol_durations(&mut part);
1874        assert_eq!(held(&part), [5.5, 4.5, 4.0]);
1875
1876        // One symbol holds to the end of everything after it.
1877        let mut stream = Stream::new();
1878        stream.insert(1.0, ChordSymbol::parse("C").unwrap());
1879        for beat in 1..7 {
1880            stream.insert(FloatType::from(beat), Note::from_name("C4").unwrap());
1881        }
1882        realize_chord_symbol_durations(&mut stream);
1883        assert_eq!(held(&stream), [6.0]);
1884
1885        // And a stream with none is left as it was.
1886        let mut bare = Stream::new();
1887        bare.push(Note::from_name("C4").unwrap());
1888        realize_chord_symbol_durations(&mut bare);
1889        assert_eq!(bare.len(), 1);
1890    }
1891}