Skip to main content

music21/interval/
mod.rs

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/// Direction of a directed interval.
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
33pub enum IntervalDirection {
34    /// The end pitch is lower than the start pitch.
35    Descending = -1,
36    /// The interval is an oblique unison.
37    Oblique = 0,
38    /// The end pitch is higher than the start pitch.
39    Ascending = 1,
40}
41
42impl IntervalDirection {
43    /// Returns `-1`, `0`, or `1` for descending, oblique, or ascending.
44    pub fn as_int(self) -> IntegerType {
45        self as IntegerType
46    }
47
48    /// Returns a display label for the direction.
49    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/// A directed musical interval with diatonic spelling and chromatic size.
61#[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    /// Two intervals are the same when they are the same written distance
72    /// and the same sounding one, which is music21's own comparison — the
73    /// pitches an interval was built between are not part of what it is.
74    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
152/// music21's `convertStaffDistanceToInterval`: a signed count of staff steps
153/// as a signed generic interval number, so `0` is a unison, `2` a third and
154/// `-1` a descending second.
155pub 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
163/// The pitch written higher on the staff, whatever it sounds like: music21's
164/// `getWrittenHigherNote`, so `C4` over `B#3`. Ties in staff position are
165/// settled by sound, and a complete tie returns the first.
166pub 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
174/// The pitch written lower on the staff: music21's `getWrittenLowerNote`,
175/// so `B#3` under `C4`.
176pub 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
184/// The pitch that sounds higher: music21's `getAbsoluteHigherNote`. Enharmonic
185/// equals return the first.
186pub 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
194/// The pitch that sounds lower: music21's `getAbsoluteLowerNote`. Enharmonic
195/// equals return the first.
196pub 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
204/// The generic interval from one pitch to another, counted by staff
205/// position: music21's `notesToGeneric`.
206pub 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
213/// The semitone distance from one pitch to another: music21's
214/// `notesToChromatic`.
215pub 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    // Saturating, so a count no specifier reaches is refused, not wrapped.
245    let diff = ((these_semis + rounding_error).round() as IntegerType).saturating_sub(normal_semis);
246
247    if g_int.is_perfectable() {
248        specifier_at(
249            &PERFECTABLE_SPECIFIERS,
250            diff.saturating_add(4),
251            "Perfect",
252            diff,
253        )
254    } else {
255        specifier_at(&MAJOR_SPECIFIERS, diff.saturating_add(5), "Major", diff)
256    }
257}
258
259/// The qualities a perfectable interval takes, widest flat to widest sharp:
260/// music21's `perfSpecifiers`, with `Perfect` in the middle.
261const PERFECTABLE_SPECIFIERS: [Specifier; 9] = [
262    Specifier::QuadrupleDiminished,
263    Specifier::TripleDiminished,
264    Specifier::DoubleDiminished,
265    Specifier::Diminished,
266    Specifier::Perfect,
267    Specifier::Augmented,
268    Specifier::DoubleAugmented,
269    Specifier::TripleAugmented,
270    Specifier::QuadrupleAugmented,
271];
272
273/// The same for an interval that is major or minor rather than perfect:
274/// music21's `specifiers`.
275const MAJOR_SPECIFIERS: [Specifier; 10] = [
276    Specifier::QuadrupleDiminished,
277    Specifier::TripleDiminished,
278    Specifier::DoubleDiminished,
279    Specifier::Diminished,
280    Specifier::Minor,
281    Specifier::Major,
282    Specifier::Augmented,
283    Specifier::DoubleAugmented,
284    Specifier::TripleAugmented,
285    Specifier::QuadrupleAugmented,
286];
287
288/// The quality at a place in one of those tables.
289///
290/// music21 indexes the table with a plain Python subscript, so a note flatter
291/// than the widest diminished it can spell does not raise — the index goes
292/// negative and Python counts back from the end, which answers an augmented
293/// interval for a flattened one. It is a strange answer and it is the one
294/// music21 gives, and the interval's own cent shift still says how far off
295/// the note really is. Only an index off the sharp end raises, as it does
296/// upstream.
297fn specifier_at(
298    table: &[Specifier],
299    index: IntegerType,
300    from: &str,
301    diff: IntegerType,
302) -> Result<Specifier> {
303    let length = table.len() as IntegerType;
304    let wrapped = if index < 0 { index + length } else { index };
305    if index >= length || wrapped < 0 {
306        return Err(Error::Interval(format!(
307            "cannot get a specifier for a note with this many semitones off of {from}: {diff}"
308        )));
309    }
310    Ok(table[wrapped as usize])
311}
312
313/// Reads the quality off a generic and a chromatic interval together:
314/// music21's `intervalsToDiatonic`, so a third of four semitones is major.
315pub fn intervals_to_diatonic(
316    g_int: &GenericInterval,
317    c_int: &ChromaticInterval,
318) -> Result<DiatonicInterval> {
319    let specifier = specifier_from_generic_chromatic(g_int, c_int)?;
320    Ok(DiatonicInterval::new(specifier, g_int))
321}
322
323/// The simplest quality and generic size for a semitone count: music21's
324/// `convertSemitoneToSpecifierGeneric`, so `6` is a diminished fifth and
325/// `-14` a descending major ninth.
326pub fn convert_semitone_to_specifier_generic(count: FloatType) -> (Specifier, IntegerType) {
327    let (specifier, generic, _) = convert_semitone_to_specifier_generic_microtone(count);
328    (specifier, generic)
329}
330
331/// The quality and generic size of a whole number of semitones within one
332/// octave, music21's `SEMITONES_TO_SPEC_GENERIC` table.
333fn semitones_to_specifier_generic(size: IntegerType) -> (Specifier, IntegerType) {
334    match size {
335        0 => (Specifier::Perfect, 1),
336        1 => (Specifier::Minor, 2),
337        2 => (Specifier::Major, 2),
338        3 => (Specifier::Minor, 3),
339        4 => (Specifier::Major, 3),
340        5 => (Specifier::Perfect, 4),
341        6 => (Specifier::Diminished, 5),
342        7 => (Specifier::Perfect, 5),
343        8 => (Specifier::Minor, 6),
344        9 => (Specifier::Major, 6),
345        10 => (Specifier::Minor, 7),
346        _ => (Specifier::Major, 7),
347    }
348}
349
350/// Like [`convert_semitone_to_specifier_generic`] for a fractional semitone
351/// count, returning the leftover in cents as well: music21's
352/// `convertSemitoneToSpecifierGenericMicrotone`, so `2.5` is a major second
353/// and fifty cents, and `-2.5` a descending minor third and fifty.
354pub fn convert_semitone_to_specifier_generic_microtone(
355    count: FloatType,
356) -> (Specifier, IntegerType, FloatType) {
357    let dir_scale = if count < 0.0 { -1 } else { 1 };
358    let mut whole = count.floor();
359    let mut cents = (count - whole) * 100.0;
360    if cents > 50.0 {
361        cents -= 100.0;
362        whole += 1.0;
363    }
364    // Unsigned, as the least integer has no positive of its own.
365    let whole = (whole as IntegerType).unsigned_abs();
366    let size = (whole % 12) as IntegerType;
367    let octave = (whole / 12) as IntegerType;
368    let (specifier, generic) = semitones_to_specifier_generic(size);
369    (specifier, (generic + octave * 7) * dir_scale, cents)
370}
371
372/// The step letter and octave of a diatonic note number, counting `C0` as
373/// `1`: music21's `convertDiatonicNumberToStep`, so `15` is `C` in octave
374/// `2`, `0` is `B` in octave `-1`, and `-19` is `D` in octave `-3`.
375pub fn convert_diatonic_number_to_step(dn: IntegerType) -> (char, IntegerType) {
376    let zero_based = dn - 1;
377    let octave = zero_based.div_euclid(7);
378    let step = StepName::ALL[zero_based.rem_euclid(7) as usize].as_char();
379    (step, octave)
380}
381
382/// Parses an interval quality from its prefix or spelled-out name: music21's
383/// `parseSpecifier`, so `"P"`, `"perfect"` and `"Perfect"` all give
384/// [`Specifier::Perfect`].
385pub fn parse_specifier(value: &str) -> Result<Specifier> {
386    Specifier::from_name(value)
387}
388
389impl Interval {
390    pub(crate) fn between(start_pitch: Pitch, end_pitch: Pitch) -> Result<Self> {
391        let generic = notes_to_generic(&start_pitch, &end_pitch)?;
392        let chromatic = notes_to_chromatic(&start_pitch, &end_pitch)?;
393        let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
394
395        Ok(Self {
396            implicit_diatonic: false,
397            diatonic,
398            chromatic,
399            pitch_start: Some(start_pitch),
400            pitch_end: Some(end_pitch),
401        })
402    }
403
404    /// Builds an interval from its two halves as given, without checking
405    /// that they agree: music21's `Interval(diatonic=..., chromatic=...)`.
406    pub fn from_diatonic_and_chromatic(
407        diatonic: DiatonicInterval,
408        chromatic: ChromaticInterval,
409    ) -> Result<Interval> {
410        Ok(Self {
411            implicit_diatonic: false,
412            diatonic,
413            chromatic,
414            pitch_start: None,
415            pitch_end: None,
416        })
417    }
418
419    /// Builds an interval from a diatonic interval alone, deriving the
420    /// semitone count: music21's `Interval(diatonic=...)`.
421    pub fn from_diatonic(diatonic: DiatonicInterval) -> Result<Self> {
422        let chromatic = diatonic.get_chromatic()?;
423        Self::from_diatonic_and_chromatic(diatonic, chromatic)
424    }
425
426    /// Builds an interval from a semitone count alone, spelling it the
427    /// simplest way and marking the spelling as implicit: music21's
428    /// `Interval(chromatic=...)`.
429    pub fn from_chromatic(chromatic: ChromaticInterval) -> Result<Self> {
430        let diatonic = chromatic.get_diatonic();
431        let mut interval = Self::from_diatonic_and_chromatic(diatonic, chromatic)?;
432        interval.implicit_diatonic = true;
433        Ok(interval)
434    }
435
436    /// The generic half of the interval.
437    pub fn generic(&self) -> &GenericInterval {
438        &self.diatonic.generic
439    }
440
441    /// The diatonic half of the interval: quality plus generic size.
442    pub fn diatonic(&self) -> &DiatonicInterval {
443        &self.diatonic
444    }
445
446    /// The chromatic half of the interval: the semitone count.
447    pub fn chromatic(&self) -> &ChromaticInterval {
448        &self.chromatic
449    }
450
451    /// The quality.
452    pub fn specifier(&self) -> Specifier {
453        self.diatonic.specifier
454    }
455
456    /// Builds an interval from a generic size and a semitone count: music21's
457    /// `intervalFromGenericAndChromatic`, so a third of four semitones is a
458    /// major third and a fifth of six a diminished fifth. Negative values
459    /// give a descending interval.
460    pub fn from_generic_and_chromatic(
461        generic: IntegerType,
462        semitones: IntegerType,
463    ) -> Result<Self> {
464        let generic = GenericInterval::from_int(generic)?;
465        let chromatic = ChromaticInterval::from_int(semitones);
466        let diatonic = intervals_to_diatonic(&generic, &chromatic)?;
467        Self::from_diatonic_and_chromatic(diatonic, chromatic)
468    }
469
470    /// The pitch the interval was measured from, when it was built from two
471    /// pitches or notes.
472    pub fn pitch_start(&self) -> Option<&Pitch> {
473        self.pitch_start.as_ref()
474    }
475
476    /// The pitch the interval was measured to, when it was built from two
477    /// pitches or notes.
478    pub fn pitch_end(&self) -> Option<&Pitch> {
479        self.pitch_end.as_ref()
480    }
481
482    /// [`Self::pitch_start`] as a note without a duration.
483    pub fn note_start(&self) -> Option<Note> {
484        self.pitch_start.clone().map(Note::from_pitch)
485    }
486
487    /// [`Self::pitch_end`] as a note without a duration.
488    pub fn note_end(&self) -> Option<Note> {
489        self.pitch_end.clone().map(Note::from_pitch)
490    }
491
492    /// Parses an interval name such as `"M3"`, `"P5"`, or `"-m6"`.
493    ///
494    /// ```
495    /// use music21::Interval;
496    ///
497    /// let fifth = Interval::from_name("P5")?;
498    /// assert_eq!(fifth.semitones(), 7.0);
499    /// assert_eq!(Interval::from_name("-m6")?.semitones(), -8.0);
500    /// # Ok::<(), music21::Error>(())
501    /// ```
502    pub fn from_name(name: impl AsRef<str>) -> Result<Self> {
503        let (diatonic, chromatic, inferred) = parse_interval_name(name.as_ref())?;
504        Ok(Self {
505            implicit_diatonic: inferred,
506            diatonic,
507            chromatic,
508            pitch_start: None,
509            pitch_end: None,
510        })
511    }
512
513    /// Creates an implicit diatonic interval from a chromatic semitone count.
514    ///
515    /// ```
516    /// use music21::Interval;
517    ///
518    /// assert_eq!(Interval::from_semitones(4)?.directed_name(), "M3");
519    /// assert_eq!(Interval::from_semitones(-7)?.directed_name(), "P-5");
520    /// # Ok::<(), music21::Error>(())
521    /// ```
522    pub fn from_semitones(semitones: IntegerType) -> Result<Self> {
523        let chromatic = ChromaticInterval::from_int(semitones);
524        let diatonic = chromatic.get_diatonic();
525        Ok(Self {
526            implicit_diatonic: true,
527            diatonic,
528            chromatic,
529            pitch_start: None,
530            pitch_end: None,
531        })
532    }
533
534    /// Returns the directed interval from `start` to `end`.
535    ///
536    /// ```
537    /// use music21::{Interval, Pitch};
538    ///
539    /// let up = Interval::between_pitches(&Pitch::from_name("C4")?, &Pitch::from_name("E-4")?)?;
540    /// assert_eq!(up.directed_name(), "m3");
541    /// let down = Interval::between_pitches(&Pitch::from_name("C4")?, &Pitch::from_name("A3")?)?;
542    /// assert_eq!(down.directed_name(), "m-3");
543    /// assert_eq!(down.semitones(), -3.0);
544    /// # Ok::<(), music21::Error>(())
545    /// ```
546    pub fn between_pitches(start: &Pitch, end: &Pitch) -> Result<Self> {
547        Self::between(start.clone(), end.clone())
548    }
549
550    /// Returns the directed interval from `start` to `end`.
551    pub fn between_notes(start: &Note, end: &Note) -> Result<Self> {
552        Self::between(start.pitch.clone(), end.pitch.clone())
553    }
554
555    /// Returns the directed chromatic size in semitones, fractional for a
556    /// microtonal interval.
557    pub fn semitones(&self) -> FloatType {
558        self.chromatic.semitones
559    }
560
561    /// Returns the directed chromatic size rounded to whole semitones.
562    pub fn whole_semitones(&self) -> IntegerType {
563        self.chromatic.whole_semitones()
564    }
565
566    /// Returns the directed interval direction.
567    pub fn direction(&self) -> IntervalDirection {
568        self.chromatic.direction()
569    }
570
571    /// Returns the human-readable interval name, such as `"Major Third"`.
572    ///
573    /// ```
574    /// use music21::Interval;
575    ///
576    /// assert_eq!(Interval::from_name("M3")?.name(), "Major Third");
577    /// assert_eq!(Interval::from_name("d5")?.name(), "Diminished Fifth");
578    /// # Ok::<(), music21::Error>(())
579    /// ```
580    pub fn name(&self) -> String {
581        self.nice_name()
582    }
583
584    /// Returns the simple or compound generic interval number.
585    pub fn generic_number(&self) -> IntegerType {
586        self.generic().simple_directed()
587    }
588
589    /// Returns `true` when the interval was inferred from semitones only.
590    pub fn is_implicit_diatonic(&self) -> bool {
591        self.implicit_diatonic
592    }
593
594    /// Returns the complementary interval inversion.
595    ///
596    /// ```
597    /// use music21::Interval;
598    ///
599    /// assert_eq!(Interval::from_name("M3")?.inversion()?.directed_name(), "m6");
600    /// # Ok::<(), music21::Error>(())
601    /// ```
602    pub fn inversion(&self) -> Result<Self> {
603        let direction = match self.direction() {
604            IntervalDirection::Oblique => 1,
605            direction => direction.as_int(),
606        };
607        let simple = self.generic().simple_undirected();
608        let inverted_generic = if simple == 1 { 1 } else { 9 - simple };
609        let generic = GenericInterval::from_int(inverted_generic * direction)?;
610        let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
611        let chromatic = diatonic.get_chromatic()?;
612        Self::from_diatonic_and_chromatic(diatonic, chromatic)
613    }
614
615    /// Returns the same interval in the opposite direction.
616    pub fn reversed(&self) -> Result<Self> {
617        self.reverse()
618    }
619
620    /// Returns the Pythagorean tuning ratio for this interval.
621    ///
622    /// The ratio is expressed as a rational fraction built from pure fifths,
623    /// matching the helper music21 uses for enharmonic scoring.
624    pub fn pythagorean_ratio(&self) -> Result<FractionType> {
625        self.pythagorean_monzo()?.ratio()
626    }
627
628    /// The interval as octaves and fifths: the monzo of its
629    /// [Pythagorean ratio](Self::pythagorean_ratio), with no limit on how far
630    /// along the chain of fifths the spelling sits.
631    ///
632    /// This is what a regular temperament tunes, so `C#` and `Db` stay apart
633    /// here however a tuning later treats them. Errors on an interval measured
634    /// in fractions of a semitone, which no spelling reaches.
635    pub fn pythagorean_monzo(&self) -> Result<crate::tuningsystem::Monzo> {
636        let spelled = self.diatonic.get_chromatic()?.semitones();
637        if self.chromatic.semitones() != spelled {
638            return Err(Error::Interval(format!(
639                "{} is not a whole number of semitones, so has no Pythagorean ratio",
640                self.chromatic.semitones()
641            )));
642        }
643        Ok(crate::tuningsystem::Monzo::pythagorean(
644            self.diatonic.generic.staff_distance(),
645            spelled as IntegerType,
646        ))
647    }
648
649    /// Transposes a pitch by this interval.
650    ///
651    /// ```
652    /// use music21::{Interval, Pitch};
653    ///
654    /// let third = Interval::from_name("M3")?;
655    /// let raised = third.transpose_pitch(&Pitch::from_name("B-3")?)?;
656    /// assert_eq!(raised.name_with_octave(), "D4");
657    /// # Ok::<(), music21::Error>(())
658    /// ```
659    pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
660        self.transpose_pitch_with_options(pitch, false, Some(4))
661    }
662
663    /// Transposes a note by this interval.
664    pub fn transpose_note(&self, note: &Note) -> Result<Note> {
665        let mut out = note.clone();
666        out.pitch = self.transpose_pitch(&note.pitch)?;
667        Ok(out)
668    }
669
670    /// Returns music21's compact undirected name, such as `"P5"` or `"m3"`.
671    ///
672    /// ```
673    /// use music21::Interval;
674    ///
675    /// assert_eq!(Interval::from_name("-M10")?.short_name(), "M10");
676    /// # Ok::<(), music21::Error>(())
677    /// ```
678    pub fn short_name(&self) -> String {
679        format!(
680            "{}{}",
681            self.diatonic.specifier.prefix(),
682            self.generic().undirected()
683        )
684    }
685
686    /// Returns the compact name folded into one octave, so a ninth is `"M2"`.
687    ///
688    /// ```
689    /// use music21::Interval;
690    ///
691    /// assert_eq!(Interval::from_name("M10")?.simple_name(), "M3");
692    /// # Ok::<(), music21::Error>(())
693    /// ```
694    pub fn simple_name(&self) -> String {
695        format!(
696            "{}{}",
697            self.diatonic.specifier.prefix(),
698            self.generic().simple_undirected()
699        )
700    }
701
702    /// Returns the compact name folded into one octave, except that octaves
703    /// stay `8` rather than becoming unisons.
704    pub fn semi_simple_name(&self) -> String {
705        format!(
706            "{}{}",
707            self.diatonic.specifier.prefix(),
708            self.generic().semi_simple_undirected()
709        )
710    }
711
712    /// Returns the compact name with music21's direction sign, such as `"m-6"`.
713    pub fn directed_name(&self) -> String {
714        format!(
715            "{}{}",
716            self.diatonic.specifier.prefix(),
717            self.generic().directed()
718        )
719    }
720
721    /// Returns whether this is a second of any quality.
722    pub fn is_diatonic_step(&self) -> bool {
723        self.generic().undirected() == 2
724    }
725
726    /// Returns whether this spans exactly one semitone.
727    pub fn is_chromatic_step(&self) -> bool {
728        self.chromatic.undirected() == 1.0
729    }
730
731    /// Returns whether this is a step diatonically or chromatically.
732    pub fn is_step(&self) -> bool {
733        self.is_chromatic_step() || self.is_diatonic_step()
734    }
735
736    /// Returns whether this is larger than a second. Unisons are neither
737    /// steps nor skips.
738    pub fn is_skip(&self) -> bool {
739        self.generic().undirected() > 2
740    }
741
742    /// Returns whether this is a common-practice consonance: a perfect unison
743    /// or fifth, or a major or minor third or sixth, in any octave.
744    pub fn is_consonant(&self) -> bool {
745        matches!(
746            (self.diatonic.specifier, self.generic().simple_undirected()),
747            (Specifier::Perfect, 1 | 5) | (Specifier::Major | Specifier::Minor, 3 | 6)
748        )
749    }
750
751    /// Returns the interval that completes this one to an octave, so a major
752    /// third becomes a minor sixth and an octave becomes a unison.
753    pub fn complement(&self) -> Result<Self> {
754        let generic = GenericInterval::from_int(9 - self.generic().semi_simple_undirected())?;
755        let diatonic = DiatonicInterval::new(self.diatonic.specifier.inversion(), &generic);
756        let chromatic = diatonic.get_chromatic()?;
757        Self::from_diatonic_and_chromatic(diatonic, chromatic)
758    }
759
760    /// Returns the interval class, the smaller of the semitone count within an
761    /// octave and its complement, from `0` to `6`.
762    pub fn interval_class(&self) -> IntegerType {
763        self.chromatic.interval_class()
764    }
765
766    /// Adds intervals end to end, as music21's `interval.add` does.
767    ///
768    /// Direction matters: a perfect fifth followed by a descending perfect
769    /// fourth is a major second.
770    pub fn sum<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
771        let start = Pitch::from_name("C4")?;
772        let mut end = start.clone();
773        let mut any = false;
774        for interval in intervals {
775            end = interval.transpose_pitch(&end)?;
776            any = true;
777        }
778        if !any {
779            return Err(Error::Interval(
780                "cannot add an empty set of intervals".to_string(),
781            ));
782        }
783        Self::between_pitches(&start, &end)
784    }
785
786    /// Subtracts every following interval from the first, as music21's
787    /// `interval.subtract` does.
788    pub fn difference<'a>(intervals: impl IntoIterator<Item = &'a Interval>) -> Result<Self> {
789        let start = Pitch::from_name("C4")?;
790        let mut intervals = intervals.into_iter();
791        let Some(first) = intervals.next() else {
792            return Err(Error::Interval(
793                "cannot subtract an empty set of intervals".to_string(),
794            ));
795        };
796        let mut end = first.transpose_pitch(&start)?;
797        for interval in intervals {
798            end = interval.reversed()?.transpose_pitch(&end)?;
799        }
800        Self::between_pitches(&start, &end)
801    }
802
803    /// Returns the compact name folded into one octave but keeping the
804    /// direction sign, such as `"A-6"` for a descending augmented thirteenth.
805    pub fn directed_simple_name(&self) -> String {
806        format!(
807            "{}{}",
808            self.diatonic.specifier.prefix(),
809            self.generic().simple_directed()
810        )
811    }
812
813    pub(crate) fn directed_simple_key(&self) -> (Specifier, IntegerType) {
814        (self.diatonic.specifier, self.generic().simple_directed())
815    }
816
817    pub(crate) fn simple_key(&self) -> (Specifier, IntegerType) {
818        (self.diatonic.specifier, self.generic().simple_undirected())
819    }
820
821    pub(crate) fn semi_simple_key(&self) -> (Specifier, IntegerType) {
822        (
823            self.diatonic.specifier,
824            self.generic().semi_simple_undirected(),
825        )
826    }
827
828    pub(crate) fn is_perfect_unison(&self) -> bool {
829        self.generic().undirected() == 1 && self.chromatic.semitones == 0.0
830    }
831
832    pub(crate) fn nice_name(&self) -> String {
833        self.diatonic.nice_name()
834    }
835
836    /// The spelled-out name with compound intervals folded to an octave at
837    /// most: music21's `semiSimpleNiceName`, so a ninth is `Minor Second`
838    /// but an octave stays `Perfect Octave`.
839    pub fn semi_simple_nice_name(&self) -> String {
840        self.diatonic.semi_simple_nice_name()
841    }
842
843    /// The spelled-out name within one octave: music21's `simpleNiceName`,
844    /// so both a ninth and a sixteenth are `Major Second` and an octave is a
845    /// `Perfect Unison`.
846    pub fn simple_nice_name(&self) -> String {
847        format!(
848            "{} {}",
849            self.diatonic.specifier.nice_name(),
850            self.generic().simple_nice_name()
851        )
852    }
853
854    /// The direction music21's `DiatonicInterval` reports, which differs from
855    /// the generic direction only on altered unisons: a diminished unison is
856    /// always `Descending` and an augmented one always `Ascending`, whatever
857    /// sign the interval was written with.
858    fn diatonic_direction(&self) -> Direction {
859        self.diatonic.direction()
860    }
861
862    fn directed(&self, name: String) -> String {
863        format!("{} {name}", self.diatonic_direction().name())
864    }
865
866    /// The spelled-out name with its direction: music21's `directedNiceName`,
867    /// `Descending Major Third`.
868    pub fn directed_nice_name(&self) -> String {
869        self.directed(self.nice_name())
870    }
871
872    /// [`Self::simple_nice_name`] with its direction: music21's
873    /// `directedSimpleNiceName`.
874    pub fn directed_simple_nice_name(&self) -> String {
875        self.directed(self.simple_nice_name())
876    }
877
878    /// [`Self::semi_simple_nice_name`] with its direction: music21's
879    /// `directedSemiSimpleNiceName`.
880    pub fn directed_semi_simple_nice_name(&self) -> String {
881        self.directed(self.semi_simple_nice_name())
882    }
883
884    /// The specifier spelled out on its own: music21's `specificName`,
885    /// `Doubly-Diminished` for `dd5`.
886    pub fn specific_name(&self) -> String {
887        self.diatonic.specifier.nice_name()
888    }
889
890    /// The chromatic size in cents, signed.
891    pub fn cents(&self) -> FloatType {
892        self.chromatic.cents()
893    }
894
895    /// How far the chromatic size runs past the diatonic spelling, in cents:
896    /// music21's `_diatonicIntervalCentShift`, `-50.0` for the augmented
897    /// unison between `C1` and a `C1` half-sharp.
898    pub fn diatonic_interval_cent_shift(&self) -> FloatType {
899        let diatonic_cents = self.diatonic.cents().unwrap_or(0.0);
900        self.chromatic.cents() - diatonic_cents
901    }
902
903    /// Whether the generic interval is a unison, whatever its quality.
904    pub fn is_unison(&self) -> bool {
905        self.generic().is_unison()
906    }
907
908    /// Whether the generic interval takes perfect rather than major and
909    /// minor qualities: unisons, fourths, fifths and their compounds.
910    pub fn is_perfectable(&self) -> bool {
911        self.generic().is_perfectable()
912    }
913
914    /// The signed number of staff steps: music21's `generic.staffDistance`,
915    /// `0` for a unison, `2` for a third and `-4` for a descending fifth.
916    pub fn staff_distance(&self) -> IntegerType {
917        self.generic().staff_distance()
918    }
919
920    /// The simple generic size from one to seven, with descending intervals
921    /// inverted: music21's `generic.mod7`, so a descending third is `6`.
922    pub fn mod7(&self) -> IntegerType {
923        self.generic().mod7()
924    }
925
926    /// The generic size of the inversion within an octave: music21's
927    /// `generic.mod7inversion`, `6` for a third and `1` for an octave.
928    pub fn mod7_inversion(&self) -> IntegerType {
929        self.generic().mod7_inversion()
930    }
931
932    /// The semitones reduced to a pitch class, `0` to `11`: music21's
933    /// `chromatic.mod12`, so a descending major third is `8`.
934    pub fn mod12(&self) -> IntegerType {
935        self.chromatic.mod12()
936    }
937
938    /// Moves a pitch by the interval the way music21's `transposePitch`
939    /// does, with its keyword arguments: `reverse` transposes by the
940    /// reversed interval, and `max_accidental` respells any result carrying
941    /// more accidentals than that (music21's default is `Some(4)`), `None`
942    /// meaning no limit. A pitch's fundamental moves with it.
943    pub fn transpose_pitch_with_options(
944        &self,
945        p: &Pitch,
946        reverse: bool,
947        max_accidental: Option<IntegerType>,
948    ) -> Result<Pitch> {
949        if reverse {
950            return self
951                .reverse()?
952                .transpose_pitch_with_options(p, false, max_accidental);
953        }
954        let mut moved = self.transpose_pitch_alone(p, max_accidental)?;
955        if let Some(fundamental) = p.fundamental() {
956            moved.fundamental_setter(self.transpose_pitch_with_options(
957                fundamental,
958                false,
959                max_accidental,
960            )?);
961        }
962        Ok(moved)
963    }
964
965    /// The pitch moved by the interval, its fundamental left as it was.
966    fn transpose_pitch_alone(
967        &self,
968        p: &Pitch,
969        max_accidental: Option<IntegerType>,
970    ) -> Result<Pitch> {
971        if self.implicit_diatonic {
972            return self.chromatic.transpose_pitch(p);
973        }
974
975        let use_implicit_octave = p.octave().is_none();
976        // A true unison, or any whole number of octaves: the accidental of
977        // what comes back is written the way the accidental it came from was.
978        let whole_semitones = self.chromatic.semitones == self.chromatic.semitones.trunc();
979        let inherit_accidental_display = self.diatonic.simple_name() == "P1" && whole_semitones;
980        // A microtonal interval already says how many cents the answer sits
981        // from its written value, so the cents the pitch came with are not
982        // carried across on top of them — music21 says the same, and it is
983        // what keeps a scale realized a quarter-tone at a time from piling
984        // the whole walk into one note's microtone.
985        let cents_origin = if p.is_twelve_tone() || !whole_semitones {
986            0.0
987        } else {
988            p.microtone().map_or(0.0, crate::pitch::Microtone::cents)
989        };
990        let new_dnn = p
991            .checked_diatonic_note_number()
992            .and_then(|dnn| dnn.checked_add(self.diatonic.generic.staff_distance()))
993            .ok_or_else(|| {
994                Error::Interval(format!(
995                    "cannot move {} past the last octave an integer counts",
996                    p.name_with_octave()
997                ))
998            })?;
999        let (new_step, new_octave) = convert_diatonic_number_to_step(new_dnn);
1000        let mut pitch2 = crate::pitch::PitchOptions::new()
1001            .step(new_step)
1002            .octave(new_octave)
1003            .build()?;
1004        let origin_ps = p.ps() - cents_origin / 100.0;
1005        let mut half_steps_to_fix = self.chromatic.semitones - (pitch2.ps() - origin_ps);
1006        while half_steps_to_fix >= 12.0 {
1007            half_steps_to_fix -= 12.0;
1008            pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) - 1));
1009        }
1010        while half_steps_to_fix <= -12.0 {
1011            half_steps_to_fix += 12.0;
1012            pitch2.octave_setter(Some(pitch2.octave().unwrap_or(4) + 1));
1013        }
1014        if half_steps_to_fix != 0.0 {
1015            if max_accidental.is_some_and(|limit| half_steps_to_fix.abs() > limit as FloatType) {
1016                pitch2.set_ps(pitch2.ps() + half_steps_to_fix);
1017            } else {
1018                pitch2.set_accidental_alter(half_steps_to_fix)?;
1019            }
1020            match (
1021                inherit_accidental_display,
1022                pitch2.written_accidental().is_some(),
1023                p.written_accidental(),
1024            ) {
1025                (false, true, Some(source)) => {
1026                    if let Some(target) = pitch2.written_accidental_mut() {
1027                        target.inherit_display(source);
1028                        target.set_display_status(None);
1029                    }
1030                }
1031                (true, false, Some(source)) => {
1032                    let mut natural = crate::pitch::Accidental::natural();
1033                    natural.inherit_display(source);
1034                    pitch2.set_accidental(natural);
1035                }
1036                (true, true, Some(source)) => {
1037                    if let Some(target) = pitch2.written_accidental_mut() {
1038                        target.inherit_display(source);
1039                    }
1040                }
1041                (true, true, None) => {
1042                    if let Some(target) = pitch2.written_accidental_mut() {
1043                        target.set_display_status(Some(false));
1044                    }
1045                }
1046                _ => {}
1047            }
1048        } else if inherit_accidental_display
1049            && p.written_accidental()
1050                .is_some_and(|accidental| accidental.name() == "natural")
1051        {
1052            pitch2.set_written_accidental(p.written_accidental().cloned());
1053        }
1054        if cents_origin != 0.0 {
1055            let cents = pitch2
1056                .microtone()
1057                .map_or(0.0, crate::pitch::Microtone::cents);
1058            pitch2.set_microtone_cents(cents + cents_origin)?;
1059        }
1060        if use_implicit_octave {
1061            pitch2.octave_setter(None);
1062        }
1063        Ok(pitch2)
1064    }
1065
1066    /// Transposes a pitch in place by this interval.
1067    pub fn transpose_pitch_in_place(&self, pitch: &mut Pitch) -> Result<()> {
1068        *pitch = self.transpose_pitch(pitch)?;
1069        Ok(())
1070    }
1071}
1072
1073impl fmt::Display for Interval {
1074    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1075        let shift = self.diatonic_interval_cent_shift();
1076        if shift == 0.0 {
1077            write!(f, "{}", self.directed_name())
1078        } else {
1079            write!(
1080                f,
1081                "{} {}",
1082                self.directed_name(),
1083                crate::pitch::Microtone::from_cents(shift, 1)
1084            )
1085        }
1086    }
1087}
1088
1089impl FromStr for Interval {
1090    type Err = Error;
1091
1092    fn from_str(value: &str) -> Result<Self> {
1093        Self::from_name(value)
1094    }
1095}
1096
1097impl TryFrom<&str> for Interval {
1098    type Error = Error;
1099
1100    fn try_from(value: &str) -> Result<Self> {
1101        Self::from_name(value)
1102    }
1103}
1104
1105impl TryFrom<String> for Interval {
1106    type Error = Error;
1107
1108    fn try_from(value: String) -> Result<Self> {
1109        Self::from_name(value)
1110    }
1111}
1112
1113impl TryFrom<IntegerType> for Interval {
1114    type Error = Error;
1115
1116    fn try_from(value: IntegerType) -> Result<Self> {
1117        Self::from_semitones(value)
1118    }
1119}
1120
1121/// The ordinary spelling of an interval name -- a specifier and a number, as
1122/// `P5`, `m3` or `M-2` -- read where it stands.
1123///
1124/// Anything else is left to the reading below, which writes the name out
1125/// several times over to find a written-out ordinal or a directional word in
1126/// it. Nearly every name a caller writes is one of these, and this is the
1127/// path a transposition, a scale and a chord symbol all take.
1128fn parse_plain_interval_name(value: &str) -> Option<(Specifier, IntegerType)> {
1129    let mut number: IntegerType = 0;
1130    let mut digits = 0;
1131    let mut descending = false;
1132    let mut specifier: Option<(usize, usize)> = None;
1133    for (index, character) in value.char_indices() {
1134        if character == '-' {
1135            // Every hyphen is dropped and the direction flips once, wherever
1136            // in the name it stands -- which is music21's own reading.
1137            descending = true;
1138        } else if let Some(digit) = character.to_digit(10) {
1139            number = number * 10 + digit as IntegerType;
1140            digits += 1;
1141        } else if digits > 0 || !character.is_ascii_alphabetic() {
1142            // A letter after the number, or anything that is not a letter at
1143            // all: not a name this reads.
1144            return None;
1145        } else {
1146            let end = index + character.len_utf8();
1147            specifier = Some((specifier.map_or(index, |(start, _)| start), end));
1148        }
1149    }
1150    let (start, end) = specifier?;
1151    if digits == 0 {
1152        return None;
1153    }
1154    let specifier = Specifier::from_name(&value[start..end]).ok()?;
1155    Some((specifier, if descending { -number } else { number }))
1156}
1157
1158/// The two halves of an interval, from the specifier and the number a name
1159/// came to.
1160fn interval_parts(
1161    specifier: Specifier,
1162    generic_number: IntegerType,
1163    inferred: bool,
1164) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1165    let g_interval = GenericInterval::from_int(generic_number)?;
1166    let d_interval = g_interval.get_diatonic(specifier);
1167    let c_interval = d_interval.get_chromatic()?;
1168    Ok((d_interval, c_interval, inferred))
1169}
1170
1171fn parse_interval_name(name: &str) -> Result<(DiatonicInterval, ChromaticInterval, bool)> {
1172    if let Some((specifier, number)) = parse_plain_interval_name(name) {
1173        return interval_parts(specifier, number, false);
1174    }
1175    // Only the unusual spellings are taken apart, so only they are copied.
1176    let mut value = name.to_string();
1177    let mut inferred = false;
1178    let mut dir_scale = 1;
1179
1180    // Check for '-' and remove them:
1181    if value.contains('-') {
1182        value = value.replace('-', "");
1183        dir_scale = -1;
1184    }
1185    // Remove directional words:
1186    {
1187        let (without_descending, found_descending) = strip_direction_word(&value, "descending");
1188        if found_descending {
1189            value = without_descending;
1190            dir_scale = -1;
1191        } else {
1192            let (without_ascending, found_ascending) = strip_direction_word(&value, "ascending");
1193            if found_ascending {
1194                value = without_ascending;
1195            }
1196        }
1197    }
1198    let value_lower = value.to_lowercase();
1199
1200    // Handle whole/half abbreviations:
1201    if value_lower == "w" || value_lower == "whole" || value_lower == "tone" {
1202        value = "M2".to_string();
1203        inferred = true;
1204    } else if value_lower == "h" || value_lower == "half" || value_lower == "semitone" {
1205        value = "m2".to_string();
1206        inferred = true;
1207    }
1208
1209    // Replace any music ordinal in the string with its index. Almost no name
1210    // holds one, and the replacement lowercases both sides every time it is
1211    // called, so the cheap containment test in front of it is what keeps
1212    // `P5` from allocating three strings per candidate, twenty-three times.
1213    let mut lowered = value.to_ascii_lowercase();
1214    for (i, ordinal) in MUSICAL_ORDINAL_STRINGS_LOWER.iter().enumerate() {
1215        if !lowered.contains(ordinal.as_str()) {
1216            continue;
1217        }
1218        let replacement = i.to_string();
1219        let (next_value, replaced) =
1220            replace_music_ordinal(&value, &MUSICAL_ORDINAL_STRINGS[i], &replacement);
1221        if replaced {
1222            value = next_value;
1223            lowered = value.to_ascii_lowercase();
1224        }
1225    }
1226
1227    // Extract number and remaining spec:
1228    let (found, remain) = get_num_from_str(&value, "0123456789");
1229    let generic_number: IntegerType = found
1230        .parse::<IntegerType>()
1231        .map_err(|_| Error::Interval(format!("cannot read an interval number from {value:?}")))?
1232        * dir_scale;
1233    let spec = Specifier::parse(&remain)?;
1234    interval_parts(spec, generic_number, inferred)
1235}
1236
1237impl Interval {
1238    fn reverse(&self) -> Result<Self> {
1239        if let (Some(start), Some(end)) = (&self.pitch_start, &self.pitch_end) {
1240            Interval::between(end.clone(), start.clone())
1241        } else {
1242            Interval::from_diatonic_and_chromatic(self.diatonic.reverse(), self.chromatic.reverse())
1243        }
1244    }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    #[test]
1250    fn an_interval_is_read_from_a_name_a_number_or_one_of_its_halves() {
1251        use super::{ChromaticInterval, DiatonicInterval, Interval, Specifier, parse_specifier};
1252        use crate::note::Note;
1253        use crate::pitch::Pitch;
1254        use std::str::FromStr;
1255
1256        assert_eq!(Interval::from_str("M3").unwrap().short_name(), "M3");
1257        assert_eq!(Interval::try_from("P5").unwrap().short_name(), "P5");
1258        assert_eq!(
1259            Interval::try_from("m6".to_string()).unwrap().short_name(),
1260            "m6"
1261        );
1262        assert_eq!(Interval::try_from(7).unwrap().short_name(), "P5");
1263        assert!(Interval::from_str("Q9").is_err());
1264        assert_eq!(parse_specifier("M").unwrap(), Specifier::Major);
1265        assert!(parse_specifier("Q").is_err());
1266
1267        let diatonic = Interval::from_diatonic(DiatonicInterval::from_name("M3").unwrap()).unwrap();
1268        assert_eq!(diatonic.semitones(), 4.0);
1269        assert!(!diatonic.is_implicit_diatonic());
1270        assert_eq!(diatonic.diatonic().name(), "M3");
1271        assert_eq!(diatonic.specifier(), Specifier::Major);
1272        let chromatic = Interval::from_chromatic(ChromaticInterval::from_int(6)).unwrap();
1273        assert!(chromatic.is_implicit_diatonic());
1274        assert_eq!(chromatic.whole_semitones(), 6);
1275        assert_eq!(chromatic.directed_simple_name(), "d5");
1276        assert_eq!(
1277            Interval::from_str("M-10").unwrap().directed_simple_name(),
1278            "M-3"
1279        );
1280        assert_eq!(
1281            Interval::from_str("M3").unwrap(),
1282            Interval::from_str("M3").unwrap()
1283        );
1284        assert_ne!(
1285            Interval::from_str("M3").unwrap(),
1286            Interval::from_str("m3").unwrap()
1287        );
1288
1289        let c = Note::from_pitch(Pitch::from_name("C4").unwrap());
1290        let g = Note::from_pitch(Pitch::from_name("G4").unwrap());
1291        let fifth = Interval::between_notes(&c, &g).unwrap();
1292        assert_eq!(fifth.short_name(), "P5");
1293        assert_eq!(
1294            fifth.transpose_note(&g).unwrap().pitch_name_with_octave(),
1295            "D5"
1296        );
1297    }
1298
1299    #[test]
1300    fn a_note_flatter_than_the_table_wraps_the_way_music21_does() {
1301        // music21 subscripts its own quality table with a plain index, so a
1302        // fifth five semitones flat comes back quadruply *augmented* with a
1303        // nine-hundred-cent shift saying how far off it really is.
1304        let interval = Interval::between_pitches(
1305            &Pitch::from_name("A##3").unwrap(),
1306            &Pitch::from_name("E---4").unwrap(),
1307        )
1308        .unwrap();
1309        assert_eq!(interval.short_name(), "AAAA5");
1310        assert_eq!(interval.chromatic().semitones(), 2.0);
1311
1312        // The sharp end of the table is reached but never passed, since the
1313        // widest accidental a pitch can spell stops short of it.
1314        let widest = Interval::between_pitches(
1315            &Pitch::from_name("C4").unwrap(),
1316            &Pitch::from_name("E####4").unwrap(),
1317        )
1318        .unwrap();
1319        assert_eq!(widest.short_name(), "AAAA3");
1320    }
1321
1322    #[test]
1323    fn generic_and_chromatic_build_the_interval_music21_does() {
1324        let cases: [(i32, i32, &str, &str); 14] = [
1325            (3, 4, "M3", "M3"),
1326            (3, 3, "m3", "m3"),
1327            (5, 7, "P5", "P5"),
1328            (5, 6, "d5", "d5"),
1329            (4, 6, "A4", "A4"),
1330            (1, 0, "P1", "P1"),
1331            (1, 1, "A1", "A1"),
1332            (8, 12, "P8", "P8"),
1333            (-3, -4, "M3", "M-3"),
1334            (2, 3, "A2", "A2"),
1335            (7, 10, "m7", "m7"),
1336            (-2, -1, "m2", "m-2"),
1337            (9, 13, "m9", "m9"),
1338            (3, 2, "d3", "d3"),
1339        ];
1340        for (generic, semitones, name, directed) in cases {
1341            let interval = Interval::from_generic_and_chromatic(generic, semitones).unwrap();
1342            assert_eq!(interval.short_name(), name, "{generic} {semitones}");
1343            assert_eq!(interval.directed_name(), directed, "{generic} {semitones}");
1344            assert_eq!(
1345                interval.semitones(),
1346                FloatType::from(semitones),
1347                "{generic} {semitones}"
1348            );
1349            assert!(interval.pitch_start().is_none());
1350        }
1351        assert!(Interval::from_generic_and_chromatic(0, 0).is_err());
1352        assert_eq!(
1353            [-3, -1, 0, 1, 2, 7].map(staff_distance_to_generic_number),
1354            [-4, -2, 1, 2, 3, 8]
1355        );
1356    }
1357
1358    #[test]
1359    fn a_pitch_moved_past_the_highest_octave_is_refused() {
1360        use super::Interval;
1361        use crate::IntegerType;
1362        use crate::pitch::Pitch;
1363
1364        let pitch = Pitch::from_name("D2147483647").unwrap();
1365        assert!(
1366            Interval::from_name("M2")
1367                .unwrap()
1368                .transpose_pitch(&pitch)
1369                .is_err()
1370        );
1371        let mut pitch = Pitch::from_name("D").unwrap();
1372        pitch.set_octave(Some(IntegerType::MIN));
1373        assert!(
1374            Interval::from_name("M-2")
1375                .unwrap()
1376                .transpose_pitch(&pitch)
1377                .is_err()
1378        );
1379    }
1380
1381    #[test]
1382    fn the_least_count_of_semitones_is_an_interval() {
1383        use super::{Interval, convert_semitone_to_specifier_generic_microtone};
1384        use crate::IntegerType;
1385
1386        let interval = Interval::from_semitones(IntegerType::MIN).unwrap();
1387        assert_eq!(interval.semitones(), f64::from(IntegerType::MIN));
1388        let (_, generic, _) = convert_semitone_to_specifier_generic_microtone(-1e300);
1389        assert!(generic < 0);
1390    }
1391
1392    #[test]
1393    fn semitones_too_many_for_any_quality_are_refused() {
1394        use super::Interval;
1395
1396        assert!(Interval::from_generic_and_chromatic(-2, -2_147_483_647).is_err());
1397        assert!(Interval::from_generic_and_chromatic(2, 2_147_483_647).is_err());
1398    }
1399
1400    #[test]
1401    fn written_and_sounding_order_match_music21() {
1402        let cases: [(&str, &str, [&str; 4]); 8] = [
1403            ("C4", "E4", ["E4", "C4", "E4", "C4"]),
1404            ("E4", "C4", ["E4", "C4", "E4", "C4"]),
1405            ("B#3", "C4", ["C4", "B#3", "B#3", "B#3"]),
1406            ("C4", "B#3", ["C4", "B#3", "C4", "C4"]),
1407            ("Cb4", "B3", ["Cb4", "B3", "Cb4", "Cb4"]),
1408            ("F#4", "Gb4", ["Gb4", "F#4", "F#4", "F#4"]),
1409            ("C4", "C4", ["C4", "C4", "C4", "C4"]),
1410            ("B3", "Cb4", ["Cb4", "B3", "B3", "B3"]),
1411        ];
1412        for (first, second, expected) in cases {
1413            let a = Pitch::from_name(first).unwrap();
1414            let b = Pitch::from_name(second).unwrap();
1415            let actual = [
1416                written_higher_pitch(&a, &b),
1417                written_lower_pitch(&a, &b),
1418                absolute_higher_pitch(&a, &b),
1419                absolute_lower_pitch(&a, &b),
1420            ]
1421            .map(Pitch::name_with_octave);
1422            assert_eq!(actual, expected, "{first} {second}");
1423        }
1424    }
1425
1426    #[test]
1427    fn intervals_remember_the_pitches_they_were_measured_between() {
1428        let c = Pitch::from_name("C4").unwrap();
1429        let g = Pitch::from_name("G4").unwrap();
1430        let fifth = Interval::between_pitches(&c, &g).unwrap();
1431        assert_eq!(fifth.pitch_start().unwrap().name_with_octave(), "C4");
1432        assert_eq!(fifth.pitch_end().unwrap().name_with_octave(), "G4");
1433        assert_eq!(fifth.note_start().unwrap().pitch_name_with_octave(), "C4");
1434        assert_eq!(fifth.note_end().unwrap().pitch_name_with_octave(), "G4");
1435        let named = Interval::from_name("P5").unwrap();
1436        assert!(named.pitch_start().is_none());
1437        assert!(named.note_end().is_none());
1438    }
1439
1440    #[test]
1441    fn nice_name_variants_match_music21() {
1442        let cases: [(&str, [&str; 7]); 15] = [
1443            (
1444                "P1",
1445                [
1446                    "Perfect Unison",
1447                    "Oblique Perfect Unison",
1448                    "Perfect Unison",
1449                    "Perfect Unison",
1450                    "Oblique Perfect Unison",
1451                    "Oblique Perfect Unison",
1452                    "Perfect",
1453                ],
1454            ),
1455            (
1456                "m2",
1457                [
1458                    "Minor Second",
1459                    "Ascending Minor Second",
1460                    "Minor Second",
1461                    "Minor Second",
1462                    "Ascending Minor Second",
1463                    "Ascending Minor Second",
1464                    "Minor",
1465                ],
1466            ),
1467            (
1468                "P8",
1469                [
1470                    "Perfect Octave",
1471                    "Ascending Perfect Octave",
1472                    "Perfect Unison",
1473                    "Perfect Octave",
1474                    "Ascending Perfect Unison",
1475                    "Ascending Perfect Octave",
1476                    "Perfect",
1477                ],
1478            ),
1479            (
1480                "m9",
1481                [
1482                    "Minor Ninth",
1483                    "Ascending Minor Ninth",
1484                    "Minor Second",
1485                    "Minor Second",
1486                    "Ascending Minor Second",
1487                    "Ascending Minor Second",
1488                    "Minor",
1489                ],
1490            ),
1491            (
1492                "M10",
1493                [
1494                    "Major Tenth",
1495                    "Ascending Major Tenth",
1496                    "Major Third",
1497                    "Major Third",
1498                    "Ascending Major Third",
1499                    "Ascending Major Third",
1500                    "Major",
1501                ],
1502            ),
1503            (
1504                "P12",
1505                [
1506                    "Perfect Twelfth",
1507                    "Ascending Perfect Twelfth",
1508                    "Perfect Fifth",
1509                    "Perfect Fifth",
1510                    "Ascending Perfect Fifth",
1511                    "Ascending Perfect Fifth",
1512                    "Perfect",
1513                ],
1514            ),
1515            (
1516                "-M3",
1517                [
1518                    "Major Third",
1519                    "Descending Major Third",
1520                    "Major Third",
1521                    "Major Third",
1522                    "Descending Major Third",
1523                    "Descending Major Third",
1524                    "Major",
1525                ],
1526            ),
1527            (
1528                "-m9",
1529                [
1530                    "Minor Ninth",
1531                    "Descending Minor Ninth",
1532                    "Minor Second",
1533                    "Minor Second",
1534                    "Descending Minor Second",
1535                    "Descending Minor Second",
1536                    "Minor",
1537                ],
1538            ),
1539            (
1540                "dd5",
1541                [
1542                    "Doubly-Diminished Fifth",
1543                    "Ascending Doubly-Diminished Fifth",
1544                    "Doubly-Diminished Fifth",
1545                    "Doubly-Diminished Fifth",
1546                    "Ascending Doubly-Diminished Fifth",
1547                    "Ascending Doubly-Diminished Fifth",
1548                    "Doubly-Diminished",
1549                ],
1550            ),
1551            (
1552                "AA4",
1553                [
1554                    "Doubly-Augmented Fourth",
1555                    "Ascending Doubly-Augmented Fourth",
1556                    "Doubly-Augmented Fourth",
1557                    "Doubly-Augmented Fourth",
1558                    "Ascending Doubly-Augmented Fourth",
1559                    "Ascending Doubly-Augmented Fourth",
1560                    "Doubly-Augmented",
1561                ],
1562            ),
1563            (
1564                "P15",
1565                [
1566                    "Perfect Double-octave",
1567                    "Ascending Perfect Double-octave",
1568                    "Perfect Unison",
1569                    "Perfect Octave",
1570                    "Ascending Perfect Unison",
1571                    "Ascending Perfect Octave",
1572                    "Perfect",
1573                ],
1574            ),
1575            (
1576                "d1",
1577                [
1578                    "Diminished Unison",
1579                    "Descending Diminished Unison",
1580                    "Diminished Unison",
1581                    "Diminished Unison",
1582                    "Descending Diminished Unison",
1583                    "Descending Diminished Unison",
1584                    "Diminished",
1585                ],
1586            ),
1587            (
1588                "A1",
1589                [
1590                    "Augmented Unison",
1591                    "Ascending Augmented Unison",
1592                    "Augmented Unison",
1593                    "Augmented Unison",
1594                    "Ascending Augmented Unison",
1595                    "Ascending Augmented Unison",
1596                    "Augmented",
1597                ],
1598            ),
1599            (
1600                "-A1",
1601                [
1602                    "Augmented Unison",
1603                    "Ascending Augmented Unison",
1604                    "Augmented Unison",
1605                    "Augmented Unison",
1606                    "Ascending Augmented Unison",
1607                    "Ascending Augmented Unison",
1608                    "Augmented",
1609                ],
1610            ),
1611            (
1612                "P-8",
1613                [
1614                    "Perfect Octave",
1615                    "Descending Perfect Octave",
1616                    "Perfect Unison",
1617                    "Perfect Octave",
1618                    "Descending Perfect Unison",
1619                    "Descending Perfect Octave",
1620                    "Perfect",
1621                ],
1622            ),
1623        ];
1624        for (name, expected) in cases {
1625            let interval = Interval::from_name(name).unwrap();
1626            let actual = [
1627                interval.name(),
1628                interval.directed_nice_name(),
1629                interval.simple_nice_name(),
1630                interval.semi_simple_nice_name(),
1631                interval.directed_simple_nice_name(),
1632                interval.directed_semi_simple_nice_name(),
1633                interval.specific_name(),
1634            ];
1635            assert_eq!(actual, expected, "{name}");
1636        }
1637    }
1638
1639    #[test]
1640    #[allow(clippy::type_complexity)]
1641    fn generic_and_chromatic_helpers_match_music21() {
1642        let cases: [(&str, f64, bool, bool, i32, i32, i32, i32); 14] = [
1643            ("P1", 0.0, true, true, 0, 1, 8, 0),
1644            ("m2", 100.0, false, false, 1, 2, 7, 1),
1645            ("P4", 500.0, false, true, 3, 4, 5, 5),
1646            ("P5", 700.0, false, true, 4, 5, 4, 7),
1647            ("M7", 1100.0, false, false, 6, 7, 2, 11),
1648            ("P8", 1200.0, false, true, 7, 1, 1, 0),
1649            ("m9", 1300.0, false, false, 8, 2, 7, 1),
1650            ("-M3", -400.0, false, false, -2, 6, 6, 8),
1651            ("-P5", -700.0, false, true, -4, 4, 4, 5),
1652            ("-m9", -1300.0, false, false, -8, 7, 7, 11),
1653            ("P15", 2400.0, false, true, 14, 1, 1, 0),
1654            ("d1", -100.0, true, true, 0, 1, 8, 11),
1655            ("-A1", -100.0, true, true, 0, 8, 8, 11),
1656            ("P-8", -1200.0, false, true, -7, 1, 1, 0),
1657        ];
1658        for (name, cents, unison, perfectable, staff, mod7, mod7_inversion, mod12) in cases {
1659            let interval = Interval::from_name(name).unwrap();
1660            assert_eq!(interval.cents(), cents, "{name} cents");
1661            assert_eq!(interval.is_unison(), unison, "{name} unison");
1662            assert_eq!(interval.is_perfectable(), perfectable, "{name} perfectable");
1663            assert_eq!(interval.staff_distance(), staff, "{name} staff distance");
1664            assert_eq!(interval.mod7(), mod7, "{name} mod7");
1665            assert_eq!(
1666                interval.mod7_inversion(),
1667                mod7_inversion,
1668                "{name} mod7 inversion"
1669            );
1670            assert_eq!(interval.mod12(), mod12, "{name} mod12");
1671        }
1672    }
1673    use super::*;
1674
1675    fn pitch(name: &str) -> Pitch {
1676        Pitch::from_name(name).expect("valid pitch")
1677    }
1678
1679    #[test]
1680    fn malformed_interval_names_error_instead_of_panicking() {
1681        // Regression: the generic number was pulled out of the string with
1682        // `.expect("Failed to parse number")`, so any name with no digits in it
1683        // panicked out of a Result-returning public API.
1684        for bad in ["", "X", "perfect", "?!", "MM"] {
1685            assert!(
1686                Interval::from_name(bad).is_err(),
1687                "Interval::from_name({bad:?}) should be an error"
1688            );
1689        }
1690    }
1691
1692    #[test]
1693    fn interval_between_microtonal_pitches_keeps_the_cent_shift() {
1694        let c1 = pitch("C1");
1695        let mut half_sharp = pitch("C1");
1696        half_sharp.set_accidental(
1697            crate::pitch::Accidental::new("half-sharp").expect("half-sharp is an accidental"),
1698        );
1699
1700        let quarter_tone = Interval::between_pitches(&c1, &half_sharp).unwrap();
1701        assert_eq!(quarter_tone.semitones(), 0.5);
1702        assert_eq!(quarter_tone.cents(), 50.0);
1703        assert_eq!(quarter_tone.directed_name(), "A1");
1704        assert_eq!(quarter_tone.diatonic_interval_cent_shift(), -50.0);
1705        assert_eq!(quarter_tone.to_string(), "A1 (-50c)");
1706        assert!(quarter_tone.pythagorean_ratio().is_err());
1707    }
1708
1709    #[test]
1710    fn interval_cents_carry_a_pitch_microtone() {
1711        let c4 = pitch("C4");
1712        let mut d4 = pitch("D4");
1713        d4.set_microtone_cents(30.0).unwrap();
1714
1715        let interval = Interval::between_pitches(&c4, &d4).unwrap();
1716        assert_eq!(interval.cents(), 230.0);
1717        assert_eq!(interval.to_string(), "M2 (+30c)");
1718    }
1719
1720    #[test]
1721    fn interval_from_string_has_expected_chromatic() {
1722        let interval = Interval::from_name("M3").unwrap();
1723        assert_eq!(interval.chromatic.semitones, 4.0);
1724        assert!(!interval.implicit_diatonic);
1725    }
1726
1727    #[test]
1728    fn interval_parser_accepts_direction_words_and_ordinals() {
1729        let descending = Interval::from_name("Descending Perfect Twelfth").unwrap();
1730        assert_eq!(descending.semitones(), -19.0);
1731        assert_eq!(descending.generic_number(), -5);
1732
1733        let ascending = Interval::from_name("ascending Major Second").unwrap();
1734        assert_eq!(ascending.semitones(), 2.0);
1735        assert_eq!(ascending.generic_number(), 2);
1736
1737        let major_third = Interval::from_name("Major Third").unwrap();
1738        assert_eq!(major_third.semitones(), 4.0);
1739        assert_eq!(major_third.generic_number(), 3);
1740    }
1741
1742    #[test]
1743    fn interval_from_int_is_implicit_diatonic() {
1744        let interval = Interval::from_semitones(1).unwrap();
1745        assert!(interval.implicit_diatonic);
1746        assert_eq!(interval.chromatic.semitones, 1.0);
1747    }
1748
1749    #[test]
1750    fn interval_between_pitches() {
1751        let c4 = pitch("C4");
1752        let g4 = pitch("G4");
1753        let interval = Interval::between(c4, g4).unwrap();
1754        assert_eq!(interval.chromatic.semitones, 7.0);
1755        assert_eq!(interval.generic().staff_distance(), 4);
1756    }
1757
1758    #[test]
1759    fn interval_transpose_pitch() {
1760        let c4 = pitch("C4");
1761        let m3 = Interval::from_name("m3").unwrap();
1762        let out = m3.transpose_pitch(&c4).unwrap();
1763        assert_eq!(out.name_with_octave(), "Eb4");
1764    }
1765
1766    #[test]
1767    fn interval_transpose_pitch_in_place() {
1768        let mut c4 = pitch("C4");
1769        Interval::from_name("M2")
1770            .unwrap()
1771            .transpose_pitch_in_place(&mut c4)
1772            .unwrap();
1773        assert_eq!(c4.name_with_octave(), "D4");
1774    }
1775
1776    #[test]
1777    fn compact_names_match_music21() {
1778        let cases = [
1779            ("P5", "P5", "P5", "P5", "P5"),
1780            ("M3", "M3", "M3", "M3", "M3"),
1781            ("m-6", "m6", "m6", "m6", "m-6"),
1782            ("AA4", "AA4", "AA4", "AA4", "AA4"),
1783            ("d8", "d8", "d1", "d8", "d8"),
1784            ("P8", "P8", "P1", "P8", "P8"),
1785            ("M9", "M9", "M2", "M2", "M9"),
1786            ("P-5", "P5", "P5", "P5", "P-5"),
1787            ("P15", "P15", "P1", "P8", "P15"),
1788        ];
1789        for (input, short, simple, semi_simple, directed) in cases {
1790            let interval = Interval::from_name(input).unwrap();
1791            assert_eq!(interval.short_name(), short, "{input}");
1792            assert_eq!(interval.simple_name(), simple, "{input}");
1793            assert_eq!(interval.semi_simple_name(), semi_simple, "{input}");
1794            assert_eq!(interval.directed_name(), directed, "{input}");
1795        }
1796    }
1797
1798    #[test]
1799    fn complement_and_interval_class_match_music21() {
1800        let cases = [
1801            ("P5", "P4", 5),
1802            ("M3", "m6", 4),
1803            ("m-6", "M3", 4),
1804            ("AA4", "dd5", 5),
1805            ("d8", "A1", 1),
1806            ("P8", "P1", 0),
1807            ("P1", "P8", 0),
1808            ("A1", "d8", 1),
1809            ("M9", "m7", 2),
1810            ("m2", "M7", 1),
1811            ("P15", "P1", 0),
1812        ];
1813        for (input, complement, interval_class) in cases {
1814            let interval = Interval::from_name(input).unwrap();
1815            assert_eq!(
1816                interval.complement().unwrap().short_name(),
1817                complement,
1818                "{input}"
1819            );
1820            assert_eq!(interval.interval_class(), interval_class, "{input}");
1821        }
1822    }
1823
1824    #[test]
1825    fn step_skip_and_consonance_match_music21() {
1826        let cases = [
1827            ("P5", false, true, true, false, false),
1828            ("M3", false, true, true, false, false),
1829            ("AA4", false, true, false, false, false),
1830            ("d8", false, true, false, false, false),
1831            ("P8", false, true, true, false, false),
1832            ("P1", false, false, true, false, false),
1833            ("A1", true, false, false, false, true),
1834            ("M9", false, true, false, false, false),
1835            ("m2", true, false, false, true, true),
1836        ];
1837        for (input, step, skip, consonant, diatonic_step, chromatic_step) in cases {
1838            let interval = Interval::from_name(input).unwrap();
1839            assert_eq!(interval.is_step(), step, "{input} step");
1840            assert_eq!(interval.is_skip(), skip, "{input} skip");
1841            assert_eq!(interval.is_consonant(), consonant, "{input} consonant");
1842            assert_eq!(
1843                interval.is_diatonic_step(),
1844                diatonic_step,
1845                "{input} diatonic"
1846            );
1847            assert_eq!(
1848                interval.is_chromatic_step(),
1849                chromatic_step,
1850                "{input} chromatic"
1851            );
1852        }
1853    }
1854
1855    #[test]
1856    fn sum_and_difference_match_music21() {
1857        fn intervals(names: &[&str]) -> Vec<Interval> {
1858            names
1859                .iter()
1860                .map(|name| Interval::from_name(*name).unwrap())
1861                .collect()
1862        }
1863        assert_eq!(
1864            Interval::sum(&intervals(&["A2", "P5"]))
1865                .unwrap()
1866                .short_name(),
1867            "A6"
1868        );
1869        assert_eq!(
1870            Interval::sum(&intervals(&["P5", "m2"]))
1871                .unwrap()
1872                .short_name(),
1873            "m6"
1874        );
1875        assert_eq!(
1876            Interval::sum(&intervals(&["W", "W", "H", "W", "W", "W", "H"]))
1877                .unwrap()
1878                .short_name(),
1879            "P8"
1880        );
1881        assert_eq!(
1882            Interval::sum(&intervals(&["P5", "P-4"]))
1883                .unwrap()
1884                .directed_name(),
1885            "M2"
1886        );
1887        assert!(Interval::sum(&[]).is_err());
1888
1889        let cases = [
1890            (&["P5", "M3"][..], "m3"),
1891            (&["P4", "d3"][..], "A2"),
1892            (&["M6", "m2", "m2"][..], "AA4"),
1893            (&["P4", "M-2"][..], "P5"),
1894            (&["A2", "A2"][..], "P1"),
1895            (&["P8", "A1"][..], "d8"),
1896        ];
1897        for (names, expected) in cases {
1898            let difference = Interval::difference(&intervals(names)).unwrap();
1899            assert_eq!(difference.short_name(), expected, "{names:?}");
1900        }
1901        let descending_unison = Interval::difference(&intervals(&["P5", "A5"])).unwrap();
1902        assert_eq!(descending_unison.directed_name(), "d1");
1903        assert_eq!(descending_unison.semitones(), -1.0);
1904        assert!(Interval::difference(&[]).is_err());
1905    }
1906
1907    #[test]
1908    fn interval_pythagorean_ratio() {
1909        let ratio = Interval::from_name("P5")
1910            .unwrap()
1911            .pythagorean_ratio()
1912            .unwrap();
1913        assert_eq!(ratio, FractionType::new(3, 2));
1914    }
1915
1916    #[test]
1917    fn a_spelled_interval_is_a_place_on_the_chain_of_fifths() {
1918        let monzo = |name: &str| {
1919            Interval::from_name(name)
1920                .unwrap()
1921                .pythagorean_monzo()
1922                .unwrap()
1923        };
1924        assert_eq!(monzo("M3").to_string(), "[-6 4⟩");
1925        assert_eq!(monzo("A1").to_string(), "[-11 7⟩");
1926        assert_eq!(monzo("m2").to_string(), "[8 -5⟩");
1927        assert_eq!(monzo("P8").to_string(), "[1⟩");
1928        let fifth_down = Interval::from_name("P5").unwrap().reversed().unwrap();
1929        assert_eq!(
1930            fifth_down.pythagorean_ratio().unwrap(),
1931            FractionType::new(2, 3)
1932        );
1933        // Twenty-one fifths up is far past a ratio of two `i32`s, but not
1934        // past a monzo.
1935        let far = Interval::from_name("AAA4").unwrap();
1936        assert_eq!(far.pythagorean_monzo().unwrap().exponent_of(3).unwrap(), 20);
1937        assert!(far.pythagorean_ratio().is_err());
1938    }
1939
1940    #[test]
1941    fn interval_inverts_oblique_unison() {
1942        let unison = Interval::from_name("P1").unwrap();
1943        let inverted = unison.inversion().unwrap();
1944
1945        assert_eq!(inverted.semitones(), 0.0);
1946        assert_eq!(inverted.generic_number(), 1);
1947    }
1948    #[test]
1949    fn specifier_case_matters_only_for_major_versus_minor() {
1950        // Verified against music21: it accepts either case for every specifier
1951        // letter, and m/M is the sole pair where case changes the interval.
1952        for (lower, upper) in [
1953            ("p5", "P5"),
1954            ("a2", "A2"),
1955            ("d5", "D5"),
1956            ("aa2", "AA2"),
1957            ("dd5", "DD5"),
1958            ("aaa2", "AAA2"),
1959            ("ddd5", "DDD5"),
1960        ] {
1961            let a = Interval::from_name(lower).expect("lowercase parses");
1962            let b = Interval::from_name(upper).expect("uppercase parses");
1963            assert_eq!(a.semitones(), b.semitones(), "{lower} vs {upper}");
1964            assert_eq!(a.name(), b.name(), "{lower} vs {upper}");
1965        }
1966
1967        // The carve-out: these must stay different.
1968        let minor = Interval::from_name("m3").expect("m3 parses");
1969        let major = Interval::from_name("M3").expect("M3 parses");
1970        assert_eq!(minor.semitones(), 3.0);
1971        assert_eq!(major.semitones(), 4.0);
1972    }
1973
1974    #[test]
1975    fn an_unknown_specifier_errors_instead_of_panicking() {
1976        for name in ["Q5", "x3", "5", "zz2"] {
1977            assert!(
1978                Interval::from_name(name).is_err(),
1979                "{name:?} should be rejected, not panic"
1980            );
1981        }
1982    }
1983
1984    #[test]
1985    fn a_hyphen_anywhere_makes_an_interval_name_descending() {
1986        // Verified against music21: it parses every form below identically,
1987        // so these assertions pin parity rather than a local accident. The
1988        // prefix form is this crate's convention; "M-2" is where music21's
1989        // directedName puts the hyphen.
1990        for name in ["-M2", "M-2"] {
1991            let interval = Interval::from_name(name).expect("descending name parses");
1992            assert_eq!(interval.semitones(), -2.0, "{name}");
1993            assert_eq!(interval.generic_number(), -2, "{name}");
1994        }
1995
1996        // Count and position are both irrelevant: hyphens do not cancel, so
1997        // repeating one leaves the interval descending rather than flipping it
1998        // back. music21 agrees on both spellings.
1999        for name in ["--M2", "-M-2"] {
2000            let interval = Interval::from_name(name).expect("repeated hyphen parses");
2001            assert_eq!(interval.semitones(), -2.0, "{name}");
2002        }
2003
2004        // A specifier other than major is unaffected by where the hyphen sits.
2005        assert_eq!(
2006            Interval::from_name("d-5").expect("d-5 parses").semitones(),
2007            -6.0
2008        );
2009    }
2010}