Skip to main content

music21_rs/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_accidental_or_natural(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        self.value = value;
279        Ok(())
280    }
281
282    pub(crate) fn simple_steps_and_octaves(&self) -> (IntegerType, IntegerType) {
283        let undirected = self.undirected();
284        let mut octaves = undirected / 7;
285        let mut steps = undirected % 7;
286        if steps == 0 {
287            octaves -= 1;
288            steps = 7;
289        }
290        (steps, octaves)
291    }
292}
293
294impl fmt::Display for GenericInterval {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        write!(f, "{}", self.value)
297    }
298}
299
300const PLAIN_ORDINALS: [&str; 23] = [
301    "zeroth",
302    "first",
303    "second",
304    "third",
305    "fourth",
306    "fifth",
307    "sixth",
308    "seventh",
309    "eighth",
310    "ninth",
311    "tenth",
312    "eleventh",
313    "twelfth",
314    "thirteenth",
315    "fourteenth",
316    "fifteenth",
317    "sixteenth",
318    "seventeenth",
319    "eighteenth",
320    "nineteenth",
321    "twentieth",
322    "twenty-first",
323    "twenty-second",
324];
325
326/// Reads `"1st"`, `"2nd"`, `"3rd"`, `"4th"` and so on; a bare number is
327/// not an ordinal, as music21's `convertGeneric` also refuses `"1"`.
328fn numbered_ordinal(value: &str) -> Option<usize> {
329    let digits: String = value.chars().take_while(char::is_ascii_digit).collect();
330    if digits.is_empty() {
331        return None;
332    }
333    let number: usize = digits.parse().ok()?;
334    let expected = match number % 100 {
335        11..=13 => "th",
336        _ => match number % 10 {
337            1 => "st",
338            2 => "nd",
339            3 => "rd",
340            _ => "th",
341        },
342    };
343    (&value[digits.len()..] == expected).then_some(number)
344}
345
346fn strip_word_prefix<'a>(value: &'a str, word: &str) -> Option<&'a str> {
347    let head = value.get(..word.len())?;
348    if head.eq_ignore_ascii_case(word) {
349        Some(&value[word.len()..])
350    } else {
351        None
352    }
353}
354
355fn name_from_interval_number(value: IntegerType) -> String {
356    let value = value.unsigned_abs() as usize;
357    if let Some(name) = MUSICAL_ORDINAL_STRINGS.get(value) {
358        return name.clone();
359    }
360    let suffix = match value % 100 {
361        11..=13 => "th",
362        _ => match value % 10 {
363            1 => "st",
364            2 => "nd",
365            3 => "rd",
366            _ => "th",
367        },
368    };
369    format!("{value}{suffix}")
370}
371
372/// music21's `convertGeneric` for a number: the number itself, since the
373/// sign already carries the direction.
374pub fn convert_generic(value: IntegerType) -> IntegerType {
375    value
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn a_zeroth_is_not_an_interval() {
384        assert!(GenericInterval::new(0).is_err());
385        assert_eq!(GenericInterval::new(-3).unwrap().value(), -3);
386    }
387
388    #[test]
389    fn generic_interval_direction_and_simple_values() {
390        let descending_ninth = GenericInterval::from_int(-9).unwrap();
391        assert_eq!(descending_ninth.simple_undirected(), 2);
392        assert_eq!(descending_ninth.simple_directed(), -2);
393        assert!(matches!(
394            descending_ninth.direction(),
395            Direction::Descending
396        ));
397    }
398
399    #[test]
400    fn generic_interval_octave_and_unison_edges() {
401        let octave = GenericInterval::from_int(8).unwrap();
402        assert_eq!(octave.simple_undirected(), 1);
403        assert_eq!(octave.semi_simple_undirected(), 8);
404        assert_eq!(octave.semi_simple_directed(), 8);
405        assert_eq!(octave.undirected_octaves(), 1);
406        assert_eq!(GenericInterval::from_int(-15).unwrap().octaves(), -2);
407        assert_eq!(
408            GenericInterval::from_int(-9)
409                .unwrap()
410                .semi_simple_directed(),
411            -2
412        );
413        let unison = GenericInterval::from_int(1).unwrap();
414        assert!(unison.is_unison());
415        assert_eq!(unison.direction(), Direction::Oblique);
416        assert_eq!(unison.staff_distance(), 0);
417        assert!(GenericInterval::from_int(0).is_err());
418    }
419
420    #[test]
421    fn generic_interval_mod7_and_complement() {
422        let descending_third = GenericInterval::from_int(-3).unwrap();
423        assert_eq!(descending_third.mod7(), 6);
424        assert_eq!(descending_third.mod7_inversion(), 6);
425        assert_eq!(descending_third.complement().value(), 6);
426        assert_eq!(descending_third.reverse().value(), 3);
427        assert_eq!(GenericInterval::from_int(1).unwrap().reverse().value(), 1);
428        assert!(GenericInterval::from_int(2).unwrap().is_step());
429        assert!(GenericInterval::from_int(-2).unwrap().is_diatonic_step());
430        assert!(GenericInterval::from_int(4).unwrap().is_skip());
431        assert!(GenericInterval::from_int(5).unwrap().is_perfectable());
432        assert!(!GenericInterval::from_int(10).unwrap().is_perfectable());
433    }
434
435    #[test]
436    fn generic_interval_names() {
437        assert_eq!(GenericInterval::from_int(3).unwrap().nice_name(), "Third");
438        assert_eq!(
439            GenericInterval::from_int(10).unwrap().simple_nice_name(),
440            "Third"
441        );
442        assert_eq!(
443            GenericInterval::from_int(15)
444                .unwrap()
445                .semi_simple_nice_name(),
446            "Octave"
447        );
448        assert_eq!(GenericInterval::from_int(23).unwrap().nice_name(), "23rd");
449        assert_eq!(GenericInterval::from_int(-3).unwrap().to_string(), "-3");
450    }
451
452    #[test]
453    fn generic_interval_from_name() {
454        assert_eq!(GenericInterval::from_name("Third").unwrap().value(), 3);
455        assert_eq!(
456            GenericInterval::from_name("descending fifth")
457                .unwrap()
458                .value(),
459            -5
460        );
461        assert_eq!(GenericInterval::from_name("octave").unwrap().value(), 8);
462        assert_eq!(GenericInterval::from_name("3rd").unwrap().value(), 3);
463        assert_eq!(
464            GenericInterval::from_name("Descending 2nd")
465                .unwrap()
466                .value(),
467            -2
468        );
469        assert!(GenericInterval::from_name("blah").is_err());
470        assert!(GenericInterval::from_name("zeroth").is_err());
471        assert!(GenericInterval::from_name("1").is_err());
472        assert!(GenericInterval::from_name("3nd").is_err());
473    }
474
475    #[test]
476    fn generic_transposition_keeps_the_accidental() {
477        let c_sharp = Pitch::from_name("C#4").unwrap();
478        let third = GenericInterval::from_int(3).unwrap();
479        assert_eq!(
480            third.transpose_pitch(&c_sharp).unwrap().name_with_octave(),
481            "E#4"
482        );
483        let b_flat = Pitch::from_name("B-4").unwrap();
484        assert_eq!(
485            GenericInterval::from_int(-3)
486                .unwrap()
487                .transpose_pitch(&b_flat)
488                .unwrap()
489                .name_with_octave(),
490            "G-4"
491        );
492        let no_octave = Pitch::from_name("C").unwrap();
493        let up = third.transpose_pitch(&no_octave).unwrap();
494        assert_eq!(up.name(), "E");
495        assert!(up.octave().is_none());
496    }
497
498    #[test]
499    fn key_aware_generic_transposition_reads_the_signature() {
500        let a_major = KeySignature::new(3);
501        let a = Pitch::from_name("A4").unwrap();
502        let third = GenericInterval::from_int(3).unwrap();
503        assert_eq!(
504            third
505                .transpose_pitch_key_aware(&a, Some(&a_major))
506                .unwrap()
507                .name_with_octave(),
508            "C#5"
509        );
510        let c_sharp = Pitch::from_name("C#5").unwrap();
511        assert_eq!(
512            third
513                .transpose_pitch_key_aware(&c_sharp, Some(&a_major))
514                .unwrap()
515                .name_with_octave(),
516            "E5"
517        );
518        let g_major = KeySignature::new(1);
519        let step = GenericInterval::from_int(2).unwrap();
520        let f_natural = Pitch::from_name("F4").unwrap();
521        assert_eq!(
522            step.transpose_pitch_key_aware(&f_natural, Some(&g_major))
523                .unwrap()
524                .name_with_octave(),
525            "G-4"
526        );
527        let e = Pitch::from_name("E4").unwrap();
528        assert_eq!(
529            step.transpose_pitch_key_aware(&e, Some(&g_major))
530                .unwrap()
531                .name_with_octave(),
532            "F#4"
533        );
534    }
535}