Skip to main content

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