1use crate::{
2 chord::{Chord, root::pitch_class},
3 chordsymbol::{ChordQuality, ChordSymbol},
4 defaults::{FloatType, IntegerType},
5 error::{Error, Result},
6 figuredbass::{Figure, Notation},
7 interval::Interval,
8 key::Key,
9 pitch::{Accidental, Pitch},
10};
11use std::fmt;
12
13mod analysis;
14mod figure;
15mod realize;
16
17pub use analysis::{
18 analyze_chord, analyze_chord_with_root, correct_rn_alteration_for_minor,
19 correct_suffix_for_chord_quality, figure_tuples, identify_as_tonic_or_dominant,
20 roman_inversion_name, roman_numeral_from_chord,
21};
22pub(crate) use figure::degree_to_roman;
23pub use figure::{
24 adjust_minor_vi_and_vii_by_quality, bass_scale_degree_from_notation, expand_shorthand,
25 parse_numeral_alone, secondary_key, split_roman_accidental_prefix, split_roman_prefix,
26 split_secondary, take_added_steps, take_bracketed_alterations, take_omitted_steps,
27};
28pub use realize::match_pitches_to_quality;
29
30use figure::*;
31use realize::*;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub enum Minor67Default {
42 #[default]
47 Quality,
48 Flat,
51 Sharp,
53 Cautionary,
57}
58
59#[derive(Clone, Debug)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62#[must_use]
63pub struct RomanNumeral {
64 figure: String,
65 key: Key,
66 degree: u8,
67 accidental: i8,
68 written_accidental: i8,
75 inversion: u8,
76 seventh: bool,
77 quality: RomanQuality,
78 implied_quality: ImpliedQuality,
82 figures: FiguredBass,
85 secondary: Option<String>,
86 kind: RomanKind,
87 sixth_minor: Minor67Default,
89 seventh_minor: Minor67Default,
90 scale: Option<crate::scale::Scale>,
98 case_matters: bool,
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111enum RomanKind {
112 Diatonic,
113 AugmentedSixth(AugmentedSixthKind),
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118enum AugmentedSixthKind {
119 Italian,
120 French,
121 German,
122 Swiss,
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
127enum RomanQuality {
128 Major,
129 Minor,
130 Diminished,
131 HalfDiminished,
132 Augmented,
133}
134
135impl AugmentedSixthKind {
136 fn from_figure(figure: &str) -> Option<Self> {
143 let figure = figure.trim();
144 let kind = augmented_sixth_prefix(figure)?;
145 let name = match kind {
146 Self::Italian => "It",
147 Self::French => "Fr",
148 Self::German => "Ger",
149 Self::Swiss => "Sw",
150 };
151 let rest = figure[name.len()..].trim_start_matches('+');
152 unslash_inversion(rest)
153 .chars()
154 .all(|written| written.is_ascii_digit())
155 .then_some(kind)
156 }
157
158 fn from_common_name(name: &str) -> Option<Self> {
159 if name.contains("Italian augmented sixth chord") {
160 Some(Self::Italian)
161 } else if name.contains("French augmented sixth chord") {
162 Some(Self::French)
163 } else if name.contains("German augmented sixth chord") {
164 Some(Self::German)
165 } else if name.contains("Swiss augmented sixth chord") {
166 Some(Self::Swiss)
167 } else {
168 None
169 }
170 }
171
172 fn default_inversion(self) -> &'static str {
175 match self {
176 Self::Italian => "6",
177 Self::French | Self::Swiss => "43",
178 Self::German => "65",
179 }
180 }
181
182 fn written_figure(self, figure: &str) -> String {
187 let rest = figure
188 .trim()
189 .trim_start_matches(['I', 't', 'G', 'e', 'r', 'F', 'S', 'w'])
190 .trim_start_matches('+');
191 let rest = unslash_inversion(rest);
193 let rest = rest.as_str();
194 let leading_digit = rest
195 .chars()
196 .next()
197 .is_some_and(|first| first.is_ascii_digit());
198 if !leading_digit {
199 return format!("{}{rest}", self.default_inversion());
200 }
201 if self != Self::Italian
202 && rest.starts_with('6')
203 && !rest[1..].starts_with(|c: char| c.is_ascii_digit())
204 {
205 return format!("{}{}", self.default_inversion(), &rest[1..]);
206 }
207 rest.to_string()
208 }
209
210 fn degree(self) -> u8 {
213 match self {
214 Self::Italian | Self::German => 4,
215 Self::French | Self::Swiss => 2,
216 }
217 }
218
219 fn written_alteration(self) -> i8 {
223 match self {
224 Self::French => 0,
225 _ => 1,
226 }
227 }
228
229 fn bracketed_alterations(self) -> Vec<(i8, u8)> {
233 let mut alterations = Vec::new();
234 if self != Self::French {
235 alterations.push((1, 1));
236 }
237 if matches!(self, Self::French | Self::Swiss) {
238 alterations.push((1, 3));
239 }
240 alterations
241 }
242
243 fn figure(self) -> &'static str {
244 match self {
245 Self::Italian => "It+6",
246 Self::French => "Fr+6",
247 Self::German => "Ger+6",
248 Self::Swiss => "Sw+6",
249 }
250 }
251}
252
253fn primary_figure(figure: &str) -> &str {
257 let secondary_start =
258 |next: char| next == '#' || (next.is_ascii_alphabetic() && next != 'o' && next != 'O');
259 figure
260 .char_indices()
261 .find(|(at, slash)| {
262 *slash == '/' && figure[at + 1..].chars().next().is_some_and(secondary_start)
263 })
264 .map_or(figure, |(at, _)| &figure[..at])
265}
266
267impl RomanNumeral {
268 pub fn new(figure: impl Into<String>, key: Key) -> Result<Self> {
288 Self::with_minor_defaults(
289 figure,
290 key,
291 Minor67Default::default(),
292 Minor67Default::default(),
293 )
294 }
295
296 pub fn with_minor_defaults(
299 figure: impl Into<String>,
300 key: Key,
301 sixth_minor: Minor67Default,
302 seventh_minor: Minor67Default,
303 ) -> Result<Self> {
304 Self::with_options(figure, key, sixth_minor, seventh_minor, true)
305 }
306
307 pub fn with_options(
310 figure: impl Into<String>,
311 key: Key,
312 sixth_minor: Minor67Default,
313 seventh_minor: Minor67Default,
314 case_matters: bool,
315 ) -> Result<Self> {
316 Self::over_scale(figure, key, None, sixth_minor, seventh_minor, case_matters)
317 }
318
319 pub fn over_scale(
326 figure: impl Into<String>,
327 key: Key,
328 scale: Option<crate::scale::Scale>,
329 sixth_minor: Minor67Default,
330 seventh_minor: Minor67Default,
331 case_matters: bool,
332 ) -> Result<Self> {
333 let figure = figure.into();
334 let written = fold_figure_symbols(figure.trim());
335 let trimmed = written.as_str();
336 if trimmed.is_empty() {
337 return Err(Error::Chord("roman numeral cannot be empty".to_string()));
338 }
339 validate_figure(trimmed)?;
340
341 let (aug6_primary, aug6_secondary) = if AugmentedSixthKind::from_figure(trimmed).is_some() {
346 (trimmed, None)
347 } else {
348 split_secondary(trimmed)
349 };
350 if let Some(kind) = AugmentedSixthKind::from_figure(aug6_primary) {
351 let column = kind.written_figure(aug6_primary);
357 return Ok(Self {
358 figure: written.clone(),
361 key,
362 degree: kind.degree(),
363 accidental: 0,
364 written_accidental: kind.written_alteration(),
365 inversion: parse_inversion(&column),
366 seventh: suffix_has_seventh(&column),
367 quality: RomanQuality::from(ImpliedQuality::Unstated),
368 implied_quality: ImpliedQuality::Unstated,
369 figures: FiguredBass {
370 column: Notation::parse(&expand_shorthand(&column).join(","))?,
371 written: column,
372 bracketed: kind.bracketed_alterations(),
373 ..FiguredBass::default()
374 },
375 secondary: aug6_secondary,
376 kind: RomanKind::AugmentedSixth(kind),
377 sixth_minor,
378 seventh_minor,
379 case_matters,
380 scale,
381 });
382 }
383
384 let (primary, secondary) = (aug6_primary, aug6_secondary);
385
386 let reading = match &secondary {
392 Some(secondary) => {
393 secondary_key(&key, secondary, sixth_minor, seventh_minor, case_matters)?
394 }
395 None => key.clone(),
396 };
397 let mut working = named_figure(primary, &reading);
398
399 let omitted = take_omitted_steps(&mut working);
402 let added = take_added_steps(&mut working);
403 let bracketed = take_bracketed_alterations(&mut working);
404
405 let (accidental, rest) = split_roman_accidental_prefix(&working);
406 let (roman, suffix) = split_roman_prefix(rest)?;
407 let degree = roman_degree(roman)?;
408 let (implied_quality, mut column) =
409 implied_quality_from_string(roman, suffix, case_matters);
410 let inversion = parse_inversion(&column);
411 let seventh = suffix_has_seventh(&column);
412
413 let mut numeral = Self {
414 figure: written.clone(),
415 key,
416 degree,
417 accidental,
418 written_accidental: accidental,
419 inversion,
420 seventh,
421 quality: RomanQuality::from(implied_quality),
422 implied_quality,
423 figures: FiguredBass::default(),
424 secondary,
425 kind: RomanKind::Diatonic,
426 sixth_minor,
427 seventh_minor,
428 case_matters,
429 scale,
430 };
431 numeral.raise_minor_sixth_and_seventh(&mut column)?;
432 let written = column.clone();
433 let column = strip_roman_addition_groups(&column);
437 numeral.figures = FiguredBass {
438 column: Notation::parse(&expand_shorthand(&column).join(","))?,
439 written,
440 omitted,
441 added,
442 bracketed,
443 };
444 Ok(numeral)
445 }
446
447 pub fn bracketed_alterations(&self) -> &[(i8, u8)] {
450 &self.figures.bracketed
451 }
452
453 pub fn omitted_steps(&self) -> &[u8] {
455 &self.figures.omitted
456 }
457
458 pub fn added_steps(&self) -> &[(i8, u8)] {
461 &self.figures.added
462 }
463
464 pub fn scale(&self) -> Option<&crate::scale::Scale> {
469 self.scale.as_ref()
470 }
471
472 pub fn implied_quality(&self) -> ImpliedQuality {
475 self.implied_quality
476 }
477
478 pub fn figure_numbers(&self) -> Vec<u8> {
481 self.figures.numbers()
482 }
483
484 pub fn case_matters(&self) -> bool {
486 self.case_matters
487 }
488
489 pub fn sixth_minor(&self) -> Minor67Default {
491 self.sixth_minor
492 }
493
494 pub fn seventh_minor(&self) -> Minor67Default {
496 self.seventh_minor
497 }
498
499 pub fn figures_written(&self) -> &str {
504 &self.figures.written
505 }
506
507 pub fn figures_notation(&self) -> &Notation {
510 &self.figures.column
511 }
512
513 pub fn figure(&self) -> &str {
524 &self.figure
525 }
526
527 pub fn degree(&self) -> u8 {
529 self.degree
530 }
531
532 pub fn written_accidental(&self) -> i8 {
537 self.written_accidental
538 }
539
540 pub fn accidental(&self) -> i8 {
543 self.accidental
544 }
545
546 pub fn inversion(&self) -> u8 {
548 self.inversion
549 }
550
551 pub fn secondary(&self) -> Option<&str> {
562 self.secondary.as_deref()
563 }
564
565 pub fn key(&self) -> &Key {
576 &self.key
577 }
578
579 pub fn roman_numeral(&self) -> String {
591 let numeral = self.roman_numeral_alone();
592 if matches!(self.kind, RomanKind::AugmentedSixth(_)) {
593 return numeral;
594 }
595 let prefix = match self.accidental {
596 0 => String::new(),
597 sharps if sharps > 0 => "#".repeat(sharps.unsigned_abs() as usize),
598 flats => "b".repeat(flats.unsigned_abs() as usize),
599 };
600 format!("{prefix}{numeral}")
601 }
602
603 pub fn front_alteration_string(&self) -> String {
609 let figure = figure::named_figure(primary_figure(&self.figure), &self.key);
610 let Some(mark) = figure
611 .chars()
612 .next()
613 .filter(|mark| matches!(mark, 'b' | '-' | '#'))
614 else {
615 return String::new();
616 };
617 figure
618 .chars()
619 .take_while(|written| *written == mark)
620 .collect()
621 }
622
623 pub fn roman_numeral_alone(&self) -> String {
626 if let RomanKind::AugmentedSixth(kind) = self.kind {
627 return kind.figure().trim_end_matches(['+', '6']).to_string();
628 }
629 let primary = self.figure.split('/').next().unwrap_or_default();
630 let reading = self.effective_key().unwrap_or_else(|_| self.key.clone());
634 let named = named_figure(primary, &reading);
635 let (_, unaltered) = split_roman_accidental_prefix(&named);
636 unaltered
637 .chars()
638 .take_while(|c| matches!(c, 'i' | 'v' | 'x' | 'I' | 'V' | 'X'))
639 .collect()
640 }
641
642 pub fn figure_and_key(&self) -> String {
645 format!(
646 "{} in {} {}",
647 self.figure,
648 self.key.tonic_pitch_name_with_case(),
649 self.key.mode()
650 )
651 }
652
653 pub fn scale_degree_with_alteration(&self) -> (u8, i8) {
656 (self.degree, self.accidental)
657 }
658
659 pub fn functionality_score(&self) -> u8 {
665 if self.secondary.is_some() {
666 let score = self.figure.split('/').fold(100.0, |score, part| {
667 score * f64::from(functionality_score_of(part).unwrap_or(0)) / 100.0
668 });
669 return score as u8;
670 }
671 if matches!(self.kind, RomanKind::AugmentedSixth(_)) {
674 return functionality_score_of(&self.figure.replace('+', "")).unwrap_or(0);
675 }
676 functionality_score_of(&self.figure).unwrap_or(0)
677 }
678
679 pub fn is_neapolitan(&self, require_first_inversion: bool) -> bool {
683 self.degree == 2
684 && self.accidental == -1
685 && self.quality == RomanQuality::Major
686 && (!require_first_inversion || self.inversion == 1)
687 }
688
689 pub fn is_mixture(&self, evaluate_secondary: bool) -> Result<bool> {
694 if evaluate_secondary && let Some(secondary) = &self.secondary {
695 return RomanNumeral::new(secondary.clone(), self.key.clone())?.is_mixture(true);
696 }
697 if self.kind != RomanKind::Diatonic || !(1..=7).contains(&self.degree) {
698 return Ok(false);
699 }
700 let chord = self.to_chord()?;
702 if !chord.is_triad() && !chord.is_seventh() {
703 return Ok(false);
704 }
705 let quality = match self.quality {
710 RomanQuality::Diminished | RomanQuality::HalfDiminished => "diminished",
711 RomanQuality::Minor => "minor",
712 RomanQuality::Major => "major",
713 RomanQuality::Augmented => return Ok(false),
714 };
715 let front = match self.accidental {
716 0 => "natural",
717 1 => "sharp",
718 -1 => "flat",
719 _ => "other",
720 };
721 let entry = (self.degree, quality, front);
722 Ok(match self.key.mode() {
723 "major" => {
724 MAJOR_KEY_MIXTURES.contains(&entry)
725 || (self.degree == 7
726 && self.seventh
727 && self.quality == RomanQuality::Diminished)
728 }
729 "minor" => {
730 MINOR_KEY_MIXTURES.contains(&entry)
731 || (self.degree == 7
732 && self.seventh
733 && self.quality == RomanQuality::HalfDiminished)
734 }
735 _ => false,
736 })
737 }
738
739 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
741 RomanNumeral::new(self.figure.clone(), self.key.transpose(interval)?)
742 }
743
744 pub fn effective_key_of(&self) -> Result<Key> {
748 self.effective_key()
749 }
750
751 fn effective_key(&self) -> Result<Key> {
752 let Some(secondary) = &self.secondary else {
753 return Ok(self.key.clone());
754 };
755
756 secondary_key(
757 &self.key,
758 secondary,
759 self.sixth_minor,
760 self.seventh_minor,
761 self.case_matters,
762 )
763 }
764}
765
766impl fmt::Display for RomanNumeral {
767 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
768 formatter.write_str(self.figure())
769 }
770}
771
772pub const FUNCTIONALITY_SCORES: [(&str, u8); 48] = [
775 ("I", 100),
776 ("i", 90),
777 ("V7", 80),
778 ("V", 70),
779 ("V65", 68),
780 ("I6", 65),
781 ("V6", 63),
782 ("V43", 61),
783 ("I64", 60),
784 ("IV", 59),
785 ("i6", 58),
786 ("viio7", 57),
787 ("V42", 55),
788 ("viio65", 53),
789 ("viio6", 52),
790 ("#viio65", 51),
791 ("ii", 50),
792 ("#viio6", 49),
793 ("ii65", 48),
794 ("ii43", 47),
795 ("ii42", 46),
796 ("IV6", 45),
797 ("ii6", 43),
798 ("VI", 42),
799 ("#VI", 41),
800 ("vi", 40),
801 ("viio", 39),
802 ("#viio", 38),
803 ("iio", 37),
804 ("iio42", 36),
805 ("bII6", 35),
806 ("It6", 34),
807 ("Ger65", 33),
808 ("iio43", 32),
809 ("iio65", 31),
810 ("Fr43", 30),
811 ("#vio", 28),
812 ("#vio6", 27),
813 ("III", 22),
814 ("Sw43", 21),
815 ("v", 20),
816 ("VII", 19),
817 ("VII7", 18),
818 ("IV65", 17),
819 ("IV7", 16),
820 ("iii", 15),
821 ("iii6", 12),
822 ("vi6", 10),
823];
824
825fn functionality_score_of(figure: &str) -> Option<u8> {
826 FUNCTIONALITY_SCORES
827 .iter()
828 .find(|(known, _)| *known == figure)
829 .map(|(_, score)| *score)
830}
831
832const MAJOR_KEY_MIXTURES: [(u8, &str, &str); 7] = [
833 (1, "minor", "natural"),
834 (2, "diminished", "natural"),
835 (3, "major", "flat"),
836 (4, "minor", "natural"),
837 (5, "minor", "natural"),
838 (6, "major", "flat"),
839 (7, "major", "flat"),
840];
841
842const MINOR_KEY_MIXTURES: [(u8, &str, &str); 5] = [
843 (1, "major", "natural"),
844 (2, "minor", "natural"),
845 (3, "minor", "sharp"),
846 (4, "major", "natural"),
847 (6, "minor", "sharp"),
848];
849
850#[derive(Clone, Debug, PartialEq)]
856#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
857#[must_use]
858pub struct FigureTuple {
859 pub deg_from_ref_pitch: u8,
861 pub alter: FloatType,
863 pub prefix: String,
866}
867
868#[derive(Clone, Debug, PartialEq)]
871#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
872#[must_use]
873pub struct PitchFigureTuple {
874 pub figure: FigureTuple,
876 pub pitch: Pitch,
878}
879
880#[derive(Clone, Debug, Default, PartialEq)]
889#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
890struct FiguredBass {
891 column: Notation,
893 written: String,
897 omitted: Vec<u8>,
899 added: Vec<(i8, u8)>,
902 bracketed: Vec<(i8, u8)>,
905}
906
907impl FiguredBass {
908 fn numbers(&self) -> Vec<u8> {
910 self.column
911 .numbers()
912 .iter()
913 .filter_map(|number| number.map(|number| number as u8))
914 .collect()
915 }
916
917 fn figures(&self) -> &[Figure] {
919 self.column.figures()
920 }
921
922 fn alters(&self, number: u8) -> bool {
925 self.column.figures().iter().any(|figure| {
926 figure.number() == Some(IntegerType::from(number))
927 && figure
928 .modifier()
929 .accidental()
930 .is_some_and(|accidental| accidental.alter() != 0.0)
931 })
932 }
933}
934
935#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
942#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
943pub enum ImpliedQuality {
944 #[default]
946 Unstated,
947 Major,
949 Minor,
951 Diminished,
953 HalfDiminished,
955 Augmented,
957 DominantSeventh,
959}
960
961impl From<ImpliedQuality> for RomanQuality {
962 fn from(quality: ImpliedQuality) -> Self {
965 match quality {
966 ImpliedQuality::Minor => Self::Minor,
967 ImpliedQuality::Diminished => Self::Diminished,
968 ImpliedQuality::HalfDiminished => Self::HalfDiminished,
969 ImpliedQuality::Augmented => Self::Augmented,
970 ImpliedQuality::Unstated | ImpliedQuality::Major | ImpliedQuality::DominantSeventh => {
971 Self::Major
972 }
973 }
974 }
975}
976
977impl ImpliedQuality {
978 pub fn from_name(name: &str) -> Self {
980 match name {
981 "major" => Self::Major,
982 "minor" => Self::Minor,
983 "diminished" => Self::Diminished,
984 "half-diminished" => Self::HalfDiminished,
985 "augmented" => Self::Augmented,
986 "minor-seventh" | "dominant-seventh" => Self::DominantSeventh,
987 _ => Self::Unstated,
988 }
989 }
990
991 pub fn name(self) -> &'static str {
994 match self {
995 Self::Unstated => "",
996 Self::Major => "major",
997 Self::Minor => "minor",
998 Self::Diminished => "diminished",
999 Self::HalfDiminished => "half-diminished",
1000 Self::Augmented => "augmented",
1001 Self::DominantSeventh => "dominant-seventh",
1002 }
1003 }
1004
1005 pub fn correct_semitones(self) -> &'static [u8] {
1008 match self {
1009 Self::Unstated => &[],
1010 Self::Major => &[4, 7],
1011 Self::Minor => &[3, 7],
1012 Self::Diminished => &[3, 6, 9],
1013 Self::HalfDiminished => &[3, 6, 10],
1014 Self::Augmented => &[4, 8],
1015 Self::DominantSeventh => &[4, 7, 10],
1016 }
1017 }
1018}
1019
1020#[derive(Clone, Debug, PartialEq, Eq)]
1028pub struct NumeralAlone {
1029 pub numeral: String,
1031 pub rest: String,
1033 pub degree: u8,
1035 pub alteration: i8,
1038 pub minor: bool,
1041 pub bracketed: Vec<(i8, u8)>,
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 #[test]
1048 fn numerals_past_seven_are_no_figures() {
1049 use super::RomanNumeral;
1050 use crate::Key;
1051
1052 for figure in ["VIII", "viii7", "IX", "bVIII"] {
1053 assert!(
1054 RomanNumeral::new(figure, Key::from_tonic("C").unwrap()).is_err(),
1055 "{figure}"
1056 );
1057 }
1058 assert!(RomanNumeral::new("VII", Key::from_tonic("C").unwrap()).is_ok());
1059 }
1060
1061 #[test]
1062 fn the_front_alteration_is_what_was_written() {
1063 use super::RomanNumeral;
1064 use crate::Key;
1065
1066 let cases = [
1067 ("It6", "C", ""),
1068 ("Ger65", "a", ""),
1069 ("Sw42", "C", ""),
1070 ("bVI", "C", "b"),
1071 ("-VI", "C", "-"),
1072 ("##iv", "C", "##"),
1073 ("N6", "C", "b"),
1074 ("N", "a", "b"),
1075 ("Cad64", "C", ""),
1076 ("V/bVI", "C", ""),
1077 ("bVII7/V", "C", "b"),
1078 ("vii", "a", ""),
1079 ("#viio7", "a", "#"),
1080 ("I", "C", ""),
1081 ];
1082 for (figure, tonic, written) in cases {
1083 let key = Key::from_tonic(tonic).unwrap();
1084 let numeral = RomanNumeral::new(figure, key).unwrap();
1085 assert_eq!(numeral.front_alteration_string(), written, "{figure}");
1086 }
1087 }
1088
1089 #[test]
1090 fn functionality_and_mixture_read_as_music21_reads_them() {
1091 use super::RomanNumeral;
1092 use crate::Key;
1093
1094 let numeral = |figure: &str, tonic: &str| {
1095 RomanNumeral::new(figure, Key::from_tonic(tonic).unwrap()).unwrap()
1096 };
1097
1098 for (figure, score) in [
1101 ("V+", 0),
1102 ("V+7", 0),
1103 ("It6", 34),
1104 ("Ger65", 33),
1105 ("V", 70),
1106 ("V7/V", 56),
1107 ] {
1108 assert_eq!(
1109 numeral(figure, "C").functionality_score(),
1110 score,
1111 "{figure}"
1112 );
1113 }
1114 assert_eq!(numeral("III+", "a").functionality_score(), 0);
1115
1116 for (figure, tonic, mixture) in [
1118 ("I[add6]", "a", false),
1119 ("ii7[no5]", "a", false),
1120 ("I", "a", true),
1121 ("i[add6]", "C", false),
1122 ("iv7[no5]", "C", false),
1123 ("iv", "C", true),
1124 ] {
1125 assert_eq!(
1126 numeral(figure, tonic).is_mixture(false).unwrap(),
1127 mixture,
1128 "{figure} in {tonic}"
1129 );
1130 }
1131 }
1132
1133 #[test]
1134 fn a_root_position_augmented_sixth_roots_on_its_raised_bass() {
1135 use super::RomanNumeral;
1136 use crate::Key;
1137
1138 let cases = [
1141 ("It53", "C", "F#4"),
1142 ("It53", "a", "D#5"),
1143 ("Ger7", "C", "F#4"),
1144 ("Ger7", "a", "D#5"),
1145 ("Sw7", "C", "D#4"),
1146 ("Sw7", "a", "B#4"),
1147 ];
1148 for (figure, tonic, root) in cases {
1149 let numeral = RomanNumeral::new(figure, Key::from_tonic(tonic).unwrap()).unwrap();
1150 let chord = numeral.to_chord().unwrap();
1151 assert_eq!(
1152 chord.root().unwrap().name_with_octave(),
1153 root,
1154 "{figure} in {tonic}"
1155 );
1156 }
1157 }
1158
1159 #[test]
1160 fn a_numeral_reports_how_it_was_read() {
1161 use super::{
1162 ImpliedQuality, Minor67Default, NumeralAlone, RomanNumeral, analyze_chord_with_root,
1163 parse_numeral_alone, secondary_key,
1164 };
1165 use crate::{Chord, Key, Pitch};
1166
1167 let key = Key::from_tonic("C").unwrap();
1168 let numeral = RomanNumeral::new("V7[no3][add4][#5]/ii", key.clone()).unwrap();
1169 assert_eq!(numeral.key().tonic().name(), "C");
1170 assert!(numeral.scale().is_none());
1171 assert!(numeral.case_matters());
1172 assert_eq!(numeral.sixth_minor(), Minor67Default::Quality);
1173 assert_eq!(numeral.seventh_minor(), Minor67Default::Quality);
1174 assert_eq!(numeral.implied_quality(), ImpliedQuality::Major);
1175 assert_eq!(numeral.written_accidental(), 0);
1176 assert_eq!(numeral.omitted_steps(), [3]);
1177 assert_eq!(numeral.added_steps(), [(0, 4)]);
1178 assert_eq!(numeral.bracketed_alterations(), [(1, 5)]);
1179 assert_eq!(numeral.figures_notation().numbers().len(), 3);
1181 assert_eq!(numeral.effective_key_of().unwrap().tonic().name(), "D");
1182 assert_eq!(numeral.roman_numeral_alone(), "V");
1183 assert_eq!(numeral.to_string(), "V7[no3][add4][#5]/ii");
1184 assert_eq!(numeral.figure_and_key(), "V7[no3][add4][#5]/ii in C major");
1185
1186 assert_eq!(
1187 ImpliedQuality::from_name("half-diminished").name(),
1188 "half-diminished"
1189 );
1190 assert_eq!(
1191 ImpliedQuality::from_name("nonsense"),
1192 ImpliedQuality::Unstated
1193 );
1194 assert_eq!(ImpliedQuality::Unstated.name(), "");
1195
1196 let alone: NumeralAlone = parse_numeral_alone("VI6").unwrap();
1197 assert_eq!((alone.numeral.as_str(), alone.rest.as_str()), ("VI", "6"));
1198 assert_eq!((alone.degree, alone.alteration, alone.minor), (6, 0, false));
1199 let german = parse_numeral_alone("Ger").unwrap();
1200 assert_eq!(
1201 (german.numeral.as_str(), german.rest.as_str()),
1202 ("Ger", "65")
1203 );
1204 assert!(german.minor);
1205 assert!(parse_numeral_alone("").is_err());
1206
1207 let dominant_of_dominant = secondary_key(
1208 &key,
1209 "V",
1210 Minor67Default::Quality,
1211 Minor67Default::Quality,
1212 true,
1213 )
1214 .unwrap();
1215 assert_eq!(dominant_of_dominant.tonic().name(), "G");
1216 assert_eq!(dominant_of_dominant.mode(), "major");
1217 let of_supertonic = secondary_key(
1218 &key,
1219 "ii",
1220 Minor67Default::Quality,
1221 Minor67Default::Quality,
1222 true,
1223 )
1224 .unwrap();
1225 assert_eq!(of_supertonic.mode(), "minor");
1226
1227 let chord = Chord::new("E4 G4 C5").unwrap();
1228 let root = Pitch::from_name("C4").unwrap();
1229 let analyzed = analyze_chord_with_root(&chord, key, &root)
1230 .unwrap()
1231 .unwrap();
1232 assert_eq!(analyzed.figure(), "I6");
1233 }
1234
1235 #[test]
1239 fn figures_are_read_above_a_reference_pitch_and_corrected_for_minor() {
1240 use super::{
1241 FigureTuple, correct_rn_alteration_for_minor, correct_suffix_for_chord_quality,
1242 figure_tuples,
1243 };
1244 use crate::{Chord, Key, Pitch};
1245
1246 let c_major = Key::from_tonic("C").unwrap();
1247 let c_minor = Key::from_tonic("c").unwrap();
1248 let figure = |name: &str, key: &Key, reference: &str| {
1249 FigureTuple::from_pitch_and_reference(
1250 &Pitch::from_name(name).unwrap(),
1251 key,
1252 &Pitch::from_name(reference).unwrap(),
1253 )
1254 .unwrap()
1255 };
1256 let read = |figure: FigureTuple| (figure.deg_from_ref_pitch, figure.alter, figure.prefix);
1257 assert_eq!(
1258 read(figure("Ab3", &c_major, "F#2")),
1259 (3, -1.0, "b".to_string())
1260 );
1261 assert_eq!(
1262 read(figure("E--4", &c_minor, "C3")),
1263 (3, -1.0, "b".to_string())
1264 );
1265 assert_eq!(read(figure("E-4", &c_minor, "C3")), (3, 0.0, String::new()));
1266 assert_eq!(
1267 read(figure("E#4", &c_minor, "C3")),
1268 (3, 2.0, "##".to_string())
1269 );
1270 assert_eq!(
1271 read(figure("A4", &c_minor, "C3")),
1272 (6, 1.0, "#".to_string())
1273 );
1274 assert_eq!(
1275 read(figure("B5", &c_minor, "C3")),
1276 (7, 1.0, "#".to_string())
1277 );
1278
1279 let tuples = figure_tuples(&Chord::new("F#2 D3 A-3 C#4").unwrap(), &c_minor).unwrap();
1280 let read_all: Vec<(u8, FloatType, String)> = tuples
1281 .iter()
1282 .map(|tuple| read(tuple.figure.clone()))
1283 .collect();
1284 assert_eq!(
1285 read_all,
1286 [
1287 (1, 1.0, "#".to_string()),
1288 (6, 0.0, String::new()),
1289 (3, 0.0, String::new()),
1290 (5, 1.0, "#".to_string()),
1291 ]
1292 );
1293 assert_eq!(tuples[2].pitch.name_with_octave(), "Ab3");
1294 assert!(
1295 figure_tuples(&Chord::new("").unwrap(), &c_minor)
1296 .unwrap()
1297 .is_empty()
1298 );
1299
1300 let sixth = |alter: FloatType, prefix: &str| FigureTuple {
1301 deg_from_ref_pitch: 6,
1302 alter,
1303 prefix: prefix.to_string(),
1304 };
1305 assert_eq!(
1306 read(correct_rn_alteration_for_minor(
1307 &sixth(-1.0, ""),
1308 &c_minor,
1309 false
1310 )),
1311 (6, -1.0, "b".to_string())
1312 );
1313 assert_eq!(
1314 read(correct_rn_alteration_for_minor(
1315 &sixth(0.0, ""),
1316 &c_minor,
1317 false
1318 )),
1319 (6, 0.0, "b".to_string())
1320 );
1321 let raised_seventh = figure("B5", &c_minor, "C3");
1322 assert_eq!(
1323 read(correct_rn_alteration_for_minor(
1324 &raised_seventh,
1325 &c_minor,
1326 false
1327 )),
1328 (7, 0.0, String::new())
1329 );
1330 assert_eq!(
1331 read(correct_rn_alteration_for_minor(
1332 &raised_seventh,
1333 &c_minor,
1334 true
1335 )),
1336 (7, 1.0, "#".to_string())
1337 );
1338 assert_eq!(
1339 read(correct_rn_alteration_for_minor(
1340 &sixth(2.0, "##"),
1341 &c_minor,
1342 false
1343 )),
1344 (6, 1.0, "#".to_string())
1345 );
1346 let in_major = sixth(-1.0, "b");
1347 assert_eq!(
1348 correct_rn_alteration_for_minor(&in_major, &c_major, false),
1349 in_major
1350 );
1351 let fourth = FigureTuple {
1352 deg_from_ref_pitch: 4,
1353 alter: -1.0,
1354 prefix: "b".to_string(),
1355 };
1356 assert_eq!(
1357 correct_rn_alteration_for_minor(&fourth, &c_minor, false),
1358 fourth
1359 );
1360
1361 assert_eq!(
1362 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G4").unwrap(), "6"),
1363 "6"
1364 );
1365 assert_eq!(
1366 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G-4").unwrap(), "6"),
1367 "o6"
1368 );
1369 assert_eq!(
1370 correct_suffix_for_chord_quality(&Chord::new("C4 E4 G#4").unwrap(), ""),
1371 "+"
1372 );
1373 assert_eq!(
1374 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A4").unwrap(), "7"),
1375 "ø7"
1376 );
1377 assert_eq!(
1378 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A-4").unwrap(), "o7"),
1379 "o7"
1380 );
1381 }
1382
1383 #[test]
1386 fn the_sixth_and_seventh_of_a_minor_key_are_raised_by_quality() {
1387 use super::{ImpliedQuality, Minor67Default, adjust_minor_vi_and_vii_by_quality};
1388 use crate::Key;
1389
1390 let minor = Key::from_tonic("c").unwrap();
1391 let major = Key::from_tonic("C").unwrap();
1392 let adjust = |key: &Key, reading, quality, accidental| {
1393 adjust_minor_vi_and_vii_by_quality(key, reading, quality, accidental)
1394 };
1395 assert_eq!(
1396 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1397 1
1398 );
1399 assert_eq!(
1400 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Major, 0),
1401 0
1402 );
1403 assert_eq!(
1404 adjust(&major, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1405 0
1406 );
1407 assert_eq!(
1408 adjust(&minor, Minor67Default::Flat, ImpliedQuality::Diminished, 0),
1409 0
1410 );
1411 assert_eq!(
1412 adjust(&minor, Minor67Default::Sharp, ImpliedQuality::Major, 0),
1413 1
1414 );
1415 assert_eq!(
1418 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 1),
1419 1
1420 );
1421 assert_eq!(
1422 adjust(
1423 &minor,
1424 Minor67Default::Cautionary,
1425 ImpliedQuality::Major,
1426 -1
1427 ),
1428 0
1429 );
1430 assert_eq!(
1431 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 0),
1432 1
1433 );
1434 }
1435
1436 #[test]
1437 fn a_numeral_reports_the_digits_it_was_written_with() {
1438 let major = Key::from_tonic("C").unwrap();
1439 for (figure, written) in [
1440 ("V65", "65"),
1441 ("V", ""),
1442 ("I7#5b3", "7#5b3"),
1443 ("viio6", "6"),
1444 ("vii\u{f8}7", "7"),
1445 ] {
1446 let numeral = RomanNumeral::new(figure, major.clone()).unwrap();
1447 assert_eq!(numeral.figures_written(), written, "{figure}");
1448 }
1449 let minor = Key::from_tonic("c").unwrap();
1450 for (figure, written) in [("Fr43", "43"), ("Fr+6", "43"), ("It+6", "6"), ("Ger", "65")] {
1451 let numeral = RomanNumeral::new(figure, minor.clone()).unwrap();
1452 assert_eq!(numeral.figures_written(), written, "{figure}");
1453 }
1454 }
1455
1456 #[test]
1457 fn a_minor_key_raises_its_sixth_and_seventh_where_the_figure_asks() {
1458 let minor = Key::from_tonic_mode("a", Some("minor")).unwrap();
1459 let leading = RomanNumeral::new("viio7", minor.clone()).unwrap();
1462 assert_eq!(
1463 leading.to_chord().unwrap().pitch_names(),
1464 ["G#", "B", "D", "F"]
1465 );
1466 assert_eq!(leading.roman_numeral(), "#vii");
1467 assert_eq!(leading.roman_numeral_alone(), "vii");
1468 assert_eq!(leading.scale_degree_with_alteration(), (7, 1));
1469
1470 assert_eq!(
1472 RomanNumeral::new("vi", minor.clone())
1473 .unwrap()
1474 .to_chord()
1475 .unwrap()
1476 .pitch_names(),
1477 ["F#", "A", "C#"]
1478 );
1479 assert_eq!(
1481 RomanNumeral::new("VI", minor.clone())
1482 .unwrap()
1483 .to_chord()
1484 .unwrap()
1485 .pitch_names(),
1486 ["F", "A", "C"]
1487 );
1488 let major = Key::from_tonic_mode("C", Some("major")).unwrap();
1490 assert_eq!(
1491 RomanNumeral::new("vi", major)
1492 .unwrap()
1493 .to_chord()
1494 .unwrap()
1495 .pitch_names(),
1496 ["A", "C", "E"]
1497 );
1498 }
1499
1500 #[test]
1501 fn the_four_readings_of_a_minor_sixth_and_seventh() {
1502 let minor = Key::from_tonic_mode("c", Some("minor")).unwrap();
1503 let read = |figure: &str, sixth: Minor67Default| {
1504 RomanNumeral::with_minor_defaults(figure, minor.clone(), sixth, sixth)
1505 .unwrap()
1506 .to_chord()
1507 .unwrap()
1508 .pitch_names()
1509 .join(" ")
1510 };
1511
1512 assert_eq!(read("vi", Minor67Default::Quality), "A C E");
1515 assert_eq!(read("VI", Minor67Default::Quality), "Ab C Eb");
1516
1517 assert_eq!(read("vi", Minor67Default::Flat), "Ab Cb Eb");
1520 assert_eq!(read("#vi", Minor67Default::Flat), "A C E");
1521
1522 assert_eq!(read("VI", Minor67Default::Sharp), "A C# E");
1524 assert_eq!(read("bVI", Minor67Default::Sharp), "Ab C Eb");
1525
1526 assert_eq!(read("#vi", Minor67Default::Cautionary), "A C E");
1530 assert_eq!(read("vi", Minor67Default::Cautionary), "A C E");
1531 assert_eq!(read("bVI", Minor67Default::Cautionary), "Ab C Eb");
1532 assert_eq!(read("VI", Minor67Default::Cautionary), "Ab C Eb");
1533 }
1534
1535 #[test]
1536 fn the_neapolitan_can_be_written_by_name() {
1537 let key = Key::from_tonic_mode("c#", Some("minor")).unwrap();
1540 let named = RomanNumeral::new("N6", key.clone()).unwrap();
1541 assert_eq!(named.figure(), "N6");
1542 assert_eq!(named.degree(), 2);
1543 assert_eq!(named.accidental(), -1);
1544 assert_eq!(named.inversion(), 1);
1545 assert!(named.is_neapolitan(true));
1546
1547 let spelled = RomanNumeral::new("bII6", key.clone()).unwrap();
1548 assert_eq!(
1549 named.to_chord().unwrap().pitch_names(),
1550 spelled.to_chord().unwrap().pitch_names()
1551 );
1552
1553 let root_position = RomanNumeral::new("N53", key).unwrap();
1555 assert_eq!(root_position.inversion(), 0);
1556 }
1557
1558 #[test]
1559 fn inversion_names_and_tonic_dominant_reading_match_music21() {
1560 let chord = |notes: &str| Chord::new(notes).unwrap();
1561 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), None), "");
1562 assert_eq!(roman_inversion_name(&chord("E4 G4 C5"), None), "6");
1563 assert_eq!(roman_inversion_name(&chord("G3 C4 E4"), None), "64");
1564 assert_eq!(roman_inversion_name(&chord("C4 E4 G4 B-4"), None), "7");
1565 assert_eq!(roman_inversion_name(&chord("E4 G4 B-4 C5"), None), "65");
1566 assert_eq!(roman_inversion_name(&chord("G3 B-3 C4 E4"), None), "43");
1567 assert_eq!(roman_inversion_name(&chord("B-3 C4 E4 G4"), None), "42");
1568 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(2)), "64");
1569 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(3)), "");
1570 assert_eq!(roman_inversion_name(&Chord::empty(), None), "");
1571
1572 let cases = [
1573 ("C4 E4 G4", "C", Some("I")),
1574 ("E4 G4 C5", "C", Some("I6")),
1575 ("G3 B3 D4 F4", "C", Some("V7")),
1576 ("D4 F4 A4", "C", Some("V43")),
1577 ("A3 C4 E4", "a", Some("i")),
1578 ("E3 G#3 B3", "a", Some("V")),
1579 ("C4 E4", "C", Some("I")),
1580 ("G4 D5", "C", Some("V")),
1581 ("B3 D4", "C", Some("V")),
1582 ("F#4 A4", "C", None),
1583 ];
1584 for (notes, key, expected) in cases {
1585 let reading =
1586 identify_as_tonic_or_dominant(&chord(notes), &Key::from_tonic(key).unwrap())
1587 .unwrap();
1588 assert_eq!(reading.as_deref(), expected, "{notes} in {key}");
1589 }
1590 }
1591
1592 #[test]
1593 #[allow(clippy::type_complexity)]
1594 fn numeral_names_scores_and_mixture_match_music21() {
1595 let cases: [(&str, &str, &str, &str, (u8, i8), u8, bool, bool, bool); 18] = [
1596 (
1597 "V7",
1598 "C",
1599 "V",
1600 "V7 in C major",
1601 (5, 0),
1602 80,
1603 false,
1604 false,
1605 false,
1606 ),
1607 (
1608 "bII6",
1609 "C",
1610 "bII",
1611 "bII6 in C major",
1612 (2, -1),
1613 35,
1614 true,
1615 true,
1616 false,
1617 ),
1618 (
1619 "bII",
1620 "C",
1621 "bII",
1622 "bII in C major",
1623 (2, -1),
1624 0,
1625 false,
1626 true,
1627 false,
1628 ),
1629 (
1630 "bII6",
1631 "a",
1632 "bII",
1633 "bII6 in a minor",
1634 (2, -1),
1635 35,
1636 true,
1637 true,
1638 false,
1639 ),
1640 (
1641 "viio7",
1642 "C",
1643 "vii",
1644 "viio7 in C major",
1645 (7, 0),
1646 57,
1647 false,
1648 false,
1649 true,
1650 ),
1651 (
1652 "I",
1653 "C",
1654 "I",
1655 "I in C major",
1656 (1, 0),
1657 100,
1658 false,
1659 false,
1660 false,
1661 ),
1662 (
1663 "i",
1664 "a",
1665 "i",
1666 "i in a minor",
1667 (1, 0),
1668 90,
1669 false,
1670 false,
1671 false,
1672 ),
1673 (
1674 "V65/V",
1675 "C",
1676 "V",
1677 "V65/V in C major",
1678 (5, 0),
1679 47,
1680 false,
1681 false,
1682 false,
1683 ),
1684 (
1685 "iv",
1686 "C",
1687 "iv",
1688 "iv in C major",
1689 (4, 0),
1690 0,
1691 false,
1692 false,
1693 true,
1694 ),
1695 (
1696 "bVI",
1697 "C",
1698 "bVI",
1699 "bVI in C major",
1700 (6, -1),
1701 0,
1702 false,
1703 false,
1704 true,
1705 ),
1706 (
1707 "I",
1708 "a",
1709 "I",
1710 "I in a minor",
1711 (1, 0),
1712 100,
1713 false,
1714 false,
1715 true,
1716 ),
1717 (
1718 "III",
1719 "a",
1720 "III",
1721 "III in a minor",
1722 (3, 0),
1723 22,
1724 false,
1725 false,
1726 false,
1727 ),
1728 (
1729 "#iii",
1730 "a",
1731 "#iii",
1732 "#iii in a minor",
1733 (3, 1),
1734 0,
1735 false,
1736 false,
1737 true,
1738 ),
1739 (
1740 "ii",
1741 "C",
1742 "ii",
1743 "ii in C major",
1744 (2, 0),
1745 50,
1746 false,
1747 false,
1748 false,
1749 ),
1750 (
1751 "iio",
1752 "C",
1753 "ii",
1754 "iio in C major",
1755 (2, 0),
1756 37,
1757 false,
1758 false,
1759 true,
1760 ),
1761 (
1762 "IV",
1763 "a",
1764 "IV",
1765 "IV in a minor",
1766 (4, 0),
1767 59,
1768 false,
1769 false,
1770 true,
1771 ),
1772 (
1773 "bVII",
1774 "C",
1775 "bVII",
1776 "bVII in C major",
1777 (7, -1),
1778 0,
1779 false,
1780 false,
1781 true,
1782 ),
1783 (
1784 "v",
1785 "C",
1786 "v",
1787 "v in C major",
1788 (5, 0),
1789 20,
1790 false,
1791 false,
1792 true,
1793 ),
1794 ];
1795 for (figure, key, numeral, figure_and_key, degree, score, neapolitan, loose, mixture) in
1796 cases
1797 {
1798 let rn = RomanNumeral::new(figure, Key::from_tonic(key).unwrap()).unwrap();
1799 assert_eq!(rn.roman_numeral(), numeral, "{figure} in {key}");
1800 assert_eq!(rn.figure_and_key(), figure_and_key, "{figure} in {key}");
1801 assert_eq!(
1802 rn.scale_degree_with_alteration(),
1803 degree,
1804 "{figure} in {key}"
1805 );
1806 assert_eq!(rn.functionality_score(), score, "{figure} in {key}");
1807 assert_eq!(rn.is_neapolitan(true), neapolitan, "{figure} in {key}");
1808 assert_eq!(rn.is_neapolitan(false), loose, "{figure} in {key}");
1809 assert_eq!(rn.is_mixture(false).unwrap(), mixture, "{figure} in {key}");
1810 assert_eq!(rn.is_mixture(true).unwrap(), mixture, "{figure} in {key}");
1811 }
1812 for (key, mixture) in [("a", true), ("A", false)] {
1816 let half = RomanNumeral::new("viiø7", Key::from_tonic(key).unwrap()).unwrap();
1817 assert_eq!(half.is_mixture(false).unwrap(), mixture, "viiø7 in {key}");
1818 }
1819
1820 let italian = RomanNumeral::new("It6", Key::from_tonic("C").unwrap()).unwrap();
1821 assert_eq!(italian.roman_numeral(), "It");
1822 assert_eq!(italian.functionality_score(), 34);
1823 assert!(!italian.is_mixture(false).unwrap());
1824 let transposed = RomanNumeral::new("bII6", Key::from_tonic("a").unwrap())
1825 .unwrap()
1826 .transpose(&Interval::from_name("M2").unwrap())
1827 .unwrap();
1828 assert_eq!(transposed.figure_and_key(), "bII6 in b minor");
1829 }
1830
1831 #[test]
1832 fn functionality_scores_table_has_no_duplicates() {
1833 let mut figures: Vec<&str> = FUNCTIONALITY_SCORES.iter().map(|(f, _)| *f).collect();
1834 figures.sort_unstable();
1835 figures.dedup();
1836 assert_eq!(figures.len(), FUNCTIONALITY_SCORES.len());
1837 }
1838 use super::*;
1839
1840 #[test]
1841 fn secondary_dominant_resolves_to_chord() {
1842 let key = Key::from_tonic_mode("C", "major").unwrap();
1843 let rn = RomanNumeral::new("V7/V", key).unwrap();
1844 assert_eq!(rn.degree(), 5);
1845 assert_eq!(rn.secondary(), Some("V"));
1846 assert_eq!(
1847 rn.to_chord().unwrap().pitched_common_name(),
1848 "D-dominant seventh chord"
1849 );
1850 }
1851
1852 #[test]
1854 fn a_chord_is_named_the_way_music21_names_it() {
1855 let cases: &[(&str, &str, &str)] = &[
1856 ("C E G", "C", "I"),
1857 ("G B D F", "C", "V43"),
1858 ("C E- G-", "C", "io5b3"),
1859 ("G B D F", "a", "bVII43"),
1860 ("B D F A", "C", "viiø65"),
1861 ("F A C", "C", "IV64"),
1862 ("A C E", "C", "vi6"),
1863 ("A C E", "a", "i6"),
1864 ("F A C", "a", "bVI64"),
1865 ("C E G#", "a", "III+"),
1866 ("D- F Ab", "C", "bII"),
1867 ("B4 D5 F5 A-5", "c", "viio7"),
1868 ("A-3 C4 F#4", "c", "It6"),
1869 ("A-3 C4 D4 F#4", "c", "Fr43"),
1870 ("A-3 C4 E-4 F#4", "c", "Ger65"),
1871 ("A-3 C4 E-4 F#4", "C", "Ger65"),
1872 ("C4 E4 G4 B-4", "F", "V7"),
1873 ("D4 F4 A4 C5", "C", "ii7"),
1874 ("D4 F4 A4 C#5", "C", "ii#7"),
1875 ("F4 A4 C5 E5", "C", "IV7"),
1876 ("E4 G4 B4 D5", "C", "iii7"),
1877 ("C4 E-4 G4 B4", "c", "i#7"),
1878 ("C4 E4 G4 B4", "C", "I7"),
1879 ("E-4 G4 B-4 D5", "c", "III7"),
1880 ("C4 E4 G4 B4 D5", "C", "I7532"),
1881 ("G3 C4 E4", "C", "I64"),
1882 ("E3 G3 C4", "C", "I6"),
1883 ("F#3 A3 C4 E-4", "G", "viiob753"),
1884 ("D F# A", "G", "V"),
1885 ("C E G B-", "C", "Ib753"),
1886 ];
1887 for (chord, key, figure) in cases {
1888 let key = Key::from_tonic(key).unwrap();
1889 let numeral = roman_numeral_from_chord(&Chord::new(*chord).unwrap(), Some(&key))
1890 .unwrap()
1891 .unwrap();
1892 assert_eq!(
1893 numeral.figure(),
1894 *figure,
1895 "{chord} in {}",
1896 key.tonic_pitch_name_with_case()
1897 );
1898 }
1899 for (chord, figure, key_name) in [
1900 ("D F# A", "I", "D major"),
1901 ("D F A", "i", "d minor"),
1902 ("E G# B D", "Ib8642", "E major"),
1903 ("C E G", "I", "C major"),
1904 ("A-3 C4 E-4 F#4", "Ger65", "c minor"),
1905 ("G B D F", "I64b3", "G major"),
1906 ] {
1907 let numeral = roman_numeral_from_chord(&Chord::new(chord).unwrap(), None)
1908 .unwrap()
1909 .unwrap();
1910 assert_eq!(numeral.figure(), figure, "{chord} with no key");
1911 let key = numeral.key();
1912 assert_eq!(
1913 format!("{} {}", key.tonic_pitch_name_with_case(), key.mode()),
1914 key_name,
1915 "{chord} with no key"
1916 );
1917 }
1918 assert!(
1919 roman_numeral_from_chord(&Chord::empty(), None)
1920 .unwrap()
1921 .is_none()
1922 );
1923 }
1924
1925 #[test]
1926 fn analyzes_chord_in_key() {
1927 let key = Key::from_tonic_mode("C", "major").unwrap();
1928 let chord = Chord::new("G B D F").unwrap();
1929 let rn = RomanNumeral::analyze(&chord, key).unwrap().unwrap();
1930 assert_eq!(rn.figure(), "V7");
1931 }
1932
1933 #[test]
1934 fn analyzes_accidentals_inversions_and_half_diminished_quality() {
1935 let key = Key::from_tonic_mode("C", "major").unwrap();
1936
1937 let neapolitan = Chord::new("D- F Ab").unwrap();
1938 let rn = RomanNumeral::analyze(&neapolitan, key.clone())
1939 .unwrap()
1940 .unwrap();
1941 assert_eq!(rn.figure(), "bII");
1942 assert_eq!(rn.degree(), 2);
1943 assert_eq!(rn.accidental(), -1);
1944
1945 let first_inversion = Chord::new("E4 G4 C5").unwrap();
1946 let rn = RomanNumeral::analyze(&first_inversion, key.clone())
1947 .unwrap()
1948 .unwrap();
1949 assert_eq!(rn.figure(), "I6");
1950
1951 let leading_tone = Chord::new("B D F A").unwrap();
1952 let rn = RomanNumeral::analyze(&leading_tone, key).unwrap().unwrap();
1953 assert_eq!(rn.figure(), "vii\u{00f8}7");
1954 }
1955
1956 #[test]
1957 fn analyzes_with_explicit_root_for_browser_style_sets() {
1958 let key = Key::from_tonic_mode("C", "major").unwrap();
1959 let root = Pitch::from_name("C").unwrap();
1960 let chord = Chord::new("C E G").unwrap();
1961 let rn = RomanNumeral::analyze_with_root(&chord, key.clone(), &root)
1962 .unwrap()
1963 .unwrap();
1964 assert_eq!(rn.figure(), "I");
1965
1966 let seventh = Chord::new("C E G B-").unwrap();
1967 let rn = RomanNumeral::analyze_with_root(&seventh, key, &root)
1968 .unwrap()
1969 .unwrap();
1970 assert_eq!(rn.figure(), "I7");
1971 }
1972
1973 #[test]
1974 fn analyzes_augmented_sixth_chords_functionally() {
1975 let key = Key::from_tonic_mode("C", "minor").unwrap();
1976 let root = Pitch::from_name("C").unwrap();
1977 let french = Chord::new("C D F# Ab").unwrap();
1978 let rn = RomanNumeral::analyze_with_root(&french, key.clone(), &root)
1979 .unwrap()
1980 .unwrap();
1981 assert_eq!(rn.figure(), "Fr+6");
1982
1983 let german = Chord::new("Ab C E- F#").unwrap();
1984 let rn = RomanNumeral::analyze(&german, key).unwrap().unwrap();
1985 assert_eq!(rn.figure(), "Ger+6");
1986 }
1987
1988 #[test]
1989 fn figured_bass_shorthands_give_music21_inversions() {
1990 let key = Key::from_tonic_mode("C", "major").unwrap();
1993 for (figure, expected) in [
1994 ("V", 0),
1995 ("V6", 1),
1996 ("V64", 2),
1997 ("V7", 0),
1998 ("V65", 1),
1999 ("V43", 2),
2000 ("V42", 3),
2001 ("V642", 3),
2002 ("V653", 1),
2003 ("V643", 2),
2004 ("V63", 1),
2005 ("V53", 0),
2006 ("V9", 0),
2007 ] {
2008 let numeral = RomanNumeral::new(figure, key.clone())
2009 .unwrap_or_else(|err| panic!("{figure} should parse: {err}"));
2010 assert_eq!(numeral.inversion(), expected, "{figure}");
2011 }
2012 }
2013
2014 #[test]
2015 fn a_figure_expands_into_a_figured_bass_column() {
2016 let numbers = |figure: &str| {
2019 RomanNumeral::new(figure, Key::from_tonic("C").unwrap())
2020 .unwrap()
2021 .figure_numbers()
2022 };
2023 assert_eq!(numbers("V"), vec![5, 3]);
2024 assert_eq!(numbers("V7"), vec![7, 5, 3]);
2025 assert_eq!(numbers("V65"), vec![6, 5, 3]);
2026 assert_eq!(numbers("V43"), vec![6, 4, 3]);
2027 assert_eq!(numbers("V9"), vec![9, 7, 5, 3]);
2028 assert_eq!(numbers("V7#5b3"), vec![7, 5, 3]);
2030 }
2031
2032 #[test]
2033 fn a_column_says_which_degree_is_in_the_bass() {
2034 assert_eq!(bass_scale_degree_from_notation(5, &[6, 3]).unwrap(), 7);
2037 assert_eq!(bass_scale_degree_from_notation(1, &[5, 3]).unwrap(), 1);
2038 assert_eq!(bass_scale_degree_from_notation(2, &[6, 5, 3]).unwrap(), 4);
2039 assert_eq!(bass_scale_degree_from_notation(5, &[5, 4]).unwrap(), 5);
2041 }
2042
2043 #[test]
2044 fn a_figure_alters_one_note_rather_than_naming_another_chord() {
2045 let names = |figure: &str, key: &str| {
2046 RomanNumeral::new(figure, Key::from_tonic(key).unwrap())
2047 .unwrap()
2048 .to_chord()
2049 .unwrap()
2050 .pitch_names()
2051 };
2052 assert_eq!(names("V7b5", "C"), ["G", "Db", "F"]);
2055 assert_eq!(names("V[no3]", "F"), ["C", "G"]);
2056 assert_eq!(names("I[add4][no3]", "C"), ["C", "F", "G"]);
2057 assert_eq!(names("i#7", "c"), ["C", "Eb", "G", "B"]);
2060 assert_eq!(names("i#7", "C"), ["C", "Eb", "G", "B#"]);
2061 }
2062
2063 #[test]
2064 fn a_numeral_can_be_read_over_a_scale_that_is_not_a_key() {
2065 let scale = crate::scale::Scale::new(
2068 crate::scale::ScaleType::Octatonic,
2069 Pitch::from_name("C2").unwrap(),
2070 );
2071 let numeral = RomanNumeral::over_scale(
2072 "I9",
2073 Key::from_tonic("C").unwrap(),
2074 Some(scale),
2075 Minor67Default::Quality,
2076 Minor67Default::Quality,
2077 false,
2078 )
2079 .unwrap();
2080 let pitches: Vec<String> = numeral
2081 .to_chord()
2082 .unwrap()
2083 .pitches()
2084 .iter()
2085 .map(Pitch::name_with_octave)
2086 .collect();
2087 assert_eq!(pitches, ["C2", "Eb2", "Gb2", "A2", "C3"]);
2088 }
2089
2090 #[test]
2091 fn roman_numerals_parse_inversions_and_qualities() {
2092 let key = Key::from_tonic_mode("C", "major").unwrap();
2093 let first_inversion = RomanNumeral::new("I6", key.clone()).unwrap();
2094 assert_eq!(first_inversion.inversion(), 1);
2095 assert_eq!(
2096 first_inversion
2097 .to_chord()
2098 .unwrap()
2099 .pitches()
2100 .into_iter()
2101 .map(|pitch| pitch.name())
2102 .collect::<Vec<_>>(),
2103 vec!["E", "G", "C"]
2104 );
2105
2106 let diminished = RomanNumeral::new("viio7", key.clone()).unwrap();
2107 assert_eq!(diminished.degree(), 7);
2108 assert!(
2109 diminished
2110 .to_chord()
2111 .unwrap()
2112 .common_name()
2113 .contains("diminished")
2114 );
2115
2116 let half_diminished = RomanNumeral::new("vii\u{00f8}7", key.clone()).unwrap();
2117 assert_eq!(half_diminished.degree(), 7);
2118 assert_eq!(half_diminished.accidental(), 0);
2119 assert!(
2120 half_diminished
2121 .to_chord()
2122 .unwrap()
2123 .common_name()
2124 .contains("half-diminished")
2125 );
2126
2127 let borrowed = RomanNumeral::new("bII", key.clone()).unwrap();
2128 assert_eq!(borrowed.degree(), 2);
2129 assert_eq!(borrowed.accidental(), -1);
2130
2131 let added_thirteenth = RomanNumeral::new("I add(13)", key.clone()).unwrap();
2132 assert_eq!(added_thirteenth.inversion(), 0);
2133 assert_eq!(
2134 added_thirteenth.to_chord().unwrap().common_name(),
2135 "major triad"
2136 );
2137
2138 let augmented = RomanNumeral::new("III+", key).unwrap();
2139 assert_eq!(
2140 augmented
2141 .to_chord()
2142 .unwrap()
2143 .pitches()
2144 .into_iter()
2145 .map(|pitch| pitch.name())
2146 .collect::<Vec<_>>(),
2147 vec!["E", "G#", "B#"]
2148 );
2149 }
2150
2151 #[test]
2152 fn roman_numerals_parse_augmented_sixth_figures() {
2153 let key = Key::from_tonic_mode("C", "minor").unwrap();
2154 let french = RomanNumeral::new("Fr+6", key).unwrap();
2155 assert_eq!(french.degree(), 2);
2159 assert_eq!(french.accidental(), 0);
2160 assert_eq!(
2161 french
2162 .to_chord()
2163 .unwrap()
2164 .pitches()
2165 .into_iter()
2166 .map(|pitch| pitch.name())
2167 .collect::<Vec<_>>(),
2168 vec!["Ab", "C", "D", "F#"]
2169 );
2170 }
2171
2172 #[test]
2173 fn roman_numerals_report_invalid_figures_and_empty_analysis() {
2174 let key = Key::from_tonic_mode("C", "major").unwrap();
2175
2176 assert!(RomanNumeral::new("", key.clone()).is_err());
2177 assert!(RomanNumeral::new("Q", key.clone()).is_err());
2178 assert!(analyze_chord(&Chord::empty(), key).unwrap().is_none());
2179 }
2180}