Skip to main content

music21/interval/
genericinterval.rs

1//! The generic half of an interval: a signed staff distance in scale steps,
2//! music21's `interval.GenericInterval`.
3
4use std::fmt;
5
6use crate::{
7    common::numbertools::MUSICAL_ORDINAL_STRINGS,
8    error::{Error, Result},
9    key::KeySignature,
10    pitch::{Accidental, Pitch},
11};
12
13use super::{
14    IntegerType, diatonicinterval::DiatonicInterval, direction::Direction, specifier::Specifier,
15};
16
17/// A directed interval counted in scale steps: `3` is a third up, `-3` a
18/// third down, `1` a unison. Zero is not an interval.
19#[derive(Clone, Debug, PartialEq, Eq, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[must_use]
22pub struct GenericInterval {
23    value: IntegerType,
24}
25
26impl GenericInterval {
27    /// A generic interval of `value` steps, signed. Zero is an error, as
28    /// music21 says: "The Zeroth is not an interval".
29    pub fn new(value: IntegerType) -> Result<Self> {
30        Self::from_int(value)
31    }
32
33    /// Parses a generic interval from music21's spellings: a number, an
34    /// ordinal such as `"third"` or `"octave"`, optionally preceded by
35    /// `"descending"` or `"ascending"`.
36    pub fn from_name(name: &str) -> Result<Self> {
37        let trimmed = name.trim();
38        let (body, scale) = if let Some(rest) = strip_word_prefix(trimmed, "descending") {
39            (rest, -1)
40        } else if let Some(rest) = strip_word_prefix(trimmed, "ascending") {
41            (rest, 1)
42        } else {
43            (trimmed, 1)
44        };
45        let body = body.trim();
46        let lower = body.to_ascii_lowercase();
47        let musical = MUSICAL_ORDINAL_STRINGS
48            .iter()
49            .position(|ordinal| ordinal.to_ascii_lowercase() == lower);
50        let plain = PLAIN_ORDINALS.iter().position(|ordinal| *ordinal == lower);
51        match musical.or(plain).or_else(|| numbered_ordinal(&lower)) {
52            Some(index) if index > 0 => Self::from_int(index as IntegerType * scale),
53            _ => Err(Error::Interval(format!(
54                "Cannot convert '{name}' to an interval."
55            ))),
56        }
57    }
58
59    /// The signed step count: music21's `value` and `directed`.
60    pub fn value(&self) -> IntegerType {
61        self.value
62    }
63
64    /// The signed step count.
65    pub fn directed(&self) -> IntegerType {
66        self.value()
67    }
68
69    /// The step count without its sign.
70    pub fn undirected(&self) -> IntegerType {
71        self.value().abs()
72    }
73
74    /// Ascending, descending, or oblique for a unison.
75    pub fn direction(&self) -> Direction {
76        let directed = self.directed();
77        if directed == 1 {
78            Direction::Oblique
79        } else if directed < 0 {
80            Direction::Descending
81        } else {
82            Direction::Ascending
83        }
84    }
85
86    /// The interval within one octave, keeping the sign, so a descending
87    /// ninth is `-2` and any octave is `1`.
88    pub fn simple_directed(&self) -> IntegerType {
89        let simple_undirected = self.simple_undirected();
90        if self.direction() == Direction::Descending && simple_undirected > 1 {
91            -simple_undirected
92        } else {
93            simple_undirected
94        }
95    }
96
97    /// The interval within one octave, without the sign.
98    pub fn simple_undirected(&self) -> IntegerType {
99        self.simple_steps_and_octaves().0
100    }
101
102    /// Like [`Self::simple_directed`], but an octave stays `8` (or `-8`)
103    /// rather than folding to a unison.
104    pub fn semi_simple_directed(&self) -> IntegerType {
105        let semi_simple_undirected = self.semi_simple_undirected();
106        if self.direction() == Direction::Descending && semi_simple_undirected > 1 {
107            -semi_simple_undirected
108        } else {
109            semi_simple_undirected
110        }
111    }
112
113    /// Like [`Self::simple_undirected`], but an octave stays `8`.
114    pub fn semi_simple_undirected(&self) -> IntegerType {
115        let simple_undirected = self.simple_undirected();
116        if self.simple_steps_and_octaves().1 >= 1 && simple_undirected == 1 {
117            8
118        } else {
119            simple_undirected
120        }
121    }
122
123    /// How many whole octaves the interval spans, ignoring direction.
124    pub fn undirected_octaves(&self) -> IntegerType {
125        self.simple_steps_and_octaves().1
126    }
127
128    /// How many whole octaves the interval spans, negative when descending.
129    pub fn octaves(&self) -> IntegerType {
130        if self.direction() == Direction::Descending {
131            -self.undirected_octaves()
132        } else {
133            self.undirected_octaves()
134        }
135    }
136
137    /// The signed number of staff positions moved: a third is `2`, a
138    /// descending third `-2`, a unison `0`.
139    pub fn staff_distance(&self) -> IntegerType {
140        let directed = self.directed();
141        if directed > 0 {
142            directed - 1
143        } else {
144            directed + 1
145        }
146    }
147
148    /// The simple interval measured upward, so a descending third is `6`.
149    pub fn mod7(&self) -> IntegerType {
150        if self.direction() == Direction::Descending {
151            self.mod7_inversion()
152        } else {
153            self.simple_undirected()
154        }
155    }
156
157    /// The inversion of the simple interval: a third becomes a sixth.
158    pub fn mod7_inversion(&self) -> IntegerType {
159        9 - self.semi_simple_undirected()
160    }
161
162    /// Whether the simple interval is a unison, fourth or fifth, the ones
163    /// that take perfect rather than major and minor qualities.
164    pub fn is_perfectable(&self) -> bool {
165        matches!(self.simple_undirected(), 1 | 4 | 5)
166    }
167
168    /// Whether the interval is a second in either direction.
169    pub fn is_step(&self) -> bool {
170        self.undirected() == 2
171    }
172
173    /// The same as [`Self::is_step`], as music21 defines `isDiatonicStep`
174    /// on a generic interval.
175    pub fn is_diatonic_step(&self) -> bool {
176        self.is_step()
177    }
178
179    /// Whether the interval is larger than a second.
180    pub fn is_skip(&self) -> bool {
181        self.undirected() > 2
182    }
183
184    /// Whether the interval is a unison.
185    pub fn is_unison(&self) -> bool {
186        self.undirected() == 1
187    }
188
189    /// The spelled-out name, `Third`, `Octave`, `Fifteenth`, `23rd`.
190    pub fn nice_name(&self) -> String {
191        name_from_interval_number(self.undirected())
192    }
193
194    /// The spelled-out name of the simple interval.
195    pub fn simple_nice_name(&self) -> String {
196        name_from_interval_number(self.simple_undirected())
197    }
198
199    /// The spelled-out name of the semi-simple interval.
200    pub fn semi_simple_nice_name(&self) -> String {
201        name_from_interval_number(self.semi_simple_undirected())
202    }
203
204    /// The interval that completes the octave: a third's complement is a
205    /// sixth, whatever the direction.
206    pub fn complement(&self) -> Self {
207        Self {
208            value: self.mod7_inversion(),
209        }
210    }
211
212    /// The same interval in the other direction; a unison stays a unison.
213    pub fn reverse(&self) -> Self {
214        if self.undirected() == 1 {
215            Self { value: 1 }
216        } else {
217            Self {
218                value: self.undirected() * -self.direction().as_int(),
219            }
220        }
221    }
222
223    /// Pairs the interval with a quality.
224    pub fn get_diatonic(&self, spec: Specifier) -> DiatonicInterval {
225        DiatonicInterval::new(spec, self)
226    }
227
228    /// Moves a pitch by the staff distance, keeping its accidental, as
229    /// music21's generic `transposePitch` does: a third above `C#4` is
230    /// `E#4`.
231    pub fn transpose_pitch(&self, pitch: &Pitch) -> Result<Pitch> {
232        self.transpose_pitch_key_aware(pitch, None)
233    }
234
235    /// Moves a pitch by the staff distance, taking the accidental from the
236    /// key signature when one is given and the pitch had none of its own.
237    pub fn transpose_pitch_key_aware(
238        &self,
239        pitch: &Pitch,
240        key_signature: Option<&KeySignature>,
241    ) -> Result<Pitch> {
242        let mut out = pitch.clone();
243        let had_octave = pitch.octave().is_some();
244        let dnn = pitch.diatonic_note_number();
245        out.set_diatonic_note_number(dnn + self.staff_distance())?;
246        if let Some(key_signature) = key_signature {
247            let step_alter = key_signature
248                .accidental_by_step(pitch.step().as_char())?
249                .map_or(0.0, |accidental| accidental.alter());
250            let offset_from_key = pitch.accidental().alter() - step_alter;
251            let new_step_alter = key_signature
252                .accidental_by_step(out.step().as_char())?
253                .map_or(0.0, |accidental| accidental.alter());
254            let alter = new_step_alter + offset_from_key;
255            let accidental = if alter == 0.0 {
256                None
257            } else {
258                Some(Accidental::new(alter)?)
259            };
260            out.set_written_accidental(accidental);
261        }
262        if !had_octave {
263            out.octave_setter(None);
264        }
265        Ok(out)
266    }
267
268    pub(crate) fn from_int(value: IntegerType) -> Result<Self> {
269        let mut slf = Self { value: 1 };
270        slf.value_setter(convert_generic(value))?;
271        Ok(slf)
272    }
273
274    fn value_setter(&mut self, value: IntegerType) -> Result<()> {
275        if value == 0 {
276            return Err(Error::Interval("The Zeroth is not an interval".to_owned()));
277        }
278        // Its undirected size would be one past the largest integer.
279        if value == IntegerType::MIN {
280            return Err(Error::Interval(format!(
281                "a generic interval of {value} is too large"
282            )));
283        }
284        self.value = value;
285        Ok(())
286    }
287
288    pub(crate) fn simple_steps_and_octaves(&self) -> (IntegerType, IntegerType) {
289        let undirected = self.undirected();
290        let mut octaves = undirected / 7;
291        let mut steps = undirected % 7;
292        if steps == 0 {
293            octaves -= 1;
294            steps = 7;
295        }
296        (steps, octaves)
297    }
298}
299
300impl fmt::Display for GenericInterval {
301    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
302        write!(f, "{}", self.value)
303    }
304}
305
306const PLAIN_ORDINALS: [&str; 23] = [
307    "zeroth",
308    "first",
309    "second",
310    "third",
311    "fourth",
312    "fifth",
313    "sixth",
314    "seventh",
315    "eighth",
316    "ninth",
317    "tenth",
318    "eleventh",
319    "twelfth",
320    "thirteenth",
321    "fourteenth",
322    "fifteenth",
323    "sixteenth",
324    "seventeenth",
325    "eighteenth",
326    "nineteenth",
327    "twentieth",
328    "twenty-first",
329    "twenty-second",
330];
331
332/// Reads `"1st"`, `"2nd"`, `"3rd"`, `"4th"` and so on; a bare number is
333/// not an ordinal, as music21's `convertGeneric` also refuses `"1"`.
334fn numbered_ordinal(value: &str) -> Option<usize> {
335    let digits: String = value.chars().take_while(char::is_ascii_digit).collect();
336    if digits.is_empty() {
337        return None;
338    }
339    let number: usize = digits.parse().ok()?;
340    let expected = match number % 100 {
341        11..=13 => "th",
342        _ => match number % 10 {
343            1 => "st",
344            2 => "nd",
345            3 => "rd",
346            _ => "th",
347        },
348    };
349    (&value[digits.len()..] == expected).then_some(number)
350}
351
352fn strip_word_prefix<'a>(value: &'a str, word: &str) -> Option<&'a str> {
353    let head = value.get(..word.len())?;
354    if head.eq_ignore_ascii_case(word) {
355        Some(&value[word.len()..])
356    } else {
357        None
358    }
359}
360
361fn name_from_interval_number(value: IntegerType) -> String {
362    let value = value.unsigned_abs() as usize;
363    if let Some(name) = MUSICAL_ORDINAL_STRINGS.get(value) {
364        return name.clone();
365    }
366    let suffix = match value % 100 {
367        11..=13 => "th",
368        _ => match value % 10 {
369            1 => "st",
370            2 => "nd",
371            3 => "rd",
372            _ => "th",
373        },
374    };
375    format!("{value}{suffix}")
376}
377
378/// music21's `convertGeneric` for a number: the number itself, since the
379/// sign already carries the direction.
380pub fn convert_generic(value: IntegerType) -> IntegerType {
381    value
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn a_zeroth_is_not_an_interval() {
390        assert!(GenericInterval::new(0).is_err());
391        assert_eq!(GenericInterval::new(-3).unwrap().value(), -3);
392    }
393
394    #[test]
395    fn a_size_with_no_undirected_size_is_refused() {
396        assert!(GenericInterval::new(IntegerType::MIN).is_err());
397        assert!(GenericInterval::new(IntegerType::MIN + 1).is_ok());
398    }
399
400    #[test]
401    fn generic_interval_direction_and_simple_values() {
402        let descending_ninth = GenericInterval::from_int(-9).unwrap();
403        assert_eq!(descending_ninth.simple_undirected(), 2);
404        assert_eq!(descending_ninth.simple_directed(), -2);
405        assert!(matches!(
406            descending_ninth.direction(),
407            Direction::Descending
408        ));
409    }
410
411    #[test]
412    fn generic_interval_octave_and_unison_edges() {
413        let octave = GenericInterval::from_int(8).unwrap();
414        assert_eq!(octave.simple_undirected(), 1);
415        assert_eq!(octave.semi_simple_undirected(), 8);
416        assert_eq!(octave.semi_simple_directed(), 8);
417        assert_eq!(octave.undirected_octaves(), 1);
418        assert_eq!(GenericInterval::from_int(-15).unwrap().octaves(), -2);
419        assert_eq!(
420            GenericInterval::from_int(-9)
421                .unwrap()
422                .semi_simple_directed(),
423            -2
424        );
425        let unison = GenericInterval::from_int(1).unwrap();
426        assert!(unison.is_unison());
427        assert_eq!(unison.direction(), Direction::Oblique);
428        assert_eq!(unison.staff_distance(), 0);
429        assert!(GenericInterval::from_int(0).is_err());
430    }
431
432    #[test]
433    fn generic_interval_mod7_and_complement() {
434        let descending_third = GenericInterval::from_int(-3).unwrap();
435        assert_eq!(descending_third.mod7(), 6);
436        assert_eq!(descending_third.mod7_inversion(), 6);
437        assert_eq!(descending_third.complement().value(), 6);
438        assert_eq!(descending_third.reverse().value(), 3);
439        assert_eq!(GenericInterval::from_int(1).unwrap().reverse().value(), 1);
440        assert!(GenericInterval::from_int(2).unwrap().is_step());
441        assert!(GenericInterval::from_int(-2).unwrap().is_diatonic_step());
442        assert!(GenericInterval::from_int(4).unwrap().is_skip());
443        assert!(GenericInterval::from_int(5).unwrap().is_perfectable());
444        assert!(!GenericInterval::from_int(10).unwrap().is_perfectable());
445    }
446
447    #[test]
448    fn generic_interval_names() {
449        assert_eq!(GenericInterval::from_int(3).unwrap().nice_name(), "Third");
450        assert_eq!(
451            GenericInterval::from_int(10).unwrap().simple_nice_name(),
452            "Third"
453        );
454        assert_eq!(
455            GenericInterval::from_int(15)
456                .unwrap()
457                .semi_simple_nice_name(),
458            "Octave"
459        );
460        assert_eq!(GenericInterval::from_int(23).unwrap().nice_name(), "23rd");
461        assert_eq!(GenericInterval::from_int(-3).unwrap().to_string(), "-3");
462    }
463
464    #[test]
465    fn generic_interval_from_name() {
466        assert_eq!(GenericInterval::from_name("Third").unwrap().value(), 3);
467        assert_eq!(
468            GenericInterval::from_name("descending fifth")
469                .unwrap()
470                .value(),
471            -5
472        );
473        assert_eq!(GenericInterval::from_name("octave").unwrap().value(), 8);
474        assert_eq!(GenericInterval::from_name("3rd").unwrap().value(), 3);
475        assert_eq!(
476            GenericInterval::from_name("Descending 2nd")
477                .unwrap()
478                .value(),
479            -2
480        );
481        assert!(GenericInterval::from_name("blah").is_err());
482        assert!(GenericInterval::from_name("zeroth").is_err());
483        assert!(GenericInterval::from_name("1").is_err());
484        assert!(GenericInterval::from_name("3nd").is_err());
485    }
486
487    #[test]
488    fn generic_transposition_keeps_the_accidental() {
489        let c_sharp = Pitch::from_name("C#4").unwrap();
490        let third = GenericInterval::from_int(3).unwrap();
491        assert_eq!(
492            third.transpose_pitch(&c_sharp).unwrap().name_with_octave(),
493            "E#4"
494        );
495        let b_flat = Pitch::from_name("B-4").unwrap();
496        assert_eq!(
497            GenericInterval::from_int(-3)
498                .unwrap()
499                .transpose_pitch(&b_flat)
500                .unwrap()
501                .name_with_octave(),
502            "Gb4"
503        );
504        let no_octave = Pitch::from_name("C").unwrap();
505        let up = third.transpose_pitch(&no_octave).unwrap();
506        assert_eq!(up.name(), "E");
507        assert!(up.octave().is_none());
508    }
509
510    #[test]
511    fn key_aware_generic_transposition_reads_the_signature() {
512        let a_major = KeySignature::new(3);
513        let a = Pitch::from_name("A4").unwrap();
514        let third = GenericInterval::from_int(3).unwrap();
515        assert_eq!(
516            third
517                .transpose_pitch_key_aware(&a, Some(&a_major))
518                .unwrap()
519                .name_with_octave(),
520            "C#5"
521        );
522        let c_sharp = Pitch::from_name("C#5").unwrap();
523        assert_eq!(
524            third
525                .transpose_pitch_key_aware(&c_sharp, Some(&a_major))
526                .unwrap()
527                .name_with_octave(),
528            "E5"
529        );
530        let g_major = KeySignature::new(1);
531        let step = GenericInterval::from_int(2).unwrap();
532        let f_natural = Pitch::from_name("F4").unwrap();
533        assert_eq!(
534            step.transpose_pitch_key_aware(&f_natural, Some(&g_major))
535                .unwrap()
536                .name_with_octave(),
537            "Gb4"
538        );
539        let e = Pitch::from_name("E4").unwrap();
540        assert_eq!(
541            step.transpose_pitch_key_aware(&e, Some(&g_major))
542                .unwrap()
543                .name_with_octave(),
544            "F#4"
545        );
546    }
547}