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        // Compared without writing either side out: lowercasing the name and
103        // both spellings of all eleven specifiers meant up to twenty-two
104        // allocations to read `P`, and every interval built from a name
105        // comes through here.
106        if let Some(found) = Self::ALL.into_iter().find(|specifier| {
107            specifier.prefix().eq_ignore_ascii_case(name)
108                || specifier.nice_name().eq_ignore_ascii_case(name)
109        }) {
110            return Ok(found);
111        }
112        match name.to_ascii_lowercase().as_str() {
113            "double augmented" => Ok(Specifier::DoubleAugmented),
114            "double diminished" => Ok(Specifier::DoubleDiminished),
115            "triple augmented" => Ok(Specifier::TripleAugmented),
116            "triple diminished" => Ok(Specifier::TripleDiminished),
117            "quadruple augmented" => Ok(Specifier::QuadrupleAugmented),
118            "quadruple diminished" => Ok(Specifier::QuadrupleDiminished),
119            _ => Err(Error::Interval(format!(
120                "Cannot find a match for value: '{name}'"
121            ))),
122        }
123    }
124
125    pub(crate) fn parse(remain: &str) -> Result<Self> {
126        Self::from_name(remain)
127    }
128
129    /// Returns the prefix music21 writes before the interval number, such as
130    /// `"P"`, `"m"` or `"AA"`.
131    pub fn prefix(self) -> &'static str {
132        match self {
133            Specifier::Perfect => "P",
134            Specifier::Major => "M",
135            Specifier::Minor => "m",
136            Specifier::Augmented => "A",
137            Specifier::Diminished => "d",
138            Specifier::DoubleAugmented => "AA",
139            Specifier::DoubleDiminished => "dd",
140            Specifier::TripleAugmented => "AAA",
141            Specifier::TripleDiminished => "ddd",
142            Specifier::QuadrupleAugmented => "AAAA",
143            Specifier::QuadrupleDiminished => "dddd",
144        }
145    }
146
147    /// The specifier of the inverted interval: major becomes minor,
148    /// augmented becomes diminished, perfect stays perfect.
149    pub fn inversion(&self) -> Self {
150        match self {
151            Specifier::Perfect => Specifier::Perfect,
152            Specifier::Major => Specifier::Minor,
153            Specifier::Minor => Specifier::Major,
154            Specifier::Augmented => Specifier::Diminished,
155            Specifier::Diminished => Specifier::Augmented,
156            Specifier::DoubleAugmented => Specifier::DoubleDiminished,
157            Specifier::DoubleDiminished => Specifier::DoubleAugmented,
158            Specifier::TripleAugmented => Specifier::TripleDiminished,
159            Specifier::TripleDiminished => Specifier::TripleAugmented,
160            Specifier::QuadrupleAugmented => Specifier::QuadrupleDiminished,
161            Specifier::QuadrupleDiminished => Specifier::QuadrupleAugmented,
162        }
163    }
164
165    /// How many semitones above perfect the specifier lies. Major and minor
166    /// cannot be compared to perfect and are an error, as in music21.
167    pub fn semitones_above_perfect(&self) -> Result<IntegerType> {
168        match self {
169            Specifier::Perfect => Ok(0),
170            Specifier::Augmented => Ok(1),
171            Specifier::DoubleAugmented => Ok(2),
172            Specifier::TripleAugmented => Ok(3),
173            Specifier::QuadrupleAugmented => Ok(4),
174            Specifier::Diminished => Ok(-1),
175            Specifier::DoubleDiminished => Ok(-2),
176            Specifier::TripleDiminished => Ok(-3),
177            Specifier::QuadrupleDiminished => Ok(-4),
178            _ => Err(Error::Interval(format!(
179                "<Specifier.{}> cannot be compared to Perfect",
180                self.python_name()
181            ))),
182        }
183    }
184
185    /// How many semitones above major the specifier lies. Perfect cannot be
186    /// compared to major and is an error, as in music21.
187    pub fn semitones_above_major(&self) -> Result<IntegerType> {
188        match self {
189            Specifier::Major => Ok(0),
190            Specifier::Minor => Ok(-1),
191            Specifier::Augmented => Ok(1),
192            Specifier::DoubleAugmented => Ok(2),
193            Specifier::TripleAugmented => Ok(3),
194            Specifier::QuadrupleAugmented => Ok(4),
195            Specifier::Diminished => Ok(-2),
196            Specifier::DoubleDiminished => Ok(-3),
197            Specifier::TripleDiminished => Ok(-4),
198            Specifier::QuadrupleDiminished => Ok(-5),
199            _ => Err(Error::Interval(format!(
200                "<Specifier.{}> cannot be compared to Major",
201                self.python_name()
202            ))),
203        }
204    }
205
206    /// music21's enum member name, `PERFECT`, `DBLAUG`, `QUADDIM`.
207    pub fn python_name(self) -> &'static str {
208        match self {
209            Specifier::Perfect => "PERFECT",
210            Specifier::Major => "MAJOR",
211            Specifier::Minor => "MINOR",
212            Specifier::Augmented => "AUGMENTED",
213            Specifier::Diminished => "DIMINISHED",
214            Specifier::DoubleAugmented => "DBLAUG",
215            Specifier::DoubleDiminished => "DBLDIM",
216            Specifier::TripleAugmented => "TRPAUG",
217            Specifier::TripleDiminished => "TRPDIM",
218            Specifier::QuadrupleAugmented => "QUADAUG",
219            Specifier::QuadrupleDiminished => "QUADDIM",
220        }
221    }
222}
223
224impl fmt::Display for Specifier {
225    /// The prefix, as `str(Specifier.PERFECT)` is `P` in music21.
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        f.write_str(self.prefix())
228    }
229}
230
231impl FromStr for Specifier {
232    type Err = Error;
233
234    fn from_str(name: &str) -> Result<Self> {
235        Self::from_name(name)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_specifier_nice_name() {
245        assert_eq!(Specifier::Perfect.nice_name(), "Perfect");
246        assert_eq!(Specifier::Major.nice_name(), "Major");
247        assert_eq!(Specifier::Minor.nice_name(), "Minor");
248        assert_eq!(Specifier::Augmented.nice_name(), "Augmented");
249        assert_eq!(Specifier::Diminished.nice_name(), "Diminished");
250        assert_eq!(Specifier::DoubleAugmented.nice_name(), "Doubly-Augmented");
251        assert_eq!(Specifier::DoubleDiminished.nice_name(), "Doubly-Diminished");
252        assert_eq!(Specifier::TripleAugmented.nice_name(), "Triply-Augmented");
253        assert_eq!(Specifier::TripleDiminished.nice_name(), "Triply-Diminished");
254        assert_eq!(
255            Specifier::QuadrupleAugmented.nice_name(),
256            "Quadruply-Augmented"
257        );
258        assert_eq!(
259            Specifier::QuadrupleDiminished.nice_name(),
260            "Quadruply-Diminished"
261        );
262    }
263
264    #[test]
265    fn test_specifier_semitones_above_perfect() {
266        assert_eq!(Specifier::Perfect.semitones_above_perfect().unwrap(), 0);
267        assert_eq!(Specifier::Augmented.semitones_above_perfect().unwrap(), 1);
268        assert_eq!(
269            Specifier::DoubleDiminished
270                .semitones_above_perfect()
271                .unwrap(),
272            -2
273        );
274        assert!(Specifier::Major.semitones_above_perfect().is_err());
275    }
276
277    #[test]
278    fn test_specifier_semitones_above_major() {
279        assert_eq!(Specifier::Major.semitones_above_major().unwrap(), 0);
280        assert_eq!(Specifier::Minor.semitones_above_major().unwrap(), -1);
281        assert_eq!(Specifier::Diminished.semitones_above_major().unwrap(), -2);
282        assert!(Specifier::Perfect.semitones_above_major().is_err());
283    }
284
285    #[test]
286    fn names_numbers_and_prefixes_round_trip() {
287        for specifier in Specifier::ALL {
288            assert_eq!(Specifier::from_value(specifier.value()).unwrap(), specifier);
289            assert_eq!(Specifier::from_name(specifier.prefix()).unwrap(), specifier);
290            assert_eq!(
291                Specifier::from_name(&specifier.nice_name()).unwrap(),
292                specifier
293            );
294            assert_eq!(specifier.to_string(), specifier.prefix());
295        }
296        assert_eq!(Specifier::from_name("perfect").unwrap(), Specifier::Perfect);
297        assert_eq!(
298            Specifier::from_name("dd").unwrap(),
299            Specifier::DoubleDiminished
300        );
301        assert_eq!("M".parse::<Specifier>().unwrap(), Specifier::Major);
302        assert!(Specifier::from_name("xyz").is_err());
303        assert!(Specifier::from_value(12).is_err());
304    }
305}