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
253impl RomanNumeral {
254 pub fn new(figure: impl Into<String>, key: Key) -> Result<Self> {
259 Self::with_minor_defaults(
260 figure,
261 key,
262 Minor67Default::default(),
263 Minor67Default::default(),
264 )
265 }
266
267 pub fn with_minor_defaults(
270 figure: impl Into<String>,
271 key: Key,
272 sixth_minor: Minor67Default,
273 seventh_minor: Minor67Default,
274 ) -> Result<Self> {
275 Self::with_options(figure, key, sixth_minor, seventh_minor, true)
276 }
277
278 pub fn with_options(
281 figure: impl Into<String>,
282 key: Key,
283 sixth_minor: Minor67Default,
284 seventh_minor: Minor67Default,
285 case_matters: bool,
286 ) -> Result<Self> {
287 Self::over_scale(figure, key, None, sixth_minor, seventh_minor, case_matters)
288 }
289
290 pub fn over_scale(
297 figure: impl Into<String>,
298 key: Key,
299 scale: Option<crate::scale::Scale>,
300 sixth_minor: Minor67Default,
301 seventh_minor: Minor67Default,
302 case_matters: bool,
303 ) -> Result<Self> {
304 let figure = figure.into();
305 let written = fold_figure_symbols(figure.trim());
306 let trimmed = written.as_str();
307 if trimmed.is_empty() {
308 return Err(Error::Chord("roman numeral cannot be empty".to_string()));
309 }
310 validate_figure(trimmed)?;
311
312 let (aug6_primary, aug6_secondary) = if AugmentedSixthKind::from_figure(trimmed).is_some() {
317 (trimmed, None)
318 } else {
319 split_secondary(trimmed)
320 };
321 if let Some(kind) = AugmentedSixthKind::from_figure(aug6_primary) {
322 let column = kind.written_figure(aug6_primary);
328 return Ok(Self {
329 figure: written.clone(),
332 key,
333 degree: kind.degree(),
334 accidental: 0,
335 written_accidental: kind.written_alteration(),
336 inversion: parse_inversion(&column),
337 seventh: suffix_has_seventh(&column),
338 quality: RomanQuality::from(ImpliedQuality::Unstated),
339 implied_quality: ImpliedQuality::Unstated,
340 figures: FiguredBass {
341 column: Notation::parse(&expand_shorthand(&column).join(","))?,
342 written: column,
343 bracketed: kind.bracketed_alterations(),
344 ..FiguredBass::default()
345 },
346 secondary: aug6_secondary,
347 kind: RomanKind::AugmentedSixth(kind),
348 sixth_minor,
349 seventh_minor,
350 case_matters,
351 scale,
352 });
353 }
354
355 let (primary, secondary) = (aug6_primary, aug6_secondary);
356
357 let reading = match &secondary {
363 Some(secondary) => {
364 secondary_key(&key, secondary, sixth_minor, seventh_minor, case_matters)?
365 }
366 None => key.clone(),
367 };
368 let mut working = named_figure(primary, &reading);
369
370 let omitted = take_omitted_steps(&mut working);
373 let added = take_added_steps(&mut working);
374 let bracketed = take_bracketed_alterations(&mut working);
375
376 let (accidental, rest) = split_roman_accidental_prefix(&working);
377 let (roman, suffix) = split_roman_prefix(rest)?;
378 let degree = roman_degree(roman)?;
379 let (implied_quality, mut column) =
380 implied_quality_from_string(roman, suffix, case_matters);
381 let inversion = parse_inversion(&column);
382 let seventh = suffix_has_seventh(&column);
383
384 let mut numeral = Self {
385 figure: written.clone(),
386 key,
387 degree,
388 accidental,
389 written_accidental: accidental,
390 inversion,
391 seventh,
392 quality: RomanQuality::from(implied_quality),
393 implied_quality,
394 figures: FiguredBass::default(),
395 secondary,
396 kind: RomanKind::Diatonic,
397 sixth_minor,
398 seventh_minor,
399 case_matters,
400 scale,
401 };
402 numeral.raise_minor_sixth_and_seventh(&mut column)?;
403 let written = column.clone();
404 let column = strip_roman_addition_groups(&column);
408 numeral.figures = FiguredBass {
409 column: Notation::parse(&expand_shorthand(&column).join(","))?,
410 written,
411 omitted,
412 added,
413 bracketed,
414 };
415 Ok(numeral)
416 }
417
418 pub fn bracketed_alterations(&self) -> &[(i8, u8)] {
421 &self.figures.bracketed
422 }
423
424 pub fn omitted_steps(&self) -> &[u8] {
426 &self.figures.omitted
427 }
428
429 pub fn added_steps(&self) -> &[(i8, u8)] {
432 &self.figures.added
433 }
434
435 pub fn scale(&self) -> Option<&crate::scale::Scale> {
440 self.scale.as_ref()
441 }
442
443 pub fn implied_quality(&self) -> ImpliedQuality {
446 self.implied_quality
447 }
448
449 pub fn figure_numbers(&self) -> Vec<u8> {
452 self.figures.numbers()
453 }
454
455 pub fn case_matters(&self) -> bool {
457 self.case_matters
458 }
459
460 pub fn sixth_minor(&self) -> Minor67Default {
462 self.sixth_minor
463 }
464
465 pub fn seventh_minor(&self) -> Minor67Default {
467 self.seventh_minor
468 }
469
470 pub fn figures_written(&self) -> &str {
475 &self.figures.written
476 }
477
478 pub fn figures_notation(&self) -> &Notation {
481 &self.figures.column
482 }
483
484 pub fn figure(&self) -> &str {
486 &self.figure
487 }
488
489 pub fn degree(&self) -> u8 {
491 self.degree
492 }
493
494 pub fn written_accidental(&self) -> i8 {
499 self.written_accidental
500 }
501
502 pub fn accidental(&self) -> i8 {
505 self.accidental
506 }
507
508 pub fn inversion(&self) -> u8 {
510 self.inversion
511 }
512
513 pub fn secondary(&self) -> Option<&str> {
515 self.secondary.as_deref()
516 }
517
518 pub fn key(&self) -> &Key {
520 &self.key
521 }
522
523 pub fn roman_numeral(&self) -> String {
527 let numeral = self.roman_numeral_alone();
528 if matches!(self.kind, RomanKind::AugmentedSixth(_)) {
529 return numeral;
530 }
531 let prefix = match self.accidental {
532 0 => String::new(),
533 sharps if sharps > 0 => "#".repeat(sharps.unsigned_abs() as usize),
534 flats => "b".repeat(flats.unsigned_abs() as usize),
535 };
536 format!("{prefix}{numeral}")
537 }
538
539 pub fn roman_numeral_alone(&self) -> String {
542 if let RomanKind::AugmentedSixth(kind) = self.kind {
543 return kind.figure().trim_end_matches(['+', '6']).to_string();
544 }
545 let primary = self.figure.split('/').next().unwrap_or_default();
546 let reading = self.effective_key().unwrap_or_else(|_| self.key.clone());
550 let named = named_figure(primary, &reading);
551 let (_, unaltered) = split_roman_accidental_prefix(&named);
552 unaltered
553 .chars()
554 .take_while(|c| matches!(c, 'i' | 'v' | 'x' | 'I' | 'V' | 'X'))
555 .collect()
556 }
557
558 pub fn figure_and_key(&self) -> String {
561 format!(
562 "{} in {} {}",
563 self.figure,
564 self.key.tonic_pitch_name_with_case(),
565 self.key.mode()
566 )
567 }
568
569 pub fn scale_degree_with_alteration(&self) -> (u8, i8) {
572 (self.degree, self.accidental)
573 }
574
575 pub fn functionality_score(&self) -> u8 {
581 if self.secondary.is_some() {
582 let score = self.figure.split('/').fold(100.0, |score, part| {
583 score * f64::from(functionality_score_of(part).unwrap_or(0)) / 100.0
584 });
585 return score as u8;
586 }
587 functionality_score_of(&self.figure.replace('+', "")).unwrap_or(0)
588 }
589
590 pub fn is_neapolitan(&self, require_first_inversion: bool) -> bool {
594 self.degree == 2
595 && self.accidental == -1
596 && self.quality == RomanQuality::Major
597 && (!require_first_inversion || self.inversion == 1)
598 }
599
600 pub fn is_mixture(&self, evaluate_secondary: bool) -> Result<bool> {
605 if evaluate_secondary && let Some(secondary) = &self.secondary {
606 return RomanNumeral::new(secondary.clone(), self.key.clone())?.is_mixture(true);
607 }
608 if self.kind != RomanKind::Diatonic || !(1..=7).contains(&self.degree) {
609 return Ok(false);
610 }
611 let quality = match self.quality {
616 RomanQuality::Diminished | RomanQuality::HalfDiminished => "diminished",
617 RomanQuality::Minor => "minor",
618 RomanQuality::Major => "major",
619 RomanQuality::Augmented => return Ok(false),
620 };
621 let front = match self.accidental {
622 0 => "natural",
623 1 => "sharp",
624 -1 => "flat",
625 _ => "other",
626 };
627 let entry = (self.degree, quality, front);
628 Ok(match self.key.mode() {
629 "major" => {
630 MAJOR_KEY_MIXTURES.contains(&entry)
631 || (self.degree == 7
632 && self.seventh
633 && self.quality == RomanQuality::Diminished)
634 }
635 "minor" => {
636 MINOR_KEY_MIXTURES.contains(&entry)
637 || (self.degree == 7
638 && self.seventh
639 && self.quality == RomanQuality::HalfDiminished)
640 }
641 _ => false,
642 })
643 }
644
645 pub fn transpose(&self, interval: &Interval) -> Result<Self> {
647 RomanNumeral::new(self.figure.clone(), self.key.transpose(interval)?)
648 }
649
650 pub fn effective_key_of(&self) -> Result<Key> {
654 self.effective_key()
655 }
656
657 fn effective_key(&self) -> Result<Key> {
658 let Some(secondary) = &self.secondary else {
659 return Ok(self.key.clone());
660 };
661
662 secondary_key(
663 &self.key,
664 secondary,
665 self.sixth_minor,
666 self.seventh_minor,
667 self.case_matters,
668 )
669 }
670}
671
672impl fmt::Display for RomanNumeral {
673 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
674 formatter.write_str(self.figure())
675 }
676}
677
678pub const FUNCTIONALITY_SCORES: [(&str, u8); 48] = [
681 ("I", 100),
682 ("i", 90),
683 ("V7", 80),
684 ("V", 70),
685 ("V65", 68),
686 ("I6", 65),
687 ("V6", 63),
688 ("V43", 61),
689 ("I64", 60),
690 ("IV", 59),
691 ("i6", 58),
692 ("viio7", 57),
693 ("V42", 55),
694 ("viio65", 53),
695 ("viio6", 52),
696 ("#viio65", 51),
697 ("ii", 50),
698 ("#viio6", 49),
699 ("ii65", 48),
700 ("ii43", 47),
701 ("ii42", 46),
702 ("IV6", 45),
703 ("ii6", 43),
704 ("VI", 42),
705 ("#VI", 41),
706 ("vi", 40),
707 ("viio", 39),
708 ("#viio", 38),
709 ("iio", 37),
710 ("iio42", 36),
711 ("bII6", 35),
712 ("It6", 34),
713 ("Ger65", 33),
714 ("iio43", 32),
715 ("iio65", 31),
716 ("Fr43", 30),
717 ("#vio", 28),
718 ("#vio6", 27),
719 ("III", 22),
720 ("Sw43", 21),
721 ("v", 20),
722 ("VII", 19),
723 ("VII7", 18),
724 ("IV65", 17),
725 ("IV7", 16),
726 ("iii", 15),
727 ("iii6", 12),
728 ("vi6", 10),
729];
730
731fn functionality_score_of(figure: &str) -> Option<u8> {
732 FUNCTIONALITY_SCORES
733 .iter()
734 .find(|(known, _)| *known == figure)
735 .map(|(_, score)| *score)
736}
737
738const MAJOR_KEY_MIXTURES: [(u8, &str, &str); 7] = [
739 (1, "minor", "natural"),
740 (2, "diminished", "natural"),
741 (3, "major", "flat"),
742 (4, "minor", "natural"),
743 (5, "minor", "natural"),
744 (6, "major", "flat"),
745 (7, "major", "flat"),
746];
747
748const MINOR_KEY_MIXTURES: [(u8, &str, &str); 5] = [
749 (1, "major", "natural"),
750 (2, "minor", "natural"),
751 (3, "minor", "sharp"),
752 (4, "major", "natural"),
753 (6, "minor", "sharp"),
754];
755
756#[derive(Clone, Debug, PartialEq)]
762#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
763#[must_use]
764pub struct FigureTuple {
765 pub deg_from_ref_pitch: u8,
767 pub alter: FloatType,
769 pub prefix: String,
772}
773
774#[derive(Clone, Debug, PartialEq)]
777#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
778#[must_use]
779pub struct PitchFigureTuple {
780 pub figure: FigureTuple,
782 pub pitch: Pitch,
784}
785
786#[derive(Clone, Debug, Default, PartialEq)]
795#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
796struct FiguredBass {
797 column: Notation,
799 written: String,
803 omitted: Vec<u8>,
805 added: Vec<(i8, u8)>,
808 bracketed: Vec<(i8, u8)>,
811}
812
813impl FiguredBass {
814 fn numbers(&self) -> Vec<u8> {
816 self.column
817 .numbers()
818 .iter()
819 .filter_map(|number| number.map(|number| number as u8))
820 .collect()
821 }
822
823 fn figures(&self) -> &[Figure] {
825 self.column.figures()
826 }
827
828 fn alters(&self, number: u8) -> bool {
831 self.column.figures().iter().any(|figure| {
832 figure.number() == Some(IntegerType::from(number))
833 && figure
834 .modifier()
835 .accidental()
836 .is_some_and(|accidental| accidental.alter() != 0.0)
837 })
838 }
839}
840
841#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
848#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
849pub enum ImpliedQuality {
850 #[default]
852 Unstated,
853 Major,
855 Minor,
857 Diminished,
859 HalfDiminished,
861 Augmented,
863 DominantSeventh,
865}
866
867impl From<ImpliedQuality> for RomanQuality {
868 fn from(quality: ImpliedQuality) -> Self {
871 match quality {
872 ImpliedQuality::Minor => Self::Minor,
873 ImpliedQuality::Diminished => Self::Diminished,
874 ImpliedQuality::HalfDiminished => Self::HalfDiminished,
875 ImpliedQuality::Augmented => Self::Augmented,
876 ImpliedQuality::Unstated | ImpliedQuality::Major | ImpliedQuality::DominantSeventh => {
877 Self::Major
878 }
879 }
880 }
881}
882
883impl ImpliedQuality {
884 pub fn from_name(name: &str) -> Self {
886 match name {
887 "major" => Self::Major,
888 "minor" => Self::Minor,
889 "diminished" => Self::Diminished,
890 "half-diminished" => Self::HalfDiminished,
891 "augmented" => Self::Augmented,
892 "minor-seventh" | "dominant-seventh" => Self::DominantSeventh,
893 _ => Self::Unstated,
894 }
895 }
896
897 pub fn name(self) -> &'static str {
900 match self {
901 Self::Unstated => "",
902 Self::Major => "major",
903 Self::Minor => "minor",
904 Self::Diminished => "diminished",
905 Self::HalfDiminished => "half-diminished",
906 Self::Augmented => "augmented",
907 Self::DominantSeventh => "dominant-seventh",
908 }
909 }
910
911 pub fn correct_semitones(self) -> &'static [u8] {
914 match self {
915 Self::Unstated => &[],
916 Self::Major => &[4, 7],
917 Self::Minor => &[3, 7],
918 Self::Diminished => &[3, 6, 9],
919 Self::HalfDiminished => &[3, 6, 10],
920 Self::Augmented => &[4, 8],
921 Self::DominantSeventh => &[4, 7, 10],
922 }
923 }
924}
925
926#[derive(Clone, Debug, PartialEq, Eq)]
934pub struct NumeralAlone {
935 pub numeral: String,
937 pub rest: String,
939 pub degree: u8,
941 pub alteration: i8,
944 pub minor: bool,
947 pub bracketed: Vec<(i8, u8)>,
949}
950
951#[cfg(test)]
952mod tests {
953 #[test]
954 fn a_numeral_reports_how_it_was_read() {
955 use super::{
956 ImpliedQuality, Minor67Default, NumeralAlone, RomanNumeral, analyze_chord_with_root,
957 parse_numeral_alone, secondary_key,
958 };
959 use crate::{Chord, Key, Pitch};
960
961 let key = Key::from_tonic("C").unwrap();
962 let numeral = RomanNumeral::new("V7[no3][add4][#5]/ii", key.clone()).unwrap();
963 assert_eq!(numeral.key().tonic().name(), "C");
964 assert!(numeral.scale().is_none());
965 assert!(numeral.case_matters());
966 assert_eq!(numeral.sixth_minor(), Minor67Default::Quality);
967 assert_eq!(numeral.seventh_minor(), Minor67Default::Quality);
968 assert_eq!(numeral.implied_quality(), ImpliedQuality::Major);
969 assert_eq!(numeral.written_accidental(), 0);
970 assert_eq!(numeral.omitted_steps(), [3]);
971 assert_eq!(numeral.added_steps(), [(0, 4)]);
972 assert_eq!(numeral.bracketed_alterations(), [(1, 5)]);
973 assert_eq!(numeral.figures_notation().numbers().len(), 3);
975 assert_eq!(numeral.effective_key_of().unwrap().tonic().name(), "D");
976 assert_eq!(numeral.roman_numeral_alone(), "V");
977 assert_eq!(numeral.to_string(), "V7[no3][add4][#5]/ii");
978 assert_eq!(numeral.figure_and_key(), "V7[no3][add4][#5]/ii in C major");
979
980 assert_eq!(
981 ImpliedQuality::from_name("half-diminished").name(),
982 "half-diminished"
983 );
984 assert_eq!(
985 ImpliedQuality::from_name("nonsense"),
986 ImpliedQuality::Unstated
987 );
988 assert_eq!(ImpliedQuality::Unstated.name(), "");
989
990 let alone: NumeralAlone = parse_numeral_alone("VI6").unwrap();
991 assert_eq!((alone.numeral.as_str(), alone.rest.as_str()), ("VI", "6"));
992 assert_eq!((alone.degree, alone.alteration, alone.minor), (6, 0, false));
993 let german = parse_numeral_alone("Ger").unwrap();
994 assert_eq!(
995 (german.numeral.as_str(), german.rest.as_str()),
996 ("Ger", "65")
997 );
998 assert!(german.minor);
999 assert!(parse_numeral_alone("").is_err());
1000
1001 let dominant_of_dominant = secondary_key(
1002 &key,
1003 "V",
1004 Minor67Default::Quality,
1005 Minor67Default::Quality,
1006 true,
1007 )
1008 .unwrap();
1009 assert_eq!(dominant_of_dominant.tonic().name(), "G");
1010 assert_eq!(dominant_of_dominant.mode(), "major");
1011 let of_supertonic = secondary_key(
1012 &key,
1013 "ii",
1014 Minor67Default::Quality,
1015 Minor67Default::Quality,
1016 true,
1017 )
1018 .unwrap();
1019 assert_eq!(of_supertonic.mode(), "minor");
1020
1021 let chord = Chord::new("E4 G4 C5").unwrap();
1022 let root = Pitch::from_name("C4").unwrap();
1023 let analyzed = analyze_chord_with_root(&chord, key, &root)
1024 .unwrap()
1025 .unwrap();
1026 assert_eq!(analyzed.figure(), "I6");
1027 }
1028
1029 #[test]
1033 fn figures_are_read_above_a_reference_pitch_and_corrected_for_minor() {
1034 use super::{
1035 FigureTuple, correct_rn_alteration_for_minor, correct_suffix_for_chord_quality,
1036 figure_tuples,
1037 };
1038 use crate::{Chord, Key, Pitch};
1039
1040 let c_major = Key::from_tonic("C").unwrap();
1041 let c_minor = Key::from_tonic("c").unwrap();
1042 let figure = |name: &str, key: &Key, reference: &str| {
1043 FigureTuple::from_pitch_and_reference(
1044 &Pitch::from_name(name).unwrap(),
1045 key,
1046 &Pitch::from_name(reference).unwrap(),
1047 )
1048 .unwrap()
1049 };
1050 let read = |figure: FigureTuple| (figure.deg_from_ref_pitch, figure.alter, figure.prefix);
1051 assert_eq!(
1052 read(figure("A-3", &c_major, "F#2")),
1053 (3, -1.0, "b".to_string())
1054 );
1055 assert_eq!(
1056 read(figure("E--4", &c_minor, "C3")),
1057 (3, -1.0, "b".to_string())
1058 );
1059 assert_eq!(read(figure("E-4", &c_minor, "C3")), (3, 0.0, String::new()));
1060 assert_eq!(
1061 read(figure("E#4", &c_minor, "C3")),
1062 (3, 2.0, "##".to_string())
1063 );
1064 assert_eq!(
1065 read(figure("A4", &c_minor, "C3")),
1066 (6, 1.0, "#".to_string())
1067 );
1068 assert_eq!(
1069 read(figure("B5", &c_minor, "C3")),
1070 (7, 1.0, "#".to_string())
1071 );
1072
1073 let tuples = figure_tuples(&Chord::new("F#2 D3 A-3 C#4").unwrap(), &c_minor).unwrap();
1074 let read_all: Vec<(u8, FloatType, String)> = tuples
1075 .iter()
1076 .map(|tuple| read(tuple.figure.clone()))
1077 .collect();
1078 assert_eq!(
1079 read_all,
1080 [
1081 (1, 1.0, "#".to_string()),
1082 (6, 0.0, String::new()),
1083 (3, 0.0, String::new()),
1084 (5, 1.0, "#".to_string()),
1085 ]
1086 );
1087 assert_eq!(tuples[2].pitch.name_with_octave(), "A-3");
1088 assert!(
1089 figure_tuples(&Chord::new("").unwrap(), &c_minor)
1090 .unwrap()
1091 .is_empty()
1092 );
1093
1094 let sixth = |alter: FloatType, prefix: &str| FigureTuple {
1095 deg_from_ref_pitch: 6,
1096 alter,
1097 prefix: prefix.to_string(),
1098 };
1099 assert_eq!(
1100 read(correct_rn_alteration_for_minor(
1101 &sixth(-1.0, ""),
1102 &c_minor,
1103 false
1104 )),
1105 (6, -1.0, "b".to_string())
1106 );
1107 assert_eq!(
1108 read(correct_rn_alteration_for_minor(
1109 &sixth(0.0, ""),
1110 &c_minor,
1111 false
1112 )),
1113 (6, 0.0, "b".to_string())
1114 );
1115 let raised_seventh = figure("B5", &c_minor, "C3");
1116 assert_eq!(
1117 read(correct_rn_alteration_for_minor(
1118 &raised_seventh,
1119 &c_minor,
1120 false
1121 )),
1122 (7, 0.0, String::new())
1123 );
1124 assert_eq!(
1125 read(correct_rn_alteration_for_minor(
1126 &raised_seventh,
1127 &c_minor,
1128 true
1129 )),
1130 (7, 1.0, "#".to_string())
1131 );
1132 assert_eq!(
1133 read(correct_rn_alteration_for_minor(
1134 &sixth(2.0, "##"),
1135 &c_minor,
1136 false
1137 )),
1138 (6, 1.0, "#".to_string())
1139 );
1140 let in_major = sixth(-1.0, "b");
1141 assert_eq!(
1142 correct_rn_alteration_for_minor(&in_major, &c_major, false),
1143 in_major
1144 );
1145 let fourth = FigureTuple {
1146 deg_from_ref_pitch: 4,
1147 alter: -1.0,
1148 prefix: "b".to_string(),
1149 };
1150 assert_eq!(
1151 correct_rn_alteration_for_minor(&fourth, &c_minor, false),
1152 fourth
1153 );
1154
1155 assert_eq!(
1156 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G4").unwrap(), "6"),
1157 "6"
1158 );
1159 assert_eq!(
1160 correct_suffix_for_chord_quality(&Chord::new("E3 C4 G-4").unwrap(), "6"),
1161 "o6"
1162 );
1163 assert_eq!(
1164 correct_suffix_for_chord_quality(&Chord::new("C4 E4 G#4").unwrap(), ""),
1165 "+"
1166 );
1167 assert_eq!(
1168 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A4").unwrap(), "7"),
1169 "ø7"
1170 );
1171 assert_eq!(
1172 correct_suffix_for_chord_quality(&Chord::new("B3 D4 F4 A-4").unwrap(), "o7"),
1173 "o7"
1174 );
1175 }
1176
1177 #[test]
1180 fn the_sixth_and_seventh_of_a_minor_key_are_raised_by_quality() {
1181 use super::{ImpliedQuality, Minor67Default, adjust_minor_vi_and_vii_by_quality};
1182 use crate::Key;
1183
1184 let minor = Key::from_tonic("c").unwrap();
1185 let major = Key::from_tonic("C").unwrap();
1186 let adjust = |key: &Key, reading, quality, accidental| {
1187 adjust_minor_vi_and_vii_by_quality(key, reading, quality, accidental)
1188 };
1189 assert_eq!(
1190 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1191 1
1192 );
1193 assert_eq!(
1194 adjust(&minor, Minor67Default::Quality, ImpliedQuality::Major, 0),
1195 0
1196 );
1197 assert_eq!(
1198 adjust(&major, Minor67Default::Quality, ImpliedQuality::Minor, 0),
1199 0
1200 );
1201 assert_eq!(
1202 adjust(&minor, Minor67Default::Flat, ImpliedQuality::Diminished, 0),
1203 0
1204 );
1205 assert_eq!(
1206 adjust(&minor, Minor67Default::Sharp, ImpliedQuality::Major, 0),
1207 1
1208 );
1209 assert_eq!(
1212 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 1),
1213 1
1214 );
1215 assert_eq!(
1216 adjust(
1217 &minor,
1218 Minor67Default::Cautionary,
1219 ImpliedQuality::Major,
1220 -1
1221 ),
1222 0
1223 );
1224 assert_eq!(
1225 adjust(&minor, Minor67Default::Cautionary, ImpliedQuality::Minor, 0),
1226 1
1227 );
1228 }
1229
1230 #[test]
1231 fn a_numeral_reports_the_digits_it_was_written_with() {
1232 let major = Key::from_tonic("C").unwrap();
1233 for (figure, written) in [
1234 ("V65", "65"),
1235 ("V", ""),
1236 ("I7#5b3", "7#5b3"),
1237 ("viio6", "6"),
1238 ("vii\u{f8}7", "7"),
1239 ] {
1240 let numeral = RomanNumeral::new(figure, major.clone()).unwrap();
1241 assert_eq!(numeral.figures_written(), written, "{figure}");
1242 }
1243 let minor = Key::from_tonic("c").unwrap();
1244 for (figure, written) in [("Fr43", "43"), ("Fr+6", "43"), ("It+6", "6"), ("Ger", "65")] {
1245 let numeral = RomanNumeral::new(figure, minor.clone()).unwrap();
1246 assert_eq!(numeral.figures_written(), written, "{figure}");
1247 }
1248 }
1249
1250 #[test]
1251 fn a_minor_key_raises_its_sixth_and_seventh_where_the_figure_asks() {
1252 let minor = Key::from_tonic_mode("a", Some("minor")).unwrap();
1253 let leading = RomanNumeral::new("viio7", minor.clone()).unwrap();
1256 assert_eq!(
1257 leading.to_chord().unwrap().pitch_names(),
1258 ["G#", "B", "D", "F"]
1259 );
1260 assert_eq!(leading.roman_numeral(), "#vii");
1261 assert_eq!(leading.roman_numeral_alone(), "vii");
1262 assert_eq!(leading.scale_degree_with_alteration(), (7, 1));
1263
1264 assert_eq!(
1266 RomanNumeral::new("vi", minor.clone())
1267 .unwrap()
1268 .to_chord()
1269 .unwrap()
1270 .pitch_names(),
1271 ["F#", "A", "C#"]
1272 );
1273 assert_eq!(
1275 RomanNumeral::new("VI", minor.clone())
1276 .unwrap()
1277 .to_chord()
1278 .unwrap()
1279 .pitch_names(),
1280 ["F", "A", "C"]
1281 );
1282 let major = Key::from_tonic_mode("C", Some("major")).unwrap();
1284 assert_eq!(
1285 RomanNumeral::new("vi", major)
1286 .unwrap()
1287 .to_chord()
1288 .unwrap()
1289 .pitch_names(),
1290 ["A", "C", "E"]
1291 );
1292 }
1293
1294 #[test]
1295 fn the_four_readings_of_a_minor_sixth_and_seventh() {
1296 let minor = Key::from_tonic_mode("c", Some("minor")).unwrap();
1297 let read = |figure: &str, sixth: Minor67Default| {
1298 RomanNumeral::with_minor_defaults(figure, minor.clone(), sixth, sixth)
1299 .unwrap()
1300 .to_chord()
1301 .unwrap()
1302 .pitch_names()
1303 .join(" ")
1304 };
1305
1306 assert_eq!(read("vi", Minor67Default::Quality), "A C E");
1309 assert_eq!(read("VI", Minor67Default::Quality), "A- C E-");
1310
1311 assert_eq!(read("vi", Minor67Default::Flat), "A- C- E-");
1314 assert_eq!(read("#vi", Minor67Default::Flat), "A C E");
1315
1316 assert_eq!(read("VI", Minor67Default::Sharp), "A C# E");
1318 assert_eq!(read("bVI", Minor67Default::Sharp), "A- C E-");
1319
1320 assert_eq!(read("#vi", Minor67Default::Cautionary), "A C E");
1324 assert_eq!(read("vi", Minor67Default::Cautionary), "A C E");
1325 assert_eq!(read("bVI", Minor67Default::Cautionary), "A- C E-");
1326 assert_eq!(read("VI", Minor67Default::Cautionary), "A- C E-");
1327 }
1328
1329 #[test]
1330 fn the_neapolitan_can_be_written_by_name() {
1331 let key = Key::from_tonic_mode("c#", Some("minor")).unwrap();
1334 let named = RomanNumeral::new("N6", key.clone()).unwrap();
1335 assert_eq!(named.figure(), "N6");
1336 assert_eq!(named.degree(), 2);
1337 assert_eq!(named.accidental(), -1);
1338 assert_eq!(named.inversion(), 1);
1339 assert!(named.is_neapolitan(true));
1340
1341 let spelled = RomanNumeral::new("bII6", key.clone()).unwrap();
1342 assert_eq!(
1343 named.to_chord().unwrap().pitch_names(),
1344 spelled.to_chord().unwrap().pitch_names()
1345 );
1346
1347 let root_position = RomanNumeral::new("N53", key).unwrap();
1349 assert_eq!(root_position.inversion(), 0);
1350 }
1351
1352 #[test]
1353 fn inversion_names_and_tonic_dominant_reading_match_music21() {
1354 let chord = |notes: &str| Chord::new(notes).unwrap();
1355 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), None), "");
1356 assert_eq!(roman_inversion_name(&chord("E4 G4 C5"), None), "6");
1357 assert_eq!(roman_inversion_name(&chord("G3 C4 E4"), None), "64");
1358 assert_eq!(roman_inversion_name(&chord("C4 E4 G4 B-4"), None), "7");
1359 assert_eq!(roman_inversion_name(&chord("E4 G4 B-4 C5"), None), "65");
1360 assert_eq!(roman_inversion_name(&chord("G3 B-3 C4 E4"), None), "43");
1361 assert_eq!(roman_inversion_name(&chord("B-3 C4 E4 G4"), None), "42");
1362 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(2)), "64");
1363 assert_eq!(roman_inversion_name(&chord("C4 E4 G4"), Some(3)), "");
1364 assert_eq!(roman_inversion_name(&Chord::empty(), None), "");
1365
1366 let cases = [
1367 ("C4 E4 G4", "C", Some("I")),
1368 ("E4 G4 C5", "C", Some("I6")),
1369 ("G3 B3 D4 F4", "C", Some("V7")),
1370 ("D4 F4 A4", "C", Some("V43")),
1371 ("A3 C4 E4", "a", Some("i")),
1372 ("E3 G#3 B3", "a", Some("V")),
1373 ("C4 E4", "C", Some("I")),
1374 ("G4 D5", "C", Some("V")),
1375 ("B3 D4", "C", Some("V")),
1376 ("F#4 A4", "C", None),
1377 ];
1378 for (notes, key, expected) in cases {
1379 let reading =
1380 identify_as_tonic_or_dominant(&chord(notes), &Key::from_tonic(key).unwrap())
1381 .unwrap();
1382 assert_eq!(reading.as_deref(), expected, "{notes} in {key}");
1383 }
1384 }
1385
1386 #[test]
1387 #[allow(clippy::type_complexity)]
1388 fn numeral_names_scores_and_mixture_match_music21() {
1389 let cases: [(&str, &str, &str, &str, (u8, i8), u8, bool, bool, bool); 18] = [
1390 (
1391 "V7",
1392 "C",
1393 "V",
1394 "V7 in C major",
1395 (5, 0),
1396 80,
1397 false,
1398 false,
1399 false,
1400 ),
1401 (
1402 "bII6",
1403 "C",
1404 "bII",
1405 "bII6 in C major",
1406 (2, -1),
1407 35,
1408 true,
1409 true,
1410 false,
1411 ),
1412 (
1413 "bII",
1414 "C",
1415 "bII",
1416 "bII in C major",
1417 (2, -1),
1418 0,
1419 false,
1420 true,
1421 false,
1422 ),
1423 (
1424 "bII6",
1425 "a",
1426 "bII",
1427 "bII6 in a minor",
1428 (2, -1),
1429 35,
1430 true,
1431 true,
1432 false,
1433 ),
1434 (
1435 "viio7",
1436 "C",
1437 "vii",
1438 "viio7 in C major",
1439 (7, 0),
1440 57,
1441 false,
1442 false,
1443 true,
1444 ),
1445 (
1446 "I",
1447 "C",
1448 "I",
1449 "I in C major",
1450 (1, 0),
1451 100,
1452 false,
1453 false,
1454 false,
1455 ),
1456 (
1457 "i",
1458 "a",
1459 "i",
1460 "i in a minor",
1461 (1, 0),
1462 90,
1463 false,
1464 false,
1465 false,
1466 ),
1467 (
1468 "V65/V",
1469 "C",
1470 "V",
1471 "V65/V in C major",
1472 (5, 0),
1473 47,
1474 false,
1475 false,
1476 false,
1477 ),
1478 (
1479 "iv",
1480 "C",
1481 "iv",
1482 "iv in C major",
1483 (4, 0),
1484 0,
1485 false,
1486 false,
1487 true,
1488 ),
1489 (
1490 "bVI",
1491 "C",
1492 "bVI",
1493 "bVI in C major",
1494 (6, -1),
1495 0,
1496 false,
1497 false,
1498 true,
1499 ),
1500 (
1501 "I",
1502 "a",
1503 "I",
1504 "I in a minor",
1505 (1, 0),
1506 100,
1507 false,
1508 false,
1509 true,
1510 ),
1511 (
1512 "III",
1513 "a",
1514 "III",
1515 "III in a minor",
1516 (3, 0),
1517 22,
1518 false,
1519 false,
1520 false,
1521 ),
1522 (
1523 "#iii",
1524 "a",
1525 "#iii",
1526 "#iii in a minor",
1527 (3, 1),
1528 0,
1529 false,
1530 false,
1531 true,
1532 ),
1533 (
1534 "ii",
1535 "C",
1536 "ii",
1537 "ii in C major",
1538 (2, 0),
1539 50,
1540 false,
1541 false,
1542 false,
1543 ),
1544 (
1545 "iio",
1546 "C",
1547 "ii",
1548 "iio in C major",
1549 (2, 0),
1550 37,
1551 false,
1552 false,
1553 true,
1554 ),
1555 (
1556 "IV",
1557 "a",
1558 "IV",
1559 "IV in a minor",
1560 (4, 0),
1561 59,
1562 false,
1563 false,
1564 true,
1565 ),
1566 (
1567 "bVII",
1568 "C",
1569 "bVII",
1570 "bVII in C major",
1571 (7, -1),
1572 0,
1573 false,
1574 false,
1575 true,
1576 ),
1577 (
1578 "v",
1579 "C",
1580 "v",
1581 "v in C major",
1582 (5, 0),
1583 20,
1584 false,
1585 false,
1586 true,
1587 ),
1588 ];
1589 for (figure, key, numeral, figure_and_key, degree, score, neapolitan, loose, mixture) in
1590 cases
1591 {
1592 let rn = RomanNumeral::new(figure, Key::from_tonic(key).unwrap()).unwrap();
1593 assert_eq!(rn.roman_numeral(), numeral, "{figure} in {key}");
1594 assert_eq!(rn.figure_and_key(), figure_and_key, "{figure} in {key}");
1595 assert_eq!(
1596 rn.scale_degree_with_alteration(),
1597 degree,
1598 "{figure} in {key}"
1599 );
1600 assert_eq!(rn.functionality_score(), score, "{figure} in {key}");
1601 assert_eq!(rn.is_neapolitan(true), neapolitan, "{figure} in {key}");
1602 assert_eq!(rn.is_neapolitan(false), loose, "{figure} in {key}");
1603 assert_eq!(rn.is_mixture(false).unwrap(), mixture, "{figure} in {key}");
1604 assert_eq!(rn.is_mixture(true).unwrap(), mixture, "{figure} in {key}");
1605 }
1606 for (key, mixture) in [("a", true), ("A", false)] {
1610 let half = RomanNumeral::new("viiø7", Key::from_tonic(key).unwrap()).unwrap();
1611 assert_eq!(half.is_mixture(false).unwrap(), mixture, "viiø7 in {key}");
1612 }
1613
1614 let italian = RomanNumeral::new("It6", Key::from_tonic("C").unwrap()).unwrap();
1615 assert_eq!(italian.roman_numeral(), "It");
1616 assert_eq!(italian.functionality_score(), 34);
1617 assert!(!italian.is_mixture(false).unwrap());
1618 let transposed = RomanNumeral::new("bII6", Key::from_tonic("a").unwrap())
1619 .unwrap()
1620 .transpose(&Interval::from_name("M2").unwrap())
1621 .unwrap();
1622 assert_eq!(transposed.figure_and_key(), "bII6 in b minor");
1623 }
1624
1625 #[test]
1626 fn functionality_scores_table_has_no_duplicates() {
1627 let mut figures: Vec<&str> = FUNCTIONALITY_SCORES.iter().map(|(f, _)| *f).collect();
1628 figures.sort_unstable();
1629 figures.dedup();
1630 assert_eq!(figures.len(), FUNCTIONALITY_SCORES.len());
1631 }
1632 use super::*;
1633
1634 #[test]
1635 fn secondary_dominant_resolves_to_chord() {
1636 let key = Key::from_tonic_mode("C", "major").unwrap();
1637 let rn = RomanNumeral::new("V7/V", key).unwrap();
1638 assert_eq!(rn.degree(), 5);
1639 assert_eq!(rn.secondary(), Some("V"));
1640 assert_eq!(
1641 rn.to_chord().unwrap().pitched_common_name(),
1642 "D-dominant seventh chord"
1643 );
1644 }
1645
1646 #[test]
1648 fn a_chord_is_named_the_way_music21_names_it() {
1649 let cases: &[(&str, &str, &str)] = &[
1650 ("C E G", "C", "I"),
1651 ("G B D F", "C", "V43"),
1652 ("C E- G-", "C", "io5b3"),
1653 ("G B D F", "a", "bVII43"),
1654 ("B D F A", "C", "viiø65"),
1655 ("F A C", "C", "IV64"),
1656 ("A C E", "C", "vi6"),
1657 ("A C E", "a", "i6"),
1658 ("F A C", "a", "bVI64"),
1659 ("C E G#", "a", "III+"),
1660 ("D- F A-", "C", "bII"),
1661 ("B4 D5 F5 A-5", "c", "viio7"),
1662 ("A-3 C4 F#4", "c", "It6"),
1663 ("A-3 C4 D4 F#4", "c", "Fr43"),
1664 ("A-3 C4 E-4 F#4", "c", "Ger65"),
1665 ("A-3 C4 E-4 F#4", "C", "Ger65"),
1666 ("C4 E4 G4 B-4", "F", "V7"),
1667 ("D4 F4 A4 C5", "C", "ii7"),
1668 ("D4 F4 A4 C#5", "C", "ii#7"),
1669 ("F4 A4 C5 E5", "C", "IV7"),
1670 ("E4 G4 B4 D5", "C", "iii7"),
1671 ("C4 E-4 G4 B4", "c", "i#7"),
1672 ("C4 E4 G4 B4", "C", "I7"),
1673 ("E-4 G4 B-4 D5", "c", "III7"),
1674 ("C4 E4 G4 B4 D5", "C", "I7532"),
1675 ("G3 C4 E4", "C", "I64"),
1676 ("E3 G3 C4", "C", "I6"),
1677 ("F#3 A3 C4 E-4", "G", "viiob753"),
1678 ("D F# A", "G", "V"),
1679 ("C E G B-", "C", "Ib753"),
1680 ];
1681 for (chord, key, figure) in cases {
1682 let key = Key::from_tonic(key).unwrap();
1683 let numeral = roman_numeral_from_chord(&Chord::new(*chord).unwrap(), Some(&key))
1684 .unwrap()
1685 .unwrap();
1686 assert_eq!(
1687 numeral.figure(),
1688 *figure,
1689 "{chord} in {}",
1690 key.tonic_pitch_name_with_case()
1691 );
1692 }
1693 for (chord, figure, key_name) in [
1694 ("D F# A", "I", "D major"),
1695 ("D F A", "i", "d minor"),
1696 ("E G# B D", "Ib8642", "E major"),
1697 ("C E G", "I", "C major"),
1698 ("A-3 C4 E-4 F#4", "Ger65", "c minor"),
1699 ("G B D F", "I64b3", "G major"),
1700 ] {
1701 let numeral = roman_numeral_from_chord(&Chord::new(chord).unwrap(), None)
1702 .unwrap()
1703 .unwrap();
1704 assert_eq!(numeral.figure(), figure, "{chord} with no key");
1705 let key = numeral.key();
1706 assert_eq!(
1707 format!("{} {}", key.tonic_pitch_name_with_case(), key.mode()),
1708 key_name,
1709 "{chord} with no key"
1710 );
1711 }
1712 assert!(
1713 roman_numeral_from_chord(&Chord::empty(), None)
1714 .unwrap()
1715 .is_none()
1716 );
1717 }
1718
1719 #[test]
1720 fn analyzes_chord_in_key() {
1721 let key = Key::from_tonic_mode("C", "major").unwrap();
1722 let chord = Chord::new("G B D F").unwrap();
1723 let rn = RomanNumeral::analyze(&chord, key).unwrap().unwrap();
1724 assert_eq!(rn.figure(), "V7");
1725 }
1726
1727 #[test]
1728 fn analyzes_accidentals_inversions_and_half_diminished_quality() {
1729 let key = Key::from_tonic_mode("C", "major").unwrap();
1730
1731 let neapolitan = Chord::new("D- F A-").unwrap();
1732 let rn = RomanNumeral::analyze(&neapolitan, key.clone())
1733 .unwrap()
1734 .unwrap();
1735 assert_eq!(rn.figure(), "bII");
1736 assert_eq!(rn.degree(), 2);
1737 assert_eq!(rn.accidental(), -1);
1738
1739 let first_inversion = Chord::new("E4 G4 C5").unwrap();
1740 let rn = RomanNumeral::analyze(&first_inversion, key.clone())
1741 .unwrap()
1742 .unwrap();
1743 assert_eq!(rn.figure(), "I6");
1744
1745 let leading_tone = Chord::new("B D F A").unwrap();
1746 let rn = RomanNumeral::analyze(&leading_tone, key).unwrap().unwrap();
1747 assert_eq!(rn.figure(), "vii\u{00f8}7");
1748 }
1749
1750 #[test]
1751 fn analyzes_with_explicit_root_for_browser_style_sets() {
1752 let key = Key::from_tonic_mode("C", "major").unwrap();
1753 let root = Pitch::from_name("C").unwrap();
1754 let chord = Chord::new("C E G").unwrap();
1755 let rn = RomanNumeral::analyze_with_root(&chord, key.clone(), &root)
1756 .unwrap()
1757 .unwrap();
1758 assert_eq!(rn.figure(), "I");
1759
1760 let seventh = Chord::new("C E G B-").unwrap();
1761 let rn = RomanNumeral::analyze_with_root(&seventh, key, &root)
1762 .unwrap()
1763 .unwrap();
1764 assert_eq!(rn.figure(), "I7");
1765 }
1766
1767 #[test]
1768 fn analyzes_augmented_sixth_chords_functionally() {
1769 let key = Key::from_tonic_mode("C", "minor").unwrap();
1770 let root = Pitch::from_name("C").unwrap();
1771 let french = Chord::new("C D F# A-").unwrap();
1772 let rn = RomanNumeral::analyze_with_root(&french, key.clone(), &root)
1773 .unwrap()
1774 .unwrap();
1775 assert_eq!(rn.figure(), "Fr+6");
1776
1777 let german = Chord::new("A- C E- F#").unwrap();
1778 let rn = RomanNumeral::analyze(&german, key).unwrap().unwrap();
1779 assert_eq!(rn.figure(), "Ger+6");
1780 }
1781
1782 #[test]
1783 fn figured_bass_shorthands_give_music21_inversions() {
1784 let key = Key::from_tonic_mode("C", "major").unwrap();
1787 for (figure, expected) in [
1788 ("V", 0),
1789 ("V6", 1),
1790 ("V64", 2),
1791 ("V7", 0),
1792 ("V65", 1),
1793 ("V43", 2),
1794 ("V42", 3),
1795 ("V642", 3),
1796 ("V653", 1),
1797 ("V643", 2),
1798 ("V63", 1),
1799 ("V53", 0),
1800 ("V9", 0),
1801 ] {
1802 let numeral = RomanNumeral::new(figure, key.clone())
1803 .unwrap_or_else(|err| panic!("{figure} should parse: {err}"));
1804 assert_eq!(numeral.inversion(), expected, "{figure}");
1805 }
1806 }
1807
1808 #[test]
1809 fn a_figure_expands_into_a_figured_bass_column() {
1810 let numbers = |figure: &str| {
1813 RomanNumeral::new(figure, Key::from_tonic("C").unwrap())
1814 .unwrap()
1815 .figure_numbers()
1816 };
1817 assert_eq!(numbers("V"), vec![5, 3]);
1818 assert_eq!(numbers("V7"), vec![7, 5, 3]);
1819 assert_eq!(numbers("V65"), vec![6, 5, 3]);
1820 assert_eq!(numbers("V43"), vec![6, 4, 3]);
1821 assert_eq!(numbers("V9"), vec![9, 7, 5, 3]);
1822 assert_eq!(numbers("V7#5b3"), vec![7, 5, 3]);
1824 }
1825
1826 #[test]
1827 fn a_column_says_which_degree_is_in_the_bass() {
1828 assert_eq!(bass_scale_degree_from_notation(5, &[6, 3]).unwrap(), 7);
1831 assert_eq!(bass_scale_degree_from_notation(1, &[5, 3]).unwrap(), 1);
1832 assert_eq!(bass_scale_degree_from_notation(2, &[6, 5, 3]).unwrap(), 4);
1833 assert_eq!(bass_scale_degree_from_notation(5, &[5, 4]).unwrap(), 5);
1835 }
1836
1837 #[test]
1838 fn a_figure_alters_one_note_rather_than_naming_another_chord() {
1839 let names = |figure: &str, key: &str| {
1840 RomanNumeral::new(figure, Key::from_tonic(key).unwrap())
1841 .unwrap()
1842 .to_chord()
1843 .unwrap()
1844 .pitch_names()
1845 };
1846 assert_eq!(names("V7b5", "C"), ["G", "D-", "F"]);
1849 assert_eq!(names("V[no3]", "F"), ["C", "G"]);
1850 assert_eq!(names("I[add4][no3]", "C"), ["C", "F", "G"]);
1851 assert_eq!(names("i#7", "c"), ["C", "E-", "G", "B"]);
1854 assert_eq!(names("i#7", "C"), ["C", "E-", "G", "B#"]);
1855 }
1856
1857 #[test]
1858 fn a_numeral_can_be_read_over_a_scale_that_is_not_a_key() {
1859 let scale = crate::scale::Scale::new(
1862 crate::scale::ScaleType::Octatonic,
1863 Pitch::from_name("C2").unwrap(),
1864 );
1865 let numeral = RomanNumeral::over_scale(
1866 "I9",
1867 Key::from_tonic("C").unwrap(),
1868 Some(scale),
1869 Minor67Default::Quality,
1870 Minor67Default::Quality,
1871 false,
1872 )
1873 .unwrap();
1874 let pitches: Vec<String> = numeral
1875 .to_chord()
1876 .unwrap()
1877 .pitches()
1878 .iter()
1879 .map(Pitch::name_with_octave)
1880 .collect();
1881 assert_eq!(pitches, ["C2", "E-2", "G-2", "A2", "C3"]);
1882 }
1883
1884 #[test]
1885 fn roman_numerals_parse_inversions_and_qualities() {
1886 let key = Key::from_tonic_mode("C", "major").unwrap();
1887 let first_inversion = RomanNumeral::new("I6", key.clone()).unwrap();
1888 assert_eq!(first_inversion.inversion(), 1);
1889 assert_eq!(
1890 first_inversion
1891 .to_chord()
1892 .unwrap()
1893 .pitches()
1894 .into_iter()
1895 .map(|pitch| pitch.name())
1896 .collect::<Vec<_>>(),
1897 vec!["E", "G", "C"]
1898 );
1899
1900 let diminished = RomanNumeral::new("viio7", key.clone()).unwrap();
1901 assert_eq!(diminished.degree(), 7);
1902 assert!(
1903 diminished
1904 .to_chord()
1905 .unwrap()
1906 .common_name()
1907 .contains("diminished")
1908 );
1909
1910 let half_diminished = RomanNumeral::new("vii\u{00f8}7", key.clone()).unwrap();
1911 assert_eq!(half_diminished.degree(), 7);
1912 assert_eq!(half_diminished.accidental(), 0);
1913 assert!(
1914 half_diminished
1915 .to_chord()
1916 .unwrap()
1917 .common_name()
1918 .contains("half-diminished")
1919 );
1920
1921 let borrowed = RomanNumeral::new("bII", key.clone()).unwrap();
1922 assert_eq!(borrowed.degree(), 2);
1923 assert_eq!(borrowed.accidental(), -1);
1924
1925 let added_thirteenth = RomanNumeral::new("I add(13)", key.clone()).unwrap();
1926 assert_eq!(added_thirteenth.inversion(), 0);
1927 assert_eq!(
1928 added_thirteenth.to_chord().unwrap().common_name(),
1929 "major triad"
1930 );
1931
1932 let augmented = RomanNumeral::new("III+", key).unwrap();
1933 assert_eq!(
1934 augmented
1935 .to_chord()
1936 .unwrap()
1937 .pitches()
1938 .into_iter()
1939 .map(|pitch| pitch.name())
1940 .collect::<Vec<_>>(),
1941 vec!["E", "G#", "B#"]
1942 );
1943 }
1944
1945 #[test]
1946 fn roman_numerals_parse_augmented_sixth_figures() {
1947 let key = Key::from_tonic_mode("C", "minor").unwrap();
1948 let french = RomanNumeral::new("Fr+6", key).unwrap();
1949 assert_eq!(french.degree(), 2);
1953 assert_eq!(french.accidental(), 0);
1954 assert_eq!(
1955 french
1956 .to_chord()
1957 .unwrap()
1958 .pitches()
1959 .into_iter()
1960 .map(|pitch| pitch.name())
1961 .collect::<Vec<_>>(),
1962 vec!["A-", "C", "D", "F#"]
1963 );
1964 }
1965
1966 #[test]
1967 fn roman_numerals_report_invalid_figures_and_empty_analysis() {
1968 let key = Key::from_tonic_mode("C", "major").unwrap();
1969
1970 assert!(RomanNumeral::new("", key.clone()).is_err());
1971 assert!(RomanNumeral::new("Q", key.clone()).is_err());
1972 assert!(analyze_chord(&Chord::empty(), key).unwrap().is_none());
1973 }
1974}