Skip to main content

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