Skip to main content

music21_rs/interval/
diatonicinterval.rs

1//! A generic interval with a quality: music21's `interval.DiatonicInterval`.
2
3use std::fmt;
4
5use crate::{
6    defaults::{FloatType, IntegerType, UnsignedIntegerType},
7    error::{Error, Result},
8    pitch::Pitch,
9};
10
11use super::{
12    GenericInterval, chromaticinterval::ChromaticInterval, direction::Direction,
13    specifier::Specifier,
14};
15
16/// A quality and a generic interval together: `M3`, `-P5`, `dd7`.
17#[derive(Clone, Debug)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19#[must_use]
20pub struct DiatonicInterval {
21    pub(crate) generic: GenericInterval,
22    pub(crate) specifier: Specifier,
23}
24
25impl PartialEq for DiatonicInterval {
26    /// Two diatonic intervals are equal when their generic value, specifier
27    /// and direction agree, as in music21.
28    fn eq(&self, other: &Self) -> bool {
29        self.generic.value() == other.generic.value()
30            && self.specifier == other.specifier
31            && self.direction() == other.direction()
32    }
33}
34
35impl Eq for DiatonicInterval {}
36
37impl DiatonicInterval {
38    /// Pairs a quality with a generic interval without checking that the
39    /// pair exists; see [`Self::try_new`] for the checked form.
40    pub fn new(specifier: Specifier, generic: &GenericInterval) -> Self {
41        Self {
42            generic: generic.clone(),
43            specifier,
44        }
45    }
46
47    /// Pairs a quality with a generic interval, rejecting the combinations
48    /// music21 rejects: major and minor on a perfectable interval, perfect
49    /// on the others, and a descending perfect unison.
50    pub fn try_new(specifier: Specifier, generic: GenericInterval) -> Result<Self> {
51        let perfectable = generic.is_perfectable();
52        let mismatched = match specifier {
53            Specifier::Major | Specifier::Minor => perfectable,
54            Specifier::Perfect => !perfectable,
55            _ => false,
56        };
57        if mismatched {
58            return Err(Error::Interval(format!(
59                "Cannot create a '{} {}'",
60                specifier.nice_name(),
61                generic.nice_name()
62            )));
63        }
64        if generic.value() == -1 && specifier == Specifier::Perfect {
65            return Err(Error::Interval(
66                "There is no such thing as a descending Perfect Unison".to_owned(),
67            ));
68        }
69        Ok(Self { generic, specifier })
70    }
71
72    /// Parses a diatonic interval from a name such as `M3`, `-P5` or
73    /// `Major Third`.
74    pub fn from_name(name: &str) -> Result<Self> {
75        let (diatonic, _, _) = super::parse_interval_name(name.to_string())?;
76        Self::try_new(diatonic.specifier, diatonic.generic)
77    }
78
79    /// The quality.
80    pub fn specifier_abbreviation(&self) -> &'static str {
81        self.specifier.prefix()
82    }
83
84    /// The quality of the interval.
85    pub fn specifier(&self) -> Specifier {
86        self.specifier
87    }
88
89    /// The generic interval.
90    pub fn generic(&self) -> &GenericInterval {
91        &self.generic
92    }
93
94    /// The short name without direction, `M3`, `P5`, `dd7`.
95    pub fn name(&self) -> String {
96        format!("{}{}", self.specifier.prefix(), self.generic.undirected())
97    }
98
99    /// The spelled-out name, `Major Third`.
100    pub fn nice_name(&self) -> String {
101        format!(
102            "{} {}",
103            self.specifier.nice_name(),
104            self.generic.nice_name()
105        )
106    }
107
108    /// The short name with the direction, `M-3` for a descending third,
109    /// which is where music21 puts the hyphen.
110    pub fn directed_name(&self) -> String {
111        format!("{}{}", self.specifier.prefix(), self.generic.directed())
112    }
113
114    /// The spelled-out name with the direction, `Descending Major Third`.
115    pub fn directed_nice_name(&self) -> String {
116        format!("{}{}", self.direction_prefix(), self.nice_name())
117    }
118
119    /// The short name of the simple interval, so `M10` is `M3`.
120    pub fn simple_name(&self) -> String {
121        format!(
122            "{}{}",
123            self.specifier.prefix(),
124            self.generic.simple_undirected()
125        )
126    }
127
128    /// The short name of the semi-simple interval, so `P15` is `P8`.
129    pub fn semi_simple_name(&self) -> String {
130        format!(
131            "{}{}",
132            self.specifier.prefix(),
133            self.generic.semi_simple_undirected()
134        )
135    }
136
137    /// The spelled-out name of the simple interval.
138    pub fn simple_nice_name(&self) -> String {
139        format!(
140            "{} {}",
141            self.specifier.nice_name(),
142            self.generic.simple_nice_name()
143        )
144    }
145
146    /// The spelled-out name of the semi-simple interval.
147    pub fn semi_simple_nice_name(&self) -> String {
148        format!(
149            "{} {}",
150            self.specifier.nice_name(),
151            self.generic.semi_simple_nice_name()
152        )
153    }
154
155    /// The short directed name of the simple interval, `M-3` for a
156    /// descending tenth.
157    pub fn directed_simple_name(&self) -> String {
158        format!(
159            "{}{}",
160            self.specifier.prefix(),
161            self.generic.simple_directed()
162        )
163    }
164
165    /// The short directed name of the semi-simple interval.
166    pub fn directed_semi_simple_name(&self) -> String {
167        format!(
168            "{}{}",
169            self.specifier.prefix(),
170            self.generic.semi_simple_directed()
171        )
172    }
173
174    /// The spelled-out directed name of the simple interval.
175    pub fn directed_simple_nice_name(&self) -> String {
176        format!("{}{}", self.direction_prefix(), self.simple_nice_name())
177    }
178
179    /// The spelled-out directed name of the semi-simple interval.
180    pub fn directed_semi_simple_nice_name(&self) -> String {
181        format!(
182            "{}{}",
183            self.direction_prefix(),
184            self.semi_simple_nice_name()
185        )
186    }
187
188    /// The quality's spelled-out name alone, `Major`.
189    pub fn specific_name(&self) -> String {
190        self.specifier.nice_name()
191    }
192
193    /// Ascending, descending, or oblique. A unison takes its direction from
194    /// the quality: an augmented unison ascends and a diminished one
195    /// descends, as music21 reads them.
196    pub fn direction(&self) -> Direction {
197        if self.generic.undirected() != 1 {
198            return self.generic.direction();
199        }
200        match self.specifier {
201            Specifier::Diminished
202            | Specifier::DoubleDiminished
203            | Specifier::TripleDiminished
204            | Specifier::QuadrupleDiminished => Direction::Descending,
205            Specifier::Augmented
206            | Specifier::DoubleAugmented
207            | Specifier::TripleAugmented
208            | Specifier::QuadrupleAugmented => Direction::Ascending,
209            Specifier::Perfect | Specifier::Major | Specifier::Minor => Direction::Oblique,
210        }
211    }
212
213    /// The size in cents, signed, from the chromatic equivalent.
214    pub fn cents(&self) -> Result<FloatType> {
215        Ok(self.get_chromatic()?.cents())
216    }
217
218    /// The name of the simple interval measured upward, so a descending
219    /// major third is `m6`.
220    pub fn mod7(&self) -> String {
221        if self.direction() == Direction::Descending {
222            self.mod7_inversion()
223        } else {
224            self.simple_name()
225        }
226    }
227
228    /// The name of the inversion of the simple interval: `M3` becomes `m6`.
229    pub fn mod7_inversion(&self) -> String {
230        format!(
231            "{}{}",
232            self.specifier.inversion().prefix(),
233            self.generic.mod7_inversion()
234        )
235    }
236
237    /// Whether the interval is a second in either direction.
238    pub fn is_step(&self) -> bool {
239        self.generic.is_step()
240    }
241
242    /// The same as [`Self::is_step`].
243    pub fn is_diatonic_step(&self) -> bool {
244        self.generic.is_diatonic_step()
245    }
246
247    /// Whether the interval is larger than a second.
248    pub fn is_skip(&self) -> bool {
249        self.generic.is_skip()
250    }
251
252    /// Whether the generic interval takes perfect qualities.
253    pub fn is_perfectable(&self) -> bool {
254        self.generic.is_perfectable()
255    }
256
257    /// The semitone count the quality and generic interval add up to.
258    pub fn get_chromatic(&self) -> Result<ChromaticInterval> {
259        let octave_offset = (self.generic.staff_distance().abs() / 7) as UnsignedIntegerType;
260        let semitones_start =
261            semitones_generic(self.generic.simple_undirected() as UnsignedIntegerType)?;
262        let semitones_adjust = if self.generic.is_perfectable() {
263            self.specifier.semitones_above_perfect()?
264        } else {
265            self.specifier.semitones_above_major()?
266        };
267        let mut semitones: IntegerType =
268            ((octave_offset * 12 + semitones_start) as IntegerType) + semitones_adjust;
269        if self.generic.direction() == Direction::Descending {
270            semitones *= -1;
271        }
272        Ok(ChromaticInterval::from_int(semitones))
273    }
274
275    /// The same interval in the other direction. A unison keeps its
276    /// generic value and inverts its quality instead, since an augmented
277    /// unison downward is a diminished one.
278    pub fn reverse(&self) -> Self {
279        if self.generic.undirected() == 1 {
280            Self::new(self.specifier.inversion(), &self.generic)
281        } else {
282            Self::new(self.specifier, &self.generic.reverse())
283        }
284    }
285
286    /// Moves a pitch by the interval, spelling the result by the quality.
287    pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
288        let interval =
289            super::Interval::from_diatonic_and_chromatic(self.clone(), self.get_chromatic()?)?;
290        interval.transpose_pitch_with_options(pitch, false, Some(4))
291    }
292
293    fn direction_prefix(&self) -> &'static str {
294        match self.direction() {
295            Direction::Descending => "Descending ",
296            Direction::Ascending => "Ascending ",
297            Direction::Oblique => "",
298        }
299    }
300}
301
302impl fmt::Display for DiatonicInterval {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        f.write_str(&self.name())
305    }
306}
307
308fn semitones_generic(r#in: UnsignedIntegerType) -> Result<UnsignedIntegerType> {
309    match r#in {
310        1 => Ok(0),
311        2 => Ok(2),
312        3 => Ok(4),
313        4 => Ok(5),
314        5 => Ok(7),
315        6 => Ok(9),
316        7 => Ok(11),
317        _ => Err(Error::Interval(format!("Invalid diatonic interval: {in}"))),
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn a_diatonic_interval_classifies_itself_and_transposes() {
327        use crate::pitch::Pitch;
328
329        let third = DiatonicInterval::from_name("M3").unwrap();
330        assert_eq!(third.specifier(), Specifier::Major);
331        assert_eq!(third.generic().value(), 3);
332        assert!(!third.is_perfectable());
333        assert!(!third.is_step());
334        assert!(!third.is_diatonic_step());
335        assert!(third.is_skip());
336        assert_eq!(
337            third
338                .transpose_pitch(&Pitch::from_name("C4").unwrap())
339                .unwrap()
340                .name_with_octave(),
341            "E4"
342        );
343        let second = DiatonicInterval::from_name("M2").unwrap();
344        assert!(second.is_step());
345        assert!(second.is_diatonic_step());
346        assert!(!second.is_skip());
347        assert!(DiatonicInterval::from_name("P5").unwrap().is_perfectable());
348    }
349
350    #[test]
351    fn the_specifier_abbreviation_is_the_quality_prefix() {
352        assert_eq!(
353            DiatonicInterval::from_name("M-10")
354                .unwrap()
355                .specifier_abbreviation(),
356            "M"
357        );
358        assert_eq!(
359            DiatonicInterval::from_name("P5")
360                .unwrap()
361                .specifier_abbreviation(),
362            "P"
363        );
364        assert_eq!(
365            DiatonicInterval::from_name("d4")
366                .unwrap()
367                .specifier_abbreviation(),
368            "d"
369        );
370    }
371
372    #[test]
373    fn diatonic_get_chromatic_major_third() {
374        let generic = GenericInterval::from_int(3).unwrap();
375        let diatonic = DiatonicInterval::new(Specifier::Major, &generic);
376        assert_eq!(diatonic.get_chromatic().unwrap().semitones, 4.0);
377    }
378
379    #[test]
380    fn diatonic_reverse_unison_inverts_specifier() {
381        let generic = GenericInterval::from_int(1).unwrap();
382        let diatonic = DiatonicInterval::new(Specifier::Augmented, &generic);
383        let reversed = diatonic.reverse();
384        assert_eq!(reversed.get_chromatic().unwrap().semitones, -1.0);
385    }
386
387    #[test]
388    fn diatonic_names_match_music21() {
389        let descending_tenth =
390            DiatonicInterval::new(Specifier::Major, &GenericInterval::from_int(-10).unwrap());
391        assert_eq!(descending_tenth.name(), "M10");
392        assert_eq!(descending_tenth.directed_name(), "M-10");
393        assert_eq!(descending_tenth.nice_name(), "Major Tenth");
394        assert_eq!(
395            descending_tenth.directed_nice_name(),
396            "Descending Major Tenth"
397        );
398        assert_eq!(descending_tenth.simple_name(), "M3");
399        assert_eq!(descending_tenth.directed_simple_name(), "M-3");
400        assert_eq!(descending_tenth.simple_nice_name(), "Major Third");
401        assert_eq!(
402            descending_tenth.directed_simple_nice_name(),
403            "Descending Major Third"
404        );
405        assert_eq!(descending_tenth.mod7(), "m6");
406        assert_eq!(descending_tenth.mod7_inversion(), "m6");
407        assert_eq!(descending_tenth.specific_name(), "Major");
408        assert_eq!(descending_tenth.cents().unwrap(), -1600.0);
409        assert_eq!(descending_tenth.to_string(), "M10");
410        let fifteenth =
411            DiatonicInterval::new(Specifier::Perfect, &GenericInterval::from_int(15).unwrap());
412        assert_eq!(fifteenth.semi_simple_name(), "P8");
413        assert_eq!(fifteenth.directed_semi_simple_name(), "P8");
414        assert_eq!(fifteenth.semi_simple_nice_name(), "Perfect Octave");
415        assert_eq!(
416            fifteenth.directed_semi_simple_nice_name(),
417            "Ascending Perfect Octave"
418        );
419    }
420
421    #[test]
422    fn diatonic_unison_direction_comes_from_the_quality() {
423        let unison = GenericInterval::from_int(1).unwrap();
424        assert_eq!(
425            DiatonicInterval::new(Specifier::Augmented, &unison).direction(),
426            Direction::Ascending
427        );
428        assert_eq!(
429            DiatonicInterval::new(Specifier::Diminished, &unison).direction(),
430            Direction::Descending
431        );
432        assert_eq!(
433            DiatonicInterval::new(Specifier::Perfect, &unison).direction(),
434            Direction::Oblique
435        );
436        assert_eq!(
437            DiatonicInterval::new(Specifier::Diminished, &unison).directed_nice_name(),
438            "Descending Diminished Unison"
439        );
440    }
441
442    #[test]
443    fn diatonic_try_new_rejects_impossible_pairs() {
444        let fifth = GenericInterval::from_int(5).unwrap();
445        let err = DiatonicInterval::try_new(Specifier::Major, fifth).unwrap_err();
446        assert_eq!(
447            err.to_string(),
448            "Interval error: Cannot create a 'Major Fifth'"
449        );
450        let third = GenericInterval::from_int(3).unwrap();
451        assert!(DiatonicInterval::try_new(Specifier::Perfect, third).is_err());
452        let down_unison = GenericInterval::from_int(-1).unwrap();
453        assert!(DiatonicInterval::try_new(Specifier::Perfect, down_unison).is_err());
454        assert!(
455            DiatonicInterval::try_new(Specifier::Augmented, GenericInterval::from_int(-1).unwrap())
456                .is_ok()
457        );
458        let from_name = DiatonicInterval::from_name("M-3").unwrap();
459        assert_eq!(from_name.directed_name(), "M-3");
460        assert_eq!(
461            DiatonicInterval::from_name("Major Third").unwrap(),
462            DiatonicInterval::from_name("M3").unwrap()
463        );
464    }
465}