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> {
273 Self::with_minor_defaults(
274 figure,
275 key,
276 Minor67Default::default(),
277 Minor67Default::default(),
278 )
279 }
280
281 pub fn with_minor_defaults(
284 figure: impl Into<String>,
285 key: Key,
286 sixth_minor: Minor67Default,
287 seventh_minor: Minor67Default,
288 ) -> Result<Self> {
289 Self::with_options(figure, key, sixth_minor, seventh_minor, true)
290 }
291
292 pub fn with_options(
295 figure: impl Into<String>,
296 key: Key,
297 sixth_minor: Minor67Default,
298 seventh_minor: Minor67Default,
299 case_matters: bool,
300 ) -> Result<Self> {
301 Self::over_scale(figure, key, None, sixth_minor, seventh_minor, case_matters)
302 }
303
304 pub fn over_scale(
311 figure: impl Into<String>,
312 key: Key,
313 scale: Option<crate::scale::Scale>,
314 sixth_minor: Minor67Default,
315 seventh_minor: Minor67Default,
316 case_matters: bool,
317 ) -> Result<Self> {
318 let figure = figure.into();
319 let written = fold_figure_symbols(figure.trim());
320 let trimmed = written.as_str();
321 if trimmed.is_empty() {
322 return Err(Error::Chord("roman numeral cannot be empty".to_string()));
323 }
324 validate_figure(trimmed)?;
325
326 let (aug6_primary, aug6_secondary) = if AugmentedSixthKind::from_figure(trimmed).is_some() {
331 (trimmed, None)
332 } else {
333 split_secondary(trimmed)
334 };
335 if let Some(kind) = AugmentedSixthKind::from_figure(aug6_primary) {
336 let column = kind.written_figure(aug6_primary);
342 return Ok(Self {
343 figure: written.clone(),
346 key,
347 degree: kind.degree(),
348 accidental: 0,
349 written_accidental: kind.written_alteration(),
350 inversion: parse_inversion(&column),
351 seventh: suffix_has_seventh(&column),
352 quality: RomanQuality::from(ImpliedQuality::Unstated),
353 implied_quality: ImpliedQuality::Unstated,
354 figures: FiguredBass {
355 column: Notation::parse(&expand_shorthand(&column).join(","))?,
356 written: column,
357 bracketed: kind.bracketed_alterations(),
358 ..FiguredBass::default()
359 },
360 secondary: aug6_secondary,
361 kind: RomanKind::AugmentedSixth(kind),
362 sixth_minor,
363 seventh_minor,
364 case_matters,
365 scale,
366 });
367 }
368
369 let (primary, secondary) = (aug6_primary, aug6_secondary);
370
371 let reading = match &secondary {
377 Some(secondary) => {
378 secondary_key(&key, secondary, sixth_minor, seventh_minor, case_matters)?
379 }
380 None => key.clone(),
381 };
382 let mut working = named_figure(primary, &reading);
383
384 let omitted = take_omitted_steps(&mut working);
387 let added = take_added_steps(&mut working);
388 let bracketed = take_bracketed_alterations(&mut working);
389
390 let (accidental, rest) = split_roman_accidental_prefix(&working);
391 let (roman, suffix) = split_roman_prefix(rest)?;
392 let degree = roman_degree(roman)?;
393 let (implied_quality, mut column) =
394 implied_quality_from_string(roman, suffix, case_matters);
395 let inversion = parse_inversion(&column);
396 let seventh = suffix_has_seventh(&column);
397
398 let mut numeral = Self {
399 figure: written.clone(),
400 key,
401 degree,
402 accidental,
403 written_accidental: accidental,
404 inversion,
405 seventh,
406 quality: RomanQuality::from(implied_quality),
407 implied_quality,
408 figures: FiguredBass::default(),
409 secondary,
410 kind: RomanKind::Diatonic,
411 sixth_minor,
412 seventh_minor,
413 case_matters,
414 scale,
415 };
416 numeral.raise_minor_sixth_and_seventh(&mut column)?;
417 let written = column.clone();
418 let column = strip_roman_addition_groups(&column);
422 numeral.figures = FiguredBass {
423 column: Notation::parse(&expand_shorthand(&column).join(","))?,
424 written,
425 omitted,
426 added,
427 bracketed,
428 };
429 Ok(numeral)
430 }
431
432 pub fn bracketed_alterations(&self) -> &[(i8, u8)] {
435 &self.figures.bracketed
436 }
437
438 pub fn omitted_steps(&self) -> &[u8] {
440 &self.figures.omitted
441 }
442
443 pub fn added_steps(&self) -> &[(i8, u8)] {
446 &self.figures.added
447 }
448
449 pub fn scale(&self) -> Option<&crate::scale::Scale> {
454 self.scale.as_ref()
455 }
456
457 pub fn implied_quality(&self) -> ImpliedQuality {
460 self.implied_quality
461 }
462
463 pub fn figure_numbers(&self) -> Vec<u8> {
466 self.figures.numbers()
467 }
468
469 pub fn case_matters(&self) -> bool {
471 self.case_matters
472 }
473
474 pub fn sixth_minor(&self) -> Minor67Default {
476 self.sixth_minor
477 }
478
479 pub fn seventh_minor(&self) -> Minor67Default {
481 self.seventh_minor
482 }
483
484 pub fn figures_written(&self) -> &str {
489 &self.figures.written
490 }
491
492 pub fn figures_notation(&self) -> &Notation {
495 &self.figures.column
496 }
497
498 pub fn figure(&self) -> &str {
500 &self.figure
501 }
502
503 pub fn degree(&self) -> u8 {
505 self.degree
506 }
507
508 pub fn written_accidental(&self) -> i8 {
513 self.written_accidental
514 }
515
516 pub fn accidental(&self) -> i8 {
519 self.accidental
520 }
521
522 pub fn inversion(&self) -> u8 {
524 self.inversion
525 }
526
527 pub fn secondary(&self) -> Option<&str> {
529 self.secondary.as_deref()
530 }
531
532 pub fn key(&self) -> &Key {
534 &self.key
535 }
536
537 pub fn roman_numeral(&self) -> String {
541 let numeral = self.roman_numeral_alone();
542 if matches!(self.kind, RomanKind::AugmentedSixth(_)) {
543 return numeral;
544 }
545 let prefix = match self.accidental {
546 0 => String::new(),
547 sharps if sharps > 0 => "#".repeat(sharps.unsigned_abs() as usize),
548 flats => "b".repeat(flats.unsigned_abs() as usize),
549 };
550 format!("{prefix}{numeral}")
551 }
552
553 pub fn front_alteration_string(&self) -> String {
559 let figure = figure::named_figure(primary_figure(&self.figure), &self.key);
560 let Some(mark) = figure
561 .chars()
562 .next()
563 .filter(|mark| matches!(mark, 'b' | '-' | '#'))
564 else {
565 return String::new();
566 };
567 figure
568 .chars()
569 .take_while(|written| *written == mark)
570 .collect()
571 }
572
573 pub fn roman_numeral_alone(&self) -> String {
576 if let RomanKind::AugmentedSixth(kind) = self.kind {
577 return kind.figure().trim_end_matches(['+', '6']).to_string();
578 }
579 let primary = self.figure.split('/').next().unwrap_or_default();
580 let reading = self.effective_key().unwrap_or_else(|_| self.key.clone());
584 let named = named_figure(primary, &reading);
585 let (_, unaltered) = split_roman_accidental_prefix(&named);
586 unaltered
587 .chars()
588 .take_while(|c| matches!(c, 'i' | 'v' | 'x' | 'I' | 'V' | 'X'))
589 .collect()
590 }
591
592 pub fn figure_and_key(&self) -> String {
595 format!(
596 "{} in {} {}",
597 self.figure,
598 self.key.tonic_pitch_name_with_case(),
599 self.key.mode()
600 )
601 }
602
603 pub fn scale_degree_with_alteration(&self) -> (u8, i8) {
606 (self.degree, self.accidental)
607 }
608
609 pub fn functionality_score(&self) -> u8 {
615 if self.secondary.is_some() {
616 let score = self.figure.split('/').fold(100.0, |score, part| {
617 score * f64::from(functionality_score_of(part).unwrap_or(0)) / 100.0
618 });
619 return score as u8;
620 }
621 if matches!(self.kind, RomanKind::AugmentedSixth(_)) {
624 return functionality_score_of(&self.figure.replace('+', "")).unwrap_or(0);
625 }
626 functionality_score_of(&self.figure).unwrap_or(0)
627 }
628
629 pub fn is_neapolitan(&self, require_first_inversion: bool) -> bool {
633 self.degree == 2
634 && self.accidental == -1
635 && self.quality == RomanQuality::Major
636 && (!require_first_inversion || self.inversion == 1)
637 }
638
639 pub fn is_mixture(&self, evaluate_secondary: bool) -> Result<bool> {
644 if evaluate_secondary && let Some(secondary) = &self.secondary {
645 return RomanNumeral::new(secondary.clone(), self.key.clone())?.is_mixture(true);
646 }
647 if self.kind != RomanKind::Diatonic || !(1..=7).contains(&self.degree) {
648 return Ok(false);
649 }
650 let chord = self.to_chord()?;
652 if !chord.is_triad() && !chord.is_seventh() {
653 return Ok(false);
654 }
655 let quality = match self.quality {
660 RomanQuality::Diminished | RomanQuality::HalfDiminished => "diminished",
661 RomanQuality::Minor => "minor",
662 RomanQuality::Major => "major",
663 RomanQuality::Augmented => return Ok(false),
664 };
665 let front = match self.accidental {
666 0 => "natural",
667 1 => "sharp",
668 -1 => "flat",
669 _ => "other",
670 };
671 let entry = (self.degree, quality, front);
672 Ok(match self.key.mode() {
673 "major" => {
674 MAJOR_KEY_MIXTURES.contains(&entry)
675 || (self.degree == 7
676 && self.seventh
677 && self.quality == RomanQuality::Diminished)
678 }
679 "minor" => {
680 MINOR_KEY_MIXTURES.contains(&entry)
681 || (self.degree == 7
682 && self.seventh
683 && self.quality == RomanQuality::HalfDiminished)
684 }
685 _ => false,
686 })
687 }
688
689 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
691 RomanNumeral::new(self.figure.clone(), self.key.transpose(interval)?)
692 }
693
694 pub fn effective_key_of(&self) -> Result<Key> {
698 self.effective_key()
699 }
700
701 fn effective_key(&self) -> Result<Key> {
702 let Some(secondary) = &self.secondary else {
703 return Ok(self.key.clone());
704 };
705
706 secondary_key(
707 &self.key,
708 secondary,
709 self.sixth_minor,
710 self.seventh_minor,
711 self.case_matters,
712 )
713 }
714}
715
716impl fmt::Display for RomanNumeral {
717 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
718 formatter.write_str(self.figure())
719 }
720}
721
722pub const FUNCTIONALITY_SCORES: [(&str, u8); 48] = [
725 ("I", 100),
726 ("i", 90),
727 ("V7", 80),
728 ("V", 70),
729 ("V65", 68),
730 ("I6", 65),
731 ("V6", 63),
732 ("V43", 61),
733 ("I64", 60),
734 ("IV", 59),
735 ("i6", 58),
736 ("viio7", 57),
737 ("V42", 55),
738 ("viio65", 53),
739 ("viio6", 52),
740 ("#viio65", 51),
741 ("ii", 50),
742 ("#viio6", 49),
743 ("ii65", 48),
744 ("ii43", 47),
745 ("ii42", 46),
746 ("IV6", 45),
747 ("ii6", 43),
748 ("VI", 42),
749 ("#VI", 41),
750 ("vi", 40),
751 ("viio", 39),
752 ("#viio", 38),
753 ("iio", 37),
754 ("iio42", 36),
755 ("bII6", 35),
756 ("It6", 34),
757 ("Ger65", 33),
758 ("iio43", 32),
759 ("iio65", 31),
760 ("Fr43", 30),
761 ("#vio", 28),
762 ("#vio6", 27),
763 ("III", 22),
764 ("Sw43", 21),
765 ("v", 20),
766 ("VII", 19),
767 ("VII7", 18),
768 ("IV65", 17),
769 ("IV7", 16),
770 ("iii", 15),
771 ("iii6", 12),
772 ("vi6", 10),
773];
774
775fn functionality_score_of(figure: &str) -> Option<u8> {
776 FUNCTIONALITY_SCORES
777 .iter()
778 .find(|(known, _)| *known == figure)
779 .map(|(_, score)| *score)
780}
781
782const MAJOR_KEY_MIXTURES: [(u8, &str, &str); 7] = [
783 (1, "minor", "natural"),
784 (2, "diminished", "natural"),
785 (3, "major", "flat"),
786 (4, "minor", "natural"),
787 (5, "minor", "natural"),
788 (6, "major", "flat"),
789 (7, "major", "flat"),
790];
791
792const MINOR_KEY_MIXTURES: [(u8, &str, &str); 5] = [
793 (1, "major", "natural"),
794 (2, "minor", "natural"),
795 (3, "minor", "sharp"),
796 (4, "major", "natural"),
797 (6, "minor", "sharp"),
798];
799
800#[derive(Clone, Debug, PartialEq)]
806#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
807#[must_use]
808pub struct FigureTuple {
809 pub deg_from_ref_pitch: u8,
811 pub alter: FloatType,
813 pub prefix: String,
816}
817
818#[derive(Clone, Debug, PartialEq)]
821#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
822#[must_use]
823pub struct PitchFigureTuple {
824 pub figure: FigureTuple,
826 pub pitch: Pitch,
828}
829
830#[derive(Clone, Debug, Default, PartialEq)]
839#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
840struct FiguredBass {
841 column: Notation,
843 written: String,
847 omitted: Vec<u8>,
849 added: Vec<(i8, u8)>,
852 bracketed: Vec<(i8, u8)>,
855}
856
857impl FiguredBass {
858 fn numbers(&self) -> Vec<u8> {
860 self.column
861 .numbers()
862 .iter()
863 .filter_map(|number| number.map(|number| number as u8))
864 .collect()
865 }
866
867 fn figures(&self) -> &[Figure] {
869 self.column.figures()
870 }
871
872 fn alters(&self, number: u8) -> bool {
875 self.column.figures().iter().any(|figure| {
876 figure.number() == Some(IntegerType::from(number))
877 && figure
878 .modifier()
879 .accidental()
880 .is_some_and(|accidental| accidental.alter() != 0.0)
881 })
882 }
883}
884
885#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
892#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
893pub enum ImpliedQuality {
894 #[default]
896 Unstated,
897 Major,
899 Minor,
901 Diminished,
903 HalfDiminished,
905 Augmented,
907 DominantSeventh,
909}
910
911impl From<ImpliedQuality> for RomanQuality {
912 fn from(quality: ImpliedQuality) -> Self {
915 match quality {
916 ImpliedQuality::Minor => Self::Minor,
917 ImpliedQuality::Diminished => Self::Diminished,
918 ImpliedQuality::HalfDiminished => Self::HalfDiminished,
919 ImpliedQuality::Augmented => Self::Augmented,
920 ImpliedQuality::Unstated | ImpliedQuality::Major | ImpliedQuality::DominantSeventh => {
921 Self::Major
922 }
923 }
924 }
925}
926
927impl ImpliedQuality {
928 pub fn from_name(name: &str) -> Self {
930 match name {
931 "major" => Self::Major,
932 "minor" => Self::Minor,
933 "diminished" => Self::Diminished,
934 "half-diminished" => Self::HalfDiminished,
935 "augmented" => Self::Augmented,
936 "minor-seventh" | "dominant-seventh" => Self::DominantSeventh,
937 _ => Self::Unstated,
938 }
939 }
940
941 pub fn name(self) -> &'static str {
944 match self {
945 Self::Unstated => "",
946 Self::Major => "major",
947 Self::Minor => "minor",
948 Self::Diminished => "diminished",
949 Self::HalfDiminished => "half-diminished",
950 Self::Augmented => "augmented",
951 Self::DominantSeventh => "dominant-seventh",
952 }
953 }
954
955 pub fn correct_semitones(self) -> &'static [u8] {
958 match self {
959 Self::Unstated => &[],
960 Self::Major => &[4, 7],
961 Self::Minor => &[3, 7],
962 Self::Diminished => &[3, 6, 9],
963 Self::HalfDiminished => &[3, 6, 10],
964 Self::Augmented => &[4, 8],
965 Self::DominantSeventh => &[4, 7, 10],
966 }
967 }
968}
969
970#[derive(Clone, Debug, PartialEq, Eq)]
978pub struct NumeralAlone {
979 pub numeral: String,
981 pub rest: String,
983 pub degree: u8,
985 pub alteration: i8,
988 pub minor: bool,
991 pub bracketed: Vec<(i8, u8)>,
993}
994
995#[cfg(test)]
996mod tests {
997 #[test]
998 fn numerals_past_seven_are_no_figures() {
999 use super::RomanNumeral;
1000 use crate::Key;
1001
1002 for figure in ["VIII", "viii7", "IX", "bVIII"] {
1003 assert!(
1004 RomanNumeral::new(figure, Key::from_tonic("C").unwrap()).is_err(),
1005 "{figure}"
1006 );
1007 }
1008 assert!(RomanNumeral::new("VII", Key::from_tonic("C").unwrap()).is_ok());
1009 }
1010
1011 #[test]
1012 fn the_front_alteration_is_what_was_written() {
1013 use super::RomanNumeral;
1014 use crate::Key;
1015
1016 let cases = [
1017 ("It6", "C", ""),
1018 ("Ger65", "a", ""),
1019 ("Sw42", "C", ""),
1020 ("bVI", "C", "b"),
1021 ("-VI", "C", "-"),
1022 ("##iv", "C", "##"),
1023 ("N6", "C", "b"),
1024 ("N", "a", "b"),
1025 ("Cad64", "C", ""),
1026 ("V/bVI", "C", ""),
1027 ("bVII7/V", "C", "b"),
1028 ("vii", "a", ""),
1029 ("#viio7", "a", "#"),
1030 ("I", "C", ""),
1031 ];
1032 for (figure, tonic, written) in cases {
1033 let key = Key::from_tonic(tonic).unwrap();
1034 let numeral = RomanNumeral::new(figure, key).unwrap();
1035 assert_eq!(numeral.front_alteration_string(), written, "{figure}");
1036 }
1037 }
1038
1039 #[test]
1040 fn functionality_and_mixture_read_as_music21_reads_them() {
1041 use super::RomanNumeral;
1042 use crate::Key;
1043
1044 let numeral = |figure: &str, tonic: &str| {
1045 RomanNumeral::new(figure, Key::from_tonic(tonic).unwrap()).unwrap()
1046 };
1047
1048 for (figure, score) in [
1051 ("V+", 0),
1052 ("V+7", 0),
1053 ("It6", 34),
1054 ("Ger65", 33),
1055 ("V", 70),
1056 ("V7/V", 56),
1057 ] {
1058 assert_eq!(
1059 numeral(figure, "C").functionality_score(),
1060 score,
1061 "{figure}"
1062 );
1063 }
1064 assert_eq!(numeral("III+", "a").functionality_score(), 0);
1065
1066 for (figure, tonic, mixture) in [
1068 ("I[add6]", "a", false),
1069 ("ii7[no5]", "a", false),
1070 ("I", "a", true),
1071 ("i[add6]", "C", false),
1072 ("iv7[no5]", "C", false),
1073 ("iv", "C", true),
1074 ] {
1075 assert_eq!(
1076 numeral(figure, tonic).is_mixture(false).unwrap(),
1077 mixture,
1078 "{figure} in {tonic}"
1079 );
1080 }
1081 }
1082
1083 #[test]
1084 fn a_root_position_augmented_sixth_roots_on_its_raised_bass() {
1085 use super::RomanNumeral;
1086 use crate::Key;
1087
1088 let cases = [
1091 ("It53", "C", "F#4"),
1092 ("It53", "a", "D#5"),
1093 ("Ger7", "C", "F#4"),
1094 ("Ger7", "a", "D#5"),
1095 ("Sw7", "C", "D#4"),
1096 ("Sw7", "a", "B#4"),
1097 ];
1098 for (figure, tonic, root) in cases {
1099 let numeral = RomanNumeral::new(figure, Key::from_tonic(tonic).unwrap()).unwrap();
1100 let chord = numeral.to_chord().unwrap();
1101 assert_eq!(
1102 chord.root().unwrap().name_with_octave(),
1103 root,
1104 "{figure} in {tonic}"
1105 );
1106 }
1107 }
1108
1109 #[test]
1110 fn a_numeral_reports_how_it_was_read() {
1111 use super::{
1112 ImpliedQuality, Minor67Default, NumeralAlone, RomanNumeral, analyze_chord_with_root,
1113 parse_numeral_alone, secondary_key,
1114 };
1115 use crate::{Chord, Key, Pitch};
1116
1117 let key = Key::from_tonic("C").unwrap();
1118 let numeral = RomanNumeral::new("V7[no3][add4][#5]/ii", key.clone()).unwrap();
1119 assert_eq!(numeral.key().tonic().name(), "C");
1120 assert!(numeral.scale().is_none());
1121 assert!(numeral.case_matters());
1122 assert_eq!(numeral.sixth_minor(), Minor67Default::Quality);
1123 assert_eq!(numeral.seventh_minor(), Minor67Default::Quality);
1124 assert_eq!(numeral.implied_quality(), ImpliedQuality::Major);
1125 assert_eq!(numeral.written_accidental(), 0);
1126 assert_eq!(numeral.omitted_steps(), [3]);
1127 assert_eq!(numeral.added_steps(), [(0, 4)]);
1128 assert_eq!(numeral.bracketed_alterations(), [(1, 5)]);
1129 assert_eq!(numeral.figures_notation().numbers().len(), 3);
1131 assert_eq!(numeral.effective_key_of().unwrap().tonic().name(), "D");
1132 assert_eq!(numeral.roman_numeral_alone(), "V");
1133 assert_eq!(numeral.to_string(), "V7[no3][add4][#5]/ii");
1134 assert_eq!(numeral.figure_and_key(), "V7[no3][add4][#5]/ii in C major");
1135
1136 assert_eq!(
1137 ImpliedQuality::from_name("half-diminished").name(),
1138 "half-diminished"
1139 );
1140 assert_eq!(
1141 ImpliedQuality::from_name("nonsense"),
1142 ImpliedQuality::Unstated
1143 );
1144 assert_eq!(ImpliedQuality::Unstated.name(), "");
1145
1146 let alone: NumeralAlone = parse_numeral_alone("VI6").unwrap();
1147 assert_eq!((alone.numeral.as_str(), alone.rest.as_str()), ("VI", "6"));
1148 assert_eq!((alone.degree, alone.alteration, alone.minor), (6, 0, false));
1149 let german = parse_numeral_alone("Ger").unwrap();
1150 assert_eq!(
1151 (german.numeral.as_str(), german.rest.as_str()),
1152 ("Ger", "65")
1153 );
1154 assert!(german.minor);
1155 assert!(parse_numeral_alone("").is_err());
1156
1157 let dominant_of_dominant = secondary_key(
1158 &key,
1159 "V",
1160 Minor67Default::Quality,
1161 Minor67Default::Quality,
1162 true,
1163 )
1164 .unwrap();
1165 assert_eq!(dominant_of_dominant.tonic().name(), "G");
1166 assert_eq!(dominant_of_dominant.mode(), "major");
1167 let of_supertonic = secondary_key(
1168 &key,
1169 "ii",
1170 Minor67Default::Quality,
1171 Minor67Default::Quality,
1172 true,
1173 )
1174 .unwrap();
1175 assert_eq!(of_supertonic.mode(), "minor");
1176
1177 let chord = Chord::new("E4 G4 C5").unwrap();
1178 let root = Pitch::from_name("C4").unwrap();
1179 let analyzed = analyze_chord_with_root(&chord, key, &root)
1180 .unwrap()
1181 .unwrap();
1182 assert_eq!(analyzed.figure(), "I6");
1183 }
1184
1185 #[test]
1189 fn figures_are_read_above_a_reference_pitch_and_corrected_for_minor() {
1190 use super::{
1191 FigureTuple, correct_rn_alteration_for_minor, correct_suffix_for_chord_quality,
1192 figure_tuples,
1193 };
1194 use crate::{Chord, Key, Pitch};
1195
1196 let c_major = Key::from_tonic("C").unwrap();
1197 let c_minor = Key::from_tonic("c").unwrap();
1198 let figure = |name: &str, key: &Key, reference: &str| {
1199 FigureTuple::from_pitch_and_reference(
1200 &Pitch::from_name(name).unwrap(),
1201 key,
1202 &Pitch::from_name(reference).unwrap(),
1203 )
1204 .unwrap()
1205 };
1206 let read = |figure: FigureTuple| (figure.deg_from_ref_pitch, figure.alter, figure.prefix);
1207 assert_eq!(
1208 read(figure("Ab3", &c_major, "F#2")),
1209 (3, -1.0, "b".to_string())
1210 );
1211 assert_eq!(
1212 read(figure("E--4", &c_minor, "C3")),
1213 (3, -1.0, "b".to_string())
1214 );
1215 assert_eq!(read(figure("E-4", &c_minor, "C3")), (3, 0.0, String::new()));
1216 assert_eq!(
1217 read(figure("E#4", &c_minor, "C3")),
1218 (3, 2.0, "##".to_string())
1219 );
1220 assert_eq!(
1221 read(figure("A4", &c_minor, "C3")),
1222 (6, 1.0, "#".to_string())
1223 );
1224 assert_eq!(
1225 read(figure("B5", &c_minor, "C3")),
1226 (7, 1.0, "#".to_string())
1227 );
1228
1229 let tuples = figure_tuples(&Chord::new("F#2 D3 A-3 C#4").unwrap(), &c_minor).unwrap();
1230 let read_all: Vec<(u8, FloatType, String)> = tuples
1231 .iter()
1232 .map(|tuple| read(tuple.figure.clone()))
1233 .collect();
1234 assert_eq!(
1235 read_all,
1236 [
1237 (1, 1.0, "#".to_string()),
1238 (6, 0.0, String::new()),
1239 (3, 0.0, String::new()),
1240 (5, 1.0, "#".to_string()),
1241 ]
1242 );
1243 assert_eq!(tuples[2].pitch.name_with_octave(), "Ab3");
1244 assert!(
1245 figure_tuples(&Chord::new("").unwrap(), &c_minor)
1246 .unwrap()
1247 .is_empty()
1248 );
1249
1250 let sixth = |alter: FloatType, prefix: &str| FigureTuple {
1251 deg_from_ref_pitch: 6,
1252 alter,
1253 prefix: prefix.to_string(),
1254 };
1255 assert_eq!(
1256 read(correct_rn_alteration_for_minor(
1257 &sixth(-1.0, ""),
1258 &c_minor,
1259 false
1260 )),
1261 (6, -1.0, "b".to_string())
1262 );
1263 assert_eq!(
1264 read(correct_rn_alteration_for_minor(
1265 &sixth(0.0, ""),
1266 &c_minor,
1267 false
1268 )),
1269 (6, 0.0, "b".to_string())
1270 );
1271 let raised_seventh = figure("B5", &c_minor, "C3");
1272 assert_eq!(
1273 read(correct_rn_alteration_for_minor(
1274 &raised_seventh,
1275 &c_minor,
1276 false
1277 )),
1278 (7, 0.0, String::new())
1279 );
1280 assert_eq!(
1281 read(correct_rn_alteration_for_minor(
1282 &raised_seventh,
1283 &c_minor,
1284 true
1285 )),
1286 (7, 1.0, "#".to_string())
1287 );
1288 assert_eq!(
1289 read(correct_rn_alteration_for_minor(
1290 &sixth(2.0, "##"),
1291 &c_minor,
1292 false
1293 )),
1294 (6, 1.0, "#".to_string())
1295 );
1296 let in_major = sixth(-1.0, "b");
1297 assert_eq!(
1298 correct_rn_alteration_for_minor(&in_major, &c_major, false),
1299 in_major
1300 );
1301 let fourth = FigureTuple {
1302 deg_from_ref_pitch: 4,
1303 alter: -1.0,
1304 prefix: "b".to_string(),
1305 };
1306 assert_eq!(
1307 correct_rn_alteration_for_minor(&fourth, &c_minor, false),
1308 fourth
1309 );
1310
1311 assert_eq!(
1312 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G4").unwrap(), "6"),
1313 "6"
1314 );
1315 assert_eq!(
1316 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G-4").unwrap(), "6"),
1317 "o6"
1318 );
1319 assert_eq!(
1320 correct_suffix_for_chord_quality(&Chord::new("C4 E4 G#4").unwrap(), ""),
1321 "+"
1322 );
1323 assert_eq!(
1324 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A4").unwrap(), "7"),
1325 "ø7"
1326 );
1327 assert_eq!(
1328 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A-4").unwrap(), "o7"),
1329 "o7"
1330 );
1331 }
1332
1333 #[test]
1336 fn the_sixth_and_seventh_of_a_minor_key_are_raised_by_quality() {
1337 use super::{ImpliedQuality, Minor67Default, adjust_minor_vi_and_vii_by_quality};
1338 use crate::Key;
1339
1340 let minor = Key::from_tonic("c").unwrap();
1341 let major = Key::from_tonic("C").unwrap();
1342 let adjust = |key: &Key, reading, quality, accidental| {
1343 adjust_minor_vi_and_vii_by_quality(key, reading, quality, accidental)
1344 };
1345 assert_eq!(
1346 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1347 1
1348 );
1349 assert_eq!(
1350 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Major, 0),
1351 0
1352 );
1353 assert_eq!(
1354 adjust(&major, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1355 0
1356 );
1357 assert_eq!(
1358 adjust(&minor, Minor67Default::Flat, ImpliedQuality::Diminished, 0),
1359 0
1360 );
1361 assert_eq!(
1362 adjust(&minor, Minor67Default::Sharp, ImpliedQuality::Major, 0),
1363 1
1364 );
1365 assert_eq!(
1368 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 1),
1369 1
1370 );
1371 assert_eq!(
1372 adjust(
1373 &minor,
1374 Minor67Default::Cautionary,
1375 ImpliedQuality::Major,
1376 -1
1377 ),
1378 0
1379 );
1380 assert_eq!(
1381 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 0),
1382 1
1383 );
1384 }
1385
1386 #[test]
1387 fn a_numeral_reports_the_digits_it_was_written_with() {
1388 let major = Key::from_tonic("C").unwrap();
1389 for (figure, written) in [
1390 ("V65", "65"),
1391 ("V", ""),
1392 ("I7#5b3", "7#5b3"),
1393 ("viio6", "6"),
1394 ("vii\u{f8}7", "7"),
1395 ] {
1396 let numeral = RomanNumeral::new(figure, major.clone()).unwrap();
1397 assert_eq!(numeral.figures_written(), written, "{figure}");
1398 }
1399 let minor = Key::from_tonic("c").unwrap();
1400 for (figure, written) in [("Fr43", "43"), ("Fr+6", "43"), ("It+6", "6"), ("Ger", "65")] {
1401 let numeral = RomanNumeral::new(figure, minor.clone()).unwrap();
1402 assert_eq!(numeral.figures_written(), written, "{figure}");
1403 }
1404 }
1405
1406 #[test]
1407 fn a_minor_key_raises_its_sixth_and_seventh_where_the_figure_asks() {
1408 let minor = Key::from_tonic_mode("a", Some("minor")).unwrap();
1409 let leading = RomanNumeral::new("viio7", minor.clone()).unwrap();
1412 assert_eq!(
1413 leading.to_chord().unwrap().pitch_names(),
1414 ["G#", "B", "D", "F"]
1415 );
1416 assert_eq!(leading.roman_numeral(), "#vii");
1417 assert_eq!(leading.roman_numeral_alone(), "vii");
1418 assert_eq!(leading.scale_degree_with_alteration(), (7, 1));
1419
1420 assert_eq!(
1422 RomanNumeral::new("vi", minor.clone())
1423 .unwrap()
1424 .to_chord()
1425 .unwrap()
1426 .pitch_names(),
1427 ["F#", "A", "C#"]
1428 );
1429 assert_eq!(
1431 RomanNumeral::new("VI", minor.clone())
1432 .unwrap()
1433 .to_chord()
1434 .unwrap()
1435 .pitch_names(),
1436 ["F", "A", "C"]
1437 );
1438 let major = Key::from_tonic_mode("C", Some("major")).unwrap();
1440 assert_eq!(
1441 RomanNumeral::new("vi", major)
1442 .unwrap()
1443 .to_chord()
1444 .unwrap()
1445 .pitch_names(),
1446 ["A", "C", "E"]
1447 );
1448 }
1449
1450 #[test]
1451 fn the_four_readings_of_a_minor_sixth_and_seventh() {
1452 let minor = Key::from_tonic_mode("c", Some("minor")).unwrap();
1453 let read = |figure: &str, sixth: Minor67Default| {
1454 RomanNumeral::with_minor_defaults(figure, minor.clone(), sixth, sixth)
1455 .unwrap()
1456 .to_chord()
1457 .unwrap()
1458 .pitch_names()
1459 .join(" ")
1460 };
1461
1462 assert_eq!(read("vi", Minor67Default::Quality), "A C E");
1465 assert_eq!(read("VI", Minor67Default::Quality), "Ab C Eb");
1466
1467 assert_eq!(read("vi", Minor67Default::Flat), "Ab Cb Eb");
1470 assert_eq!(read("#vi", Minor67Default::Flat), "A C E");
1471
1472 assert_eq!(read("VI", Minor67Default::Sharp), "A C# E");
1474 assert_eq!(read("bVI", Minor67Default::Sharp), "Ab C Eb");
1475
1476 assert_eq!(read("#vi", Minor67Default::Cautionary), "A C E");
1480 assert_eq!(read("vi", Minor67Default::Cautionary), "A C E");
1481 assert_eq!(read("bVI", Minor67Default::Cautionary), "Ab C Eb");
1482 assert_eq!(read("VI", Minor67Default::Cautionary), "Ab C Eb");
1483 }
1484
1485 #[test]
1486 fn the_neapolitan_can_be_written_by_name() {
1487 let key = Key::from_tonic_mode("c#", Some("minor")).unwrap();
1490 let named = RomanNumeral::new("N6", key.clone()).unwrap();
1491 assert_eq!(named.figure(), "N6");
1492 assert_eq!(named.degree(), 2);
1493 assert_eq!(named.accidental(), -1);
1494 assert_eq!(named.inversion(), 1);
1495 assert!(named.is_neapolitan(true));
1496
1497 let spelled = RomanNumeral::new("bII6", key.clone()).unwrap();
1498 assert_eq!(
1499 named.to_chord().unwrap().pitch_names(),
1500 spelled.to_chord().unwrap().pitch_names()
1501 );
1502
1503 let root_position = RomanNumeral::new("N53", key).unwrap();
1505 assert_eq!(root_position.inversion(), 0);
1506 }
1507
1508 #[test]
1509 fn inversion_names_and_tonic_dominant_reading_match_music21() {
1510 let chord = |notes: &str| Chord::new(notes).unwrap();
1511 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), None), "");
1512 assert_eq!(roman_inversion_name(&chord("E4 G4 C5"), None), "6");
1513 assert_eq!(roman_inversion_name(&chord("G3 C4 E4"), None), "64");
1514 assert_eq!(roman_inversion_name(&chord("C4 E4 G4 B-4"), None), "7");
1515 assert_eq!(roman_inversion_name(&chord("E4 G4 B-4 C5"), None), "65");
1516 assert_eq!(roman_inversion_name(&chord("G3 B-3 C4 E4"), None), "43");
1517 assert_eq!(roman_inversion_name(&chord("B-3 C4 E4 G4"), None), "42");
1518 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(2)), "64");
1519 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(3)), "");
1520 assert_eq!(roman_inversion_name(&Chord::empty(), None), "");
1521
1522 let cases = [
1523 ("C4 E4 G4", "C", Some("I")),
1524 ("E4 G4 C5", "C", Some("I6")),
1525 ("G3 B3 D4 F4", "C", Some("V7")),
1526 ("D4 F4 A4", "C", Some("V43")),
1527 ("A3 C4 E4", "a", Some("i")),
1528 ("E3 G#3 B3", "a", Some("V")),
1529 ("C4 E4", "C", Some("I")),
1530 ("G4 D5", "C", Some("V")),
1531 ("B3 D4", "C", Some("V")),
1532 ("F#4 A4", "C", None),
1533 ];
1534 for (notes, key, expected) in cases {
1535 let reading =
1536 identify_as_tonic_or_dominant(&chord(notes), &Key::from_tonic(key).unwrap())
1537 .unwrap();
1538 assert_eq!(reading.as_deref(), expected, "{notes} in {key}");
1539 }
1540 }
1541
1542 #[test]
1543 #[allow(clippy::type_complexity)]
1544 fn numeral_names_scores_and_mixture_match_music21() {
1545 let cases: [(&str, &str, &str, &str, (u8, i8), u8, bool, bool, bool); 18] = [
1546 (
1547 "V7",
1548 "C",
1549 "V",
1550 "V7 in C major",
1551 (5, 0),
1552 80,
1553 false,
1554 false,
1555 false,
1556 ),
1557 (
1558 "bII6",
1559 "C",
1560 "bII",
1561 "bII6 in C major",
1562 (2, -1),
1563 35,
1564 true,
1565 true,
1566 false,
1567 ),
1568 (
1569 "bII",
1570 "C",
1571 "bII",
1572 "bII in C major",
1573 (2, -1),
1574 0,
1575 false,
1576 true,
1577 false,
1578 ),
1579 (
1580 "bII6",
1581 "a",
1582 "bII",
1583 "bII6 in a minor",
1584 (2, -1),
1585 35,
1586 true,
1587 true,
1588 false,
1589 ),
1590 (
1591 "viio7",
1592 "C",
1593 "vii",
1594 "viio7 in C major",
1595 (7, 0),
1596 57,
1597 false,
1598 false,
1599 true,
1600 ),
1601 (
1602 "I",
1603 "C",
1604 "I",
1605 "I in C major",
1606 (1, 0),
1607 100,
1608 false,
1609 false,
1610 false,
1611 ),
1612 (
1613 "i",
1614 "a",
1615 "i",
1616 "i in a minor",
1617 (1, 0),
1618 90,
1619 false,
1620 false,
1621 false,
1622 ),
1623 (
1624 "V65/V",
1625 "C",
1626 "V",
1627 "V65/V in C major",
1628 (5, 0),
1629 47,
1630 false,
1631 false,
1632 false,
1633 ),
1634 (
1635 "iv",
1636 "C",
1637 "iv",
1638 "iv in C major",
1639 (4, 0),
1640 0,
1641 false,
1642 false,
1643 true,
1644 ),
1645 (
1646 "bVI",
1647 "C",
1648 "bVI",
1649 "bVI in C major",
1650 (6, -1),
1651 0,
1652 false,
1653 false,
1654 true,
1655 ),
1656 (
1657 "I",
1658 "a",
1659 "I",
1660 "I in a minor",
1661 (1, 0),
1662 100,
1663 false,
1664 false,
1665 true,
1666 ),
1667 (
1668 "III",
1669 "a",
1670 "III",
1671 "III in a minor",
1672 (3, 0),
1673 22,
1674 false,
1675 false,
1676 false,
1677 ),
1678 (
1679 "#iii",
1680 "a",
1681 "#iii",
1682 "#iii in a minor",
1683 (3, 1),
1684 0,
1685 false,
1686 false,
1687 true,
1688 ),
1689 (
1690 "ii",
1691 "C",
1692 "ii",
1693 "ii in C major",
1694 (2, 0),
1695 50,
1696 false,
1697 false,
1698 false,
1699 ),
1700 (
1701 "iio",
1702 "C",
1703 "ii",
1704 "iio in C major",
1705 (2, 0),
1706 37,
1707 false,
1708 false,
1709 true,
1710 ),
1711 (
1712 "IV",
1713 "a",
1714 "IV",
1715 "IV in a minor",
1716 (4, 0),
1717 59,
1718 false,
1719 false,
1720 true,
1721 ),
1722 (
1723 "bVII",
1724 "C",
1725 "bVII",
1726 "bVII in C major",
1727 (7, -1),
1728 0,
1729 false,
1730 false,
1731 true,
1732 ),
1733 (
1734 "v",
1735 "C",
1736 "v",
1737 "v in C major",
1738 (5, 0),
1739 20,
1740 false,
1741 false,
1742 true,
1743 ),
1744 ];
1745 for (figure, key, numeral, figure_and_key, degree, score, neapolitan, loose, mixture) in
1746 cases
1747 {
1748 let rn = RomanNumeral::new(figure, Key::from_tonic(key).unwrap()).unwrap();
1749 assert_eq!(rn.roman_numeral(), numeral, "{figure} in {key}");
1750 assert_eq!(rn.figure_and_key(), figure_and_key, "{figure} in {key}");
1751 assert_eq!(
1752 rn.scale_degree_with_alteration(),
1753 degree,
1754 "{figure} in {key}"
1755 );
1756 assert_eq!(rn.functionality_score(), score, "{figure} in {key}");
1757 assert_eq!(rn.is_neapolitan(true), neapolitan, "{figure} in {key}");
1758 assert_eq!(rn.is_neapolitan(false), loose, "{figure} in {key}");
1759 assert_eq!(rn.is_mixture(false).unwrap(), mixture, "{figure} in {key}");
1760 assert_eq!(rn.is_mixture(true).unwrap(), mixture, "{figure} in {key}");
1761 }
1762 for (key, mixture) in [("a", true), ("A", false)] {
1766 let half = RomanNumeral::new("viiø7", Key::from_tonic(key).unwrap()).unwrap();
1767 assert_eq!(half.is_mixture(false).unwrap(), mixture, "viiø7 in {key}");
1768 }
1769
1770 let italian = RomanNumeral::new("It6", Key::from_tonic("C").unwrap()).unwrap();
1771 assert_eq!(italian.roman_numeral(), "It");
1772 assert_eq!(italian.functionality_score(), 34);
1773 assert!(!italian.is_mixture(false).unwrap());
1774 let transposed = RomanNumeral::new("bII6", Key::from_tonic("a").unwrap())
1775 .unwrap()
1776 .transpose(&Interval::from_name("M2").unwrap())
1777 .unwrap();
1778 assert_eq!(transposed.figure_and_key(), "bII6 in b minor");
1779 }
1780
1781 #[test]
1782 fn functionality_scores_table_has_no_duplicates() {
1783 let mut figures: Vec<&str> = FUNCTIONALITY_SCORES.iter().map(|(f, _)| *f).collect();
1784 figures.sort_unstable();
1785 figures.dedup();
1786 assert_eq!(figures.len(), FUNCTIONALITY_SCORES.len());
1787 }
1788 use super::*;
1789
1790 #[test]
1791 fn secondary_dominant_resolves_to_chord() {
1792 let key = Key::from_tonic_mode("C", "major").unwrap();
1793 let rn = RomanNumeral::new("V7/V", key).unwrap();
1794 assert_eq!(rn.degree(), 5);
1795 assert_eq!(rn.secondary(), Some("V"));
1796 assert_eq!(
1797 rn.to_chord().unwrap().pitched_common_name(),
1798 "D-dominant seventh chord"
1799 );
1800 }
1801
1802 #[test]
1804 fn a_chord_is_named_the_way_music21_names_it() {
1805 let cases: &[(&str, &str, &str)] = &[
1806 ("C E G", "C", "I"),
1807 ("G B D F", "C", "V43"),
1808 ("C E- G-", "C", "io5b3"),
1809 ("G B D F", "a", "bVII43"),
1810 ("B D F A", "C", "viiø65"),
1811 ("F A C", "C", "IV64"),
1812 ("A C E", "C", "vi6"),
1813 ("A C E", "a", "i6"),
1814 ("F A C", "a", "bVI64"),
1815 ("C E G#", "a", "III+"),
1816 ("D- F Ab", "C", "bII"),
1817 ("B4 D5 F5 A-5", "c", "viio7"),
1818 ("A-3 C4 F#4", "c", "It6"),
1819 ("A-3 C4 D4 F#4", "c", "Fr43"),
1820 ("A-3 C4 E-4 F#4", "c", "Ger65"),
1821 ("A-3 C4 E-4 F#4", "C", "Ger65"),
1822 ("C4 E4 G4 B-4", "F", "V7"),
1823 ("D4 F4 A4 C5", "C", "ii7"),
1824 ("D4 F4 A4 C#5", "C", "ii#7"),
1825 ("F4 A4 C5 E5", "C", "IV7"),
1826 ("E4 G4 B4 D5", "C", "iii7"),
1827 ("C4 E-4 G4 B4", "c", "i#7"),
1828 ("C4 E4 G4 B4", "C", "I7"),
1829 ("E-4 G4 B-4 D5", "c", "III7"),
1830 ("C4 E4 G4 B4 D5", "C", "I7532"),
1831 ("G3 C4 E4", "C", "I64"),
1832 ("E3 G3 C4", "C", "I6"),
1833 ("F#3 A3 C4 E-4", "G", "viiob753"),
1834 ("D F# A", "G", "V"),
1835 ("C E G B-", "C", "Ib753"),
1836 ];
1837 for (chord, key, figure) in cases {
1838 let key = Key::from_tonic(key).unwrap();
1839 let numeral = roman_numeral_from_chord(&Chord::new(*chord).unwrap(), Some(&key))
1840 .unwrap()
1841 .unwrap();
1842 assert_eq!(
1843 numeral.figure(),
1844 *figure,
1845 "{chord} in {}",
1846 key.tonic_pitch_name_with_case()
1847 );
1848 }
1849 for (chord, figure, key_name) in [
1850 ("D F# A", "I", "D major"),
1851 ("D F A", "i", "d minor"),
1852 ("E G# B D", "Ib8642", "E major"),
1853 ("C E G", "I", "C major"),
1854 ("A-3 C4 E-4 F#4", "Ger65", "c minor"),
1855 ("G B D F", "I64b3", "G major"),
1856 ] {
1857 let numeral = roman_numeral_from_chord(&Chord::new(chord).unwrap(), None)
1858 .unwrap()
1859 .unwrap();
1860 assert_eq!(numeral.figure(), figure, "{chord} with no key");
1861 let key = numeral.key();
1862 assert_eq!(
1863 format!("{} {}", key.tonic_pitch_name_with_case(), key.mode()),
1864 key_name,
1865 "{chord} with no key"
1866 );
1867 }
1868 assert!(
1869 roman_numeral_from_chord(&Chord::empty(), None)
1870 .unwrap()
1871 .is_none()
1872 );
1873 }
1874
1875 #[test]
1876 fn analyzes_chord_in_key() {
1877 let key = Key::from_tonic_mode("C", "major").unwrap();
1878 let chord = Chord::new("G B D F").unwrap();
1879 let rn = RomanNumeral::analyze(&chord, key).unwrap().unwrap();
1880 assert_eq!(rn.figure(), "V7");
1881 }
1882
1883 #[test]
1884 fn analyzes_accidentals_inversions_and_half_diminished_quality() {
1885 let key = Key::from_tonic_mode("C", "major").unwrap();
1886
1887 let neapolitan = Chord::new("D- F Ab").unwrap();
1888 let rn = RomanNumeral::analyze(&neapolitan, key.clone())
1889 .unwrap()
1890 .unwrap();
1891 assert_eq!(rn.figure(), "bII");
1892 assert_eq!(rn.degree(), 2);
1893 assert_eq!(rn.accidental(), -1);
1894
1895 let first_inversion = Chord::new("E4 G4 C5").unwrap();
1896 let rn = RomanNumeral::analyze(&first_inversion, key.clone())
1897 .unwrap()
1898 .unwrap();
1899 assert_eq!(rn.figure(), "I6");
1900
1901 let leading_tone = Chord::new("B D F A").unwrap();
1902 let rn = RomanNumeral::analyze(&leading_tone, key).unwrap().unwrap();
1903 assert_eq!(rn.figure(), "vii\u{00f8}7");
1904 }
1905
1906 #[test]
1907 fn analyzes_with_explicit_root_for_browser_style_sets() {
1908 let key = Key::from_tonic_mode("C", "major").unwrap();
1909 let root = Pitch::from_name("C").unwrap();
1910 let chord = Chord::new("C E G").unwrap();
1911 let rn = RomanNumeral::analyze_with_root(&chord, key.clone(), &root)
1912 .unwrap()
1913 .unwrap();
1914 assert_eq!(rn.figure(), "I");
1915
1916 let seventh = Chord::new("C E G B-").unwrap();
1917 let rn = RomanNumeral::analyze_with_root(&seventh, key, &root)
1918 .unwrap()
1919 .unwrap();
1920 assert_eq!(rn.figure(), "I7");
1921 }
1922
1923 #[test]
1924 fn analyzes_augmented_sixth_chords_functionally() {
1925 let key = Key::from_tonic_mode("C", "minor").unwrap();
1926 let root = Pitch::from_name("C").unwrap();
1927 let french = Chord::new("C D F# Ab").unwrap();
1928 let rn = RomanNumeral::analyze_with_root(&french, key.clone(), &root)
1929 .unwrap()
1930 .unwrap();
1931 assert_eq!(rn.figure(), "Fr+6");
1932
1933 let german = Chord::new("Ab C E- F#").unwrap();
1934 let rn = RomanNumeral::analyze(&german, key).unwrap().unwrap();
1935 assert_eq!(rn.figure(), "Ger+6");
1936 }
1937
1938 #[test]
1939 fn figured_bass_shorthands_give_music21_inversions() {
1940 let key = Key::from_tonic_mode("C", "major").unwrap();
1943 for (figure, expected) in [
1944 ("V", 0),
1945 ("V6", 1),
1946 ("V64", 2),
1947 ("V7", 0),
1948 ("V65", 1),
1949 ("V43", 2),
1950 ("V42", 3),
1951 ("V642", 3),
1952 ("V653", 1),
1953 ("V643", 2),
1954 ("V63", 1),
1955 ("V53", 0),
1956 ("V9", 0),
1957 ] {
1958 let numeral = RomanNumeral::new(figure, key.clone())
1959 .unwrap_or_else(|err| panic!("{figure} should parse: {err}"));
1960 assert_eq!(numeral.inversion(), expected, "{figure}");
1961 }
1962 }
1963
1964 #[test]
1965 fn a_figure_expands_into_a_figured_bass_column() {
1966 let numbers = |figure: &str| {
1969 RomanNumeral::new(figure, Key::from_tonic("C").unwrap())
1970 .unwrap()
1971 .figure_numbers()
1972 };
1973 assert_eq!(numbers("V"), vec![5, 3]);
1974 assert_eq!(numbers("V7"), vec![7, 5, 3]);
1975 assert_eq!(numbers("V65"), vec![6, 5, 3]);
1976 assert_eq!(numbers("V43"), vec![6, 4, 3]);
1977 assert_eq!(numbers("V9"), vec![9, 7, 5, 3]);
1978 assert_eq!(numbers("V7#5b3"), vec![7, 5, 3]);
1980 }
1981
1982 #[test]
1983 fn a_column_says_which_degree_is_in_the_bass() {
1984 assert_eq!(bass_scale_degree_from_notation(5, &[6, 3]).unwrap(), 7);
1987 assert_eq!(bass_scale_degree_from_notation(1, &[5, 3]).unwrap(), 1);
1988 assert_eq!(bass_scale_degree_from_notation(2, &[6, 5, 3]).unwrap(), 4);
1989 assert_eq!(bass_scale_degree_from_notation(5, &[5, 4]).unwrap(), 5);
1991 }
1992
1993 #[test]
1994 fn a_figure_alters_one_note_rather_than_naming_another_chord() {
1995 let names = |figure: &str, key: &str| {
1996 RomanNumeral::new(figure, Key::from_tonic(key).unwrap())
1997 .unwrap()
1998 .to_chord()
1999 .unwrap()
2000 .pitch_names()
2001 };
2002 assert_eq!(names("V7b5", "C"), ["G", "Db", "F"]);
2005 assert_eq!(names("V[no3]", "F"), ["C", "G"]);
2006 assert_eq!(names("I[add4][no3]", "C"), ["C", "F", "G"]);
2007 assert_eq!(names("i#7", "c"), ["C", "Eb", "G", "B"]);
2010 assert_eq!(names("i#7", "C"), ["C", "Eb", "G", "B#"]);
2011 }
2012
2013 #[test]
2014 fn a_numeral_can_be_read_over_a_scale_that_is_not_a_key() {
2015 let scale = crate::scale::Scale::new(
2018 crate::scale::ScaleType::Octatonic,
2019 Pitch::from_name("C2").unwrap(),
2020 );
2021 let numeral = RomanNumeral::over_scale(
2022 "I9",
2023 Key::from_tonic("C").unwrap(),
2024 Some(scale),
2025 Minor67Default::Quality,
2026 Minor67Default::Quality,
2027 false,
2028 )
2029 .unwrap();
2030 let pitches: Vec<String> = numeral
2031 .to_chord()
2032 .unwrap()
2033 .pitches()
2034 .iter()
2035 .map(Pitch::name_with_octave)
2036 .collect();
2037 assert_eq!(pitches, ["C2", "Eb2", "Gb2", "A2", "C3"]);
2038 }
2039
2040 #[test]
2041 fn roman_numerals_parse_inversions_and_qualities() {
2042 let key = Key::from_tonic_mode("C", "major").unwrap();
2043 let first_inversion = RomanNumeral::new("I6", key.clone()).unwrap();
2044 assert_eq!(first_inversion.inversion(), 1);
2045 assert_eq!(
2046 first_inversion
2047 .to_chord()
2048 .unwrap()
2049 .pitches()
2050 .into_iter()
2051 .map(|pitch| pitch.name())
2052 .collect::<Vec<_>>(),
2053 vec!["E", "G", "C"]
2054 );
2055
2056 let diminished = RomanNumeral::new("viio7", key.clone()).unwrap();
2057 assert_eq!(diminished.degree(), 7);
2058 assert!(
2059 diminished
2060 .to_chord()
2061 .unwrap()
2062 .common_name()
2063 .contains("diminished")
2064 );
2065
2066 let half_diminished = RomanNumeral::new("vii\u{00f8}7", key.clone()).unwrap();
2067 assert_eq!(half_diminished.degree(), 7);
2068 assert_eq!(half_diminished.accidental(), 0);
2069 assert!(
2070 half_diminished
2071 .to_chord()
2072 .unwrap()
2073 .common_name()
2074 .contains("half-diminished")
2075 );
2076
2077 let borrowed = RomanNumeral::new("bII", key.clone()).unwrap();
2078 assert_eq!(borrowed.degree(), 2);
2079 assert_eq!(borrowed.accidental(), -1);
2080
2081 let added_thirteenth = RomanNumeral::new("I add(13)", key.clone()).unwrap();
2082 assert_eq!(added_thirteenth.inversion(), 0);
2083 assert_eq!(
2084 added_thirteenth.to_chord().unwrap().common_name(),
2085 "major triad"
2086 );
2087
2088 let augmented = RomanNumeral::new("III+", key).unwrap();
2089 assert_eq!(
2090 augmented
2091 .to_chord()
2092 .unwrap()
2093 .pitches()
2094 .into_iter()
2095 .map(|pitch| pitch.name())
2096 .collect::<Vec<_>>(),
2097 vec!["E", "G#", "B#"]
2098 );
2099 }
2100
2101 #[test]
2102 fn roman_numerals_parse_augmented_sixth_figures() {
2103 let key = Key::from_tonic_mode("C", "minor").unwrap();
2104 let french = RomanNumeral::new("Fr+6", key).unwrap();
2105 assert_eq!(french.degree(), 2);
2109 assert_eq!(french.accidental(), 0);
2110 assert_eq!(
2111 french
2112 .to_chord()
2113 .unwrap()
2114 .pitches()
2115 .into_iter()
2116 .map(|pitch| pitch.name())
2117 .collect::<Vec<_>>(),
2118 vec!["Ab", "C", "D", "F#"]
2119 );
2120 }
2121
2122 #[test]
2123 fn roman_numerals_report_invalid_figures_and_empty_analysis() {
2124 let key = Key::from_tonic_mode("C", "major").unwrap();
2125
2126 assert!(RomanNumeral::new("", key.clone()).is_err());
2127 assert!(RomanNumeral::new("Q", key.clone()).is_err());
2128 assert!(analyze_chord(&Chord::empty(), key).unwrap().is_none());
2129 }
2130}