1pub(crate) mod chromaticinterval;
2pub(crate) mod diatonicinterval;
3pub(crate) mod direction;
4pub(crate) mod genericinterval;
5pub(crate) mod specifier;
6
7pub(crate) mod constants;
8
9pub use chromaticinterval::ChromaticInterval;
10pub use diatonicinterval::DiatonicInterval;
11pub use genericinterval::{GenericInterval, convert_generic};
12pub use specifier::Specifier;
13
14use direction::Direction;
15
16use std::cmp::Ordering;
17use std::fmt;
18use std::str::FromStr;
19
20use crate::common::numbertools::{MUSICAL_ORDINAL_STRINGS, MUSICAL_ORDINAL_STRINGS_LOWER};
21use crate::common::stringtools::get_num_from_str;
22use crate::error::{Error, Result};
23use crate::{
24 defaults::{FloatType, FractionType, IntegerType},
25 note::Note,
26 pitch::Pitch,
27 stepname::StepName,
28};
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub enum IntervalDirection {
34 Descending = -1,
36 Oblique = 0,
38 Ascending = 1,
40}
41
42impl IntervalDirection {
43 pub fn as_int(self) -> IntegerType {
45 self as IntegerType
46 }
47
48 pub fn name(self) -> &'static str {
50 match self {
51 Self::Descending => "Descending",
52 Self::Oblique => "Oblique",
53 Self::Ascending => "Ascending",
54 }
55 }
56}
57
58#[derive(Clone, Debug)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60#[must_use]
62pub struct Interval {
63 pub(crate) implicit_diatonic: bool,
64 pub(crate) diatonic: DiatonicInterval,
65 pub(crate) chromatic: ChromaticInterval,
66 pitch_start: Option<Pitch>,
67 pitch_end: Option<Pitch>,
68}
69
70impl PartialEq for Interval {
71 fn eq(&self, other: &Self) -> bool {
75 self.diatonic == other.diatonic && self.chromatic == other.chromatic
76 }
77}
78
79fn strip_direction_word(value: &str, word: &str) -> (String, bool) {
80 replace_case_insensitive(value, word, "", false, true)
81}
82
83fn replace_music_ordinal(value: &str, ordinal: &str, replacement: &str) -> (String, bool) {
84 replace_case_insensitive(value, ordinal, replacement, true, true)
85}
86
87fn replace_case_insensitive(
88 value: &str,
89 needle: &str,
90 replacement: &str,
91 consume_leading_whitespace: bool,
92 consume_trailing_whitespace: bool,
93) -> (String, bool) {
94 let needle_lower = needle.to_ascii_lowercase();
95 let value_lower = value.to_ascii_lowercase();
96 let mut output = String::with_capacity(value.len());
97 let mut pos = 0;
98 let mut replaced = false;
99
100 while let Some(relative_start) = value_lower[pos..].find(&needle_lower) {
101 let match_start = pos + relative_start;
102 let match_end = match_start + needle.len();
103 let mut copy_end = match_start;
104 let mut next_pos = match_end;
105
106 if consume_leading_whitespace {
107 while copy_end > pos {
108 let Some(ch) = value[pos..copy_end].chars().next_back() else {
109 break;
110 };
111 if !ch.is_whitespace() {
112 break;
113 }
114 copy_end -= ch.len_utf8();
115 }
116 }
117
118 if consume_trailing_whitespace {
119 while next_pos < value.len() {
120 let Some(ch) = value[next_pos..].chars().next() else {
121 break;
122 };
123 if !ch.is_whitespace() {
124 break;
125 }
126 next_pos += ch.len_utf8();
127 }
128 }
129
130 output.push_str(&value[pos..copy_end]);
131 output.push_str(replacement);
132 pos = next_pos;
133 replaced = true;
134 }
135
136 if !replaced {
137 return (value.to_string(), false);
138 }
139
140 output.push_str(&value[pos..]);
141 (output, true)
142}
143
144fn convert_staff_distance_to_interval(staff_dist: IntegerType) -> IntegerType {
145 match staff_dist.cmp(&0) {
146 Ordering::Equal => 1,
147 Ordering::Greater => staff_dist + 1,
148 Ordering::Less => staff_dist - 1,
149 }
150}
151
152pub fn staff_distance_to_generic_number(staff_distance: IntegerType) -> IntegerType {
156 convert_staff_distance_to_interval(staff_distance)
157}
158
159fn diatonic_note_number(pitch: &Pitch) -> IntegerType {
160 pitch.step().step_to_dnn_offset() + (7 * pitch.octave().unwrap_or(4))
161}
162
163pub fn written_higher_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
167 match diatonic_note_number(first).cmp(&diatonic_note_number(second)) {
168 Ordering::Greater => first,
169 Ordering::Less => second,
170 Ordering::Equal => absolute_higher_pitch(first, second),
171 }
172}
173
174pub fn written_lower_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
177 match diatonic_note_number(first).cmp(&diatonic_note_number(second)) {
178 Ordering::Less => first,
179 Ordering::Greater => second,
180 Ordering::Equal => absolute_lower_pitch(first, second),
181 }
182}
183
184pub fn absolute_higher_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
187 if second.ps() > first.ps() {
188 second
189 } else {
190 first
191 }
192}
193
194pub fn absolute_lower_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
197 if second.ps() < first.ps() {
198 second
199 } else {
200 first
201 }
202}
203
204pub fn notes_to_generic(p1: &Pitch, p2: &Pitch) -> Result<GenericInterval> {
207 let dnn1 = p1.step().step_to_dnn_offset() + (7 * p1.octave().unwrap_or(4));
208 let dnn2 = p2.step().step_to_dnn_offset() + (7 * p2.octave().unwrap_or(4));
209 let staff_dist = dnn2 - dnn1;
210 GenericInterval::from_int(convert_staff_distance_to_interval(staff_dist))
211}
212
213pub fn notes_to_chromatic(p1: &Pitch, p2: &Pitch) -> Result<ChromaticInterval> {
216 ChromaticInterval::new(p2.ps() - p1.ps())
217}
218
219fn specifier_from_generic_chromatic(
220 g_int: &GenericInterval,
221 c_int: &ChromaticInterval,
222) -> Result<Specifier> {
223 let normal_semis = StepName::ALL[(g_int.simple_undirected() - 1) as usize].step_ref()
224 + 12 * g_int.simple_steps_and_octaves().1;
225
226 let c_direction = c_int.direction();
227
228 let these_semis = if g_int.direction() != c_direction
229 && g_int.direction() != direction::Direction::Oblique
230 && c_direction != direction::Direction::Oblique
231 {
232 -c_int.undirected()
233 } else if g_int.undirected() == 1 {
234 c_int.directed()
235 } else {
236 c_int.undirected()
237 };
238
239 let rounding_error = if c_int.undirected() > 0.0 {
240 0.0001
241 } else {
242 -0.0001
243 };
244 let diff = ((these_semis + rounding_error).round() as IntegerType).saturating_sub(normal_semis);
246
247 if g_int.is_perfectable() {
248 specifier_at(
249 &PERFECTABLE_SPECIFIERS,
250 diff.saturating_add(4),
251 "Perfect",
252 diff,
253 )
254 } else {
255 specifier_at(&MAJOR_SPECIFIERS, diff.saturating_add(5), "Major", diff)
256 }
257}
258
259const PERFECTABLE_SPECIFIERS: [Specifier; 9] = [
262 Specifier::QuadrupleDiminished,
263 Specifier::TripleDiminished,
264 Specifier::DoubleDiminished,
265 Specifier::Diminished,
266 Specifier::Perfect,
267 Specifier::Augmented,
268 Specifier::DoubleAugmented,
269 Specifier::TripleAugmented,
270 Specifier::QuadrupleAugmented,
271];
272
273const MAJOR_SPECIFIERS: [Specifier; 10] = [
276 Specifier::QuadrupleDiminished,
277 Specifier::TripleDiminished,
278 Specifier::DoubleDiminished,
279 Specifier::Diminished,
280 Specifier::Minor,
281 Specifier::Major,
282 Specifier::Augmented,
283 Specifier::DoubleAugmented,
284 Specifier::TripleAugmented,
285 Specifier::QuadrupleAugmented,
286];
287
288fn specifier_at(
298 table: &[Specifier],
299 index: IntegerType,
300 from: &str,
301 diff: IntegerType,
302) -> Result<Specifier> {
303 let length = table.len() as IntegerType;
304 let wrapped = if index < 0 { index + length } else { index };
305 if index >= length || wrapped < 0 {
306 return Err(Error::Interval(format!(
307 "cannot get a specifier for a note with this many semitones off of {from}: {diff}"
308 )));
309 }
310 Ok(table[wrapped as usize])
311}
312
313pub fn intervals_to_diatonic(
316 g_int: &GenericInterval,
317 c_int: &ChromaticInterval,
318) -> Result<DiatonicInterval> {
319 let specifier = specifier_from_generic_chromatic(g_int, c_int)?;
320 Ok(DiatonicInterval::new(specifier, g_int))
321}
322
323pub fn convert_semitone_to_specifier_generic(count: FloatType) -> (Specifier, IntegerType) {
327 let (specifier, generic, _) = convert_semitone_to_specifier_generic_microtone(count);
328 (specifier, generic)
329}
330
331fn semitones_to_specifier_generic(size: IntegerType) -> (Specifier, IntegerType) {
334 match size {
335 0 => (Specifier::Perfect, 1),
336 1 => (Specifier::Minor, 2),
337 2 => (Specifier::Major, 2),
338 3 => (Specifier::Minor, 3),
339 4 => (Specifier::Major, 3),
340 5 => (Specifier::Perfect, 4),
341 6 => (Specifier::Diminished, 5),
342 7 => (Specifier::Perfect, 5),
343 8 => (Specifier::Minor, 6),
344 9 => (Specifier::Major, 6),
345 10 => (Specifier::Minor, 7),
346 _ => (Specifier::Major, 7),
347 }
348}
349
350pub fn convert_semitone_to_specifier_generic_microtone(
355 count: FloatType,
356) -> (Specifier, IntegerType, FloatType) {
357 let dir_scale = if count < 0.0 { -1 } else { 1 };
358 let mut whole = count.floor();
359 let mut cents = (count - whole) * 100.0;
360 if cents > 50.0 {
361 cents -= 100.0;
362 whole += 1.0;
363 }
364 let whole = (whole as IntegerType).unsigned_abs();
366 let size = (whole % 12) as IntegerType;
367 let octave = (whole / 12) as IntegerType;
368 let (specifier, generic) = semitones_to_specifier_generic(size);
369 (specifier, (generic + octave * 7) * dir_scale, cents)
370}
371
372pub fn convert_diatonic_number_to_step(dn: IntegerType) -> (char, IntegerType) {
376 let zero_based = dn - 1;
377 let octave = zero_based.div_euclid(7);
378 let step = StepName::ALL[zero_based.rem_euclid(7) as usize].as_char();
379 (step, octave)
380}
381
382pub fn parse_specifier(value: &str) -> Result<Specifier> {
386 Specifier::from_name(value)
387}
388
389impl Interval {
390 pub(crate) fn between(start_pitch: Pitch, end_pitch: Pitch) -> Result<Self> {
391 let generic = notes_to_generic(&start_pitch, &end_pitch)?;
392 let chromatic = notes_to_chromatic(&start_pitch, &end_pitch)?;
393 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
394
395 Ok(Self {
396 implicit_diatonic: false,
397 diatonic,
398 chromatic,
399 pitch_start: Some(start_pitch),
400 pitch_end: Some(end_pitch),
401 })
402 }
403
404 pub fn from_diatonic_and_chromatic(
407 diatonic: DiatonicInterval,
408 chromatic: ChromaticInterval,
409 ) -> Result<Interval> {
410 Ok(Self {
411 implicit_diatonic: false,
412 diatonic,
413 chromatic,
414 pitch_start: None,
415 pitch_end: None,
416 })
417 }
418
419 pub fn from_diatonic(diatonic: DiatonicInterval) -> Result<Self> {
422 let chromatic = diatonic.get_chromatic()?;
423 Self::from_diatonic_and_chromatic(diatonic, chromatic)
424 }
425
426 pub fn from_chromatic(chromatic: ChromaticInterval) -> Result<Self> {
430 let diatonic = chromatic.get_diatonic();
431 let mut interval = Self::from_diatonic_and_chromatic(diatonic, chromatic)?;
432 interval.implicit_diatonic = true;
433 Ok(interval)
434 }
435
436 pub fn generic(&self) -> &GenericInterval {
438 &self.diatonic.generic
439 }
440
441 pub fn diatonic(&self) -> &DiatonicInterval {
443 &self.diatonic
444 }
445
446 pub fn chromatic(&self) -> &ChromaticInterval {
448 &self.chromatic
449 }
450
451 pub fn specifier(&self) -> Specifier {
453 self.diatonic.specifier
454 }
455
456 pub fn from_generic_and_chromatic(
461 generic: IntegerType,
462 semitones: IntegerType,
463 ) -> Result<Self> {
464 let generic = GenericInterval::from_int(generic)?;
465 let chromatic = ChromaticInterval::from_int(semitones);
466 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
467 Self::from_diatonic_and_chromatic(diatonic, chromatic)
468 }
469
470 pub fn pitch_start(&self) -> Option<&Pitch> {
473 self.pitch_start.as_ref()
474 }
475
476 pub fn pitch_end(&self) -> Option<&Pitch> {
479 self.pitch_end.as_ref()
480 }
481
482 pub fn note_start(&self) -> Option<Note> {
484 self.pitch_start.clone().map(Note::from_pitch)
485 }
486
487 pub fn note_end(&self) -> Option<Note> {
489 self.pitch_end.clone().map(Note::from_pitch)
490 }
491
492 pub fn from_name(name: impl AsRef<str>) -> Result<Self> {
503 let (diatonic, chromatic, inferred) = parse_interval_name(name.as_ref())?;
504 Ok(Self {
505 implicit_diatonic: inferred,
506 diatonic,
507 chromatic,
508 pitch_start: None,
509 pitch_end: None,
510 })
511 }
512
513 pub fn from_semitones(semitones: IntegerType) -> Result<Self> {
523 let chromatic = ChromaticInterval::from_int(semitones);
524 let diatonic = chromatic.get_diatonic();
525 Ok(Self {
526 implicit_diatonic: true,
527 diatonic,
528 chromatic,
529 pitch_start: None,
530 pitch_end: None,
531 })
532 }
533
534 pub fn between_pitches(start: &Pitch, end: &Pitch) -> Result<Self> {
547 Self::between(start.clone(), end.clone())
548 }
549
550 pub fn between_notes(start: &Note, end: &Note) -> Result<Self> {
552 Self::between(start.pitch.clone(), end.pitch.clone())
553 }
554
555 pub fn semitones(&self) -> FloatType {
558 self.chromatic.semitones
559 }
560
561 pub fn whole_semitones(&self) -> IntegerType {
563 self.chromatic.whole_semitones()
564 }
565
566 pub fn direction(&self) -> IntervalDirection {
568 self.chromatic.direction()
569 }
570
571 pub fn name(&self) -> String {
581 self.nice_name()
582 }
583
584 pub fn generic_number(&self) -> IntegerType {
586 self.generic().simple_directed()
587 }
588
589 pub fn is_implicit_diatonic(&self) -> bool {
591 self.implicit_diatonic
592 }
593
594 pub fn inversion(&self) -> Result<Self> {
603 let direction = match self.direction() {
604 IntervalDirection::Oblique => 1,
605 direction => direction.as_int(),
606 };
607 let simple = self.generic().simple_undirected();
608 let inverted_generic = if simple == 1 { 1 } else { 9 - simple };
609 let generic = GenericInterval::from_int(inverted_generic * direction)?;
610 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
611 let chromatic = diatonic.get_chromatic()?;
612 Self::from_diatonic_and_chromatic(diatonic, chromatic)
613 }
614
615 pub fn reversed(&self) -> Result<Self> {
617 self.reverse()
618 }
619
620 pub fn pythagorean_ratio(&self) -> Result<FractionType> {
625 self.pythagorean_monzo()?.ratio()
626 }
627
628 pub fn pythagorean_monzo(&self) -> Result<crate::tuningsystem::Monzo> {
636 let spelled = self.diatonic.get_chromatic()?.semitones();
637 if self.chromatic.semitones() != spelled {
638 return Err(Error::Interval(format!(
639 "{} is not a whole number of semitones, so has no Pythagorean ratio",
640 self.chromatic.semitones()
641 )));
642 }
643 Ok(crate::tuningsystem::Monzo::pythagorean(
644 self.diatonic.generic.staff_distance(),
645 spelled as IntegerType,
646 ))
647 }
648
649 pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
660 self.transpose_pitch_with_options(pitch, false, Some(4))
661 }
662
663 pub fn transpose_note(&self, note: &Note) -> Result<Note> {
665 let mut out = note.clone();
666 out.pitch = self.transpose_pitch(¬e.pitch)?;
667 Ok(out)
668 }
669
670 pub fn short_name(&self) -> String {
679 format!(
680 "{}{}",
681 self.diatonic.specifier.prefix(),
682 self.generic().undirected()
683 )
684 }
685
686 pub fn simple_name(&self) -> String {
695 format!(
696 "{}{}",
697 self.diatonic.specifier.prefix(),
698 self.generic().simple_undirected()
699 )
700 }
701
702 pub fn semi_simple_name(&self) -> String {
705 format!(
706 "{}{}",
707 self.diatonic.specifier.prefix(),
708 self.generic().semi_simple_undirected()
709 )
710 }
711
712 pub fn directed_name(&self) -> String {
714 format!(
715 "{}{}",
716 self.diatonic.specifier.prefix(),
717 self.generic().directed()
718 )
719 }
720
721 pub fn is_diatonic_step(&self) -> bool {
723 self.generic().undirected() == 2
724 }
725
726 pub fn is_chromatic_step(&self) -> bool {
728 self.chromatic.undirected() == 1.0
729 }
730
731 pub fn is_step(&self) -> bool {
733 self.is_chromatic_step() || self.is_diatonic_step()
734 }
735
736 pub fn is_skip(&self) -> bool {
739 self.generic().undirected() > 2
740 }
741
742 pub fn is_consonant(&self) -> bool {
745 matches!(
746 (self.diatonic.specifier, self.generic().simple_undirected()),
747 (Specifier::Perfect, 1 | 5) | (Specifier::Major | Specifier::Minor, 3 | 6)
748 )
749 }
750
751 pub fn complement(&self) -> Result<Self> {
754 let generic = GenericInterval::from_int(9 - self.generic().semi_simple_undirected())?;
755 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
756 let chromatic = diatonic.get_chromatic()?;
757 Self::from_diatonic_and_chromatic(diatonic, chromatic)
758 }
759
760 pub fn interval_class(&self) -> IntegerType {
763 self.chromatic.interval_class()
764 }
765
766 pub fn sum<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
771 let start = Pitch::from_name("C4")?;
772 let mut end = start.clone();
773 let mut any = false;
774 for interval in intervals {
775 end = interval.transpose_pitch(&end)?;
776 any = true;
777 }
778 if !any {
779 return Err(Error::Interval(
780 "cannot add an empty set of intervals".to_string(),
781 ));
782 }
783 Self::between_pitches(&start, &end)
784 }
785
786 pub fn difference<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
789 let start = Pitch::from_name("C4")?;
790 let mut intervals = intervals.into_iter();
791 let Some(first) = intervals.next() else {
792 return Err(Error::Interval(
793 "cannot subtract an empty set of intervals".to_string(),
794 ));
795 };
796 let mut end = first.transpose_pitch(&start)?;
797 for interval in intervals {
798 end = interval.reversed()?.transpose_pitch(&end)?;
799 }
800 Self::between_pitches(&start, &end)
801 }
802
803 pub fn directed_simple_name(&self) -> String {
806 format!(
807 "{}{}",
808 self.diatonic.specifier.prefix(),
809 self.generic().simple_directed()
810 )
811 }
812
813 pub(crate) fn directed_simple_key(&self) -> (Specifier, IntegerType) {
814 (self.diatonic.specifier, self.generic().simple_directed())
815 }
816
817 pub(crate) fn simple_key(&self) -> (Specifier, IntegerType) {
818 (self.diatonic.specifier, self.generic().simple_undirected())
819 }
820
821 pub(crate) fn semi_simple_key(&self) -> (Specifier, IntegerType) {
822 (
823 self.diatonic.specifier,
824 self.generic().semi_simple_undirected(),
825 )
826 }
827
828 pub(crate) fn is_perfect_unison(&self) -> bool {
829 self.generic().undirected() == 1 && self.chromatic.semitones == 0.0
830 }
831
832 pub(crate) fn nice_name(&self) -> String {
833 self.diatonic.nice_name()
834 }
835
836 pub fn semi_simple_nice_name(&self) -> String {
840 self.diatonic.semi_simple_nice_name()
841 }
842
843 pub fn simple_nice_name(&self) -> String {
847 format!(
848 "{} {}",
849 self.diatonic.specifier.nice_name(),
850 self.generic().simple_nice_name()
851 )
852 }
853
854 fn diatonic_direction(&self) -> Direction {
859 self.diatonic.direction()
860 }
861
862 fn directed(&self, name: String) -> String {
863 format!("{} {name}", self.diatonic_direction().name())
864 }
865
866 pub fn directed_nice_name(&self) -> String {
869 self.directed(self.nice_name())
870 }
871
872 pub fn directed_simple_nice_name(&self) -> String {
875 self.directed(self.simple_nice_name())
876 }
877
878 pub fn directed_semi_simple_nice_name(&self) -> String {
881 self.directed(self.semi_simple_nice_name())
882 }
883
884 pub fn specific_name(&self) -> String {
887 self.diatonic.specifier.nice_name()
888 }
889
890 pub fn cents(&self) -> FloatType {
892 self.chromatic.cents()
893 }
894
895 pub fn diatonic_interval_cent_shift(&self) -> FloatType {
899 let diatonic_cents = self.diatonic.cents().unwrap_or(0.0);
900 self.chromatic.cents() - diatonic_cents
901 }
902
903 pub fn is_unison(&self) -> bool {
905 self.generic().is_unison()
906 }
907
908 pub fn is_perfectable(&self) -> bool {
911 self.generic().is_perfectable()
912 }
913
914 pub fn staff_distance(&self) -> IntegerType {
917 self.generic().staff_distance()
918 }
919
920 pub fn mod7(&self) -> IntegerType {
923 self.generic().mod7()
924 }
925
926 pub fn mod7_inversion(&self) -> IntegerType {
929 self.generic().mod7_inversion()
930 }
931
932 pub fn mod12(&self) -> IntegerType {
935 self.chromatic.mod12()
936 }
937
938 pub fn transpose_pitch_with_options(
944 &self,
945 p: &Pitch,
946 reverse: bool,
947 max_accidental: Option<IntegerType>,
948 ) -> Result<Pitch> {
949 if reverse {
950 return self
951 .reverse()?
952 .transpose_pitch_with_options(p, false, max_accidental);
953 }
954 let mut moved = self.transpose_pitch_alone(p, max_accidental)?;
955 if let Some(fundamental) = p.fundamental() {
956 moved.fundamental_setter(self.transpose_pitch_with_options(
957 fundamental,
958 false,
959 max_accidental,
960 )?);
961 }
962 Ok(moved)
963 }
964
965 fn transpose_pitch_alone(
967 &self,
968 p: &Pitch,
969 max_accidental: Option<IntegerType>,
970 ) -> Result<Pitch> {
971 if self.implicit_diatonic {
972 return self.chromatic.transpose_pitch(p);
973 }
974
975 let use_implicit_octave = p.octave().is_none();
976 let whole_semitones = self.chromatic.semitones == self.chromatic.semitones.trunc();
979 let inherit_accidental_display = self.diatonic.simple_name() == "P1" && whole_semitones;
980 let cents_origin = if p.is_twelve_tone() || !whole_semitones {
986 0.0
987 } else {
988 p.microtone().map_or(0.0, crate::pitch::Microtone::cents)
989 };
990 let new_dnn = p
991 .checked_diatonic_note_number()
992 .and_then(|dnn| dnn.checked_add(self.diatonic.generic.staff_distance()))
993 .ok_or_else(|| {
994 Error::Interval(format!(
995 "cannot move {} past the last octave an integer counts",
996 p.name_with_octave()
997 ))
998 })?;
999 let (new_step, new_octave) = convert_diatonic_number_to_step(new_dnn);
1000 let mut pitch2 = crate::pitch::PitchOptions::new()
1001 .step(new_step)
1002 .octave(new_octave)
1003 .build()?;
1004 let origin_ps = p.ps() - cents_origin / 100.0;
1005 let mut half_steps_to_fix = self.chromatic.semitones - (pitch2.ps() - origin_ps);
1006 while half_steps_to_fix >= 12.0 {
1007 half_steps_to_fix -= 12.0;
1008 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) - 1));
1009 }
1010 while half_steps_to_fix <= -12.0 {
1011 half_steps_to_fix += 12.0;
1012 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) + 1));
1013 }
1014 if half_steps_to_fix != 0.0 {
1015 if max_accidental.is_some_and(|limit| half_steps_to_fix.abs() > limit as FloatType) {
1016 pitch2.set_ps(pitch2.ps() + half_steps_to_fix);
1017 } else {
1018 pitch2.set_accidental_alter(half_steps_to_fix)?;
1019 }
1020 match (
1021 inherit_accidental_display,
1022 pitch2.written_accidental().is_some(),
1023 p.written_accidental(),
1024 ) {
1025 (false, true, Some(source)) => {
1026 if let Some(target) = pitch2.written_accidental_mut() {
1027 target.inherit_display(source);
1028 target.set_display_status(None);
1029 }
1030 }
1031 (true, false, Some(source)) => {
1032 let mut natural = crate::pitch::Accidental::natural();
1033 natural.inherit_display(source);
1034 pitch2.set_accidental(natural);
1035 }
1036 (true, true, Some(source)) => {
1037 if let Some(target) = pitch2.written_accidental_mut() {
1038 target.inherit_display(source);
1039 }
1040 }
1041 (true, true, None) => {
1042 if let Some(target) = pitch2.written_accidental_mut() {
1043 target.set_display_status(Some(false));
1044 }
1045 }
1046 _ => {}
1047 }
1048 } else if inherit_accidental_display
1049 && p.written_accidental()
1050 .is_some_and(|accidental| accidental.name() == "natural")
1051 {
1052 pitch2.set_written_accidental(p.written_accidental().cloned());
1053 }
1054 if cents_origin != 0.0 {
1055 let cents = pitch2
1056 .microtone()
1057 .map_or(0.0, crate::pitch::Microtone::cents);
1058 pitch2.set_microtone_cents(cents + cents_origin)?;
1059 }
1060 if use_implicit_octave {
1061 pitch2.octave_setter(None);
1062 }
1063 Ok(pitch2)
1064 }
1065
1066 pub fn transpose_pitch_in_place(&self, pitch: &mut Pitch) -> Result<()> {
1068 *pitch = self.transpose_pitch(pitch)?;
1069 Ok(())
1070 }
1071}
1072
1073impl fmt::Display for Interval {
1074 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1075 let shift = self.diatonic_interval_cent_shift();
1076 if shift == 0.0 {
1077 write!(f, "{}", self.directed_name())
1078 } else {
1079 write!(
1080 f,
1081 "{} {}",
1082 self.directed_name(),
1083 crate::pitch::Microtone::from_cents(shift, 1)
1084 )
1085 }
1086 }
1087}
1088
1089impl FromStr for Interval {
1090 type Err = Error;
1091
1092 fn from_str(value: &str) -> Result<Self> {
1093 Self::from_name(value)
1094 }
1095}
1096
1097impl TryFrom<&str> for Interval {
1098 type Error = Error;
1099
1100 fn try_from(value: &str) -> Result<Self> {
1101 Self::from_name(value)
1102 }
1103}
1104
1105impl TryFrom<String> for Interval {
1106 type Error = Error;
1107
1108 fn try_from(value: String) -> Result<Self> {
1109 Self::from_name(value)
1110 }
1111}
1112
1113impl TryFrom<IntegerType> for Interval {
1114 type Error = Error;
1115
1116 fn try_from(value: IntegerType) -> Result<Self> {
1117 Self::from_semitones(value)
1118 }
1119}
1120
1121fn parse_plain_interval_name(value: &str) -> Option<(Specifier, IntegerType)> {
1129 let mut number: IntegerType = 0;
1130 let mut digits = 0;
1131 let mut descending = false;
1132 let mut specifier: Option<(usize, usize)> = None;
1133 for (index, character) in value.char_indices() {
1134 if character == '-' {
1135 descending = true;
1138 } else if let Some(digit) = character.to_digit(10) {
1139 number = number * 10 + digit as IntegerType;
1140 digits += 1;
1141 } else if digits > 0 || !character.is_ascii_alphabetic() {
1142 return None;
1145 } else {
1146 let end = index + character.len_utf8();
1147 specifier = Some((specifier.map_or(index, |(start, _)| start), end));
1148 }
1149 }
1150 let (start, end) = specifier?;
1151 if digits == 0 {
1152 return None;
1153 }
1154 let specifier = Specifier::from_name(&value[start..end]).ok()?;
1155 Some((specifier, if descending { -number } else { number }))
1156}
1157
1158fn interval_parts(
1161 specifier: Specifier,
1162 generic_number: IntegerType,
1163 inferred: bool,
1164) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1165 let g_interval = GenericInterval::from_int(generic_number)?;
1166 let d_interval = g_interval.get_diatonic(specifier);
1167 let c_interval = d_interval.get_chromatic()?;
1168 Ok((d_interval, c_interval, inferred))
1169}
1170
1171fn parse_interval_name(name: &str) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1172 if let Some((specifier, number)) = parse_plain_interval_name(name) {
1173 return interval_parts(specifier, number, false);
1174 }
1175 let mut value = name.to_string();
1177 let mut inferred = false;
1178 let mut dir_scale = 1;
1179
1180 if value.contains('-') {
1182 value = value.replace('-', "");
1183 dir_scale = -1;
1184 }
1185 {
1187 let (without_descending, found_descending) = strip_direction_word(&value, "descending");
1188 if found_descending {
1189 value = without_descending;
1190 dir_scale = -1;
1191 } else {
1192 let (without_ascending, found_ascending) = strip_direction_word(&value, "ascending");
1193 if found_ascending {
1194 value = without_ascending;
1195 }
1196 }
1197 }
1198 let value_lower = value.to_lowercase();
1199
1200 if value_lower == "w" || value_lower == "whole" || value_lower == "tone" {
1202 value = "M2".to_string();
1203 inferred = true;
1204 } else if value_lower == "h" || value_lower == "half" || value_lower == "semitone" {
1205 value = "m2".to_string();
1206 inferred = true;
1207 }
1208
1209 let mut lowered = value.to_ascii_lowercase();
1214 for (i, ordinal) in MUSICAL_ORDINAL_STRINGS_LOWER.iter().enumerate() {
1215 if !lowered.contains(ordinal.as_str()) {
1216 continue;
1217 }
1218 let replacement = i.to_string();
1219 let (next_value, replaced) =
1220 replace_music_ordinal(&value, &MUSICAL_ORDINAL_STRINGS[i], &replacement);
1221 if replaced {
1222 value = next_value;
1223 lowered = value.to_ascii_lowercase();
1224 }
1225 }
1226
1227 let (found, remain) = get_num_from_str(&value, "0123456789");
1229 let generic_number: IntegerType = found
1230 .parse::<IntegerType>()
1231 .map_err(|_| Error::Interval(format!("cannot read an interval number from {value:?}")))?
1232 * dir_scale;
1233 let spec = Specifier::parse(&remain)?;
1234 interval_parts(spec, generic_number, inferred)
1235}
1236
1237impl Interval {
1238 fn reverse(&self) -> Result<Self> {
1239 if let (Some(start), Some(end)) = (&self.pitch_start, &self.pitch_end) {
1240 Interval::between(end.clone(), start.clone())
1241 } else {
1242 Interval::from_diatonic_and_chromatic(self.diatonic.reverse(), self.chromatic.reverse())
1243 }
1244 }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249 #[test]
1250 fn an_interval_is_read_from_a_name_a_number_or_one_of_its_halves() {
1251 use super::{ChromaticInterval, DiatonicInterval, Interval, Specifier, parse_specifier};
1252 use crate::note::Note;
1253 use crate::pitch::Pitch;
1254 use std::str::FromStr;
1255
1256 assert_eq!(Interval::from_str("M3").unwrap().short_name(), "M3");
1257 assert_eq!(Interval::try_from("P5").unwrap().short_name(), "P5");
1258 assert_eq!(
1259 Interval::try_from("m6".to_string()).unwrap().short_name(),
1260 "m6"
1261 );
1262 assert_eq!(Interval::try_from(7).unwrap().short_name(), "P5");
1263 assert!(Interval::from_str("Q9").is_err());
1264 assert_eq!(parse_specifier("M").unwrap(), Specifier::Major);
1265 assert!(parse_specifier("Q").is_err());
1266
1267 let diatonic = Interval::from_diatonic(DiatonicInterval::from_name("M3").unwrap()).unwrap();
1268 assert_eq!(diatonic.semitones(), 4.0);
1269 assert!(!diatonic.is_implicit_diatonic());
1270 assert_eq!(diatonic.diatonic().name(), "M3");
1271 assert_eq!(diatonic.specifier(), Specifier::Major);
1272 let chromatic = Interval::from_chromatic(ChromaticInterval::from_int(6)).unwrap();
1273 assert!(chromatic.is_implicit_diatonic());
1274 assert_eq!(chromatic.whole_semitones(), 6);
1275 assert_eq!(chromatic.directed_simple_name(), "d5");
1276 assert_eq!(
1277 Interval::from_str("M-10").unwrap().directed_simple_name(),
1278 "M-3"
1279 );
1280 assert_eq!(
1281 Interval::from_str("M3").unwrap(),
1282 Interval::from_str("M3").unwrap()
1283 );
1284 assert_ne!(
1285 Interval::from_str("M3").unwrap(),
1286 Interval::from_str("m3").unwrap()
1287 );
1288
1289 let c = Note::from_pitch(Pitch::from_name("C4").unwrap());
1290 let g = Note::from_pitch(Pitch::from_name("G4").unwrap());
1291 let fifth = Interval::between_notes(&c, &g).unwrap();
1292 assert_eq!(fifth.short_name(), "P5");
1293 assert_eq!(
1294 fifth.transpose_note(&g).unwrap().pitch_name_with_octave(),
1295 "D5"
1296 );
1297 }
1298
1299 #[test]
1300 fn a_note_flatter_than_the_table_wraps_the_way_music21_does() {
1301 let interval = Interval::between_pitches(
1305 &Pitch::from_name("A##3").unwrap(),
1306 &Pitch::from_name("E---4").unwrap(),
1307 )
1308 .unwrap();
1309 assert_eq!(interval.short_name(), "AAAA5");
1310 assert_eq!(interval.chromatic().semitones(), 2.0);
1311
1312 let widest = Interval::between_pitches(
1315 &Pitch::from_name("C4").unwrap(),
1316 &Pitch::from_name("E####4").unwrap(),
1317 )
1318 .unwrap();
1319 assert_eq!(widest.short_name(), "AAAA3");
1320 }
1321
1322 #[test]
1323 fn generic_and_chromatic_build_the_interval_music21_does() {
1324 let cases: [(i32, i32, &str, &str); 14] = [
1325 (3, 4, "M3", "M3"),
1326 (3, 3, "m3", "m3"),
1327 (5, 7, "P5", "P5"),
1328 (5, 6, "d5", "d5"),
1329 (4, 6, "A4", "A4"),
1330 (1, 0, "P1", "P1"),
1331 (1, 1, "A1", "A1"),
1332 (8, 12, "P8", "P8"),
1333 (-3, -4, "M3", "M-3"),
1334 (2, 3, "A2", "A2"),
1335 (7, 10, "m7", "m7"),
1336 (-2, -1, "m2", "m-2"),
1337 (9, 13, "m9", "m9"),
1338 (3, 2, "d3", "d3"),
1339 ];
1340 for (generic, semitones, name, directed) in cases {
1341 let interval = Interval::from_generic_and_chromatic(generic, semitones).unwrap();
1342 assert_eq!(interval.short_name(), name, "{generic} {semitones}");
1343 assert_eq!(interval.directed_name(), directed, "{generic} {semitones}");
1344 assert_eq!(
1345 interval.semitones(),
1346 FloatType::from(semitones),
1347 "{generic} {semitones}"
1348 );
1349 assert!(interval.pitch_start().is_none());
1350 }
1351 assert!(Interval::from_generic_and_chromatic(0, 0).is_err());
1352 assert_eq!(
1353 [-3, -1, 0, 1, 2, 7].map(staff_distance_to_generic_number),
1354 [-4, -2, 1, 2, 3, 8]
1355 );
1356 }
1357
1358 #[test]
1359 fn a_pitch_moved_past_the_highest_octave_is_refused() {
1360 use super::Interval;
1361 use crate::IntegerType;
1362 use crate::pitch::Pitch;
1363
1364 let pitch = Pitch::from_name("D2147483647").unwrap();
1365 assert!(
1366 Interval::from_name("M2")
1367 .unwrap()
1368 .transpose_pitch(&pitch)
1369 .is_err()
1370 );
1371 let mut pitch = Pitch::from_name("D").unwrap();
1372 pitch.set_octave(Some(IntegerType::MIN));
1373 assert!(
1374 Interval::from_name("M-2")
1375 .unwrap()
1376 .transpose_pitch(&pitch)
1377 .is_err()
1378 );
1379 }
1380
1381 #[test]
1382 fn the_least_count_of_semitones_is_an_interval() {
1383 use super::{Interval, convert_semitone_to_specifier_generic_microtone};
1384 use crate::IntegerType;
1385
1386 let interval = Interval::from_semitones(IntegerType::MIN).unwrap();
1387 assert_eq!(interval.semitones(), f64::from(IntegerType::MIN));
1388 let (_, generic, _) = convert_semitone_to_specifier_generic_microtone(-1e300);
1389 assert!(generic < 0);
1390 }
1391
1392 #[test]
1393 fn semitones_too_many_for_any_quality_are_refused() {
1394 use super::Interval;
1395
1396 assert!(Interval::from_generic_and_chromatic(-2, -2_147_483_647).is_err());
1397 assert!(Interval::from_generic_and_chromatic(2, 2_147_483_647).is_err());
1398 }
1399
1400 #[test]
1401 fn written_and_sounding_order_match_music21() {
1402 let cases: [(&str, &str, [&str; 4]); 8] = [
1403 ("C4", "E4", ["E4", "C4", "E4", "C4"]),
1404 ("E4", "C4", ["E4", "C4", "E4", "C4"]),
1405 ("B#3", "C4", ["C4", "B#3", "B#3", "B#3"]),
1406 ("C4", "B#3", ["C4", "B#3", "C4", "C4"]),
1407 ("Cb4", "B3", ["Cb4", "B3", "Cb4", "Cb4"]),
1408 ("F#4", "Gb4", ["Gb4", "F#4", "F#4", "F#4"]),
1409 ("C4", "C4", ["C4", "C4", "C4", "C4"]),
1410 ("B3", "Cb4", ["Cb4", "B3", "B3", "B3"]),
1411 ];
1412 for (first, second, expected) in cases {
1413 let a = Pitch::from_name(first).unwrap();
1414 let b = Pitch::from_name(second).unwrap();
1415 let actual = [
1416 written_higher_pitch(&a, &b),
1417 written_lower_pitch(&a, &b),
1418 absolute_higher_pitch(&a, &b),
1419 absolute_lower_pitch(&a, &b),
1420 ]
1421 .map(Pitch::name_with_octave);
1422 assert_eq!(actual, expected, "{first} {second}");
1423 }
1424 }
1425
1426 #[test]
1427 fn intervals_remember_the_pitches_they_were_measured_between() {
1428 let c = Pitch::from_name("C4").unwrap();
1429 let g = Pitch::from_name("G4").unwrap();
1430 let fifth = Interval::between_pitches(&c, &g).unwrap();
1431 assert_eq!(fifth.pitch_start().unwrap().name_with_octave(), "C4");
1432 assert_eq!(fifth.pitch_end().unwrap().name_with_octave(), "G4");
1433 assert_eq!(fifth.note_start().unwrap().pitch_name_with_octave(), "C4");
1434 assert_eq!(fifth.note_end().unwrap().pitch_name_with_octave(), "G4");
1435 let named = Interval::from_name("P5").unwrap();
1436 assert!(named.pitch_start().is_none());
1437 assert!(named.note_end().is_none());
1438 }
1439
1440 #[test]
1441 fn nice_name_variants_match_music21() {
1442 let cases: [(&str, [&str; 7]); 15] = [
1443 (
1444 "P1",
1445 [
1446 "Perfect Unison",
1447 "Oblique Perfect Unison",
1448 "Perfect Unison",
1449 "Perfect Unison",
1450 "Oblique Perfect Unison",
1451 "Oblique Perfect Unison",
1452 "Perfect",
1453 ],
1454 ),
1455 (
1456 "m2",
1457 [
1458 "Minor Second",
1459 "Ascending Minor Second",
1460 "Minor Second",
1461 "Minor Second",
1462 "Ascending Minor Second",
1463 "Ascending Minor Second",
1464 "Minor",
1465 ],
1466 ),
1467 (
1468 "P8",
1469 [
1470 "Perfect Octave",
1471 "Ascending Perfect Octave",
1472 "Perfect Unison",
1473 "Perfect Octave",
1474 "Ascending Perfect Unison",
1475 "Ascending Perfect Octave",
1476 "Perfect",
1477 ],
1478 ),
1479 (
1480 "m9",
1481 [
1482 "Minor Ninth",
1483 "Ascending Minor Ninth",
1484 "Minor Second",
1485 "Minor Second",
1486 "Ascending Minor Second",
1487 "Ascending Minor Second",
1488 "Minor",
1489 ],
1490 ),
1491 (
1492 "M10",
1493 [
1494 "Major Tenth",
1495 "Ascending Major Tenth",
1496 "Major Third",
1497 "Major Third",
1498 "Ascending Major Third",
1499 "Ascending Major Third",
1500 "Major",
1501 ],
1502 ),
1503 (
1504 "P12",
1505 [
1506 "Perfect Twelfth",
1507 "Ascending Perfect Twelfth",
1508 "Perfect Fifth",
1509 "Perfect Fifth",
1510 "Ascending Perfect Fifth",
1511 "Ascending Perfect Fifth",
1512 "Perfect",
1513 ],
1514 ),
1515 (
1516 "-M3",
1517 [
1518 "Major Third",
1519 "Descending Major Third",
1520 "Major Third",
1521 "Major Third",
1522 "Descending Major Third",
1523 "Descending Major Third",
1524 "Major",
1525 ],
1526 ),
1527 (
1528 "-m9",
1529 [
1530 "Minor Ninth",
1531 "Descending Minor Ninth",
1532 "Minor Second",
1533 "Minor Second",
1534 "Descending Minor Second",
1535 "Descending Minor Second",
1536 "Minor",
1537 ],
1538 ),
1539 (
1540 "dd5",
1541 [
1542 "Doubly-Diminished Fifth",
1543 "Ascending Doubly-Diminished Fifth",
1544 "Doubly-Diminished Fifth",
1545 "Doubly-Diminished Fifth",
1546 "Ascending Doubly-Diminished Fifth",
1547 "Ascending Doubly-Diminished Fifth",
1548 "Doubly-Diminished",
1549 ],
1550 ),
1551 (
1552 "AA4",
1553 [
1554 "Doubly-Augmented Fourth",
1555 "Ascending Doubly-Augmented Fourth",
1556 "Doubly-Augmented Fourth",
1557 "Doubly-Augmented Fourth",
1558 "Ascending Doubly-Augmented Fourth",
1559 "Ascending Doubly-Augmented Fourth",
1560 "Doubly-Augmented",
1561 ],
1562 ),
1563 (
1564 "P15",
1565 [
1566 "Perfect Double-octave",
1567 "Ascending Perfect Double-octave",
1568 "Perfect Unison",
1569 "Perfect Octave",
1570 "Ascending Perfect Unison",
1571 "Ascending Perfect Octave",
1572 "Perfect",
1573 ],
1574 ),
1575 (
1576 "d1",
1577 [
1578 "Diminished Unison",
1579 "Descending Diminished Unison",
1580 "Diminished Unison",
1581 "Diminished Unison",
1582 "Descending Diminished Unison",
1583 "Descending Diminished Unison",
1584 "Diminished",
1585 ],
1586 ),
1587 (
1588 "A1",
1589 [
1590 "Augmented Unison",
1591 "Ascending Augmented Unison",
1592 "Augmented Unison",
1593 "Augmented Unison",
1594 "Ascending Augmented Unison",
1595 "Ascending Augmented Unison",
1596 "Augmented",
1597 ],
1598 ),
1599 (
1600 "-A1",
1601 [
1602 "Augmented Unison",
1603 "Ascending Augmented Unison",
1604 "Augmented Unison",
1605 "Augmented Unison",
1606 "Ascending Augmented Unison",
1607 "Ascending Augmented Unison",
1608 "Augmented",
1609 ],
1610 ),
1611 (
1612 "P-8",
1613 [
1614 "Perfect Octave",
1615 "Descending Perfect Octave",
1616 "Perfect Unison",
1617 "Perfect Octave",
1618 "Descending Perfect Unison",
1619 "Descending Perfect Octave",
1620 "Perfect",
1621 ],
1622 ),
1623 ];
1624 for (name, expected) in cases {
1625 let interval = Interval::from_name(name).unwrap();
1626 let actual = [
1627 interval.name(),
1628 interval.directed_nice_name(),
1629 interval.simple_nice_name(),
1630 interval.semi_simple_nice_name(),
1631 interval.directed_simple_nice_name(),
1632 interval.directed_semi_simple_nice_name(),
1633 interval.specific_name(),
1634 ];
1635 assert_eq!(actual, expected, "{name}");
1636 }
1637 }
1638
1639 #[test]
1640 #[allow(clippy::type_complexity)]
1641 fn generic_and_chromatic_helpers_match_music21() {
1642 let cases: [(&str, f64, bool, bool, i32, i32, i32, i32); 14] = [
1643 ("P1", 0.0, true, true, 0, 1, 8, 0),
1644 ("m2", 100.0, false, false, 1, 2, 7, 1),
1645 ("P4", 500.0, false, true, 3, 4, 5, 5),
1646 ("P5", 700.0, false, true, 4, 5, 4, 7),
1647 ("M7", 1100.0, false, false, 6, 7, 2, 11),
1648 ("P8", 1200.0, false, true, 7, 1, 1, 0),
1649 ("m9", 1300.0, false, false, 8, 2, 7, 1),
1650 ("-M3", -400.0, false, false, -2, 6, 6, 8),
1651 ("-P5", -700.0, false, true, -4, 4, 4, 5),
1652 ("-m9", -1300.0, false, false, -8, 7, 7, 11),
1653 ("P15", 2400.0, false, true, 14, 1, 1, 0),
1654 ("d1", -100.0, true, true, 0, 1, 8, 11),
1655 ("-A1", -100.0, true, true, 0, 8, 8, 11),
1656 ("P-8", -1200.0, false, true, -7, 1, 1, 0),
1657 ];
1658 for (name, cents, unison, perfectable, staff, mod7, mod7_inversion, mod12) in cases {
1659 let interval = Interval::from_name(name).unwrap();
1660 assert_eq!(interval.cents(), cents, "{name} cents");
1661 assert_eq!(interval.is_unison(), unison, "{name} unison");
1662 assert_eq!(interval.is_perfectable(), perfectable, "{name} perfectable");
1663 assert_eq!(interval.staff_distance(), staff, "{name} staff distance");
1664 assert_eq!(interval.mod7(), mod7, "{name} mod7");
1665 assert_eq!(
1666 interval.mod7_inversion(),
1667 mod7_inversion,
1668 "{name} mod7 inversion"
1669 );
1670 assert_eq!(interval.mod12(), mod12, "{name} mod12");
1671 }
1672 }
1673 use super::*;
1674
1675 fn pitch(name: &str) -> Pitch {
1676 Pitch::from_name(name).expect("valid pitch")
1677 }
1678
1679 #[test]
1680 fn malformed_interval_names_error_instead_of_panicking() {
1681 for bad in ["", "X", "perfect", "?!", "MM"] {
1685 assert!(
1686 Interval::from_name(bad).is_err(),
1687 "Interval::from_name({bad:?}) should be an error"
1688 );
1689 }
1690 }
1691
1692 #[test]
1693 fn interval_between_microtonal_pitches_keeps_the_cent_shift() {
1694 let c1 = pitch("C1");
1695 let mut half_sharp = pitch("C1");
1696 half_sharp.set_accidental(
1697 crate::pitch::Accidental::new("half-sharp").expect("half-sharp is an accidental"),
1698 );
1699
1700 let quarter_tone = Interval::between_pitches(&c1, &half_sharp).unwrap();
1701 assert_eq!(quarter_tone.semitones(), 0.5);
1702 assert_eq!(quarter_tone.cents(), 50.0);
1703 assert_eq!(quarter_tone.directed_name(), "A1");
1704 assert_eq!(quarter_tone.diatonic_interval_cent_shift(), -50.0);
1705 assert_eq!(quarter_tone.to_string(), "A1 (-50c)");
1706 assert!(quarter_tone.pythagorean_ratio().is_err());
1707 }
1708
1709 #[test]
1710 fn interval_cents_carry_a_pitch_microtone() {
1711 let c4 = pitch("C4");
1712 let mut d4 = pitch("D4");
1713 d4.set_microtone_cents(30.0).unwrap();
1714
1715 let interval = Interval::between_pitches(&c4, &d4).unwrap();
1716 assert_eq!(interval.cents(), 230.0);
1717 assert_eq!(interval.to_string(), "M2 (+30c)");
1718 }
1719
1720 #[test]
1721 fn interval_from_string_has_expected_chromatic() {
1722 let interval = Interval::from_name("M3").unwrap();
1723 assert_eq!(interval.chromatic.semitones, 4.0);
1724 assert!(!interval.implicit_diatonic);
1725 }
1726
1727 #[test]
1728 fn interval_parser_accepts_direction_words_and_ordinals() {
1729 let descending = Interval::from_name("Descending Perfect Twelfth").unwrap();
1730 assert_eq!(descending.semitones(), -19.0);
1731 assert_eq!(descending.generic_number(), -5);
1732
1733 let ascending = Interval::from_name("ascending Major Second").unwrap();
1734 assert_eq!(ascending.semitones(), 2.0);
1735 assert_eq!(ascending.generic_number(), 2);
1736
1737 let major_third = Interval::from_name("Major Third").unwrap();
1738 assert_eq!(major_third.semitones(), 4.0);
1739 assert_eq!(major_third.generic_number(), 3);
1740 }
1741
1742 #[test]
1743 fn interval_from_int_is_implicit_diatonic() {
1744 let interval = Interval::from_semitones(1).unwrap();
1745 assert!(interval.implicit_diatonic);
1746 assert_eq!(interval.chromatic.semitones, 1.0);
1747 }
1748
1749 #[test]
1750 fn interval_between_pitches() {
1751 let c4 = pitch("C4");
1752 let g4 = pitch("G4");
1753 let interval = Interval::between(c4, g4).unwrap();
1754 assert_eq!(interval.chromatic.semitones, 7.0);
1755 assert_eq!(interval.generic().staff_distance(), 4);
1756 }
1757
1758 #[test]
1759 fn interval_transpose_pitch() {
1760 let c4 = pitch("C4");
1761 let m3 = Interval::from_name("m3").unwrap();
1762 let out = m3.transpose_pitch(&c4).unwrap();
1763 assert_eq!(out.name_with_octave(), "Eb4");
1764 }
1765
1766 #[test]
1767 fn interval_transpose_pitch_in_place() {
1768 let mut c4 = pitch("C4");
1769 Interval::from_name("M2")
1770 .unwrap()
1771 .transpose_pitch_in_place(&mut c4)
1772 .unwrap();
1773 assert_eq!(c4.name_with_octave(), "D4");
1774 }
1775
1776 #[test]
1777 fn compact_names_match_music21() {
1778 let cases = [
1779 ("P5", "P5", "P5", "P5", "P5"),
1780 ("M3", "M3", "M3", "M3", "M3"),
1781 ("m-6", "m6", "m6", "m6", "m-6"),
1782 ("AA4", "AA4", "AA4", "AA4", "AA4"),
1783 ("d8", "d8", "d1", "d8", "d8"),
1784 ("P8", "P8", "P1", "P8", "P8"),
1785 ("M9", "M9", "M2", "M2", "M9"),
1786 ("P-5", "P5", "P5", "P5", "P-5"),
1787 ("P15", "P15", "P1", "P8", "P15"),
1788 ];
1789 for (input, short, simple, semi_simple, directed) in cases {
1790 let interval = Interval::from_name(input).unwrap();
1791 assert_eq!(interval.short_name(), short, "{input}");
1792 assert_eq!(interval.simple_name(), simple, "{input}");
1793 assert_eq!(interval.semi_simple_name(), semi_simple, "{input}");
1794 assert_eq!(interval.directed_name(), directed, "{input}");
1795 }
1796 }
1797
1798 #[test]
1799 fn complement_and_interval_class_match_music21() {
1800 let cases = [
1801 ("P5", "P4", 5),
1802 ("M3", "m6", 4),
1803 ("m-6", "M3", 4),
1804 ("AA4", "dd5", 5),
1805 ("d8", "A1", 1),
1806 ("P8", "P1", 0),
1807 ("P1", "P8", 0),
1808 ("A1", "d8", 1),
1809 ("M9", "m7", 2),
1810 ("m2", "M7", 1),
1811 ("P15", "P1", 0),
1812 ];
1813 for (input, complement, interval_class) in cases {
1814 let interval = Interval::from_name(input).unwrap();
1815 assert_eq!(
1816 interval.complement().unwrap().short_name(),
1817 complement,
1818 "{input}"
1819 );
1820 assert_eq!(interval.interval_class(), interval_class, "{input}");
1821 }
1822 }
1823
1824 #[test]
1825 fn step_skip_and_consonance_match_music21() {
1826 let cases = [
1827 ("P5", false, true, true, false, false),
1828 ("M3", false, true, true, false, false),
1829 ("AA4", false, true, false, false, false),
1830 ("d8", false, true, false, false, false),
1831 ("P8", false, true, true, false, false),
1832 ("P1", false, false, true, false, false),
1833 ("A1", true, false, false, false, true),
1834 ("M9", false, true, false, false, false),
1835 ("m2", true, false, false, true, true),
1836 ];
1837 for (input, step, skip, consonant, diatonic_step, chromatic_step) in cases {
1838 let interval = Interval::from_name(input).unwrap();
1839 assert_eq!(interval.is_step(), step, "{input} step");
1840 assert_eq!(interval.is_skip(), skip, "{input} skip");
1841 assert_eq!(interval.is_consonant(), consonant, "{input} consonant");
1842 assert_eq!(
1843 interval.is_diatonic_step(),
1844 diatonic_step,
1845 "{input} diatonic"
1846 );
1847 assert_eq!(
1848 interval.is_chromatic_step(),
1849 chromatic_step,
1850 "{input} chromatic"
1851 );
1852 }
1853 }
1854
1855 #[test]
1856 fn sum_and_difference_match_music21() {
1857 fn intervals(names: &[&str]) -> Vec<Interval> {
1858 names
1859 .iter()
1860 .map(|name| Interval::from_name(*name).unwrap())
1861 .collect()
1862 }
1863 assert_eq!(
1864 Interval::sum(&intervals(&["A2", "P5"]))
1865 .unwrap()
1866 .short_name(),
1867 "A6"
1868 );
1869 assert_eq!(
1870 Interval::sum(&intervals(&["P5", "m2"]))
1871 .unwrap()
1872 .short_name(),
1873 "m6"
1874 );
1875 assert_eq!(
1876 Interval::sum(&intervals(&["W", "W", "H", "W", "W", "W", "H"]))
1877 .unwrap()
1878 .short_name(),
1879 "P8"
1880 );
1881 assert_eq!(
1882 Interval::sum(&intervals(&["P5", "P-4"]))
1883 .unwrap()
1884 .directed_name(),
1885 "M2"
1886 );
1887 assert!(Interval::sum(&[]).is_err());
1888
1889 let cases = [
1890 (&["P5", "M3"][..], "m3"),
1891 (&["P4", "d3"][..], "A2"),
1892 (&["M6", "m2", "m2"][..], "AA4"),
1893 (&["P4", "M-2"][..], "P5"),
1894 (&["A2", "A2"][..], "P1"),
1895 (&["P8", "A1"][..], "d8"),
1896 ];
1897 for (names, expected) in cases {
1898 let difference = Interval::difference(&intervals(names)).unwrap();
1899 assert_eq!(difference.short_name(), expected, "{names:?}");
1900 }
1901 let descending_unison = Interval::difference(&intervals(&["P5", "A5"])).unwrap();
1902 assert_eq!(descending_unison.directed_name(), "d1");
1903 assert_eq!(descending_unison.semitones(), -1.0);
1904 assert!(Interval::difference(&[]).is_err());
1905 }
1906
1907 #[test]
1908 fn interval_pythagorean_ratio() {
1909 let ratio = Interval::from_name("P5")
1910 .unwrap()
1911 .pythagorean_ratio()
1912 .unwrap();
1913 assert_eq!(ratio, FractionType::new(3, 2));
1914 }
1915
1916 #[test]
1917 fn a_spelled_interval_is_a_place_on_the_chain_of_fifths() {
1918 let monzo = |name: &str| {
1919 Interval::from_name(name)
1920 .unwrap()
1921 .pythagorean_monzo()
1922 .unwrap()
1923 };
1924 assert_eq!(monzo("M3").to_string(), "[-6 4⟩");
1925 assert_eq!(monzo("A1").to_string(), "[-11 7⟩");
1926 assert_eq!(monzo("m2").to_string(), "[8 -5⟩");
1927 assert_eq!(monzo("P8").to_string(), "[1⟩");
1928 let fifth_down = Interval::from_name("P5").unwrap().reversed().unwrap();
1929 assert_eq!(
1930 fifth_down.pythagorean_ratio().unwrap(),
1931 FractionType::new(2, 3)
1932 );
1933 let far = Interval::from_name("AAA4").unwrap();
1936 assert_eq!(far.pythagorean_monzo().unwrap().exponent_of(3).unwrap(), 20);
1937 assert!(far.pythagorean_ratio().is_err());
1938 }
1939
1940 #[test]
1941 fn interval_inverts_oblique_unison() {
1942 let unison = Interval::from_name("P1").unwrap();
1943 let inverted = unison.inversion().unwrap();
1944
1945 assert_eq!(inverted.semitones(), 0.0);
1946 assert_eq!(inverted.generic_number(), 1);
1947 }
1948 #[test]
1949 fn specifier_case_matters_only_for_major_versus_minor() {
1950 for (lower, upper) in [
1953 ("p5", "P5"),
1954 ("a2", "A2"),
1955 ("d5", "D5"),
1956 ("aa2", "AA2"),
1957 ("dd5", "DD5"),
1958 ("aaa2", "AAA2"),
1959 ("ddd5", "DDD5"),
1960 ] {
1961 let a = Interval::from_name(lower).expect("lowercase parses");
1962 let b = Interval::from_name(upper).expect("uppercase parses");
1963 assert_eq!(a.semitones(), b.semitones(), "{lower} vs {upper}");
1964 assert_eq!(a.name(), b.name(), "{lower} vs {upper}");
1965 }
1966
1967 let minor = Interval::from_name("m3").expect("m3 parses");
1969 let major = Interval::from_name("M3").expect("M3 parses");
1970 assert_eq!(minor.semitones(), 3.0);
1971 assert_eq!(major.semitones(), 4.0);
1972 }
1973
1974 #[test]
1975 fn an_unknown_specifier_errors_instead_of_panicking() {
1976 for name in ["Q5", "x3", "5", "zz2"] {
1977 assert!(
1978 Interval::from_name(name).is_err(),
1979 "{name:?} should be rejected, not panic"
1980 );
1981 }
1982 }
1983
1984 #[test]
1985 fn a_hyphen_anywhere_makes_an_interval_name_descending() {
1986 for name in ["-M2", "M-2"] {
1991 let interval = Interval::from_name(name).expect("descending name parses");
1992 assert_eq!(interval.semitones(), -2.0, "{name}");
1993 assert_eq!(interval.generic_number(), -2, "{name}");
1994 }
1995
1996 for name in ["--M2", "-M-2"] {
2000 let interval = Interval::from_name(name).expect("repeated hyphen parses");
2001 assert_eq!(interval.semitones(), -2.0, "{name}");
2002 }
2003
2004 assert_eq!(
2006 Interval::from_name("d-5").expect("d-5 parses").semitones(),
2007 -6.0
2008 );
2009 }
2010}