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