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 - normal_semis;
245
246 if g_int.is_perfectable() {
247 specifier_at(&PERFECTABLE_SPECIFIERS, 4 + diff, "Perfect", diff)
248 } else {
249 specifier_at(&MAJOR_SPECIFIERS, 5 + diff, "Major", diff)
250 }
251}
252
253const PERFECTABLE_SPECIFIERS: [Specifier; 9] = [
256 Specifier::QuadrupleDiminished,
257 Specifier::TripleDiminished,
258 Specifier::DoubleDiminished,
259 Specifier::Diminished,
260 Specifier::Perfect,
261 Specifier::Augmented,
262 Specifier::DoubleAugmented,
263 Specifier::TripleAugmented,
264 Specifier::QuadrupleAugmented,
265];
266
267const MAJOR_SPECIFIERS: [Specifier; 10] = [
270 Specifier::QuadrupleDiminished,
271 Specifier::TripleDiminished,
272 Specifier::DoubleDiminished,
273 Specifier::Diminished,
274 Specifier::Minor,
275 Specifier::Major,
276 Specifier::Augmented,
277 Specifier::DoubleAugmented,
278 Specifier::TripleAugmented,
279 Specifier::QuadrupleAugmented,
280];
281
282fn specifier_at(
292 table: &[Specifier],
293 index: IntegerType,
294 from: &str,
295 diff: IntegerType,
296) -> Result<Specifier> {
297 let length = table.len() as IntegerType;
298 let wrapped = if index < 0 { index + length } else { index };
299 if index >= length || wrapped < 0 {
300 return Err(Error::Interval(format!(
301 "cannot get a specifier for a note with this many semitones off of {from}: {diff}"
302 )));
303 }
304 Ok(table[wrapped as usize])
305}
306
307pub fn intervals_to_diatonic(
310 g_int: &GenericInterval,
311 c_int: &ChromaticInterval,
312) -> Result<DiatonicInterval> {
313 let specifier = specifier_from_generic_chromatic(g_int, c_int)?;
314 Ok(DiatonicInterval::new(specifier, g_int))
315}
316
317pub fn convert_semitone_to_specifier_generic(count: FloatType) -> (Specifier, IntegerType) {
321 let (specifier, generic, _) = convert_semitone_to_specifier_generic_microtone(count);
322 (specifier, generic)
323}
324
325fn semitones_to_specifier_generic(size: IntegerType) -> (Specifier, IntegerType) {
328 match size {
329 0 => (Specifier::Perfect, 1),
330 1 => (Specifier::Minor, 2),
331 2 => (Specifier::Major, 2),
332 3 => (Specifier::Minor, 3),
333 4 => (Specifier::Major, 3),
334 5 => (Specifier::Perfect, 4),
335 6 => (Specifier::Diminished, 5),
336 7 => (Specifier::Perfect, 5),
337 8 => (Specifier::Minor, 6),
338 9 => (Specifier::Major, 6),
339 10 => (Specifier::Minor, 7),
340 _ => (Specifier::Major, 7),
341 }
342}
343
344pub fn convert_semitone_to_specifier_generic_microtone(
349 count: FloatType,
350) -> (Specifier, IntegerType, FloatType) {
351 let dir_scale = if count < 0.0 { -1 } else { 1 };
352 let mut whole = count.floor();
353 let mut cents = (count - whole) * 100.0;
354 if cents > 50.0 {
355 cents -= 100.0;
356 whole += 1.0;
357 }
358 let whole = whole as IntegerType;
359 let size = whole.abs() % 12;
360 let octave = whole.abs() / 12;
361 let (specifier, generic) = semitones_to_specifier_generic(size);
362 (specifier, (generic + octave * 7) * dir_scale, cents)
363}
364
365pub fn convert_diatonic_number_to_step(dn: IntegerType) -> (char, IntegerType) {
369 let zero_based = dn - 1;
370 let octave = zero_based.div_euclid(7);
371 let step = StepName::ALL[zero_based.rem_euclid(7) as usize].as_char();
372 (step, octave)
373}
374
375pub fn parse_specifier(value: &str) -> Result<Specifier> {
379 Specifier::from_name(value)
380}
381
382impl Interval {
383 pub(crate) fn between(start_pitch: Pitch, end_pitch: Pitch) -> Result<Self> {
384 let generic = notes_to_generic(&start_pitch, &end_pitch)?;
385 let chromatic = notes_to_chromatic(&start_pitch, &end_pitch)?;
386 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
387
388 Ok(Self {
389 implicit_diatonic: false,
390 diatonic,
391 chromatic,
392 pitch_start: Some(start_pitch),
393 pitch_end: Some(end_pitch),
394 })
395 }
396
397 pub fn from_diatonic_and_chromatic(
400 diatonic: DiatonicInterval,
401 chromatic: ChromaticInterval,
402 ) -> Result<Interval> {
403 Ok(Self {
404 implicit_diatonic: false,
405 diatonic,
406 chromatic,
407 pitch_start: None,
408 pitch_end: None,
409 })
410 }
411
412 pub fn from_diatonic(diatonic: DiatonicInterval) -> Result<Self> {
415 let chromatic = diatonic.get_chromatic()?;
416 Self::from_diatonic_and_chromatic(diatonic, chromatic)
417 }
418
419 pub fn from_chromatic(chromatic: ChromaticInterval) -> Result<Self> {
423 let diatonic = chromatic.get_diatonic();
424 let mut interval = Self::from_diatonic_and_chromatic(diatonic, chromatic)?;
425 interval.implicit_diatonic = true;
426 Ok(interval)
427 }
428
429 pub fn generic(&self) -> &GenericInterval {
431 &self.diatonic.generic
432 }
433
434 pub fn diatonic(&self) -> &DiatonicInterval {
436 &self.diatonic
437 }
438
439 pub fn chromatic(&self) -> &ChromaticInterval {
441 &self.chromatic
442 }
443
444 pub fn specifier(&self) -> Specifier {
446 self.diatonic.specifier
447 }
448
449 pub fn from_generic_and_chromatic(
454 generic: IntegerType,
455 semitones: IntegerType,
456 ) -> Result<Self> {
457 let generic = GenericInterval::from_int(generic)?;
458 let chromatic = ChromaticInterval::from_int(semitones);
459 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
460 Self::from_diatonic_and_chromatic(diatonic, chromatic)
461 }
462
463 pub fn pitch_start(&self) -> Option<&Pitch> {
466 self.pitch_start.as_ref()
467 }
468
469 pub fn pitch_end(&self) -> Option<&Pitch> {
472 self.pitch_end.as_ref()
473 }
474
475 pub fn note_start(&self) -> Option<Note> {
477 self.pitch_start.clone().map(Note::from_pitch)
478 }
479
480 pub fn note_end(&self) -> Option<Note> {
482 self.pitch_end.clone().map(Note::from_pitch)
483 }
484
485 pub fn from_name(name: impl AsRef<str>) -> Result<Self> {
487 let (diatonic, chromatic, inferred) = parse_interval_name(name.as_ref())?;
488 Ok(Self {
489 implicit_diatonic: inferred,
490 diatonic,
491 chromatic,
492 pitch_start: None,
493 pitch_end: None,
494 })
495 }
496
497 pub fn from_semitones(semitones: IntegerType) -> Result<Self> {
499 let chromatic = ChromaticInterval::from_int(semitones);
500 let diatonic = chromatic.get_diatonic();
501 Ok(Self {
502 implicit_diatonic: true,
503 diatonic,
504 chromatic,
505 pitch_start: None,
506 pitch_end: None,
507 })
508 }
509
510 pub fn between_pitches(start: &Pitch, end: &Pitch) -> Result<Self> {
512 Self::between(start.clone(), end.clone())
513 }
514
515 pub fn between_notes(start: &Note, end: &Note) -> Result<Self> {
517 Self::between(start.pitch.clone(), end.pitch.clone())
518 }
519
520 pub fn semitones(&self) -> FloatType {
523 self.chromatic.semitones
524 }
525
526 pub fn whole_semitones(&self) -> IntegerType {
528 self.chromatic.whole_semitones()
529 }
530
531 pub fn direction(&self) -> IntervalDirection {
533 self.chromatic.direction()
534 }
535
536 pub fn name(&self) -> String {
538 self.nice_name()
539 }
540
541 pub fn generic_number(&self) -> IntegerType {
543 self.generic().simple_directed()
544 }
545
546 pub fn is_implicit_diatonic(&self) -> bool {
548 self.implicit_diatonic
549 }
550
551 pub fn inversion(&self) -> Result<Self> {
553 let direction = match self.direction() {
554 IntervalDirection::Oblique => 1,
555 direction => direction.as_int(),
556 };
557 let simple = self.generic().simple_undirected();
558 let inverted_generic = if simple == 1 { 1 } else { 9 - simple };
559 let generic = GenericInterval::from_int(inverted_generic * direction)?;
560 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
561 let chromatic = diatonic.get_chromatic()?;
562 Self::from_diatonic_and_chromatic(diatonic, chromatic)
563 }
564
565 pub fn reversed(&self) -> Result<Self> {
567 self.reverse()
568 }
569
570 pub fn pythagorean_ratio(&self) -> Result<FractionType> {
575 self.pythagorean_monzo()?.ratio()
576 }
577
578 pub fn pythagorean_monzo(&self) -> Result<crate::tuningsystem::Monzo> {
586 let spelled = self.diatonic.get_chromatic()?.semitones();
587 if self.chromatic.semitones() != spelled {
588 return Err(Error::Interval(format!(
589 "{} is not a whole number of semitones, so has no Pythagorean ratio",
590 self.chromatic.semitones()
591 )));
592 }
593 Ok(crate::tuningsystem::Monzo::pythagorean(
594 self.diatonic.generic.staff_distance(),
595 spelled as IntegerType,
596 ))
597 }
598
599 pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
601 self.transpose_pitch_with_options(pitch, false, Some(4))
602 }
603
604 pub fn transpose_note(&self, note: &Note) -> Result<Note> {
606 let mut out = note.clone();
607 out.pitch = self.transpose_pitch(¬e.pitch)?;
608 Ok(out)
609 }
610
611 pub fn short_name(&self) -> String {
613 format!(
614 "{}{}",
615 self.diatonic.specifier.prefix(),
616 self.generic().undirected()
617 )
618 }
619
620 pub fn simple_name(&self) -> String {
622 format!(
623 "{}{}",
624 self.diatonic.specifier.prefix(),
625 self.generic().simple_undirected()
626 )
627 }
628
629 pub fn semi_simple_name(&self) -> String {
632 format!(
633 "{}{}",
634 self.diatonic.specifier.prefix(),
635 self.generic().semi_simple_undirected()
636 )
637 }
638
639 pub fn directed_name(&self) -> String {
641 format!(
642 "{}{}",
643 self.diatonic.specifier.prefix(),
644 self.generic().directed()
645 )
646 }
647
648 pub fn is_diatonic_step(&self) -> bool {
650 self.generic().undirected() == 2
651 }
652
653 pub fn is_chromatic_step(&self) -> bool {
655 self.chromatic.undirected() == 1.0
656 }
657
658 pub fn is_step(&self) -> bool {
660 self.is_chromatic_step() || self.is_diatonic_step()
661 }
662
663 pub fn is_skip(&self) -> bool {
666 self.generic().undirected() > 2
667 }
668
669 pub fn is_consonant(&self) -> bool {
672 matches!(
673 (self.diatonic.specifier, self.generic().simple_undirected()),
674 (Specifier::Perfect, 1 | 5) | (Specifier::Major | Specifier::Minor, 3 | 6)
675 )
676 }
677
678 pub fn complement(&self) -> Result<Self> {
681 let generic = GenericInterval::from_int(9 - self.generic().semi_simple_undirected())?;
682 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
683 let chromatic = diatonic.get_chromatic()?;
684 Self::from_diatonic_and_chromatic(diatonic, chromatic)
685 }
686
687 pub fn interval_class(&self) -> IntegerType {
690 self.chromatic.interval_class()
691 }
692
693 pub fn sum<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
698 let start = Pitch::from_name("C4")?;
699 let mut end = start.clone();
700 let mut any = false;
701 for interval in intervals {
702 end = interval.transpose_pitch(&end)?;
703 any = true;
704 }
705 if !any {
706 return Err(Error::Interval(
707 "cannot add an empty set of intervals".to_string(),
708 ));
709 }
710 Self::between_pitches(&start, &end)
711 }
712
713 pub fn difference<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
716 let start = Pitch::from_name("C4")?;
717 let mut intervals = intervals.into_iter();
718 let Some(first) = intervals.next() else {
719 return Err(Error::Interval(
720 "cannot subtract an empty set of intervals".to_string(),
721 ));
722 };
723 let mut end = first.transpose_pitch(&start)?;
724 for interval in intervals {
725 end = interval.reversed()?.transpose_pitch(&end)?;
726 }
727 Self::between_pitches(&start, &end)
728 }
729
730 pub fn directed_simple_name(&self) -> String {
733 format!(
734 "{}{}",
735 self.diatonic.specifier.prefix(),
736 self.generic().simple_directed()
737 )
738 }
739
740 pub(crate) fn directed_simple_key(&self) -> (Specifier, IntegerType) {
741 (self.diatonic.specifier, self.generic().simple_directed())
742 }
743
744 pub(crate) fn simple_key(&self) -> (Specifier, IntegerType) {
745 (self.diatonic.specifier, self.generic().simple_undirected())
746 }
747
748 pub(crate) fn semi_simple_key(&self) -> (Specifier, IntegerType) {
749 (
750 self.diatonic.specifier,
751 self.generic().semi_simple_undirected(),
752 )
753 }
754
755 pub(crate) fn is_perfect_unison(&self) -> bool {
756 self.generic().undirected() == 1 && self.chromatic.semitones == 0.0
757 }
758
759 pub(crate) fn nice_name(&self) -> String {
760 self.diatonic.nice_name()
761 }
762
763 pub fn semi_simple_nice_name(&self) -> String {
767 self.diatonic.semi_simple_nice_name()
768 }
769
770 pub fn simple_nice_name(&self) -> String {
774 format!(
775 "{} {}",
776 self.diatonic.specifier.nice_name(),
777 self.generic().simple_nice_name()
778 )
779 }
780
781 fn diatonic_direction(&self) -> Direction {
786 self.diatonic.direction()
787 }
788
789 fn directed(&self, name: String) -> String {
790 format!("{} {name}", self.diatonic_direction().name())
791 }
792
793 pub fn directed_nice_name(&self) -> String {
796 self.directed(self.nice_name())
797 }
798
799 pub fn directed_simple_nice_name(&self) -> String {
802 self.directed(self.simple_nice_name())
803 }
804
805 pub fn directed_semi_simple_nice_name(&self) -> String {
808 self.directed(self.semi_simple_nice_name())
809 }
810
811 pub fn specific_name(&self) -> String {
814 self.diatonic.specifier.nice_name()
815 }
816
817 pub fn cents(&self) -> FloatType {
819 self.chromatic.cents()
820 }
821
822 pub fn diatonic_interval_cent_shift(&self) -> FloatType {
826 let diatonic_cents = self.diatonic.cents().unwrap_or(0.0);
827 self.chromatic.cents() - diatonic_cents
828 }
829
830 pub fn is_unison(&self) -> bool {
832 self.generic().is_unison()
833 }
834
835 pub fn is_perfectable(&self) -> bool {
838 self.generic().is_perfectable()
839 }
840
841 pub fn staff_distance(&self) -> IntegerType {
844 self.generic().staff_distance()
845 }
846
847 pub fn mod7(&self) -> IntegerType {
850 self.generic().mod7()
851 }
852
853 pub fn mod7_inversion(&self) -> IntegerType {
856 self.generic().mod7_inversion()
857 }
858
859 pub fn mod12(&self) -> IntegerType {
862 self.chromatic.mod12()
863 }
864
865 pub fn transpose_pitch_with_options(
871 &self,
872 p: &Pitch,
873 reverse: bool,
874 max_accidental: Option<IntegerType>,
875 ) -> Result<Pitch> {
876 if reverse {
877 return self
878 .reverse()?
879 .transpose_pitch_with_options(p, false, max_accidental);
880 }
881 let mut moved = self.transpose_pitch_alone(p, max_accidental)?;
882 if let Some(fundamental) = p.fundamental() {
883 moved.fundamental_setter(self.transpose_pitch_with_options(
884 fundamental,
885 false,
886 max_accidental,
887 )?);
888 }
889 Ok(moved)
890 }
891
892 fn transpose_pitch_alone(
894 &self,
895 p: &Pitch,
896 max_accidental: Option<IntegerType>,
897 ) -> Result<Pitch> {
898 if self.implicit_diatonic {
899 return self.chromatic.transpose_pitch(p);
900 }
901
902 let use_implicit_octave = p.octave().is_none();
903 let whole_semitones = self.chromatic.semitones == self.chromatic.semitones.trunc();
906 let inherit_accidental_display = self.diatonic.simple_name() == "P1" && whole_semitones;
907 let cents_origin = if p.is_twelve_tone() || !whole_semitones {
913 0.0
914 } else {
915 p.microtone().map_or(0.0, crate::pitch::Microtone::cents)
916 };
917 let new_dnn = p.diatonic_note_number() + self.diatonic.generic.staff_distance();
918 let (new_step, new_octave) = convert_diatonic_number_to_step(new_dnn);
919 let mut pitch2 = crate::pitch::PitchOptions::new()
920 .step(new_step)
921 .octave(new_octave)
922 .build()?;
923 let origin_ps = p.ps() - cents_origin / 100.0;
924 let mut half_steps_to_fix = self.chromatic.semitones - (pitch2.ps() - origin_ps);
925 while half_steps_to_fix >= 12.0 {
926 half_steps_to_fix -= 12.0;
927 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) - 1));
928 }
929 while half_steps_to_fix <= -12.0 {
930 half_steps_to_fix += 12.0;
931 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) + 1));
932 }
933 if half_steps_to_fix != 0.0 {
934 if max_accidental.is_some_and(|limit| half_steps_to_fix.abs() > limit as FloatType) {
935 pitch2.set_ps(pitch2.ps() + half_steps_to_fix);
936 } else {
937 pitch2.set_accidental_alter(half_steps_to_fix)?;
938 }
939 match (
940 inherit_accidental_display,
941 pitch2.written_accidental().is_some(),
942 p.written_accidental(),
943 ) {
944 (false, true, Some(source)) => {
945 if let Some(target) = pitch2.written_accidental_mut() {
946 target.inherit_display(source);
947 target.set_display_status(None);
948 }
949 }
950 (true, false, Some(source)) => {
951 let mut natural = crate::pitch::Accidental::natural();
952 natural.inherit_display(source);
953 pitch2.set_accidental(natural);
954 }
955 (true, true, Some(source)) => {
956 if let Some(target) = pitch2.written_accidental_mut() {
957 target.inherit_display(source);
958 }
959 }
960 (true, true, None) => {
961 if let Some(target) = pitch2.written_accidental_mut() {
962 target.set_display_status(Some(false));
963 }
964 }
965 _ => {}
966 }
967 } else if inherit_accidental_display
968 && p.written_accidental()
969 .is_some_and(|accidental| accidental.name() == "natural")
970 {
971 pitch2.set_written_accidental(p.written_accidental().cloned());
972 }
973 if cents_origin != 0.0 {
974 let cents = pitch2
975 .microtone()
976 .map_or(0.0, crate::pitch::Microtone::cents);
977 pitch2.set_microtone_cents(cents + cents_origin)?;
978 }
979 if use_implicit_octave {
980 pitch2.octave_setter(None);
981 }
982 Ok(pitch2)
983 }
984
985 pub fn transpose_pitch_in_place(&self, pitch: &mut Pitch) -> Result<()> {
987 *pitch = self.transpose_pitch(pitch)?;
988 Ok(())
989 }
990}
991
992impl fmt::Display for Interval {
993 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
994 let shift = self.diatonic_interval_cent_shift();
995 if shift == 0.0 {
996 write!(f, "{}", self.directed_name())
997 } else {
998 write!(
999 f,
1000 "{} {}",
1001 self.directed_name(),
1002 crate::pitch::Microtone::from_cents(shift, 1)
1003 )
1004 }
1005 }
1006}
1007
1008impl FromStr for Interval {
1009 type Err = Error;
1010
1011 fn from_str(value: &str) -> Result<Self> {
1012 Self::from_name(value)
1013 }
1014}
1015
1016impl TryFrom<&str> for Interval {
1017 type Error = Error;
1018
1019 fn try_from(value: &str) -> Result<Self> {
1020 Self::from_name(value)
1021 }
1022}
1023
1024impl TryFrom<String> for Interval {
1025 type Error = Error;
1026
1027 fn try_from(value: String) -> Result<Self> {
1028 Self::from_name(value)
1029 }
1030}
1031
1032impl TryFrom<IntegerType> for Interval {
1033 type Error = Error;
1034
1035 fn try_from(value: IntegerType) -> Result<Self> {
1036 Self::from_semitones(value)
1037 }
1038}
1039
1040fn parse_plain_interval_name(value: &str) -> Option<(Specifier, IntegerType)> {
1048 let mut number: IntegerType = 0;
1049 let mut digits = 0;
1050 let mut descending = false;
1051 let mut specifier: Option<(usize, usize)> = None;
1052 for (index, character) in value.char_indices() {
1053 if character == '-' {
1054 descending = true;
1057 } else if let Some(digit) = character.to_digit(10) {
1058 number = number * 10 + digit as IntegerType;
1059 digits += 1;
1060 } else if digits > 0 || !character.is_ascii_alphabetic() {
1061 return None;
1064 } else {
1065 let end = index + character.len_utf8();
1066 specifier = Some((specifier.map_or(index, |(start, _)| start), end));
1067 }
1068 }
1069 let (start, end) = specifier?;
1070 if digits == 0 {
1071 return None;
1072 }
1073 let specifier = Specifier::from_name(&value[start..end]).ok()?;
1074 Some((specifier, if descending { -number } else { number }))
1075}
1076
1077fn interval_parts(
1080 specifier: Specifier,
1081 generic_number: IntegerType,
1082 inferred: bool,
1083) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1084 let g_interval = GenericInterval::from_int(generic_number)?;
1085 let d_interval = g_interval.get_diatonic(specifier);
1086 let c_interval = d_interval.get_chromatic()?;
1087 Ok((d_interval, c_interval, inferred))
1088}
1089
1090fn parse_interval_name(name: &str) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1091 if let Some((specifier, number)) = parse_plain_interval_name(name) {
1092 return interval_parts(specifier, number, false);
1093 }
1094 let mut value = name.to_string();
1096 let mut inferred = false;
1097 let mut dir_scale = 1;
1098
1099 if value.contains('-') {
1101 value = value.replace('-', "");
1102 dir_scale = -1;
1103 }
1104 {
1106 let (without_descending, found_descending) = strip_direction_word(&value, "descending");
1107 if found_descending {
1108 value = without_descending;
1109 dir_scale = -1;
1110 } else {
1111 let (without_ascending, found_ascending) = strip_direction_word(&value, "ascending");
1112 if found_ascending {
1113 value = without_ascending;
1114 }
1115 }
1116 }
1117 let value_lower = value.to_lowercase();
1118
1119 if value_lower == "w" || value_lower == "whole" || value_lower == "tone" {
1121 value = "M2".to_string();
1122 inferred = true;
1123 } else if value_lower == "h" || value_lower == "half" || value_lower == "semitone" {
1124 value = "m2".to_string();
1125 inferred = true;
1126 }
1127
1128 let mut lowered = value.to_ascii_lowercase();
1133 for (i, ordinal) in MUSICAL_ORDINAL_STRINGS_LOWER.iter().enumerate() {
1134 if !lowered.contains(ordinal.as_str()) {
1135 continue;
1136 }
1137 let replacement = i.to_string();
1138 let (next_value, replaced) =
1139 replace_music_ordinal(&value, &MUSICAL_ORDINAL_STRINGS[i], &replacement);
1140 if replaced {
1141 value = next_value;
1142 lowered = value.to_ascii_lowercase();
1143 }
1144 }
1145
1146 let (found, remain) = get_num_from_str(&value, "0123456789");
1148 let generic_number: IntegerType = found
1149 .parse::<IntegerType>()
1150 .map_err(|_| Error::Interval(format!("cannot read an interval number from {value:?}")))?
1151 * dir_scale;
1152 let spec = Specifier::parse(&remain)?;
1153 interval_parts(spec, generic_number, inferred)
1154}
1155
1156impl Interval {
1157 fn reverse(&self) -> Result<Self> {
1158 if let (Some(start), Some(end)) = (&self.pitch_start, &self.pitch_end) {
1159 Interval::between(end.clone(), start.clone())
1160 } else {
1161 Interval::from_diatonic_and_chromatic(self.diatonic.reverse(), self.chromatic.reverse())
1162 }
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 #[test]
1169 fn an_interval_is_read_from_a_name_a_number_or_one_of_its_halves() {
1170 use super::{ChromaticInterval, DiatonicInterval, Interval, Specifier, parse_specifier};
1171 use crate::note::Note;
1172 use crate::pitch::Pitch;
1173 use std::str::FromStr;
1174
1175 assert_eq!(Interval::from_str("M3").unwrap().short_name(), "M3");
1176 assert_eq!(Interval::try_from("P5").unwrap().short_name(), "P5");
1177 assert_eq!(
1178 Interval::try_from("m6".to_string()).unwrap().short_name(),
1179 "m6"
1180 );
1181 assert_eq!(Interval::try_from(7).unwrap().short_name(), "P5");
1182 assert!(Interval::from_str("Q9").is_err());
1183 assert_eq!(parse_specifier("M").unwrap(), Specifier::Major);
1184 assert!(parse_specifier("Q").is_err());
1185
1186 let diatonic = Interval::from_diatonic(DiatonicInterval::from_name("M3").unwrap()).unwrap();
1187 assert_eq!(diatonic.semitones(), 4.0);
1188 assert!(!diatonic.is_implicit_diatonic());
1189 assert_eq!(diatonic.diatonic().name(), "M3");
1190 assert_eq!(diatonic.specifier(), Specifier::Major);
1191 let chromatic = Interval::from_chromatic(ChromaticInterval::from_int(6)).unwrap();
1192 assert!(chromatic.is_implicit_diatonic());
1193 assert_eq!(chromatic.whole_semitones(), 6);
1194 assert_eq!(chromatic.directed_simple_name(), "d5");
1195 assert_eq!(
1196 Interval::from_str("M-10").unwrap().directed_simple_name(),
1197 "M-3"
1198 );
1199 assert_eq!(
1200 Interval::from_str("M3").unwrap(),
1201 Interval::from_str("M3").unwrap()
1202 );
1203 assert_ne!(
1204 Interval::from_str("M3").unwrap(),
1205 Interval::from_str("m3").unwrap()
1206 );
1207
1208 let c = Note::from_pitch(Pitch::from_name("C4").unwrap());
1209 let g = Note::from_pitch(Pitch::from_name("G4").unwrap());
1210 let fifth = Interval::between_notes(&c, &g).unwrap();
1211 assert_eq!(fifth.short_name(), "P5");
1212 assert_eq!(
1213 fifth.transpose_note(&g).unwrap().pitch_name_with_octave(),
1214 "D5"
1215 );
1216 }
1217
1218 #[test]
1219 fn a_note_flatter_than_the_table_wraps_the_way_music21_does() {
1220 let interval = Interval::between_pitches(
1224 &Pitch::from_name("A##3").unwrap(),
1225 &Pitch::from_name("E---4").unwrap(),
1226 )
1227 .unwrap();
1228 assert_eq!(interval.short_name(), "AAAA5");
1229 assert_eq!(interval.chromatic().semitones(), 2.0);
1230
1231 let widest = Interval::between_pitches(
1234 &Pitch::from_name("C4").unwrap(),
1235 &Pitch::from_name("E####4").unwrap(),
1236 )
1237 .unwrap();
1238 assert_eq!(widest.short_name(), "AAAA3");
1239 }
1240
1241 #[test]
1242 fn generic_and_chromatic_build_the_interval_music21_does() {
1243 let cases: [(i32, i32, &str, &str); 14] = [
1244 (3, 4, "M3", "M3"),
1245 (3, 3, "m3", "m3"),
1246 (5, 7, "P5", "P5"),
1247 (5, 6, "d5", "d5"),
1248 (4, 6, "A4", "A4"),
1249 (1, 0, "P1", "P1"),
1250 (1, 1, "A1", "A1"),
1251 (8, 12, "P8", "P8"),
1252 (-3, -4, "M3", "M-3"),
1253 (2, 3, "A2", "A2"),
1254 (7, 10, "m7", "m7"),
1255 (-2, -1, "m2", "m-2"),
1256 (9, 13, "m9", "m9"),
1257 (3, 2, "d3", "d3"),
1258 ];
1259 for (generic, semitones, name, directed) in cases {
1260 let interval = Interval::from_generic_and_chromatic(generic, semitones).unwrap();
1261 assert_eq!(interval.short_name(), name, "{generic} {semitones}");
1262 assert_eq!(interval.directed_name(), directed, "{generic} {semitones}");
1263 assert_eq!(
1264 interval.semitones(),
1265 FloatType::from(semitones),
1266 "{generic} {semitones}"
1267 );
1268 assert!(interval.pitch_start().is_none());
1269 }
1270 assert!(Interval::from_generic_and_chromatic(0, 0).is_err());
1271 assert_eq!(
1272 [-3, -1, 0, 1, 2, 7].map(staff_distance_to_generic_number),
1273 [-4, -2, 1, 2, 3, 8]
1274 );
1275 }
1276
1277 #[test]
1278 fn written_and_sounding_order_match_music21() {
1279 let cases: [(&str, &str, [&str; 4]); 8] = [
1280 ("C4", "E4", ["E4", "C4", "E4", "C4"]),
1281 ("E4", "C4", ["E4", "C4", "E4", "C4"]),
1282 ("B#3", "C4", ["C4", "B#3", "B#3", "B#3"]),
1283 ("C4", "B#3", ["C4", "B#3", "C4", "C4"]),
1284 ("Cb4", "B3", ["Cb4", "B3", "Cb4", "Cb4"]),
1285 ("F#4", "Gb4", ["Gb4", "F#4", "F#4", "F#4"]),
1286 ("C4", "C4", ["C4", "C4", "C4", "C4"]),
1287 ("B3", "Cb4", ["Cb4", "B3", "B3", "B3"]),
1288 ];
1289 for (first, second, expected) in cases {
1290 let a = Pitch::from_name(first).unwrap();
1291 let b = Pitch::from_name(second).unwrap();
1292 let actual = [
1293 written_higher_pitch(&a, &b),
1294 written_lower_pitch(&a, &b),
1295 absolute_higher_pitch(&a, &b),
1296 absolute_lower_pitch(&a, &b),
1297 ]
1298 .map(Pitch::name_with_octave);
1299 assert_eq!(actual, expected, "{first} {second}");
1300 }
1301 }
1302
1303 #[test]
1304 fn intervals_remember_the_pitches_they_were_measured_between() {
1305 let c = Pitch::from_name("C4").unwrap();
1306 let g = Pitch::from_name("G4").unwrap();
1307 let fifth = Interval::between_pitches(&c, &g).unwrap();
1308 assert_eq!(fifth.pitch_start().unwrap().name_with_octave(), "C4");
1309 assert_eq!(fifth.pitch_end().unwrap().name_with_octave(), "G4");
1310 assert_eq!(fifth.note_start().unwrap().pitch_name_with_octave(), "C4");
1311 assert_eq!(fifth.note_end().unwrap().pitch_name_with_octave(), "G4");
1312 let named = Interval::from_name("P5").unwrap();
1313 assert!(named.pitch_start().is_none());
1314 assert!(named.note_end().is_none());
1315 }
1316
1317 #[test]
1318 fn nice_name_variants_match_music21() {
1319 let cases: [(&str, [&str; 7]); 15] = [
1320 (
1321 "P1",
1322 [
1323 "Perfect Unison",
1324 "Oblique Perfect Unison",
1325 "Perfect Unison",
1326 "Perfect Unison",
1327 "Oblique Perfect Unison",
1328 "Oblique Perfect Unison",
1329 "Perfect",
1330 ],
1331 ),
1332 (
1333 "m2",
1334 [
1335 "Minor Second",
1336 "Ascending Minor Second",
1337 "Minor Second",
1338 "Minor Second",
1339 "Ascending Minor Second",
1340 "Ascending Minor Second",
1341 "Minor",
1342 ],
1343 ),
1344 (
1345 "P8",
1346 [
1347 "Perfect Octave",
1348 "Ascending Perfect Octave",
1349 "Perfect Unison",
1350 "Perfect Octave",
1351 "Ascending Perfect Unison",
1352 "Ascending Perfect Octave",
1353 "Perfect",
1354 ],
1355 ),
1356 (
1357 "m9",
1358 [
1359 "Minor Ninth",
1360 "Ascending Minor Ninth",
1361 "Minor Second",
1362 "Minor Second",
1363 "Ascending Minor Second",
1364 "Ascending Minor Second",
1365 "Minor",
1366 ],
1367 ),
1368 (
1369 "M10",
1370 [
1371 "Major Tenth",
1372 "Ascending Major Tenth",
1373 "Major Third",
1374 "Major Third",
1375 "Ascending Major Third",
1376 "Ascending Major Third",
1377 "Major",
1378 ],
1379 ),
1380 (
1381 "P12",
1382 [
1383 "Perfect Twelfth",
1384 "Ascending Perfect Twelfth",
1385 "Perfect Fifth",
1386 "Perfect Fifth",
1387 "Ascending Perfect Fifth",
1388 "Ascending Perfect Fifth",
1389 "Perfect",
1390 ],
1391 ),
1392 (
1393 "-M3",
1394 [
1395 "Major Third",
1396 "Descending Major Third",
1397 "Major Third",
1398 "Major Third",
1399 "Descending Major Third",
1400 "Descending Major Third",
1401 "Major",
1402 ],
1403 ),
1404 (
1405 "-m9",
1406 [
1407 "Minor Ninth",
1408 "Descending Minor Ninth",
1409 "Minor Second",
1410 "Minor Second",
1411 "Descending Minor Second",
1412 "Descending Minor Second",
1413 "Minor",
1414 ],
1415 ),
1416 (
1417 "dd5",
1418 [
1419 "Doubly-Diminished Fifth",
1420 "Ascending Doubly-Diminished Fifth",
1421 "Doubly-Diminished Fifth",
1422 "Doubly-Diminished Fifth",
1423 "Ascending Doubly-Diminished Fifth",
1424 "Ascending Doubly-Diminished Fifth",
1425 "Doubly-Diminished",
1426 ],
1427 ),
1428 (
1429 "AA4",
1430 [
1431 "Doubly-Augmented Fourth",
1432 "Ascending Doubly-Augmented Fourth",
1433 "Doubly-Augmented Fourth",
1434 "Doubly-Augmented Fourth",
1435 "Ascending Doubly-Augmented Fourth",
1436 "Ascending Doubly-Augmented Fourth",
1437 "Doubly-Augmented",
1438 ],
1439 ),
1440 (
1441 "P15",
1442 [
1443 "Perfect Double-octave",
1444 "Ascending Perfect Double-octave",
1445 "Perfect Unison",
1446 "Perfect Octave",
1447 "Ascending Perfect Unison",
1448 "Ascending Perfect Octave",
1449 "Perfect",
1450 ],
1451 ),
1452 (
1453 "d1",
1454 [
1455 "Diminished Unison",
1456 "Descending Diminished Unison",
1457 "Diminished Unison",
1458 "Diminished Unison",
1459 "Descending Diminished Unison",
1460 "Descending Diminished Unison",
1461 "Diminished",
1462 ],
1463 ),
1464 (
1465 "A1",
1466 [
1467 "Augmented Unison",
1468 "Ascending Augmented Unison",
1469 "Augmented Unison",
1470 "Augmented Unison",
1471 "Ascending Augmented Unison",
1472 "Ascending Augmented Unison",
1473 "Augmented",
1474 ],
1475 ),
1476 (
1477 "-A1",
1478 [
1479 "Augmented Unison",
1480 "Ascending Augmented Unison",
1481 "Augmented Unison",
1482 "Augmented Unison",
1483 "Ascending Augmented Unison",
1484 "Ascending Augmented Unison",
1485 "Augmented",
1486 ],
1487 ),
1488 (
1489 "P-8",
1490 [
1491 "Perfect Octave",
1492 "Descending Perfect Octave",
1493 "Perfect Unison",
1494 "Perfect Octave",
1495 "Descending Perfect Unison",
1496 "Descending Perfect Octave",
1497 "Perfect",
1498 ],
1499 ),
1500 ];
1501 for (name, expected) in cases {
1502 let interval = Interval::from_name(name).unwrap();
1503 let actual = [
1504 interval.name(),
1505 interval.directed_nice_name(),
1506 interval.simple_nice_name(),
1507 interval.semi_simple_nice_name(),
1508 interval.directed_simple_nice_name(),
1509 interval.directed_semi_simple_nice_name(),
1510 interval.specific_name(),
1511 ];
1512 assert_eq!(actual, expected, "{name}");
1513 }
1514 }
1515
1516 #[test]
1517 #[allow(clippy::type_complexity)]
1518 fn generic_and_chromatic_helpers_match_music21() {
1519 let cases: [(&str, f64, bool, bool, i32, i32, i32, i32); 14] = [
1520 ("P1", 0.0, true, true, 0, 1, 8, 0),
1521 ("m2", 100.0, false, false, 1, 2, 7, 1),
1522 ("P4", 500.0, false, true, 3, 4, 5, 5),
1523 ("P5", 700.0, false, true, 4, 5, 4, 7),
1524 ("M7", 1100.0, false, false, 6, 7, 2, 11),
1525 ("P8", 1200.0, false, true, 7, 1, 1, 0),
1526 ("m9", 1300.0, false, false, 8, 2, 7, 1),
1527 ("-M3", -400.0, false, false, -2, 6, 6, 8),
1528 ("-P5", -700.0, false, true, -4, 4, 4, 5),
1529 ("-m9", -1300.0, false, false, -8, 7, 7, 11),
1530 ("P15", 2400.0, false, true, 14, 1, 1, 0),
1531 ("d1", -100.0, true, true, 0, 1, 8, 11),
1532 ("-A1", -100.0, true, true, 0, 8, 8, 11),
1533 ("P-8", -1200.0, false, true, -7, 1, 1, 0),
1534 ];
1535 for (name, cents, unison, perfectable, staff, mod7, mod7_inversion, mod12) in cases {
1536 let interval = Interval::from_name(name).unwrap();
1537 assert_eq!(interval.cents(), cents, "{name} cents");
1538 assert_eq!(interval.is_unison(), unison, "{name} unison");
1539 assert_eq!(interval.is_perfectable(), perfectable, "{name} perfectable");
1540 assert_eq!(interval.staff_distance(), staff, "{name} staff distance");
1541 assert_eq!(interval.mod7(), mod7, "{name} mod7");
1542 assert_eq!(
1543 interval.mod7_inversion(),
1544 mod7_inversion,
1545 "{name} mod7 inversion"
1546 );
1547 assert_eq!(interval.mod12(), mod12, "{name} mod12");
1548 }
1549 }
1550 use super::*;
1551
1552 fn pitch(name: &str) -> Pitch {
1553 Pitch::from_name(name).expect("valid pitch")
1554 }
1555
1556 #[test]
1557 fn malformed_interval_names_error_instead_of_panicking() {
1558 for bad in ["", "X", "perfect", "?!", "MM"] {
1562 assert!(
1563 Interval::from_name(bad).is_err(),
1564 "Interval::from_name({bad:?}) should be an error"
1565 );
1566 }
1567 }
1568
1569 #[test]
1570 fn interval_between_microtonal_pitches_keeps_the_cent_shift() {
1571 let c1 = pitch("C1");
1572 let mut half_sharp = pitch("C1");
1573 half_sharp.set_accidental(
1574 crate::pitch::Accidental::new("half-sharp").expect("half-sharp is an accidental"),
1575 );
1576
1577 let quarter_tone = Interval::between_pitches(&c1, &half_sharp).unwrap();
1578 assert_eq!(quarter_tone.semitones(), 0.5);
1579 assert_eq!(quarter_tone.cents(), 50.0);
1580 assert_eq!(quarter_tone.directed_name(), "A1");
1581 assert_eq!(quarter_tone.diatonic_interval_cent_shift(), -50.0);
1582 assert_eq!(quarter_tone.to_string(), "A1 (-50c)");
1583 assert!(quarter_tone.pythagorean_ratio().is_err());
1584 }
1585
1586 #[test]
1587 fn interval_cents_carry_a_pitch_microtone() {
1588 let c4 = pitch("C4");
1589 let mut d4 = pitch("D4");
1590 d4.set_microtone_cents(30.0).unwrap();
1591
1592 let interval = Interval::between_pitches(&c4, &d4).unwrap();
1593 assert_eq!(interval.cents(), 230.0);
1594 assert_eq!(interval.to_string(), "M2 (+30c)");
1595 }
1596
1597 #[test]
1598 fn interval_from_string_has_expected_chromatic() {
1599 let interval = Interval::from_name("M3").unwrap();
1600 assert_eq!(interval.chromatic.semitones, 4.0);
1601 assert!(!interval.implicit_diatonic);
1602 }
1603
1604 #[test]
1605 fn interval_parser_accepts_direction_words_and_ordinals() {
1606 let descending = Interval::from_name("Descending Perfect Twelfth").unwrap();
1607 assert_eq!(descending.semitones(), -19.0);
1608 assert_eq!(descending.generic_number(), -5);
1609
1610 let ascending = Interval::from_name("ascending Major Second").unwrap();
1611 assert_eq!(ascending.semitones(), 2.0);
1612 assert_eq!(ascending.generic_number(), 2);
1613
1614 let major_third = Interval::from_name("Major Third").unwrap();
1615 assert_eq!(major_third.semitones(), 4.0);
1616 assert_eq!(major_third.generic_number(), 3);
1617 }
1618
1619 #[test]
1620 fn interval_from_int_is_implicit_diatonic() {
1621 let interval = Interval::from_semitones(1).unwrap();
1622 assert!(interval.implicit_diatonic);
1623 assert_eq!(interval.chromatic.semitones, 1.0);
1624 }
1625
1626 #[test]
1627 fn interval_between_pitches() {
1628 let c4 = pitch("C4");
1629 let g4 = pitch("G4");
1630 let interval = Interval::between(c4, g4).unwrap();
1631 assert_eq!(interval.chromatic.semitones, 7.0);
1632 assert_eq!(interval.generic().staff_distance(), 4);
1633 }
1634
1635 #[test]
1636 fn interval_transpose_pitch() {
1637 let c4 = pitch("C4");
1638 let m3 = Interval::from_name("m3").unwrap();
1639 let out = m3.transpose_pitch(&c4).unwrap();
1640 assert_eq!(out.name_with_octave(), "Eb4");
1641 }
1642
1643 #[test]
1644 fn interval_transpose_pitch_in_place() {
1645 let mut c4 = pitch("C4");
1646 Interval::from_name("M2")
1647 .unwrap()
1648 .transpose_pitch_in_place(&mut c4)
1649 .unwrap();
1650 assert_eq!(c4.name_with_octave(), "D4");
1651 }
1652
1653 #[test]
1654 fn compact_names_match_music21() {
1655 let cases = [
1656 ("P5", "P5", "P5", "P5", "P5"),
1657 ("M3", "M3", "M3", "M3", "M3"),
1658 ("m-6", "m6", "m6", "m6", "m-6"),
1659 ("AA4", "AA4", "AA4", "AA4", "AA4"),
1660 ("d8", "d8", "d1", "d8", "d8"),
1661 ("P8", "P8", "P1", "P8", "P8"),
1662 ("M9", "M9", "M2", "M2", "M9"),
1663 ("P-5", "P5", "P5", "P5", "P-5"),
1664 ("P15", "P15", "P1", "P8", "P15"),
1665 ];
1666 for (input, short, simple, semi_simple, directed) in cases {
1667 let interval = Interval::from_name(input).unwrap();
1668 assert_eq!(interval.short_name(), short, "{input}");
1669 assert_eq!(interval.simple_name(), simple, "{input}");
1670 assert_eq!(interval.semi_simple_name(), semi_simple, "{input}");
1671 assert_eq!(interval.directed_name(), directed, "{input}");
1672 }
1673 }
1674
1675 #[test]
1676 fn complement_and_interval_class_match_music21() {
1677 let cases = [
1678 ("P5", "P4", 5),
1679 ("M3", "m6", 4),
1680 ("m-6", "M3", 4),
1681 ("AA4", "dd5", 5),
1682 ("d8", "A1", 1),
1683 ("P8", "P1", 0),
1684 ("P1", "P8", 0),
1685 ("A1", "d8", 1),
1686 ("M9", "m7", 2),
1687 ("m2", "M7", 1),
1688 ("P15", "P1", 0),
1689 ];
1690 for (input, complement, interval_class) in cases {
1691 let interval = Interval::from_name(input).unwrap();
1692 assert_eq!(
1693 interval.complement().unwrap().short_name(),
1694 complement,
1695 "{input}"
1696 );
1697 assert_eq!(interval.interval_class(), interval_class, "{input}");
1698 }
1699 }
1700
1701 #[test]
1702 fn step_skip_and_consonance_match_music21() {
1703 let cases = [
1704 ("P5", false, true, true, false, false),
1705 ("M3", false, true, true, false, false),
1706 ("AA4", false, true, false, false, false),
1707 ("d8", false, true, false, false, false),
1708 ("P8", false, true, true, false, false),
1709 ("P1", false, false, true, false, false),
1710 ("A1", true, false, false, false, true),
1711 ("M9", false, true, false, false, false),
1712 ("m2", true, false, false, true, true),
1713 ];
1714 for (input, step, skip, consonant, diatonic_step, chromatic_step) in cases {
1715 let interval = Interval::from_name(input).unwrap();
1716 assert_eq!(interval.is_step(), step, "{input} step");
1717 assert_eq!(interval.is_skip(), skip, "{input} skip");
1718 assert_eq!(interval.is_consonant(), consonant, "{input} consonant");
1719 assert_eq!(
1720 interval.is_diatonic_step(),
1721 diatonic_step,
1722 "{input} diatonic"
1723 );
1724 assert_eq!(
1725 interval.is_chromatic_step(),
1726 chromatic_step,
1727 "{input} chromatic"
1728 );
1729 }
1730 }
1731
1732 #[test]
1733 fn sum_and_difference_match_music21() {
1734 fn intervals(names: &[&str]) -> Vec<Interval> {
1735 names
1736 .iter()
1737 .map(|name| Interval::from_name(*name).unwrap())
1738 .collect()
1739 }
1740 assert_eq!(
1741 Interval::sum(&intervals(&["A2", "P5"]))
1742 .unwrap()
1743 .short_name(),
1744 "A6"
1745 );
1746 assert_eq!(
1747 Interval::sum(&intervals(&["P5", "m2"]))
1748 .unwrap()
1749 .short_name(),
1750 "m6"
1751 );
1752 assert_eq!(
1753 Interval::sum(&intervals(&["W", "W", "H", "W", "W", "W", "H"]))
1754 .unwrap()
1755 .short_name(),
1756 "P8"
1757 );
1758 assert_eq!(
1759 Interval::sum(&intervals(&["P5", "P-4"]))
1760 .unwrap()
1761 .directed_name(),
1762 "M2"
1763 );
1764 assert!(Interval::sum(&[]).is_err());
1765
1766 let cases = [
1767 (&["P5", "M3"][..], "m3"),
1768 (&["P4", "d3"][..], "A2"),
1769 (&["M6", "m2", "m2"][..], "AA4"),
1770 (&["P4", "M-2"][..], "P5"),
1771 (&["A2", "A2"][..], "P1"),
1772 (&["P8", "A1"][..], "d8"),
1773 ];
1774 for (names, expected) in cases {
1775 let difference = Interval::difference(&intervals(names)).unwrap();
1776 assert_eq!(difference.short_name(), expected, "{names:?}");
1777 }
1778 let descending_unison = Interval::difference(&intervals(&["P5", "A5"])).unwrap();
1779 assert_eq!(descending_unison.directed_name(), "d1");
1780 assert_eq!(descending_unison.semitones(), -1.0);
1781 assert!(Interval::difference(&[]).is_err());
1782 }
1783
1784 #[test]
1785 fn interval_pythagorean_ratio() {
1786 let ratio = Interval::from_name("P5")
1787 .unwrap()
1788 .pythagorean_ratio()
1789 .unwrap();
1790 assert_eq!(ratio, FractionType::new(3, 2));
1791 }
1792
1793 #[test]
1794 fn a_spelled_interval_is_a_place_on_the_chain_of_fifths() {
1795 let monzo = |name: &str| {
1796 Interval::from_name(name)
1797 .unwrap()
1798 .pythagorean_monzo()
1799 .unwrap()
1800 };
1801 assert_eq!(monzo("M3").to_string(), "[-6 4⟩");
1802 assert_eq!(monzo("A1").to_string(), "[-11 7⟩");
1803 assert_eq!(monzo("m2").to_string(), "[8 -5⟩");
1804 assert_eq!(monzo("P8").to_string(), "[1⟩");
1805 let fifth_down = Interval::from_name("P5").unwrap().reversed().unwrap();
1806 assert_eq!(
1807 fifth_down.pythagorean_ratio().unwrap(),
1808 FractionType::new(2, 3)
1809 );
1810 let far = Interval::from_name("AAA4").unwrap();
1813 assert_eq!(far.pythagorean_monzo().unwrap().exponent_of(3).unwrap(), 20);
1814 assert!(far.pythagorean_ratio().is_err());
1815 }
1816
1817 #[test]
1818 fn interval_inverts_oblique_unison() {
1819 let unison = Interval::from_name("P1").unwrap();
1820 let inverted = unison.inversion().unwrap();
1821
1822 assert_eq!(inverted.semitones(), 0.0);
1823 assert_eq!(inverted.generic_number(), 1);
1824 }
1825 #[test]
1826 fn specifier_case_matters_only_for_major_versus_minor() {
1827 for (lower, upper) in [
1830 ("p5", "P5"),
1831 ("a2", "A2"),
1832 ("d5", "D5"),
1833 ("aa2", "AA2"),
1834 ("dd5", "DD5"),
1835 ("aaa2", "AAA2"),
1836 ("ddd5", "DDD5"),
1837 ] {
1838 let a = Interval::from_name(lower).expect("lowercase parses");
1839 let b = Interval::from_name(upper).expect("uppercase parses");
1840 assert_eq!(a.semitones(), b.semitones(), "{lower} vs {upper}");
1841 assert_eq!(a.name(), b.name(), "{lower} vs {upper}");
1842 }
1843
1844 let minor = Interval::from_name("m3").expect("m3 parses");
1846 let major = Interval::from_name("M3").expect("M3 parses");
1847 assert_eq!(minor.semitones(), 3.0);
1848 assert_eq!(major.semitones(), 4.0);
1849 }
1850
1851 #[test]
1852 fn an_unknown_specifier_errors_instead_of_panicking() {
1853 for name in ["Q5", "x3", "5", "zz2"] {
1854 assert!(
1855 Interval::from_name(name).is_err(),
1856 "{name:?} should be rejected, not panic"
1857 );
1858 }
1859 }
1860
1861 #[test]
1862 fn a_hyphen_anywhere_makes_an_interval_name_descending() {
1863 for name in ["-M2", "M-2"] {
1868 let interval = Interval::from_name(name).expect("descending name parses");
1869 assert_eq!(interval.semitones(), -2.0, "{name}");
1870 assert_eq!(interval.generic_number(), -2, "{name}");
1871 }
1872
1873 for name in ["--M2", "-M-2"] {
1877 let interval = Interval::from_name(name).expect("repeated hyphen parses");
1878 assert_eq!(interval.semitones(), -2.0, "{name}");
1879 }
1880
1881 assert_eq!(
1883 Interval::from_name("d-5").expect("d-5 parses").semitones(),
1884 -6.0
1885 );
1886 }
1887}