Skip to main content

music21_rs/interval/
specifier.rs

1//! The quality of an interval: music21's `interval.Specifier`.
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::{
7    defaults::IntegerType,
8    error::{Error, Result},
9};
10
11/// An interval quality, numbered as music21 numbers its `Specifier` enum.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14#[must_use]
15pub enum Specifier {
16    /// `P`
17    Perfect = 1,
18    /// `M`
19    Major = 2,
20    /// `m`
21    Minor = 3,
22    /// `A`
23    Augmented = 4,
24    /// `d`
25    Diminished = 5,
26    /// `AA`
27    DoubleAugmented = 6,
28    /// `dd`
29    DoubleDiminished = 7,
30    /// `AAA`
31    TripleAugmented = 8,
32    /// `ddd`
33    TripleDiminished = 9,
34    /// `AAAA`
35    QuadrupleAugmented = 10,
36    /// `dddd`
37    QuadrupleDiminished = 11,
38}
39
40impl Specifier {
41    /// Every specifier, in music21's numbering order.
42    pub const ALL: [Specifier; 11] = [
43        Specifier::Perfect,
44        Specifier::Major,
45        Specifier::Minor,
46        Specifier::Augmented,
47        Specifier::Diminished,
48        Specifier::DoubleAugmented,
49        Specifier::DoubleDiminished,
50        Specifier::TripleAugmented,
51        Specifier::TripleDiminished,
52        Specifier::QuadrupleAugmented,
53        Specifier::QuadrupleDiminished,
54    ];
55
56    /// music21's number for the specifier, `1` for perfect through `11`.
57    pub fn value(self) -> IntegerType {
58        self as IntegerType
59    }
60
61    /// The specifier with music21's number, if there is one.
62    pub fn from_value(value: IntegerType) -> Result<Self> {
63        Self::ALL
64            .into_iter()
65            .find(|specifier| specifier.value() == value)
66            .ok_or_else(|| Error::Interval(format!("{value} is not a valid Specifier")))
67    }
68
69    /// Returns a human-friendly name for the specifier, music21's `niceName`.
70    pub fn nice_name(&self) -> String {
71        match self {
72            Specifier::Perfect => "Perfect".to_string(),
73            Specifier::Major => "Major".to_string(),
74            Specifier::Minor => "Minor".to_string(),
75            Specifier::Augmented => "Augmented".to_string(),
76            Specifier::Diminished => "Diminished".to_string(),
77            Specifier::DoubleAugmented => "Doubly-Augmented".to_string(),
78            Specifier::DoubleDiminished => "Doubly-Diminished".to_string(),
79            Specifier::TripleAugmented => "Triply-Augmented".to_string(),
80            Specifier::TripleDiminished => "Triply-Diminished".to_string(),
81            Specifier::QuadrupleAugmented => "Quadruply-Augmented".to_string(),
82            Specifier::QuadrupleDiminished => "Quadruply-Diminished".to_string(),
83        }
84    }
85
86    /// Parses a specifier the way music21's `parseSpecifier` does: a prefix
87    /// such as `"P"`, `"m"` or `"AA"`, or a spelled-out name such as
88    /// `"Perfect"` or `"Doubly-Augmented"`.
89    ///
90    /// Case is significant only for `m`/`M`: minor and major are the one pair
91    /// where both spellings already denote different intervals. Every other
92    /// specifier accepts either case, which is exactly the distinction music21
93    /// draws — `a2`/`A2`, `d5`/`D5`, `p5`/`P5` and the doubled and tripled
94    /// forms all parse there, while `m3` and `M3` stay distinct. Lowercasing
95    /// the input wholesale would silently turn every major third minor.
96    pub fn from_name(name: &str) -> Result<Self> {
97        match name {
98            "M" => return Ok(Specifier::Major),
99            "m" => return Ok(Specifier::Minor),
100            _ => {}
101        }
102        let lower = name.to_ascii_lowercase();
103        if let Some(found) = Self::ALL.into_iter().find(|specifier| {
104            specifier.prefix().to_ascii_lowercase() == lower
105                || specifier.nice_name().to_ascii_lowercase() == lower
106        }) {
107            return Ok(found);
108        }
109        match lower.as_str() {
110            "double augmented" => Ok(Specifier::DoubleAugmented),
111            "double diminished" => Ok(Specifier::DoubleDiminished),
112            "triple augmented" => Ok(Specifier::TripleAugmented),
113            "triple diminished" => Ok(Specifier::TripleDiminished),
114            "quadruple augmented" => Ok(Specifier::QuadrupleAugmented),
115            "quadruple diminished" => Ok(Specifier::QuadrupleDiminished),
116            _ => Err(Error::Interval(format!(
117                "Cannot find a match for value: '{name}'"
118            ))),
119        }
120    }
121
122    pub(crate) fn parse(remain: &str) -> Result<Self> {
123        Self::from_name(remain)
124    }
125
126    /// Returns the prefix music21 writes before the interval number, such as
127    /// `"P"`, `"m"` or `"AA"`.
128    pub fn prefix(self) -> &'static str {
129        match self {
130            Specifier::Perfect => "P",
131            Specifier::Major => "M",
132            Specifier::Minor => "m",
133            Specifier::Augmented => "A",
134            Specifier::Diminished => "d",
135            Specifier::DoubleAugmented => "AA",
136            Specifier::DoubleDiminished => "dd",
137            Specifier::TripleAugmented => "AAA",
138            Specifier::TripleDiminished => "ddd",
139            Specifier::QuadrupleAugmented => "AAAA",
140            Specifier::QuadrupleDiminished => "dddd",
141        }
142    }
143
144    /// The specifier of the inverted interval: major becomes minor,
145    /// augmented becomes diminished, perfect stays perfect.
146    pub fn inversion(&self) -> Self {
147        match self {
148            Specifier::Perfect => Specifier::Perfect,
149            Specifier::Major => Specifier::Minor,
150            Specifier::Minor => Specifier::Major,
151            Specifier::Augmented => Specifier::Diminished,
152            Specifier::Diminished => Specifier::Augmented,
153            Specifier::DoubleAugmented => Specifier::DoubleDiminished,
154            Specifier::DoubleDiminished => Specifier::DoubleAugmented,
155            Specifier::TripleAugmented => Specifier::TripleDiminished,
156            Specifier::TripleDiminished => Specifier::TripleAugmented,
157            Specifier::QuadrupleAugmented => Specifier::QuadrupleDiminished,
158            Specifier::QuadrupleDiminished => Specifier::QuadrupleAugmented,
159        }
160    }
161
162    /// How many semitones above perfect the specifier lies. Major and minor
163    /// cannot be compared to perfect and are an error, as in music21.
164    pub fn semitones_above_perfect(&self) -> Result<IntegerType> {
165        match self {
166            Specifier::Perfect => Ok(0),
167            Specifier::Augmented => Ok(1),
168            Specifier::DoubleAugmented => Ok(2),
169            Specifier::TripleAugmented => Ok(3),
170            Specifier::QuadrupleAugmented => Ok(4),
171            Specifier::Diminished => Ok(-1),
172            Specifier::DoubleDiminished => Ok(-2),
173            Specifier::TripleDiminished => Ok(-3),
174            Specifier::QuadrupleDiminished => Ok(-4),
175            _ => Err(Error::Interval(format!(
176                "<Specifier.{}> cannot be compared to Perfect",
177                self.python_name()
178            ))),
179        }
180    }
181
182    /// How many semitones above major the specifier lies. Perfect cannot be
183    /// compared to major and is an error, as in music21.
184    pub fn semitones_above_major(&self) -> Result<IntegerType> {
185        match self {
186            Specifier::Major => Ok(0),
187            Specifier::Minor => Ok(-1),
188            Specifier::Augmented => Ok(1),
189            Specifier::DoubleAugmented => Ok(2),
190            Specifier::TripleAugmented => Ok(3),
191            Specifier::QuadrupleAugmented => Ok(4),
192            Specifier::Diminished => Ok(-2),
193            Specifier::DoubleDiminished => Ok(-3),
194            Specifier::TripleDiminished => Ok(-4),
195            Specifier::QuadrupleDiminished => Ok(-5),
196            _ => Err(Error::Interval(format!(
197                "<Specifier.{}> cannot be compared to Major",
198                self.python_name()
199            ))),
200        }
201    }
202
203    /// music21's enum member name, `PERFECT`, `DBLAUG`, `QUADDIM`.
204    pub fn python_name(self) -> &'static str {
205        match self {
206            Specifier::Perfect => "PERFECT",
207            Specifier::Major => "MAJOR",
208            Specifier::Minor => "MINOR",
209            Specifier::Augmented => "AUGMENTED",
210            Specifier::Diminished => "DIMINISHED",
211            Specifier::DoubleAugmented => "DBLAUG",
212            Specifier::DoubleDiminished => "DBLDIM",
213            Specifier::TripleAugmented => "TRPAUG",
214            Specifier::TripleDiminished => "TRPDIM",
215            Specifier::QuadrupleAugmented => "QUADAUG",
216            Specifier::QuadrupleDiminished => "QUADDIM",
217        }
218    }
219}
220
221impl fmt::Display for Specifier {
222    /// The prefix, as `str(Specifier.PERFECT)` is `P` in music21.
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        f.write_str(self.prefix())
225    }
226}
227
228impl FromStr for Specifier {
229    type Err = Error;
230
231    fn from_str(name: &str) -> Result<Self> {
232        Self::from_name(name)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_specifier_nice_name() {
242        assert_eq!(Specifier::Perfect.nice_name(), "Perfect");
243        assert_eq!(Specifier::Major.nice_name(), "Major");
244        assert_eq!(Specifier::Minor.nice_name(), "Minor");
245        assert_eq!(Specifier::Augmented.nice_name(), "Augmented");
246        assert_eq!(Specifier::Diminished.nice_name(), "Diminished");
247        assert_eq!(Specifier::DoubleAugmented.nice_name(), "Doubly-Augmented");
248        assert_eq!(Specifier::DoubleDiminished.nice_name(), "Doubly-Diminished");
249        assert_eq!(Specifier::TripleAugmented.nice_name(), "Triply-Augmented");
250        assert_eq!(Specifier::TripleDiminished.nice_name(), "Triply-Diminished");
251        assert_eq!(
252            Specifier::QuadrupleAugmented.nice_name(),
253            "Quadruply-Augmented"
254        );
255        assert_eq!(
256            Specifier::QuadrupleDiminished.nice_name(),
257            "Quadruply-Diminished"
258        );
259    }
260
261    #[test]
262    fn test_specifier_semitones_above_perfect() {
263        assert_eq!(Specifier::Perfect.semitones_above_perfect().unwrap(), 0);
264        assert_eq!(Specifier::Augmented.semitones_above_perfect().unwrap(), 1);
265        assert_eq!(
266            Specifier::DoubleDiminished
267                .semitones_above_perfect()
268                .unwrap(),
269            -2
270        );
271        assert!(Specifier::Major.semitones_above_perfect().is_err());
272    }
273
274    #[test]
275    fn test_specifier_semitones_above_major() {
276        assert_eq!(Specifier::Major.semitones_above_major().unwrap(), 0);
277        assert_eq!(Specifier::Minor.semitones_above_major().unwrap(), -1);
278        assert_eq!(Specifier::Diminished.semitones_above_major().unwrap(), -2);
279        assert!(Specifier::Perfect.semitones_above_major().is_err());
280    }
281
282    #[test]
283    fn names_numbers_and_prefixes_round_trip() {
284        for specifier in Specifier::ALL {
285            assert_eq!(Specifier::from_value(specifier.value()).unwrap(), specifier);
286            assert_eq!(Specifier::from_name(specifier.prefix()).unwrap(), specifier);
287            assert_eq!(
288                Specifier::from_name(&specifier.nice_name()).unwrap(),
289                specifier
290            );
291            assert_eq!(specifier.to_string(), specifier.prefix());
292        }
293        assert_eq!(Specifier::from_name("perfect").unwrap(), Specifier::Perfect);
294        assert_eq!(
295            Specifier::from_name("dd").unwrap(),
296            Specifier::DoubleDiminished
297        );
298        assert_eq!("M".parse::<Specifier>().unwrap(), Specifier::Major);
299        assert!(Specifier::from_name("xyz").is_err());
300        assert!(Specifier::from_value(12).is_err());
301    }
302}