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 fraction_pow::FractionPow,
26 note::Note,
27 pitch::Pitch,
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
79pub(crate) enum PitchOrNote {
80 Pitch(Pitch),
81 Note(Note),
82}
83
84use constants::{PERFECT_FIFTH_DOWN, PERFECT_FIFTH_UP};
85
86fn extract_pitch(arg: PitchOrNote) -> Pitch {
87 match arg {
88 PitchOrNote::Pitch(pitch) => pitch,
89 PitchOrNote::Note(note) => note.pitch,
90 }
91}
92
93fn strip_direction_word(value: &str, word: &str) -> (String, bool) {
94 replace_case_insensitive(value, word, "", false, true)
95}
96
97fn replace_music_ordinal(value: &str, ordinal: &str, replacement: &str) -> (String, bool) {
98 replace_case_insensitive(value, ordinal, replacement, true, true)
99}
100
101fn replace_case_insensitive(
102 value: &str,
103 needle: &str,
104 replacement: &str,
105 consume_leading_whitespace: bool,
106 consume_trailing_whitespace: bool,
107) -> (String, bool) {
108 let needle_lower = needle.to_ascii_lowercase();
109 let value_lower = value.to_ascii_lowercase();
110 let mut output = String::with_capacity(value.len());
111 let mut pos = 0;
112 let mut replaced = false;
113
114 while let Some(relative_start) = value_lower[pos..].find(&needle_lower) {
115 let match_start = pos + relative_start;
116 let match_end = match_start + needle.len();
117 let mut copy_end = match_start;
118 let mut next_pos = match_end;
119
120 if consume_leading_whitespace {
121 while copy_end > pos {
122 let Some(ch) = value[pos..copy_end].chars().next_back() else {
123 break;
124 };
125 if !ch.is_whitespace() {
126 break;
127 }
128 copy_end -= ch.len_utf8();
129 }
130 }
131
132 if consume_trailing_whitespace {
133 while next_pos < value.len() {
134 let Some(ch) = value[next_pos..].chars().next() else {
135 break;
136 };
137 if !ch.is_whitespace() {
138 break;
139 }
140 next_pos += ch.len_utf8();
141 }
142 }
143
144 output.push_str(&value[pos..copy_end]);
145 output.push_str(replacement);
146 pos = next_pos;
147 replaced = true;
148 }
149
150 if !replaced {
151 return (value.to_string(), false);
152 }
153
154 output.push_str(&value[pos..]);
155 (output, true)
156}
157
158fn convert_staff_distance_to_interval(staff_dist: IntegerType) -> IntegerType {
159 match staff_dist.cmp(&0) {
160 Ordering::Equal => 1,
161 Ordering::Greater => staff_dist + 1,
162 Ordering::Less => staff_dist - 1,
163 }
164}
165
166pub fn staff_distance_to_generic_number(staff_distance: IntegerType) -> IntegerType {
170 convert_staff_distance_to_interval(staff_distance)
171}
172
173fn diatonic_note_number(pitch: &Pitch) -> IntegerType {
174 pitch.step().step_to_dnn_offset() + (7 * pitch.octave().unwrap_or(4))
175}
176
177pub fn written_higher_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
181 match diatonic_note_number(first).cmp(&diatonic_note_number(second)) {
182 Ordering::Greater => first,
183 Ordering::Less => second,
184 Ordering::Equal => absolute_higher_pitch(first, second),
185 }
186}
187
188pub fn written_lower_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
191 match diatonic_note_number(first).cmp(&diatonic_note_number(second)) {
192 Ordering::Less => first,
193 Ordering::Greater => second,
194 Ordering::Equal => absolute_lower_pitch(first, second),
195 }
196}
197
198pub fn absolute_higher_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
201 if second.ps() > first.ps() {
202 second
203 } else {
204 first
205 }
206}
207
208pub fn absolute_lower_pitch<'a>(first: &'a Pitch, second: &'a Pitch) -> &'a Pitch {
211 if second.ps() < first.ps() {
212 second
213 } else {
214 first
215 }
216}
217
218pub fn notes_to_generic(p1: &Pitch, p2: &Pitch) -> Result<GenericInterval> {
221 let dnn1 = p1.step().step_to_dnn_offset() + (7 * p1.octave().unwrap_or(4));
222 let dnn2 = p2.step().step_to_dnn_offset() + (7 * p2.octave().unwrap_or(4));
223 let staff_dist = dnn2 - dnn1;
224 GenericInterval::from_int(convert_staff_distance_to_interval(staff_dist))
225}
226
227pub fn notes_to_chromatic(p1: &Pitch, p2: &Pitch) -> Result<ChromaticInterval> {
230 ChromaticInterval::new(p2.ps() - p1.ps())
231}
232
233fn specifier_from_generic_chromatic(
234 g_int: &GenericInterval,
235 c_int: &ChromaticInterval,
236) -> Result<Specifier> {
237 let note_vals: [IntegerType; 7] = [0, 2, 4, 5, 7, 9, 11];
238 let normal_semis = note_vals[(g_int.simple_undirected() - 1) as usize]
239 + 12 * g_int.simple_steps_and_octaves().1;
240
241 let c_direction = c_int.direction();
242
243 let these_semis = if g_int.direction() != c_direction
244 && g_int.direction() != direction::Direction::Oblique
245 && c_direction != direction::Direction::Oblique
246 {
247 -c_int.undirected()
248 } else if g_int.undirected() == 1 {
249 c_int.directed()
250 } else {
251 c_int.undirected()
252 };
253
254 let rounding_error = if c_int.undirected() > 0.0 {
255 0.0001
256 } else {
257 -0.0001
258 };
259 let diff = (these_semis + rounding_error).round() as IntegerType - normal_semis;
260
261 if g_int.is_perfectable() {
262 specifier_at(&PERFECTABLE_SPECIFIERS, 4 + diff, "Perfect", diff)
263 } else {
264 specifier_at(&MAJOR_SPECIFIERS, 5 + diff, "Major", diff)
265 }
266}
267
268const PERFECTABLE_SPECIFIERS: [Specifier; 9] = [
271 Specifier::QuadrupleDiminished,
272 Specifier::TripleDiminished,
273 Specifier::DoubleDiminished,
274 Specifier::Diminished,
275 Specifier::Perfect,
276 Specifier::Augmented,
277 Specifier::DoubleAugmented,
278 Specifier::TripleAugmented,
279 Specifier::QuadrupleAugmented,
280];
281
282const MAJOR_SPECIFIERS: [Specifier; 10] = [
285 Specifier::QuadrupleDiminished,
286 Specifier::TripleDiminished,
287 Specifier::DoubleDiminished,
288 Specifier::Diminished,
289 Specifier::Minor,
290 Specifier::Major,
291 Specifier::Augmented,
292 Specifier::DoubleAugmented,
293 Specifier::TripleAugmented,
294 Specifier::QuadrupleAugmented,
295];
296
297fn specifier_at(
307 table: &[Specifier],
308 index: IntegerType,
309 from: &str,
310 diff: IntegerType,
311) -> Result<Specifier> {
312 let length = table.len() as IntegerType;
313 let wrapped = if index < 0 { index + length } else { index };
314 if index >= length || wrapped < 0 {
315 return Err(Error::Interval(format!(
316 "cannot get a specifier for a note with this many semitones off of {from}: {diff}"
317 )));
318 }
319 Ok(table[wrapped as usize])
320}
321
322pub fn intervals_to_diatonic(
325 g_int: &GenericInterval,
326 c_int: &ChromaticInterval,
327) -> Result<DiatonicInterval> {
328 let specifier = specifier_from_generic_chromatic(g_int, c_int)?;
329 Ok(DiatonicInterval::new(specifier, g_int))
330}
331
332pub fn convert_semitone_to_specifier_generic(count: FloatType) -> (Specifier, IntegerType) {
336 let (specifier, generic, _) = convert_semitone_to_specifier_generic_microtone(count);
337 (specifier, generic)
338}
339
340fn semitones_to_specifier_generic(size: IntegerType) -> (Specifier, IntegerType) {
343 match size {
344 0 => (Specifier::Perfect, 1),
345 1 => (Specifier::Minor, 2),
346 2 => (Specifier::Major, 2),
347 3 => (Specifier::Minor, 3),
348 4 => (Specifier::Major, 3),
349 5 => (Specifier::Perfect, 4),
350 6 => (Specifier::Diminished, 5),
351 7 => (Specifier::Perfect, 5),
352 8 => (Specifier::Minor, 6),
353 9 => (Specifier::Major, 6),
354 10 => (Specifier::Minor, 7),
355 _ => (Specifier::Major, 7),
356 }
357}
358
359pub fn convert_semitone_to_specifier_generic_microtone(
364 count: FloatType,
365) -> (Specifier, IntegerType, FloatType) {
366 let dir_scale = if count < 0.0 { -1 } else { 1 };
367 let mut whole = count.floor();
368 let mut cents = (count - whole) * 100.0;
369 if cents > 50.0 {
370 cents -= 100.0;
371 whole += 1.0;
372 }
373 let whole = whole as IntegerType;
374 let size = whole.abs() % 12;
375 let octave = whole.abs() / 12;
376 let (specifier, generic) = semitones_to_specifier_generic(size);
377 (specifier, (generic + octave * 7) * dir_scale, cents)
378}
379
380pub fn convert_diatonic_number_to_step(dn: IntegerType) -> (char, IntegerType) {
384 const STEPS: [char; 7] = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
385 let zero_based = dn - 1;
386 let octave = zero_based.div_euclid(7);
387 let step = STEPS[zero_based.rem_euclid(7) as usize];
388 (step, octave)
389}
390
391pub fn parse_specifier(value: &str) -> Result<Specifier> {
395 Specifier::from_name(value)
396}
397
398impl Interval {
399 pub(crate) fn between(start: PitchOrNote, end: PitchOrNote) -> Result<Self> {
400 let start_pitch = extract_pitch(start);
401 let end_pitch = extract_pitch(end);
402 let generic = notes_to_generic(&start_pitch, &end_pitch)?;
403 let chromatic = notes_to_chromatic(&start_pitch, &end_pitch)?;
404 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
405
406 Ok(Self {
407 implicit_diatonic: false,
408 diatonic,
409 chromatic,
410 pitch_start: Some(start_pitch),
411 pitch_end: Some(end_pitch),
412 })
413 }
414
415 pub fn from_diatonic_and_chromatic(
418 diatonic: DiatonicInterval,
419 chromatic: ChromaticInterval,
420 ) -> Result<Interval> {
421 Ok(Self {
422 implicit_diatonic: false,
423 diatonic,
424 chromatic,
425 pitch_start: None,
426 pitch_end: None,
427 })
428 }
429
430 pub fn from_diatonic(diatonic: DiatonicInterval) -> Result<Self> {
433 let chromatic = diatonic.get_chromatic()?;
434 Self::from_diatonic_and_chromatic(diatonic, chromatic)
435 }
436
437 pub fn from_chromatic(chromatic: ChromaticInterval) -> Result<Self> {
441 let diatonic = chromatic.get_diatonic();
442 let mut interval = Self::from_diatonic_and_chromatic(diatonic, chromatic)?;
443 interval.implicit_diatonic = true;
444 Ok(interval)
445 }
446
447 pub fn generic(&self) -> &GenericInterval {
449 &self.diatonic.generic
450 }
451
452 pub fn diatonic(&self) -> &DiatonicInterval {
454 &self.diatonic
455 }
456
457 pub fn chromatic(&self) -> &ChromaticInterval {
459 &self.chromatic
460 }
461
462 pub fn specifier(&self) -> Specifier {
464 self.diatonic.specifier
465 }
466
467 pub fn from_generic_and_chromatic(
472 generic: IntegerType,
473 semitones: IntegerType,
474 ) -> Result<Self> {
475 let generic = GenericInterval::from_int(generic)?;
476 let chromatic = ChromaticInterval::from_int(semitones);
477 let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
478 Self::from_diatonic_and_chromatic(diatonic, chromatic)
479 }
480
481 pub fn pitch_start(&self) -> Option<&Pitch> {
484 self.pitch_start.as_ref()
485 }
486
487 pub fn pitch_end(&self) -> Option<&Pitch> {
490 self.pitch_end.as_ref()
491 }
492
493 pub fn note_start(&self) -> Option<Note> {
495 self.pitch_start.clone().map(Note::from_pitch)
496 }
497
498 pub fn note_end(&self) -> Option<Note> {
500 self.pitch_end.clone().map(Note::from_pitch)
501 }
502
503 pub fn from_name(name: impl Into<String>) -> Result<Self> {
505 let (diatonic, chromatic, inferred) = parse_interval_name(name.into())?;
506 Ok(Self {
507 implicit_diatonic: inferred,
508 diatonic,
509 chromatic,
510 pitch_start: None,
511 pitch_end: None,
512 })
513 }
514
515 pub fn from_semitones(semitones: IntegerType) -> Result<Self> {
517 let chromatic = ChromaticInterval::from_int(semitones);
518 let diatonic = chromatic.get_diatonic();
519 Ok(Self {
520 implicit_diatonic: true,
521 diatonic,
522 chromatic,
523 pitch_start: None,
524 pitch_end: None,
525 })
526 }
527
528 pub fn between_pitches(start: &Pitch, end: &Pitch) -> Result<Self> {
530 Self::between(
531 PitchOrNote::Pitch(start.clone()),
532 PitchOrNote::Pitch(end.clone()),
533 )
534 }
535
536 pub fn between_notes(start: &Note, end: &Note) -> Result<Self> {
538 Self::between(
539 PitchOrNote::Note(start.clone()),
540 PitchOrNote::Note(end.clone()),
541 )
542 }
543
544 pub fn semitones(&self) -> FloatType {
547 self.chromatic.semitones
548 }
549
550 pub fn whole_semitones(&self) -> IntegerType {
552 self.chromatic.whole_semitones()
553 }
554
555 pub fn direction(&self) -> IntervalDirection {
557 self.chromatic.direction()
558 }
559
560 pub fn name(&self) -> String {
562 self.nice_name()
563 }
564
565 pub fn generic_number(&self) -> IntegerType {
567 self.generic().simple_directed()
568 }
569
570 pub fn is_implicit_diatonic(&self) -> bool {
572 self.implicit_diatonic
573 }
574
575 pub fn inversion(&self) -> Result<Self> {
577 let direction = match self.direction() {
578 IntervalDirection::Oblique => 1,
579 direction => direction.as_int(),
580 };
581 let simple = self.generic().simple_undirected();
582 let inverted_generic = if simple == 1 { 1 } else { 9 - simple };
583 let generic = GenericInterval::from_int(inverted_generic * direction)?;
584 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
585 let chromatic = diatonic.get_chromatic()?;
586 Self::from_diatonic_and_chromatic(diatonic, chromatic)
587 }
588
589 pub fn reversed(&self) -> Result<Self> {
591 self.reverse()
592 }
593
594 pub fn pythagorean_ratio(&self) -> Result<FractionType> {
599 interval_to_pythagorean_ratio(self)
600 }
601
602 pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
604 self.transpose_pitch_with_options(pitch, false, Some(4))
605 }
606
607 pub fn transpose_note(&self, note: &Note) -> Result<Note> {
609 let mut out = note.clone();
610 out.pitch = self.transpose_pitch(¬e.pitch)?;
611 Ok(out)
612 }
613
614 pub fn short_name(&self) -> String {
616 format!(
617 "{}{}",
618 self.diatonic.specifier.prefix(),
619 self.generic().undirected()
620 )
621 }
622
623 pub fn simple_name(&self) -> String {
625 format!(
626 "{}{}",
627 self.diatonic.specifier.prefix(),
628 self.generic().simple_undirected()
629 )
630 }
631
632 pub fn semi_simple_name(&self) -> String {
635 format!(
636 "{}{}",
637 self.diatonic.specifier.prefix(),
638 self.generic().semi_simple_undirected()
639 )
640 }
641
642 pub fn directed_name(&self) -> String {
644 format!(
645 "{}{}",
646 self.diatonic.specifier.prefix(),
647 self.generic().directed()
648 )
649 }
650
651 pub fn is_diatonic_step(&self) -> bool {
653 self.generic().undirected() == 2
654 }
655
656 pub fn is_chromatic_step(&self) -> bool {
658 self.chromatic.undirected() == 1.0
659 }
660
661 pub fn is_step(&self) -> bool {
663 self.is_chromatic_step() || self.is_diatonic_step()
664 }
665
666 pub fn is_skip(&self) -> bool {
669 self.generic().undirected() > 2
670 }
671
672 pub fn is_consonant(&self) -> bool {
675 matches!(
676 (self.diatonic.specifier, self.generic().simple_undirected()),
677 (Specifier::Perfect, 1 | 5) | (Specifier::Major | Specifier::Minor, 3 | 6)
678 )
679 }
680
681 pub fn complement(&self) -> Result<Self> {
684 let generic = GenericInterval::from_int(9 - self.generic().semi_simple_undirected())?;
685 let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
686 let chromatic = diatonic.get_chromatic()?;
687 Self::from_diatonic_and_chromatic(diatonic, chromatic)
688 }
689
690 pub fn interval_class(&self) -> IntegerType {
693 self.chromatic.interval_class()
694 }
695
696 pub fn sum<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
701 let start = Pitch::from_name("C4")?;
702 let mut end = start.clone();
703 let mut any = false;
704 for interval in intervals {
705 end = interval.transpose_pitch(&end)?;
706 any = true;
707 }
708 if !any {
709 return Err(Error::Interval(
710 "cannot add an empty set of intervals".to_string(),
711 ));
712 }
713 Self::between_pitches(&start, &end)
714 }
715
716 pub fn difference<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
719 let start = Pitch::from_name("C4")?;
720 let mut intervals = intervals.into_iter();
721 let Some(first) = intervals.next() else {
722 return Err(Error::Interval(
723 "cannot subtract an empty set of intervals".to_string(),
724 ));
725 };
726 let mut end = first.transpose_pitch(&start)?;
727 for interval in intervals {
728 end = interval.reversed()?.transpose_pitch(&end)?;
729 }
730 Self::between_pitches(&start, &end)
731 }
732
733 pub fn directed_simple_name(&self) -> String {
736 format!(
737 "{}{}",
738 self.diatonic.specifier.prefix(),
739 self.generic().simple_directed()
740 )
741 }
742
743 pub(crate) fn directed_simple_key(&self) -> (Specifier, IntegerType) {
744 (self.diatonic.specifier, self.generic().simple_directed())
745 }
746
747 pub(crate) fn simple_key(&self) -> (Specifier, IntegerType) {
748 (self.diatonic.specifier, self.generic().simple_undirected())
749 }
750
751 pub(crate) fn semi_simple_key(&self) -> (Specifier, IntegerType) {
752 (
753 self.diatonic.specifier,
754 self.generic().semi_simple_undirected(),
755 )
756 }
757
758 pub(crate) fn is_perfect_unison(&self) -> bool {
759 self.generic().undirected() == 1 && self.chromatic.semitones == 0.0
760 }
761
762 pub(crate) fn nice_name(&self) -> String {
763 self.diatonic.nice_name()
764 }
765
766 pub fn semi_simple_nice_name(&self) -> String {
770 self.diatonic.semi_simple_nice_name()
771 }
772
773 pub fn simple_nice_name(&self) -> String {
777 format!(
778 "{} {}",
779 self.diatonic.specifier.nice_name(),
780 self.generic().simple_nice_name()
781 )
782 }
783
784 fn diatonic_direction(&self) -> Direction {
789 self.diatonic.direction()
790 }
791
792 fn directed(&self, name: String) -> String {
793 format!("{} {name}", self.diatonic_direction().name())
794 }
795
796 pub fn directed_nice_name(&self) -> String {
799 self.directed(self.nice_name())
800 }
801
802 pub fn directed_simple_nice_name(&self) -> String {
805 self.directed(self.simple_nice_name())
806 }
807
808 pub fn directed_semi_simple_nice_name(&self) -> String {
811 self.directed(self.semi_simple_nice_name())
812 }
813
814 pub fn specific_name(&self) -> String {
817 self.diatonic.specifier.nice_name()
818 }
819
820 pub fn cents(&self) -> FloatType {
822 self.chromatic.cents()
823 }
824
825 pub fn diatonic_interval_cent_shift(&self) -> FloatType {
829 let diatonic_cents = self.diatonic.cents().unwrap_or(0.0);
830 self.chromatic.cents() - diatonic_cents
831 }
832
833 pub fn is_unison(&self) -> bool {
835 self.generic().is_unison()
836 }
837
838 pub fn is_perfectable(&self) -> bool {
841 self.generic().is_perfectable()
842 }
843
844 pub fn staff_distance(&self) -> IntegerType {
847 self.generic().staff_distance()
848 }
849
850 pub fn mod7(&self) -> IntegerType {
853 self.generic().mod7()
854 }
855
856 pub fn mod7_inversion(&self) -> IntegerType {
859 self.generic().mod7_inversion()
860 }
861
862 pub fn mod12(&self) -> IntegerType {
865 self.chromatic.mod12()
866 }
867
868 pub fn transpose_pitch_with_options(
874 &self,
875 p: &Pitch,
876 reverse: bool,
877 max_accidental: Option<IntegerType>,
878 ) -> Result<Pitch> {
879 if reverse {
880 return self
881 .reverse()?
882 .transpose_pitch_with_options(p, false, max_accidental);
883 }
884 if self.implicit_diatonic {
885 return self.chromatic.transpose_pitch(p);
886 }
887
888 let use_implicit_octave = p.octave().is_none();
889 let whole_semitones = self.chromatic.semitones == self.chromatic.semitones.trunc();
892 let inherit_accidental_display = self.diatonic.simple_name() == "P1" && whole_semitones;
893 let cents_origin = if p.is_twelve_tone() || !whole_semitones {
899 0.0
900 } else {
901 p.microtone().map_or(0.0, crate::pitch::Microtone::cents)
902 };
903 let new_dnn = p.diatonic_note_number() + self.diatonic.generic.staff_distance();
904 let (new_step, new_octave) = convert_diatonic_number_to_step(new_dnn);
905 let mut pitch2 = crate::pitch::PitchOptions::new()
906 .step(new_step)
907 .octave(new_octave)
908 .build()?;
909 let origin_ps = p.ps() - cents_origin / 100.0;
910 let mut half_steps_to_fix = self.chromatic.semitones - (pitch2.ps() - origin_ps);
911 while half_steps_to_fix >= 12.0 {
912 half_steps_to_fix -= 12.0;
913 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) - 1));
914 }
915 while half_steps_to_fix <= -12.0 {
916 half_steps_to_fix += 12.0;
917 pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) + 1));
918 }
919 if half_steps_to_fix != 0.0 {
920 if max_accidental.is_some_and(|limit| half_steps_to_fix.abs() > limit as FloatType) {
921 pitch2.set_ps(pitch2.ps() + half_steps_to_fix);
922 } else {
923 pitch2.set_accidental_alter(half_steps_to_fix)?;
924 }
925 match (
926 inherit_accidental_display,
927 pitch2.has_accidental(),
928 p.explicit_accidental(),
929 ) {
930 (false, true, Some(source)) => {
931 if let Some(target) = pitch2.explicit_accidental_mut() {
932 target.inherit_display(source);
933 target.set_display_status(None);
934 }
935 }
936 (true, false, Some(source)) => {
937 let mut natural = crate::pitch::Accidental::natural();
938 natural.inherit_display(source);
939 pitch2.set_accidental(Some(natural));
940 }
941 (true, true, Some(source)) => {
942 if let Some(target) = pitch2.explicit_accidental_mut() {
943 target.inherit_display(source);
944 }
945 }
946 (true, true, None) => {
947 if let Some(target) = pitch2.explicit_accidental_mut() {
948 target.set_display_status(Some(false));
949 }
950 }
951 _ => {}
952 }
953 } else if inherit_accidental_display
954 && p.explicit_accidental()
955 .is_some_and(|accidental| accidental.name() == "natural")
956 {
957 pitch2.set_accidental(p.explicit_accidental().cloned());
958 }
959 if cents_origin != 0.0 {
960 let cents = pitch2
961 .microtone()
962 .map_or(0.0, crate::pitch::Microtone::cents);
963 pitch2.set_microtone_cents(cents + cents_origin)?;
964 }
965 if use_implicit_octave {
966 pitch2.octave_setter(None);
967 }
968 Ok(pitch2)
969 }
970
971 pub fn transpose_pitch_in_place(&self, pitch: &mut Pitch) -> Result<()> {
973 *pitch = self.transpose_pitch(pitch)?;
974 Ok(())
975 }
976}
977
978impl fmt::Display for Interval {
979 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
980 let shift = self.diatonic_interval_cent_shift();
981 if shift == 0.0 {
982 write!(f, "{}", self.directed_name())
983 } else {
984 write!(
985 f,
986 "{} {}",
987 self.directed_name(),
988 crate::pitch::Microtone::from_cents(shift, 1)
989 )
990 }
991 }
992}
993
994impl FromStr for Interval {
995 type Err = Error;
996
997 fn from_str(value: &str) -> Result<Self> {
998 Self::from_name(value)
999 }
1000}
1001
1002impl TryFrom<&str> for Interval {
1003 type Error = Error;
1004
1005 fn try_from(value: &str) -> Result<Self> {
1006 Self::from_name(value)
1007 }
1008}
1009
1010impl TryFrom<String> for Interval {
1011 type Error = Error;
1012
1013 fn try_from(value: String) -> Result<Self> {
1014 Self::from_name(value)
1015 }
1016}
1017
1018impl TryFrom<IntegerType> for Interval {
1019 type Error = Error;
1020
1021 fn try_from(value: IntegerType) -> Result<Self> {
1022 Self::from_semitones(value)
1023 }
1024}
1025
1026fn parse_interval_name(mut value: String) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1027 let mut inferred = false;
1028 let mut dir_scale = 1;
1029
1030 if value.contains('-') {
1032 value = value.replace('-', "");
1033 dir_scale = -1;
1034 }
1035 {
1037 let (without_descending, found_descending) = strip_direction_word(&value, "descending");
1038 if found_descending {
1039 value = without_descending;
1040 dir_scale = -1;
1041 } else {
1042 let (without_ascending, found_ascending) = strip_direction_word(&value, "ascending");
1043 if found_ascending {
1044 value = without_ascending;
1045 }
1046 }
1047 }
1048 let value_lower = value.to_lowercase();
1049
1050 if value_lower == "w" || value_lower == "whole" || value_lower == "tone" {
1052 value = "M2".to_string();
1053 inferred = true;
1054 } else if value_lower == "h" || value_lower == "half" || value_lower == "semitone" {
1055 value = "m2".to_string();
1056 inferred = true;
1057 }
1058
1059 let mut lowered = value.to_ascii_lowercase();
1064 for (i, ordinal) in MUSICAL_ORDINAL_STRINGS_LOWER.iter().enumerate() {
1065 if !lowered.contains(ordinal.as_str()) {
1066 continue;
1067 }
1068 let replacement = i.to_string();
1069 let (next_value, replaced) =
1070 replace_music_ordinal(&value, &MUSICAL_ORDINAL_STRINGS[i], &replacement);
1071 if replaced {
1072 value = next_value;
1073 lowered = value.to_ascii_lowercase();
1074 }
1075 }
1076
1077 let (found, remain) = get_num_from_str(&value, "0123456789");
1079 let generic_number: IntegerType = found
1080 .parse::<IntegerType>()
1081 .map_err(|_| Error::Interval(format!("cannot read an interval number from {value:?}")))?
1082 * dir_scale;
1083 let spec = Specifier::parse(&remain)?;
1084
1085 let g_interval = GenericInterval::from_int(generic_number)?;
1086 let d_interval = g_interval.get_diatonic(spec);
1087 let c_interval = d_interval.get_chromatic()?;
1088 Ok((d_interval, c_interval, inferred))
1089}
1090
1091impl Interval {
1092 fn reverse(&self) -> Result<Self> {
1093 if let (Some(start), Some(end)) = (&self.pitch_start, &self.pitch_end) {
1094 Interval::between(
1095 PitchOrNote::Pitch(end.clone()),
1096 PitchOrNote::Pitch(start.clone()),
1097 )
1098 } else {
1099 Interval::from_diatonic_and_chromatic(self.diatonic.reverse(), self.chromatic.reverse())
1100 }
1101 }
1102}
1103
1104pub(crate) fn interval_to_pythagorean_ratio(interval: &Interval) -> Result<FractionType> {
1105 let start_pitch = Pitch::from_name("C1")?;
1106
1107 let end_pitch_wanted = interval.transpose_pitch_with_options(&start_pitch, false, Some(4))?;
1108
1109 let wanted_name = end_pitch_wanted.name();
1110
1111 let mut end_pitch_up = start_pitch.clone();
1112 let mut end_pitch_down = start_pitch.clone();
1113 let mut found: Option<(Pitch, FractionType)> = None;
1114 let fifth_up: &Interval = &PERFECT_FIFTH_UP;
1115 let fifth_down: &Interval = &PERFECT_FIFTH_DOWN;
1116
1117 for counter in 0..37 {
1118 if end_pitch_up.name() == wanted_name {
1119 if counter > 18 {
1120 return Err(Error::Interval(format!(
1121 "pythagorean ratio for {wanted_name} exceeds integer range"
1122 )));
1123 }
1124 found = Some((
1125 end_pitch_up.clone(),
1126 FractionType::new(3i32, 2i32).powi(counter),
1127 ));
1128 break;
1129 } else if end_pitch_down.name() == wanted_name {
1130 if counter > 18 {
1131 return Err(Error::Interval(format!(
1132 "pythagorean ratio for {wanted_name} exceeds integer range"
1133 )));
1134 }
1135 found = Some((
1136 end_pitch_down.clone(),
1137 FractionType::new(2i32, 3i32).powi(counter),
1138 ));
1139 break;
1140 } else {
1141 end_pitch_up = fifth_up.transpose_pitch_with_options(&end_pitch_up, false, Some(4))?;
1142 end_pitch_down =
1143 fifth_down.transpose_pitch_with_options(&end_pitch_down, false, Some(4))?;
1144 }
1145 }
1146
1147 let (found_pitch, found_ratio) = match found {
1148 Some(val) => val,
1149 None => {
1150 return Err(Error::Interval(format!(
1151 "Could not find a pythagorean ratio for {interval:?}"
1152 )));
1153 }
1154 };
1155
1156 let octaves = (end_pitch_wanted.ps() - found_pitch.ps()) / 12.0;
1157 let octave_multiplier = FractionType::new(2i32, 1i32).powi(octaves as IntegerType);
1158
1159 Ok(found_ratio * octave_multiplier)
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 #[test]
1165 fn an_interval_is_read_from_a_name_a_number_or_one_of_its_halves() {
1166 use super::{ChromaticInterval, DiatonicInterval, Interval, Specifier, parse_specifier};
1167 use crate::note::Note;
1168 use crate::pitch::Pitch;
1169 use std::str::FromStr;
1170
1171 assert_eq!(Interval::from_str("M3").unwrap().short_name(), "M3");
1172 assert_eq!(Interval::try_from("P5").unwrap().short_name(), "P5");
1173 assert_eq!(
1174 Interval::try_from("m6".to_string()).unwrap().short_name(),
1175 "m6"
1176 );
1177 assert_eq!(Interval::try_from(7).unwrap().short_name(), "P5");
1178 assert!(Interval::from_str("Q9").is_err());
1179 assert_eq!(parse_specifier("M").unwrap(), Specifier::Major);
1180 assert!(parse_specifier("Q").is_err());
1181
1182 let diatonic = Interval::from_diatonic(DiatonicInterval::from_name("M3").unwrap()).unwrap();
1183 assert_eq!(diatonic.semitones(), 4.0);
1184 assert!(!diatonic.is_implicit_diatonic());
1185 assert_eq!(diatonic.diatonic().name(), "M3");
1186 assert_eq!(diatonic.specifier(), Specifier::Major);
1187 let chromatic = Interval::from_chromatic(ChromaticInterval::from_int(6)).unwrap();
1188 assert!(chromatic.is_implicit_diatonic());
1189 assert_eq!(chromatic.whole_semitones(), 6);
1190 assert_eq!(chromatic.directed_simple_name(), "d5");
1191 assert_eq!(
1192 Interval::from_str("M-10").unwrap().directed_simple_name(),
1193 "M-3"
1194 );
1195 assert_eq!(
1196 Interval::from_str("M3").unwrap(),
1197 Interval::from_str("M3").unwrap()
1198 );
1199 assert_ne!(
1200 Interval::from_str("M3").unwrap(),
1201 Interval::from_str("m3").unwrap()
1202 );
1203
1204 let c = Note::from_pitch(Pitch::from_name("C4").unwrap());
1205 let g = Note::from_pitch(Pitch::from_name("G4").unwrap());
1206 let fifth = Interval::between_notes(&c, &g).unwrap();
1207 assert_eq!(fifth.short_name(), "P5");
1208 assert_eq!(
1209 fifth.transpose_note(&g).unwrap().pitch_name_with_octave(),
1210 "D5"
1211 );
1212 }
1213
1214 #[test]
1215 fn a_note_flatter_than_the_table_wraps_the_way_music21_does() {
1216 let interval = Interval::between_pitches(
1220 &Pitch::from_name("A##3").unwrap(),
1221 &Pitch::from_name("E---4").unwrap(),
1222 )
1223 .unwrap();
1224 assert_eq!(interval.short_name(), "AAAA5");
1225 assert_eq!(interval.chromatic().semitones(), 2.0);
1226
1227 let widest = Interval::between_pitches(
1230 &Pitch::from_name("C4").unwrap(),
1231 &Pitch::from_name("E####4").unwrap(),
1232 )
1233 .unwrap();
1234 assert_eq!(widest.short_name(), "AAAA3");
1235 }
1236
1237 #[test]
1238 fn generic_and_chromatic_build_the_interval_music21_does() {
1239 let cases: [(i32, i32, &str, &str); 14] = [
1240 (3, 4, "M3", "M3"),
1241 (3, 3, "m3", "m3"),
1242 (5, 7, "P5", "P5"),
1243 (5, 6, "d5", "d5"),
1244 (4, 6, "A4", "A4"),
1245 (1, 0, "P1", "P1"),
1246 (1, 1, "A1", "A1"),
1247 (8, 12, "P8", "P8"),
1248 (-3, -4, "M3", "M-3"),
1249 (2, 3, "A2", "A2"),
1250 (7, 10, "m7", "m7"),
1251 (-2, -1, "m2", "m-2"),
1252 (9, 13, "m9", "m9"),
1253 (3, 2, "d3", "d3"),
1254 ];
1255 for (generic, semitones, name, directed) in cases {
1256 let interval = Interval::from_generic_and_chromatic(generic, semitones).unwrap();
1257 assert_eq!(interval.short_name(), name, "{generic} {semitones}");
1258 assert_eq!(interval.directed_name(), directed, "{generic} {semitones}");
1259 assert_eq!(
1260 interval.semitones(),
1261 FloatType::from(semitones),
1262 "{generic} {semitones}"
1263 );
1264 assert!(interval.pitch_start().is_none());
1265 }
1266 assert!(Interval::from_generic_and_chromatic(0, 0).is_err());
1267 assert_eq!(
1268 [-3, -1, 0, 1, 2, 7].map(staff_distance_to_generic_number),
1269 [-4, -2, 1, 2, 3, 8]
1270 );
1271 }
1272
1273 #[test]
1274 fn written_and_sounding_order_match_music21() {
1275 let cases: [(&str, &str, [&str; 4]); 8] = [
1276 ("C4", "E4", ["E4", "C4", "E4", "C4"]),
1277 ("E4", "C4", ["E4", "C4", "E4", "C4"]),
1278 ("B#3", "C4", ["C4", "B#3", "B#3", "B#3"]),
1279 ("C4", "B#3", ["C4", "B#3", "C4", "C4"]),
1280 ("C-4", "B3", ["C-4", "B3", "C-4", "C-4"]),
1281 ("F#4", "G-4", ["G-4", "F#4", "F#4", "F#4"]),
1282 ("C4", "C4", ["C4", "C4", "C4", "C4"]),
1283 ("B3", "C-4", ["C-4", "B3", "B3", "B3"]),
1284 ];
1285 for (first, second, expected) in cases {
1286 let a = Pitch::from_name(first).unwrap();
1287 let b = Pitch::from_name(second).unwrap();
1288 let actual = [
1289 written_higher_pitch(&a, &b),
1290 written_lower_pitch(&a, &b),
1291 absolute_higher_pitch(&a, &b),
1292 absolute_lower_pitch(&a, &b),
1293 ]
1294 .map(Pitch::name_with_octave);
1295 assert_eq!(actual, expected, "{first} {second}");
1296 }
1297 }
1298
1299 #[test]
1300 fn intervals_remember_the_pitches_they_were_measured_between() {
1301 let c = Pitch::from_name("C4").unwrap();
1302 let g = Pitch::from_name("G4").unwrap();
1303 let fifth = Interval::between_pitches(&c, &g).unwrap();
1304 assert_eq!(fifth.pitch_start().unwrap().name_with_octave(), "C4");
1305 assert_eq!(fifth.pitch_end().unwrap().name_with_octave(), "G4");
1306 assert_eq!(fifth.note_start().unwrap().pitch_name_with_octave(), "C4");
1307 assert_eq!(fifth.note_end().unwrap().pitch_name_with_octave(), "G4");
1308 let named = Interval::from_name("P5").unwrap();
1309 assert!(named.pitch_start().is_none());
1310 assert!(named.note_end().is_none());
1311 }
1312
1313 #[test]
1314 fn nice_name_variants_match_music21() {
1315 let cases: [(&str, [&str; 7]); 15] = [
1316 (
1317 "P1",
1318 [
1319 "Perfect Unison",
1320 "Oblique Perfect Unison",
1321 "Perfect Unison",
1322 "Perfect Unison",
1323 "Oblique Perfect Unison",
1324 "Oblique Perfect Unison",
1325 "Perfect",
1326 ],
1327 ),
1328 (
1329 "m2",
1330 [
1331 "Minor Second",
1332 "Ascending Minor Second",
1333 "Minor Second",
1334 "Minor Second",
1335 "Ascending Minor Second",
1336 "Ascending Minor Second",
1337 "Minor",
1338 ],
1339 ),
1340 (
1341 "P8",
1342 [
1343 "Perfect Octave",
1344 "Ascending Perfect Octave",
1345 "Perfect Unison",
1346 "Perfect Octave",
1347 "Ascending Perfect Unison",
1348 "Ascending Perfect Octave",
1349 "Perfect",
1350 ],
1351 ),
1352 (
1353 "m9",
1354 [
1355 "Minor Ninth",
1356 "Ascending Minor Ninth",
1357 "Minor Second",
1358 "Minor Second",
1359 "Ascending Minor Second",
1360 "Ascending Minor Second",
1361 "Minor",
1362 ],
1363 ),
1364 (
1365 "M10",
1366 [
1367 "Major Tenth",
1368 "Ascending Major Tenth",
1369 "Major Third",
1370 "Major Third",
1371 "Ascending Major Third",
1372 "Ascending Major Third",
1373 "Major",
1374 ],
1375 ),
1376 (
1377 "P12",
1378 [
1379 "Perfect Twelfth",
1380 "Ascending Perfect Twelfth",
1381 "Perfect Fifth",
1382 "Perfect Fifth",
1383 "Ascending Perfect Fifth",
1384 "Ascending Perfect Fifth",
1385 "Perfect",
1386 ],
1387 ),
1388 (
1389 "-M3",
1390 [
1391 "Major Third",
1392 "Descending Major Third",
1393 "Major Third",
1394 "Major Third",
1395 "Descending Major Third",
1396 "Descending Major Third",
1397 "Major",
1398 ],
1399 ),
1400 (
1401 "-m9",
1402 [
1403 "Minor Ninth",
1404 "Descending Minor Ninth",
1405 "Minor Second",
1406 "Minor Second",
1407 "Descending Minor Second",
1408 "Descending Minor Second",
1409 "Minor",
1410 ],
1411 ),
1412 (
1413 "dd5",
1414 [
1415 "Doubly-Diminished Fifth",
1416 "Ascending Doubly-Diminished Fifth",
1417 "Doubly-Diminished Fifth",
1418 "Doubly-Diminished Fifth",
1419 "Ascending Doubly-Diminished Fifth",
1420 "Ascending Doubly-Diminished Fifth",
1421 "Doubly-Diminished",
1422 ],
1423 ),
1424 (
1425 "AA4",
1426 [
1427 "Doubly-Augmented Fourth",
1428 "Ascending Doubly-Augmented Fourth",
1429 "Doubly-Augmented Fourth",
1430 "Doubly-Augmented Fourth",
1431 "Ascending Doubly-Augmented Fourth",
1432 "Ascending Doubly-Augmented Fourth",
1433 "Doubly-Augmented",
1434 ],
1435 ),
1436 (
1437 "P15",
1438 [
1439 "Perfect Double-octave",
1440 "Ascending Perfect Double-octave",
1441 "Perfect Unison",
1442 "Perfect Octave",
1443 "Ascending Perfect Unison",
1444 "Ascending Perfect Octave",
1445 "Perfect",
1446 ],
1447 ),
1448 (
1449 "d1",
1450 [
1451 "Diminished Unison",
1452 "Descending Diminished Unison",
1453 "Diminished Unison",
1454 "Diminished Unison",
1455 "Descending Diminished Unison",
1456 "Descending Diminished Unison",
1457 "Diminished",
1458 ],
1459 ),
1460 (
1461 "A1",
1462 [
1463 "Augmented Unison",
1464 "Ascending Augmented Unison",
1465 "Augmented Unison",
1466 "Augmented Unison",
1467 "Ascending Augmented Unison",
1468 "Ascending Augmented Unison",
1469 "Augmented",
1470 ],
1471 ),
1472 (
1473 "-A1",
1474 [
1475 "Augmented Unison",
1476 "Ascending Augmented Unison",
1477 "Augmented Unison",
1478 "Augmented Unison",
1479 "Ascending Augmented Unison",
1480 "Ascending Augmented Unison",
1481 "Augmented",
1482 ],
1483 ),
1484 (
1485 "P-8",
1486 [
1487 "Perfect Octave",
1488 "Descending Perfect Octave",
1489 "Perfect Unison",
1490 "Perfect Octave",
1491 "Descending Perfect Unison",
1492 "Descending Perfect Octave",
1493 "Perfect",
1494 ],
1495 ),
1496 ];
1497 for (name, expected) in cases {
1498 let interval = Interval::from_name(name).unwrap();
1499 let actual = [
1500 interval.name(),
1501 interval.directed_nice_name(),
1502 interval.simple_nice_name(),
1503 interval.semi_simple_nice_name(),
1504 interval.directed_simple_nice_name(),
1505 interval.directed_semi_simple_nice_name(),
1506 interval.specific_name(),
1507 ];
1508 assert_eq!(actual, expected, "{name}");
1509 }
1510 }
1511
1512 #[test]
1513 #[allow(clippy::type_complexity)]
1514 fn generic_and_chromatic_helpers_match_music21() {
1515 let cases: [(&str, f64, bool, bool, i32, i32, i32, i32); 14] = [
1516 ("P1", 0.0, true, true, 0, 1, 8, 0),
1517 ("m2", 100.0, false, false, 1, 2, 7, 1),
1518 ("P4", 500.0, false, true, 3, 4, 5, 5),
1519 ("P5", 700.0, false, true, 4, 5, 4, 7),
1520 ("M7", 1100.0, false, false, 6, 7, 2, 11),
1521 ("P8", 1200.0, false, true, 7, 1, 1, 0),
1522 ("m9", 1300.0, false, false, 8, 2, 7, 1),
1523 ("-M3", -400.0, false, false, -2, 6, 6, 8),
1524 ("-P5", -700.0, false, true, -4, 4, 4, 5),
1525 ("-m9", -1300.0, false, false, -8, 7, 7, 11),
1526 ("P15", 2400.0, false, true, 14, 1, 1, 0),
1527 ("d1", -100.0, true, true, 0, 1, 8, 11),
1528 ("-A1", -100.0, true, true, 0, 8, 8, 11),
1529 ("P-8", -1200.0, false, true, -7, 1, 1, 0),
1530 ];
1531 for (name, cents, unison, perfectable, staff, mod7, mod7_inversion, mod12) in cases {
1532 let interval = Interval::from_name(name).unwrap();
1533 assert_eq!(interval.cents(), cents, "{name} cents");
1534 assert_eq!(interval.is_unison(), unison, "{name} unison");
1535 assert_eq!(interval.is_perfectable(), perfectable, "{name} perfectable");
1536 assert_eq!(interval.staff_distance(), staff, "{name} staff distance");
1537 assert_eq!(interval.mod7(), mod7, "{name} mod7");
1538 assert_eq!(
1539 interval.mod7_inversion(),
1540 mod7_inversion,
1541 "{name} mod7 inversion"
1542 );
1543 assert_eq!(interval.mod12(), mod12, "{name} mod12");
1544 }
1545 }
1546 use super::*;
1547
1548 fn pitch(name: &str) -> Pitch {
1549 Pitch::from_name(name).expect("valid pitch")
1550 }
1551
1552 #[test]
1553 fn malformed_interval_names_error_instead_of_panicking() {
1554 for bad in ["", "X", "perfect", "?!", "MM"] {
1558 assert!(
1559 Interval::from_name(bad).is_err(),
1560 "Interval::from_name({bad:?}) should be an error"
1561 );
1562 }
1563 }
1564
1565 #[test]
1566 fn interval_between_microtonal_pitches_keeps_the_cent_shift() {
1567 let c1 = pitch("C1");
1568 let mut half_sharp = pitch("C1");
1569 half_sharp.set_accidental(Some(
1570 crate::pitch::Accidental::new("half-sharp").expect("half-sharp is an accidental"),
1571 ));
1572
1573 let quarter_tone = Interval::between_pitches(&c1, &half_sharp).unwrap();
1574 assert_eq!(quarter_tone.semitones(), 0.5);
1575 assert_eq!(quarter_tone.cents(), 50.0);
1576 assert_eq!(quarter_tone.directed_name(), "A1");
1577 assert_eq!(quarter_tone.diatonic_interval_cent_shift(), -50.0);
1578 assert_eq!(quarter_tone.to_string(), "A1 (-50c)");
1579 assert!(quarter_tone.pythagorean_ratio().is_err());
1580 }
1581
1582 #[test]
1583 fn interval_cents_carry_a_pitch_microtone() {
1584 let c4 = pitch("C4");
1585 let mut d4 = pitch("D4");
1586 d4.set_microtone_cents(30.0).unwrap();
1587
1588 let interval = Interval::between_pitches(&c4, &d4).unwrap();
1589 assert_eq!(interval.cents(), 230.0);
1590 assert_eq!(interval.to_string(), "M2 (+30c)");
1591 }
1592
1593 #[test]
1594 fn interval_from_string_has_expected_chromatic() {
1595 let interval = Interval::from_name("M3").unwrap();
1596 assert_eq!(interval.chromatic.semitones, 4.0);
1597 assert!(!interval.implicit_diatonic);
1598 }
1599
1600 #[test]
1601 fn interval_parser_accepts_direction_words_and_ordinals() {
1602 let descending = Interval::from_name("Descending Perfect Twelfth").unwrap();
1603 assert_eq!(descending.semitones(), -19.0);
1604 assert_eq!(descending.generic_number(), -5);
1605
1606 let ascending = Interval::from_name("ascending Major Second").unwrap();
1607 assert_eq!(ascending.semitones(), 2.0);
1608 assert_eq!(ascending.generic_number(), 2);
1609
1610 let major_third = Interval::from_name("Major Third").unwrap();
1611 assert_eq!(major_third.semitones(), 4.0);
1612 assert_eq!(major_third.generic_number(), 3);
1613 }
1614
1615 #[test]
1616 fn interval_from_int_is_implicit_diatonic() {
1617 let interval = Interval::from_semitones(1).unwrap();
1618 assert!(interval.implicit_diatonic);
1619 assert_eq!(interval.chromatic.semitones, 1.0);
1620 }
1621
1622 #[test]
1623 fn interval_between_pitches() {
1624 let c4 = pitch("C4");
1625 let g4 = pitch("G4");
1626 let interval = Interval::between(PitchOrNote::Pitch(c4), PitchOrNote::Pitch(g4)).unwrap();
1627 assert_eq!(interval.chromatic.semitones, 7.0);
1628 assert_eq!(interval.generic().staff_distance(), 4);
1629 }
1630
1631 #[test]
1632 fn interval_transpose_pitch() {
1633 let c4 = pitch("C4");
1634 let m3 = Interval::from_name("m3").unwrap();
1635 let out = m3.transpose_pitch(&c4).unwrap();
1636 assert_eq!(out.name_with_octave(), "E-4");
1637 }
1638
1639 #[test]
1640 fn interval_transpose_pitch_in_place() {
1641 let mut c4 = pitch("C4");
1642 Interval::from_name("M2")
1643 .unwrap()
1644 .transpose_pitch_in_place(&mut c4)
1645 .unwrap();
1646 assert_eq!(c4.name_with_octave(), "D4");
1647 }
1648
1649 #[test]
1650 fn compact_names_match_music21() {
1651 let cases = [
1652 ("P5", "P5", "P5", "P5", "P5"),
1653 ("M3", "M3", "M3", "M3", "M3"),
1654 ("m-6", "m6", "m6", "m6", "m-6"),
1655 ("AA4", "AA4", "AA4", "AA4", "AA4"),
1656 ("d8", "d8", "d1", "d8", "d8"),
1657 ("P8", "P8", "P1", "P8", "P8"),
1658 ("M9", "M9", "M2", "M2", "M9"),
1659 ("P-5", "P5", "P5", "P5", "P-5"),
1660 ("P15", "P15", "P1", "P8", "P15"),
1661 ];
1662 for (input, short, simple, semi_simple, directed) in cases {
1663 let interval = Interval::from_name(input).unwrap();
1664 assert_eq!(interval.short_name(), short, "{input}");
1665 assert_eq!(interval.simple_name(), simple, "{input}");
1666 assert_eq!(interval.semi_simple_name(), semi_simple, "{input}");
1667 assert_eq!(interval.directed_name(), directed, "{input}");
1668 }
1669 }
1670
1671 #[test]
1672 fn complement_and_interval_class_match_music21() {
1673 let cases = [
1674 ("P5", "P4", 5),
1675 ("M3", "m6", 4),
1676 ("m-6", "M3", 4),
1677 ("AA4", "dd5", 5),
1678 ("d8", "A1", 1),
1679 ("P8", "P1", 0),
1680 ("P1", "P8", 0),
1681 ("A1", "d8", 1),
1682 ("M9", "m7", 2),
1683 ("m2", "M7", 1),
1684 ("P15", "P1", 0),
1685 ];
1686 for (input, complement, interval_class) in cases {
1687 let interval = Interval::from_name(input).unwrap();
1688 assert_eq!(
1689 interval.complement().unwrap().short_name(),
1690 complement,
1691 "{input}"
1692 );
1693 assert_eq!(interval.interval_class(), interval_class, "{input}");
1694 }
1695 }
1696
1697 #[test]
1698 fn step_skip_and_consonance_match_music21() {
1699 let cases = [
1700 ("P5", false, true, true, false, false),
1701 ("M3", false, true, true, false, false),
1702 ("AA4", false, true, false, false, false),
1703 ("d8", false, true, false, false, false),
1704 ("P8", false, true, true, false, false),
1705 ("P1", false, false, true, false, false),
1706 ("A1", true, false, false, false, true),
1707 ("M9", false, true, false, false, false),
1708 ("m2", true, false, false, true, true),
1709 ];
1710 for (input, step, skip, consonant, diatonic_step, chromatic_step) in cases {
1711 let interval = Interval::from_name(input).unwrap();
1712 assert_eq!(interval.is_step(), step, "{input} step");
1713 assert_eq!(interval.is_skip(), skip, "{input} skip");
1714 assert_eq!(interval.is_consonant(), consonant, "{input} consonant");
1715 assert_eq!(
1716 interval.is_diatonic_step(),
1717 diatonic_step,
1718 "{input} diatonic"
1719 );
1720 assert_eq!(
1721 interval.is_chromatic_step(),
1722 chromatic_step,
1723 "{input} chromatic"
1724 );
1725 }
1726 }
1727
1728 #[test]
1729 fn sum_and_difference_match_music21() {
1730 fn intervals(names: &[&str]) -> Vec<Interval> {
1731 names
1732 .iter()
1733 .map(|name| Interval::from_name(*name).unwrap())
1734 .collect()
1735 }
1736 assert_eq!(
1737 Interval::sum(&intervals(&["A2", "P5"]))
1738 .unwrap()
1739 .short_name(),
1740 "A6"
1741 );
1742 assert_eq!(
1743 Interval::sum(&intervals(&["P5", "m2"]))
1744 .unwrap()
1745 .short_name(),
1746 "m6"
1747 );
1748 assert_eq!(
1749 Interval::sum(&intervals(&["W", "W", "H", "W", "W", "W", "H"]))
1750 .unwrap()
1751 .short_name(),
1752 "P8"
1753 );
1754 assert_eq!(
1755 Interval::sum(&intervals(&["P5", "P-4"]))
1756 .unwrap()
1757 .directed_name(),
1758 "M2"
1759 );
1760 assert!(Interval::sum(&[]).is_err());
1761
1762 let cases = [
1763 (&["P5", "M3"][..], "m3"),
1764 (&["P4", "d3"][..], "A2"),
1765 (&["M6", "m2", "m2"][..], "AA4"),
1766 (&["P4", "M-2"][..], "P5"),
1767 (&["A2", "A2"][..], "P1"),
1768 (&["P8", "A1"][..], "d8"),
1769 ];
1770 for (names, expected) in cases {
1771 let difference = Interval::difference(&intervals(names)).unwrap();
1772 assert_eq!(difference.short_name(), expected, "{names:?}");
1773 }
1774 let descending_unison = Interval::difference(&intervals(&["P5", "A5"])).unwrap();
1775 assert_eq!(descending_unison.directed_name(), "d1");
1776 assert_eq!(descending_unison.semitones(), -1.0);
1777 assert!(Interval::difference(&[]).is_err());
1778 }
1779
1780 #[test]
1781 fn interval_pythagorean_ratio() {
1782 let ratio = Interval::from_name("P5")
1783 .unwrap()
1784 .pythagorean_ratio()
1785 .unwrap();
1786 assert_eq!(ratio, FractionType::new(3, 2));
1787 }
1788
1789 #[test]
1790 fn interval_inverts_oblique_unison() {
1791 let unison = Interval::from_name("P1").unwrap();
1792 let inverted = unison.inversion().unwrap();
1793
1794 assert_eq!(inverted.semitones(), 0.0);
1795 assert_eq!(inverted.generic_number(), 1);
1796 }
1797 #[test]
1798 fn specifier_case_matters_only_for_major_versus_minor() {
1799 for (lower, upper) in [
1802 ("p5", "P5"),
1803 ("a2", "A2"),
1804 ("d5", "D5"),
1805 ("aa2", "AA2"),
1806 ("dd5", "DD5"),
1807 ("aaa2", "AAA2"),
1808 ("ddd5", "DDD5"),
1809 ] {
1810 let a = Interval::from_name(lower).expect("lowercase parses");
1811 let b = Interval::from_name(upper).expect("uppercase parses");
1812 assert_eq!(a.semitones(), b.semitones(), "{lower} vs {upper}");
1813 assert_eq!(a.name(), b.name(), "{lower} vs {upper}");
1814 }
1815
1816 let minor = Interval::from_name("m3").expect("m3 parses");
1818 let major = Interval::from_name("M3").expect("M3 parses");
1819 assert_eq!(minor.semitones(), 3.0);
1820 assert_eq!(major.semitones(), 4.0);
1821 }
1822
1823 #[test]
1824 fn an_unknown_specifier_errors_instead_of_panicking() {
1825 for name in ["Q5", "x3", "5", "zz2"] {
1826 assert!(
1827 Interval::from_name(name).is_err(),
1828 "{name:?} should be rejected, not panic"
1829 );
1830 }
1831 }
1832
1833 #[test]
1834 fn a_hyphen_anywhere_makes_an_interval_name_descending() {
1835 for name in ["-M2", "M-2"] {
1840 let interval = Interval::from_name(name).expect("descending name parses");
1841 assert_eq!(interval.semitones(), -2.0, "{name}");
1842 assert_eq!(interval.generic_number(), -2, "{name}");
1843 }
1844
1845 for name in ["--M2", "-M-2"] {
1849 let interval = Interval::from_name(name).expect("repeated hyphen parses");
1850 assert_eq!(interval.semitones(), -2.0, "{name}");
1851 }
1852
1853 assert_eq!(
1855 Interval::from_name("d-5").expect("d-5 parses").semitones(),
1856 -6.0
1857 );
1858 }
1859}