1use std::str::FromStr;
2
3use crate::{
4 chord::Chord,
5 chord::root::{pitch_class, step_num},
6 defaults::{FloatType, IntegerType},
7 error::{Error, Result},
8 interval::Interval,
9 pitch::Pitch,
10};
11use std::collections::BTreeSet;
12
13mod figure;
14mod parse;
15mod tables;
16
17pub use figure::{
18 ChordSymbolFigure, chord_symbol_figure_from_chord, chord_symbol_from_chord,
19 chord_symbol_kind_from_chord,
20};
21pub(crate) use figure::{chord_symbol_spellings, chord_symbol_spellings_with_root};
22pub use tables::{
23 Music21ChordType, abbreviations_for_kind, current_abbreviation_for_kind,
24 known_chord_symbol_types, notation_for_kind,
25};
26
27use parse::*;
28use tables::*;
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub enum ChordQuality {
34 Major,
36 Minor,
38 Dominant,
40 Diminished,
42 Augmented,
44 HalfDiminished,
46 Suspended2,
48 Suspended4,
50 Power,
52 Pedal,
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub struct ChordAlteration {
60 degree: u8,
61 semitones: IntegerType,
62}
63
64impl ChordAlteration {
65 pub fn new(degree: u8, semitones: IntegerType) -> Self {
67 Self { degree, semitones }
68 }
69
70 pub fn degree(&self) -> u8 {
72 self.degree
73 }
74
75 pub fn semitones(&self) -> IntegerType {
77 self.semitones
78 }
79}
80
81#[derive(Clone, Debug, PartialEq)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84#[must_use]
85pub struct ChordSymbol {
86 figure: String,
87 root: Pitch,
88 bass: Option<Pitch>,
89 quality: ChordQuality,
90 extensions: Vec<u8>,
91 alterations: Vec<ChordAlteration>,
92 #[cfg_attr(feature = "serde", serde(default))]
93 omissions: Vec<u8>,
94 #[cfg_attr(feature = "serde", serde(default))]
95 additions: Vec<ChordAlteration>,
96 #[cfg_attr(feature = "serde", serde(default))]
98 kind: Option<String>,
99 #[cfg_attr(feature = "serde", serde(default))]
102 modifications: Vec<Modification>,
103}
104
105#[derive(Clone, Debug, PartialEq, Eq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109struct Modification {
110 kind: ModificationKind,
111 degree: u8,
112 alter: IntegerType,
113}
114
115#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
119enum ModificationKind {
120 Add,
121 Subtract,
122 Alter,
123}
124
125fn music21_kind(shorthand: &str) -> Option<(&'static Music21ChordType, usize)> {
130 let mut cut = shorthand.len();
131 for marker in ["add", "alter", "omit", "subtract"] {
132 if let Some(at) = shorthand.find(marker) {
133 cut = cut.min(at);
134 }
135 }
136 let mut head = &shorthand[..cut];
137 if let Some(at) = head.find('#')
138 && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
139 {
140 head = &head[..at];
141 }
142 if let Some(at) = head.find('b')
143 && at < head.len() - 1
144 && head[at + 1..].starts_with(|c: char| c.is_ascii_digit())
145 && !head.contains("ob9")
146 && !head.contains("øb9")
147 {
148 head = &head[..at];
149 }
150 MUSIC21_CHORD_TYPES
151 .iter()
152 .find(|chord_type| chord_type.abbreviations.contains(&head))
153 .map(|chord_type| (chord_type, head.len()))
154}
155
156fn music21_modifications(remaining: &str) -> Option<Vec<Modification>> {
161 const MARKERS: [(&str, ModificationKind); 4] = [
162 ("add", ModificationKind::Add),
163 ("alter", ModificationKind::Alter),
164 ("omit", ModificationKind::Subtract),
165 ("subtract", ModificationKind::Subtract),
166 ];
167 let mut out = Vec::new();
168 let first_marker = MARKERS
169 .iter()
170 .filter_map(|(marker, _)| remaining.find(marker))
171 .min();
172 if let Some(start) = first_marker {
173 let mut text = &remaining[start..];
174 while let Some((at, marker, kind)) = MARKERS
175 .iter()
176 .filter_map(|(marker, kind)| text.find(marker).map(|at| (at, *marker, *kind)))
177 .min_by_key(|(at, _, _)| *at)
178 {
179 let mut rest = &text[at + marker.len()..];
180 let mut alter = 0;
181 if let Some(after) = rest.strip_prefix('b') {
182 alter = -1;
183 rest = after;
184 } else if let Some(after) = rest.strip_prefix('#') {
185 alter = 1;
186 rest = after;
187 }
188 let digits = rest.chars().take_while(char::is_ascii_digit).count().min(2);
189 let degree_text = &rest[..digits];
190 if let Ok(degree) = degree_text.parse::<u8>() {
191 out.push(Modification {
192 kind,
193 degree,
194 alter,
195 });
196 }
197 text = &rest[digits..];
198 }
199 }
200
201 let before: String = remaining[..first_marker.unwrap_or(remaining.len())].replace(',', "");
204 let mut items: Vec<String> = Vec::new();
205 if before.contains(['b', '#']) {
206 let chars: Vec<char> = before.chars().collect();
207 let mut current = String::new();
208 let mut index = 0;
209 while index < chars.len() {
210 if matches!(chars[index], 'b' | '#') {
211 if !current.is_empty() {
212 items.push(std::mem::take(&mut current));
213 }
214 let mut group = String::new();
215 while index < chars.len() && matches!(chars[index], 'b' | '#') {
216 group.push(chars[index]);
217 index += 1;
218 }
219 while index < chars.len() && !matches!(chars[index], 'b' | '#') {
220 group.push(chars[index]);
221 index += 1;
222 }
223 items.push(group);
224 } else {
225 current.push(chars[index]);
226 index += 1;
227 }
228 }
229 if !current.is_empty() {
230 items.push(current);
231 }
232 } else if !before.is_empty() {
233 items.push(before);
234 }
235
236 let mut tokens: Vec<String> = Vec::new();
237 for item in items {
238 let digits: String = item.chars().filter(char::is_ascii_digit).collect();
239 let number: u32 = digits.parse().ok()?;
240 if number > 20 {
241 let chars: Vec<char> = item.chars().collect();
244 let mut prefix = String::new();
245 let mut skip = false;
246 for (index, &ch) in chars.iter().enumerate() {
247 if skip {
248 skip = false;
249 continue;
250 }
251 if ch == '1' {
252 let mut token = String::from(ch);
253 if let Some(next) = chars.get(index + 1) {
254 token.push(*next);
255 }
256 tokens.push(token);
257 skip = true;
258 } else if matches!(ch, 'b' | '#') {
259 prefix.push(ch);
260 } else {
261 prefix.push(ch);
262 tokens.push(std::mem::take(&mut prefix));
263 }
264 }
265 } else {
266 tokens.push(item);
267 }
268 }
269 for token in tokens {
270 let alter = if token.contains('b') {
271 -(token.matches('b').count() as IntegerType)
272 } else {
273 token.matches('#').count() as IntegerType
274 };
275 let digits: String = token
276 .chars()
277 .skip_while(|c| !c.is_ascii_digit())
278 .take_while(char::is_ascii_digit)
279 .collect();
280 if let Ok(degree) = digits.parse::<u8>() {
281 out.push(Modification {
282 kind: ModificationKind::Add,
283 degree,
284 alter,
285 });
286 }
287 }
288 Some(out)
289}
290
291impl ChordSymbol {
292 pub fn parse(figure: impl Into<String>) -> Result<Self> {
294 let figure = figure.into();
295 let trimmed = figure.trim();
296 if trimmed.is_empty() {
297 return Err(Error::Chord("chord symbol cannot be empty".to_string()));
298 }
299 let compact: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
302
303 let (body, bass_segment) = match compact.split_once('/') {
304 Some((body, bass)) => (body, Some(bass)),
305 None => (compact.as_str(), None),
306 };
307 let body_parts = split_music21_pitch_modifiers(body);
308 let bass_parts = bass_segment.map(split_music21_pitch_modifiers);
309 let bass = bass_parts
310 .as_ref()
311 .map(|parts| parse_pitch_only(&parts.base))
312 .transpose()?;
313
314 let (root_name, suffix) = parse_pitch_prefix(&body_parts.base)?;
315 let root = Pitch::from_name(root_name)?;
316 let suffix_without_additions = strip_addition_groups(suffix);
317 let mut additions = parse_additions(suffix);
318 let mut omissions = parse_omissions(suffix);
319 for pitch_name in body_parts
320 .additions
321 .iter()
322 .chain(bass_parts.iter().flat_map(|parts| parts.additions.iter()))
323 {
324 if let Some(addition) = pitch_name_addition(&root, pitch_name) {
325 additions.push(addition);
326 }
327 }
328 for pitch_name in body_parts
329 .omissions
330 .iter()
331 .chain(bass_parts.iter().flat_map(|parts| parts.omissions.iter()))
332 {
333 if let Some(omission) = pitch_name_degree(&root, pitch_name)
334 && !omissions.contains(&omission)
335 {
336 omissions.push(omission);
337 }
338 }
339
340 let mut alterations = parse_alterations(&suffix_without_additions);
341 add_implicit_music21_alterations(&suffix_without_additions, &mut alterations);
342 let extensions = parse_extensions(&suffix_without_additions, &alterations);
343 let quality = parse_quality(&suffix_without_additions, &alterations);
344 let full_suffix = &body[body_parts.base.len() - suffix.len()..];
351 let (kind, modifications) = match music21_kind(full_suffix) {
352 Some((chord_type, taken)) => match music21_modifications(&full_suffix[taken..]) {
353 Some(modifications) => (Some(chord_type.kind.to_string()), modifications),
354 None => (None, Vec::new()),
355 },
356 None => (None, Vec::new()),
357 };
358
359 Ok(Self {
360 figure: trimmed.to_string(),
361 root,
362 bass,
363 quality,
364 extensions,
365 alterations,
366 omissions,
367 additions,
368 kind,
369 modifications,
370 })
371 }
372
373 pub fn kind(&self) -> Option<&str> {
377 self.kind.as_deref()
378 }
379
380 pub fn figure(&self) -> &str {
382 &self.figure
383 }
384
385 pub fn root(&self) -> &Pitch {
387 &self.root
388 }
389
390 pub fn bass(&self) -> Option<&Pitch> {
392 self.bass.as_ref()
393 }
394
395 pub fn quality(&self) -> ChordQuality {
397 self.quality
398 }
399
400 pub fn extensions(&self) -> &[u8] {
402 &self.extensions
403 }
404
405 pub fn alterations(&self) -> &[ChordAlteration] {
407 &self.alterations
408 }
409
410 pub fn omissions(&self) -> &[u8] {
412 &self.omissions
413 }
414
415 pub fn additions(&self) -> &[ChordAlteration] {
417 &self.additions
418 }
419
420 pub fn find_figure(&self) -> String {
425 let mut figure = self.root.name();
426 figure.push_str(&self.quality_and_extension());
427 for alteration in &self.alterations {
428 if alteration.degree == 5 && self.fifth_is_implied() {
429 continue;
430 }
431 figure.push_str(&alteration_text(alteration));
432 }
433 for addition in &self.additions {
434 figure.push_str(&format!("add({})", alteration_text(addition)));
435 }
436 for omission in &self.omissions {
437 figure.push_str(&format!("no{omission}"));
438 }
439 if let Some(bass) = &self.bass
440 && bass.name() != self.root.name()
441 {
442 figure.push('/');
443 figure.push_str(&bass.name());
444 }
445 figure
446 }
447
448 pub fn transpose(&self, interval: &Interval) -> Result<ChordSymbol> {
452 let mut transposed = self.clone();
453 transposed.root = self.root.transpose(interval)?;
454 transposed.bass = self
455 .bass
456 .as_ref()
457 .map(|bass| bass.transpose(interval))
458 .transpose()?;
459 transposed.figure = transposed.find_figure();
460 Ok(transposed)
461 }
462
463 pub fn inversion_is_valid(&self, inversion: u8) -> bool {
468 let highest = self
469 .extensions
470 .iter()
471 .copied()
472 .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
473 .max()
474 .unwrap_or(5);
475 match inversion {
476 1 | 2 => true,
477 3 => highest >= 7,
478 4 => highest >= 9,
479 5 => highest >= 11,
480 _ => false,
481 }
482 }
483
484 fn fifth_is_implied(&self) -> bool {
485 matches!(
486 self.quality,
487 ChordQuality::Diminished | ChordQuality::Augmented | ChordQuality::HalfDiminished
488 )
489 }
490
491 fn quality_and_extension(&self) -> String {
494 let seventh_family = self
495 .extensions
496 .iter()
497 .copied()
498 .filter(|degree| matches!(degree, 7 | 9 | 11 | 13))
499 .filter(|degree| {
500 !self
501 .alterations
502 .iter()
503 .any(|alteration| alteration.degree == *degree)
504 })
505 .max()
506 .or_else(|| {
507 self.extensions
508 .iter()
509 .any(|degree| matches!(degree, 7 | 9 | 11 | 13))
510 .then_some(7)
511 });
512 let sixth = self.extensions.contains(&6);
513 let extension = |written: &str| -> String {
514 match seventh_family {
515 Some(degree) => format!("{written}{degree}"),
516 None if sixth => format!("{written}6"),
517 None => written.to_string(),
518 }
519 };
520 match self.quality {
521 ChordQuality::Major => match seventh_family {
522 Some(degree) => format!("maj{degree}"),
523 None if sixth => "6".to_string(),
524 None => String::new(),
525 },
526 ChordQuality::Minor => extension("m"),
527 ChordQuality::Dominant => seventh_family.unwrap_or(7).to_string(),
528 ChordQuality::Diminished => extension("dim"),
529 ChordQuality::Augmented => extension("aug"),
530 ChordQuality::HalfDiminished => format!("m{}b5", seventh_family.unwrap_or(7)),
531 ChordQuality::Suspended2 => match seventh_family {
532 Some(degree) => format!("{degree}sus2"),
533 None => "sus2".to_string(),
534 },
535 ChordQuality::Suspended4 => match seventh_family {
536 Some(degree) => format!("{degree}sus4"),
537 None => "sus4".to_string(),
538 },
539 ChordQuality::Power => "5".to_string(),
540 ChordQuality::Pedal => "pedal".to_string(),
541 }
542 }
543
544 pub fn to_chord(&self) -> Result<Chord> {
548 let mut pitches = match self.kind_notation() {
549 Some(notation) => self.music21_pitches(notation)?,
550 None => {
551 let mut intervals = self.spelled_intervals()?;
552 intervals.sort_unstable_by_key(|(degree, _)| *degree);
553 intervals.dedup();
554 intervals
555 .into_iter()
556 .map(|(_, name)| Interval::from_name(&name)?.transpose_pitch(&self.root))
557 .collect::<Result<Vec<_>>>()?
558 }
559 };
560
561 if let Some(bass) = &self.bass {
562 if let Some(index) = pitches.iter().position(|pitch| pitch.name() == bass.name()) {
563 let bass = pitches.remove(index);
564 pitches.insert(0, bass);
565 } else {
566 pitches.insert(0, bass.clone());
567 }
568 }
569
570 Chord::new(pitches.as_slice())
571 }
572
573 fn kind_notation(&self) -> Option<&'static str> {
577 notation_for_kind(self.kind.as_deref()?)
578 }
579
580 fn music21_pitches(&self, notation: &'static str) -> Result<Vec<Pitch>> {
585 let mut degrees: Vec<String> = notation.split(',').map(str::to_string).collect();
586 let mut pitches = notation_intervals(notation)?
587 .into_iter()
588 .map(|(_, name)| Interval::from_name(&name)?.transpose_pitch(&self.root))
589 .collect::<Result<Vec<_>>>()?;
590 let scale = crate::scale::Scale::new(crate::scale::ScaleType::Major, self.root.clone());
591 let degree_number =
592 |written: &str| -> Option<u8> { written.trim_matches(['-', '#', 'A']).parse().ok() };
593 let semitone = |pitch: &Pitch, alter: IntegerType| -> Result<Pitch> {
594 let step = Interval::from_name(if alter > 0 { "A1" } else { "A-1" })?;
595 let mut moved = pitch.clone();
596 for _ in 0..alter.unsigned_abs() {
597 moved = moved.transpose(&step)?;
598 }
599 Ok(moved)
600 };
601
602 for modification in &self.modifications {
603 match modification.kind {
604 ModificationKind::Add => {
605 let folded = IntegerType::from((modification.degree - 1) % 7 + 1);
606 let mut to_add = scale.pitch_at_degree(folded)?;
607 if modification.alter != 0 {
608 if modification.degree == 7 && modification.alter > 0 {
614 to_add = Pitch::from_number(to_add.ps() - 1.0)?;
615 }
616 to_add = semitone(&to_add, modification.alter)?;
617 }
618 if degrees
619 .iter()
620 .any(|written| *written == modification.degree.to_string())
621 {
622 let mut replaced = Vec::new();
627 for (index, pitch) in pitches.iter().enumerate() {
628 if scale.degree_of(pitch)? == Some(usize::from(modification.degree)) {
629 replaced.push(index);
630 }
631 }
632 for index in replaced {
633 pitches[index] = to_add.clone();
634 }
635 } else {
636 pitches.push(to_add);
637 }
638 }
639 ModificationKind::Subtract => {
640 let found: Vec<usize> = degrees
641 .iter()
642 .enumerate()
643 .filter(|(index, written)| {
644 *index < pitches.len()
645 && degree_number(written) == Some(modification.degree)
646 })
647 .map(|(index, _)| index)
648 .collect();
649 if found.is_empty() {
650 return Err(Error::Chord(format!(
651 "Degree not in specified chord: {}",
652 modification.degree
653 )));
654 }
655 for index in found.into_iter().rev() {
656 let _ = pitches.remove(index);
657 let _ = degrees.remove(index);
658 }
659 }
660 ModificationKind::Alter => {
661 let mut found = false;
662 for (index, written) in degrees.iter().enumerate() {
663 if index < pitches.len()
664 && degree_number(written) == Some(modification.degree)
665 {
666 pitches[index] = semitone(&pitches[index], modification.alter)?;
667 found = true;
668 }
669 }
670 if !found {
671 let folded = IntegerType::from((modification.degree - 1) % 7 + 1);
672 let mut to_add = scale.pitch_at_degree(folded)?;
673 if modification.alter != 0 {
674 to_add = semitone(&to_add, modification.alter)?;
675 }
676 pitches.push(to_add);
677 }
678 }
679 }
680 }
681
682 if !self.omissions.is_empty() {
685 let kept: Vec<bool> = degrees
686 .iter()
687 .map(|written| {
688 degree_number(written).is_none_or(|degree| !self.omissions.contains(°ree))
689 })
690 .collect();
691 let mut index = 0;
692 pitches.retain(|_| {
693 let keep = kept.get(index).copied().unwrap_or(true);
694 index += 1;
695 keep
696 });
697 }
698 for addition in &self.additions {
699 let (_, name) = added_interval(addition)?;
700 let pitch = Interval::from_name(name)?.transpose_pitch(&self.root)?;
701 if !pitches
702 .iter()
703 .any(|existing| existing.name() == pitch.name())
704 {
705 pitches.push(pitch);
706 }
707 }
708 Ok(pitches)
709 }
710
711 fn spelled_intervals(&self) -> Result<Vec<(u8, String)>> {
714 let mut intervals = self.base_intervals();
715
716 let highest = self
717 .extensions
718 .iter()
719 .copied()
720 .filter(|degree| !self.alterations.iter().any(|alt| alt.degree == *degree))
721 .max()
722 .unwrap_or(0);
723 for extension in [6, 9, 11, 13] {
724 let implied = match extension {
725 9 => highest >= 11,
726 11 => highest == 13,
727 _ => false,
728 };
729 if (self.extensions.contains(&extension) || implied)
730 && !self.alterations.iter().any(|alt| alt.degree == extension)
731 && !self.omissions.contains(&extension)
732 {
733 intervals.push((extension, default_extension_interval(extension)));
734 }
735 }
736
737 for alteration in &self.alterations {
738 if alteration.degree == 5 {
739 continue;
740 }
741 let altered = altered_interval(alteration)?;
742 intervals.retain(|(degree, _)| *degree != altered.0);
746 intervals.push(altered);
747 }
748
749 for addition in &self.additions {
750 intervals.push(added_interval(addition)?);
751 }
752
753 Ok(intervals
754 .into_iter()
755 .map(|(degree, name)| (degree, name.to_string()))
756 .collect())
757 }
758
759 fn base_intervals(&self) -> Vec<(u8, &'static str)> {
760 let altered_fifth = self
761 .alterations
762 .iter()
763 .find(|alteration| alteration.degree == 5)
764 .and_then(|alteration| match alteration.semitones {
765 -1 => Some("d5"),
766 1 => Some("a5"),
767 _ => None,
768 });
769
770 let fifth = altered_fifth.unwrap_or("P5");
771 let has_seventh = self
772 .extensions
773 .iter()
774 .any(|degree| matches!(degree, 7 | 9 | 11 | 13));
775
776 let intervals = match self.quality {
777 ChordQuality::Major => {
778 if has_seventh {
779 vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "M7")]
780 } else {
781 vec![(1, "P1"), (3, "M3"), (5, fifth)]
782 }
783 }
784 ChordQuality::Minor => {
785 if has_seventh {
786 vec![(1, "P1"), (3, "m3"), (5, fifth), (7, "m7")]
787 } else {
788 vec![(1, "P1"), (3, "m3"), (5, fifth)]
789 }
790 }
791 ChordQuality::Dominant => vec![(1, "P1"), (3, "M3"), (5, fifth), (7, "m7")],
792 ChordQuality::Diminished => {
793 if has_seventh {
794 vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "d7")]
795 } else {
796 vec![(1, "P1"), (3, "m3"), (5, "d5")]
797 }
798 }
799 ChordQuality::Augmented => vec![(1, "P1"), (3, "M3"), (5, "a5")],
800 ChordQuality::HalfDiminished => vec![(1, "P1"), (3, "m3"), (5, "d5"), (7, "m7")],
801 ChordQuality::Suspended2 => vec![(1, "P1"), (2, "M2"), (5, fifth)],
802 ChordQuality::Suspended4 => vec![(1, "P1"), (4, "P4"), (5, fifth)],
803 ChordQuality::Power => vec![(1, "P1"), (5, fifth)],
804 ChordQuality::Pedal => vec![(1, "P1")],
805 };
806
807 intervals
808 .into_iter()
809 .filter(|(degree, _)| !self.omissions.contains(degree))
810 .collect()
811 }
812}
813
814impl FromStr for ChordSymbol {
815 type Err = Error;
816
817 fn from_str(value: &str) -> Result<Self> {
818 Self::parse(value)
819 }
820}
821
822impl TryFrom<&str> for ChordSymbol {
823 type Error = Error;
824
825 fn try_from(value: &str) -> Result<Self> {
826 Self::parse(value)
827 }
828}
829
830impl TryFrom<String> for ChordSymbol {
831 type Error = Error;
832
833 fn try_from(value: String) -> Result<Self> {
834 Self::parse(value)
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 #[test]
843 fn every_music21_kind_realizes_its_own_notation() {
844 use super::{ChordSymbol, MUSIC21_CHORD_TYPES};
845 use crate::pitch::Accidental;
846 use crate::{Key, Pitch};
847 use std::collections::BTreeSet;
848
849 let major = Key::from_tonic("C").unwrap();
850 for chord_type in MUSIC21_CHORD_TYPES {
851 let expected: BTreeSet<String> = chord_type
852 .notation
853 .split(',')
854 .map(|token| {
855 let degree: usize = token.trim_matches(['#', '-']).parse().unwrap();
856 let alter =
857 token.matches('#').count() as f64 - token.matches('-').count() as f64;
858 let mut pitch = major.pitch_from_degree((degree - 1) % 7 + 1).unwrap();
859 if alter != 0.0 {
860 pitch.set_accidental(Some(Accidental::new(alter).unwrap()));
861 }
862 pitch.name()
863 })
864 .collect();
865 for abbreviation in chord_type.abbreviations {
866 if music21_kind(abbreviation).is_none_or(|(read, _)| read.kind != chord_type.kind) {
871 continue;
872 }
873 let symbol = ChordSymbol::parse(format!("C{abbreviation}")).unwrap();
874 assert_eq!(symbol.kind(), Some(chord_type.kind), "C{abbreviation}");
875 let names: BTreeSet<String> = symbol
876 .to_chord()
877 .unwrap()
878 .pitch_names()
879 .into_iter()
880 .collect();
881 assert_eq!(names, expected, "C{abbreviation} ({})", chord_type.kind);
882 }
883 }
884 assert_eq!(
887 ChordSymbol::parse("C7#11").unwrap().kind(),
888 Some("dominant-seventh")
889 );
890 assert_eq!(
891 ChordSymbol::parse("CN6/E")
892 .unwrap()
893 .to_chord()
894 .unwrap()
895 .pitch_names(),
896 ["E", "C", "D-", "G-"]
897 );
898 let _ = Pitch::from_name("C").unwrap();
899 }
900
901 #[test]
904 fn figures_are_written_as_music21_writes_them() {
905 use super::{
906 ChordSymbolFigure, chord_symbol_figure_from_chord, chord_symbol_kind_from_chord,
907 };
908 use crate::{Chord, Pitch};
909
910 let figure =
911 |notes: &str| chord_symbol_figure_from_chord(&Chord::new(notes).unwrap()).unwrap();
912 let kind = |notes: &str| chord_symbol_kind_from_chord(&Chord::new(notes).unwrap());
913 assert_eq!(figure("F3 A3 C#4 E-4 G4 B-4").as_deref(), Some("F+11"));
914 assert_eq!(kind("F3 A3 C#4 E-4 G4 B-4"), Some("augmented-11th"));
915 assert_eq!(figure("C3 E3 B3 D4").as_deref(), Some("CM9"));
916 assert_eq!(figure("C3 D-3 E3 G-3").as_deref(), Some("CN6"));
917 assert_eq!(kind("C3 D-3 E3 G-3"), Some("Neapolitan"));
918 assert_eq!(figure("C3 D3 G3").as_deref(), Some("Csus2"));
919 assert_eq!(figure("C3 E3 G3 D-4").as_deref(), Some("CaddD-"));
920 assert_eq!(figure("C3").as_deref(), Some("Cpedal"));
921 assert_eq!(figure("").as_deref(), Some(""));
922
923 let inverted = Chord::new("C3 F3 G3").unwrap();
925 let parts = ChordSymbolFigure::from_chord(&inverted).unwrap();
926 assert_eq!(parts.root, "C");
927 assert_eq!(
928 (parts.kind, parts.abbreviation, parts.bass.as_deref()),
929 ("suspended-fourth", "sus", None)
930 );
931 assert_eq!(parts.to_string(), "Csus");
932 assert_eq!(parts.written_with("sus4"), "Csus4");
933
934 let with_root = |notes: &str, root: &str| {
936 ChordSymbolFigure::from_chord_with_root(
937 &Chord::new(notes).unwrap(),
938 &Pitch::from_name(root).unwrap(),
939 )
940 .unwrap()
941 };
942 assert_eq!(with_root("C3 F#3 A-3", "C3").to_string(), "CIt+6");
943 assert_eq!(with_root("C3 D3 F#3 A-3", "C3").to_string(), "CFr+6");
944 assert_eq!(with_root("C3 E-3 F#3 A-3", "C3").to_string(), "CGr+6");
945 assert_eq!(with_root("F2 B2 D#3 G#3", "F2").to_string(), "Ftristan");
946 assert_eq!(with_root("F2 B2 D#3 G#3", "F2").kind, "Tristan");
947
948 let parts = ChordSymbolFigure::from_chord(&Chord::new("E3 G3 C4 B-4").unwrap()).unwrap();
951 assert_eq!(parts.to_string(), "C7/E");
952 assert_eq!(figure("D3 C4 E-4 G4").as_deref(), Some("Cm/D"));
954 assert!(ChordSymbolFigure::from_chord(&Chord::new("C4 C~4").unwrap()).is_none());
955 assert!(ChordSymbolFigure::from_chord(&Chord::new("").unwrap()).is_none());
956 }
957
958 #[test]
959 fn a_symbol_is_parsed_through_try_from_and_reports_its_alterations() {
960 use super::{ChordSymbol, chord_symbol_kind_from_chord};
961 use crate::chord::Chord;
962
963 let symbol = ChordSymbol::try_from("C7#11").unwrap();
964 assert_eq!(symbol.alterations().len(), 1);
965 assert_eq!(symbol.alterations()[0].degree(), 11);
966 assert_eq!(symbol.alterations()[0].semitones(), 1);
967 assert!(symbol.omissions().is_empty());
968 let omitting = ChordSymbol::try_from("C[no3]".to_string()).unwrap();
969 assert_eq!(omitting.omissions(), [3]);
970 assert!(ChordSymbol::try_from("").is_err());
971
972 assert_eq!(
973 chord_symbol_kind_from_chord(&Chord::new("C").unwrap()),
974 Some("pedal")
975 );
976 assert_eq!(
977 chord_symbol_kind_from_chord(&Chord::new("C G").unwrap()),
978 Some("power")
979 );
980 assert_eq!(
981 chord_symbol_kind_from_chord(&Chord::new("C E G").unwrap()),
982 Some("major")
983 );
984 assert_eq!(chord_symbol_kind_from_chord(&Chord::new("").unwrap()), None);
985 }
986
987 #[test]
988 fn chord_symbol_figures_match_music21() {
989 let cases: [(&str, Option<&str>); 26] = [
990 ("C4 E4 G4", Some("C")),
991 ("C4 E-4 G4", Some("Cm")),
992 ("C4 E4 G#4", Some("C+")),
993 ("C4 E-4 G-4", Some("Cdim")),
994 ("C4 E4 G4 B-4", Some("C7")),
995 ("C4 E4 G4 B4", Some("Cmaj7")),
996 ("C4 E-4 G4 B-4", Some("Cm7")),
997 ("C4 E-4 G-4 B--4", Some("Co7")),
998 ("C4 E-4 G-4 B-4", Some("C\u{00f8}7")),
999 ("E4 G4 C5", Some("C/E")),
1000 ("G3 C4 E4", Some("C/G")),
1001 ("E4 G4 B-4 C5", Some("C7/E")),
1002 ("C4 E4 G4 B-4 D5", Some("C9")),
1003 ("C4 E4 G4 B4 D5", Some("CM9")),
1004 ("C4 D4 G4", Some("Csus2")),
1005 ("C4 F4 G4", Some("Csus")),
1006 ("C4", Some("Cpedal")),
1007 ("C4 G4", Some("Cpower")),
1008 ("C4 E4 G4 B-4 D5 F5", Some("C11")),
1009 ("C4 E4 G4 B-4 D5 F5 A5", Some("C13")),
1010 ("C4 E4 G4 A4", Some("Am7/C")),
1011 ("C4 E-4 G4 A4", Some("A\u{00f8}7/C")),
1012 ("C4 E4 G4 D5", Some("CaddD")),
1013 ("F4 A4 C5 D5", Some("Dm7/F")),
1014 ("B3 D4 F4", Some("Bdim")),
1015 ("C4 D-4 E4", None),
1016 ];
1017 for (notes, expected) in cases {
1018 let chord = Chord::new(notes).unwrap();
1019 assert_eq!(
1020 chord_symbol_figure_from_chord(&chord).unwrap().as_deref(),
1021 expected,
1022 "{notes}"
1023 );
1024 }
1025 assert_eq!(
1026 chord_symbol_figure_from_chord(&Chord::empty())
1027 .unwrap()
1028 .as_deref(),
1029 Some("")
1030 );
1031
1032 let symbol = chord_symbol_from_chord(&Chord::new("E4 G4 B-4 C5").unwrap())
1033 .unwrap()
1034 .unwrap();
1035 assert_eq!(symbol.figure(), "C7/E");
1036 assert_eq!(symbol.root().name(), "C");
1037 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("E"));
1038 assert!(
1039 chord_symbol_from_chord(&Chord::new("C4 D-4 E4").unwrap())
1040 .unwrap()
1041 .is_none()
1042 );
1043 assert!(chord_symbol_from_chord(&Chord::empty()).unwrap().is_none());
1044 }
1045
1046 #[test]
1047 fn kind_lookups_match_music21() {
1048 assert_eq!(
1049 abbreviations_for_kind("dominant-seventh"),
1050 Some(&["7", "dom7"][..])
1051 );
1052 assert_eq!(notation_for_kind("dominant-seventh"), Some("1,3,5,-7"));
1053 assert_eq!(current_abbreviation_for_kind("dominant-seventh"), Some("7"));
1054 assert_eq!(abbreviations_for_kind("major"), Some(&["", "M", "maj"][..]));
1055 assert_eq!(current_abbreviation_for_kind("major"), Some(""));
1056 assert_eq!(abbreviations_for_kind("nonsense"), None);
1057 for chord_type in known_chord_symbol_types() {
1058 assert_eq!(
1059 chord_type.abbreviations.first(),
1060 Some(&chord_type.abbreviation)
1061 );
1062 }
1063 }
1064
1065 #[test]
1066 fn find_figure_writes_one_canonical_spelling_that_parses_back() {
1067 let cases = [
1068 ("C", "C", "D"),
1069 ("Cm7", "Cm7", "Dm7"),
1070 ("F#dim", "F#dim", "G#dim"),
1071 ("B-/D", "B-/D", "C/E"),
1072 ("G7/B", "G7/B", "A7/C#"),
1073 ("Am/C", "Am/C", "Bm/D"),
1074 ("Cmaj7", "Cmaj7", "Dmaj7"),
1075 ("Dm7b5", "Dm7b5", "Em7b5"),
1076 ("C\u{00f8}7", "Cm7b5", "Dm7b5"),
1077 ("E7#9", "E7#9", "F#7#9"),
1078 ("Csus4", "Csus4", "Dsus4"),
1079 ("G7sus4", "G7sus4", "A7sus4"),
1080 ("Cadd(9)", "Cadd(9)", "Dadd(9)"),
1081 ("C6", "C6", "D6"),
1082 ("Cm6", "Cm6", "Dm6"),
1083 ("Cdim7", "Cdim7", "Ddim7"),
1084 ("Caug", "Caug", "Daug"),
1085 ("C+", "Caug", "Daug"),
1086 ("C9", "C9", "D9"),
1087 ("Cmaj7#11", "Cmaj7#11", "Dmaj7#11"),
1088 ("C5", "C5", "D5"),
1089 ("Cno3", "Cno3", "Dno3"),
1090 ];
1091 let whole_tone = Interval::from_name("M2").unwrap();
1092 for (typed, canonical, transposed) in cases {
1093 let symbol = ChordSymbol::parse(typed).unwrap();
1094 assert_eq!(symbol.find_figure(), canonical, "{typed}");
1095 let reparsed = ChordSymbol::parse(symbol.find_figure()).unwrap();
1096 assert_eq!(reparsed.quality(), symbol.quality(), "{typed}");
1097 assert_eq!(
1098 reparsed.to_chord().unwrap().pitch_names(),
1099 symbol.to_chord().unwrap().pitch_names(),
1100 "{typed}"
1101 );
1102 let up = symbol.transpose(&whole_tone).unwrap();
1103 assert_eq!(up.figure(), transposed, "{typed}");
1104 assert_eq!(up.find_figure(), transposed, "{typed}");
1105 }
1106 }
1107
1108 #[test]
1109 fn inversion_validity_follows_the_chord_size() {
1110 let valid = |figure: &str| -> Vec<u8> {
1111 let symbol = ChordSymbol::parse(figure).unwrap();
1112 (0..=6).filter(|n| symbol.inversion_is_valid(*n)).collect()
1113 };
1114 assert_eq!(valid("C"), [1, 2]);
1115 assert_eq!(valid("C6"), [1, 2]);
1116 assert_eq!(valid("Cm7"), [1, 2, 3]);
1117 assert_eq!(valid("C9"), [1, 2, 3, 4]);
1118 assert_eq!(valid("Cmaj11"), [1, 2, 3, 4, 5]);
1119 assert_eq!(valid("C13"), [1, 2, 3, 4, 5]);
1120 assert_eq!(valid("E7#9"), [1, 2, 3, 4]);
1121 }
1122 use super::*;
1123
1124 #[test]
1125 fn parses_major_seventh_symbol() {
1126 let symbol: ChordSymbol = "Cmaj7".parse().unwrap();
1127 assert_eq!(symbol.root().name(), "C");
1128 assert_eq!(symbol.quality(), ChordQuality::Major);
1129 assert_eq!(symbol.extensions(), &[7]);
1130 assert_eq!(
1131 symbol.to_chord().unwrap().pitched_common_name(),
1132 "C-major seventh chord"
1133 );
1134 }
1135
1136 #[test]
1137 fn parses_half_diminished_symbol() {
1138 let symbol = ChordSymbol::parse("F#m7b5").unwrap();
1139 assert_eq!(symbol.root().name(), "F#");
1140 assert_eq!(symbol.quality(), ChordQuality::HalfDiminished);
1141 assert_eq!(
1142 symbol.to_chord().unwrap().pitched_common_name(),
1143 "F#-half-diminished seventh chord"
1144 );
1145 }
1146
1147 #[test]
1148 fn parses_dominant_altered_symbol() {
1149 let symbol = ChordSymbol::parse("Bb7#11").unwrap();
1150 assert_eq!(symbol.root().name(), "B-");
1151 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1152 assert_eq!(symbol.extensions(), &[7, 11]);
1153 assert_eq!(symbol.alterations()[0], ChordAlteration::new(11, 1));
1154 let names = symbol
1155 .to_chord()
1156 .unwrap()
1157 .pitches()
1158 .iter()
1159 .map(Pitch::name)
1160 .collect::<Vec<_>>();
1161 assert_eq!(names, vec!["B-", "D", "F", "A-", "E"]);
1162 }
1163
1164 #[test]
1165 fn parses_added_tones_without_changing_the_base_chord() {
1166 let symbol = ChordSymbol::parse("Cdim9 add(#5)").unwrap();
1167 assert_eq!(symbol.extensions(), &[9]);
1168 assert_eq!(symbol.additions(), &[ChordAlteration::new(5, 1)]);
1169 assert_eq!(
1170 symbol.to_chord().unwrap().pitch_classes(),
1171 vec![0, 2, 3, 6, 8, 9]
1172 );
1173 }
1174
1175 #[test]
1176 fn parses_altered_dominant_with_slash_bass() {
1177 let symbol = ChordSymbol::parse("D7b9#11/C").unwrap();
1178 assert_eq!(symbol.root().name(), "D");
1179 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1180 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1181 assert_eq!(symbol.extensions(), &[7, 9, 11]);
1182 assert_eq!(
1183 symbol.alterations(),
1184 &[ChordAlteration::new(9, -1), ChordAlteration::new(11, 1)]
1185 );
1186 assert_eq!(
1187 symbol.to_chord().unwrap().pitch_classes(),
1188 vec![0, 2, 3, 6, 8, 9]
1189 );
1190 }
1191
1192 #[test]
1193 fn parses_music21_pitch_name_additions() {
1194 let symbol = ChordSymbol::parse("Ddom7dim5/CaddA,E-").unwrap();
1195
1196 assert_eq!(symbol.root().name(), "D");
1197 assert_eq!(symbol.bass().map(Pitch::name).as_deref(), Some("C"));
1198 assert_eq!(symbol.quality(), ChordQuality::Dominant);
1199 assert_eq!(symbol.alterations(), &[ChordAlteration::new(5, -1)]);
1200 assert_eq!(
1201 symbol.additions(),
1202 &[ChordAlteration::new(5, 0), ChordAlteration::new(9, -1)]
1203 );
1204 assert_eq!(
1205 symbol.to_chord().unwrap().pitch_classes(),
1206 vec![0, 2, 3, 6, 8, 9]
1207 );
1208 }
1209
1210 #[test]
1211 fn generates_petrushka_chord_symbol_name() {
1212 let chord = Chord::new("C4 D4 Eb4 F#4 Ab4 A4").unwrap();
1213 let names = chord_symbol_spellings(&chord);
1214
1215 assert_eq!(
1216 names.first().map(String::as_str),
1217 Some("Ddom7dim5/CaddA,E-")
1218 );
1219 assert!(names.iter().any(|name| name == "Ddom7dim5/CaddA,E-"));
1220 }
1221
1222 #[test]
1223 fn generates_common_chord_symbols() {
1224 let major_seventh = Chord::new("C E G B").unwrap();
1225 let dominant_ninth = Chord::new("C E G B- D").unwrap();
1226
1227 assert_eq!(
1228 chord_symbol_spellings(&major_seventh)
1229 .first()
1230 .map(String::as_str),
1231 Some("Cmaj7")
1232 );
1233 assert_eq!(
1234 chord_symbol_spellings(&dominant_ninth)
1235 .first()
1236 .map(String::as_str),
1237 Some("C9")
1238 );
1239 }
1240
1241 #[test]
1242 fn split_third_triads_do_not_spell_lower_third_as_sharp_nine() {
1243 let split_third = Chord::new("D4 A4 F#4 F4").unwrap();
1244 let names = chord_symbol_spellings(&split_third);
1245
1246 assert_eq!(names.first().map(String::as_str), Some("DaddF"));
1247 assert!(!names.iter().any(|name| name == "D add(#9)"));
1248 }
1249
1250 #[test]
1251 fn altered_dominants_use_music21_pitch_name_additions() {
1252 let altered_dominant = Chord::new("C4 E4 G4 Bb4 Eb5").unwrap();
1253 let names = chord_symbol_spellings(&altered_dominant);
1254
1255 assert_eq!(names.first().map(String::as_str), Some("C7addE-"));
1256 }
1257
1258 #[test]
1259 fn unrecognized_music21_figures_return_no_symbol() {
1260 let chord = Chord::new("F4 C5 D5 E-5").unwrap();
1261 let names = chord_symbol_spellings(&chord);
1262
1263 assert!(names.is_empty());
1264 }
1265
1266 #[test]
1267 fn generates_music21_figures_with_explicit_root() {
1268 let major_triad = Chord::new("G3 C4 E4").unwrap();
1269 let dominant_seventh = Chord::new("G3 B-3 C4 E4").unwrap();
1270 let power_chord = Chord::new("C4 G4").unwrap();
1271 let unsupported_dyad = Chord::new("C4 A4").unwrap();
1272
1273 assert_eq!(
1274 chord_symbol_spellings_with_root(&major_triad, 0)
1275 .first()
1276 .map(String::as_str),
1277 Some("C/G")
1278 );
1279 assert_eq!(
1280 chord_symbol_spellings_with_root(&dominant_seventh, 0)
1281 .first()
1282 .map(String::as_str),
1283 Some("C7/G")
1284 );
1285 assert_eq!(
1286 chord_symbol_spellings_with_root(&power_chord, 0)
1287 .first()
1288 .map(String::as_str),
1289 Some("Cpower")
1290 );
1291 assert!(chord_symbol_spellings_with_root(&unsupported_dyad, 0).is_empty());
1292 }
1293
1294 #[test]
1295 fn dense_sets_follow_music21_fallback_matching() {
1296 let chord = Chord::new("C4 D-4 E-4 E4 F#4 G4 A-4 A4").unwrap();
1297
1298 assert_eq!(
1299 chord_symbol_spellings(&chord).first().map(String::as_str),
1300 Some("CsusaddA,A-,D-,E,E-,F#,omitF")
1301 );
1302 }
1303}