Skip to main content

music21_rs/
duration.rs

1use crate::{
2    defaults::{FloatType, IntegerType},
3    error::{Error, Result},
4};
5
6use std::fmt::{Display, Formatter};
7use std::str::FromStr;
8
9/// A note-value name, as music21's `duration.typeToDuration` defines them.
10///
11/// Each type is a power-of-two multiple of a quarter note, from the
12/// `duplex-maxima` (sixteen whole notes) down to the `2048th`, plus the `zero`
13/// length music21 uses for grace notes.
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16#[non_exhaustive]
17pub enum DurationType {
18    /// Duplex maxima, sixteen whole notes.
19    DuplexMaxima,
20    /// Maxima, eight whole notes.
21    Maxima,
22    /// Longa, four whole notes.
23    Longa,
24    /// Breve, or double whole note.
25    Breve,
26    /// Whole note.
27    Whole,
28    /// Half note.
29    Half,
30    /// Quarter note.
31    Quarter,
32    /// Eighth note.
33    Eighth,
34    /// Sixteenth note.
35    Sixteenth,
36    /// Thirty-second note.
37    ThirtySecond,
38    /// Sixty-fourth note.
39    SixtyFourth,
40    /// Hundred-twenty-eighth note.
41    HundredTwentyEighth,
42    /// Two-hundred-fifty-sixth note.
43    TwoHundredFiftySixth,
44    /// Five-hundred-twelfth note.
45    FiveHundredTwelfth,
46    /// Ten-twenty-fourth note.
47    TenTwentyFourth,
48    /// Twenty-forty-eighth note.
49    TwentyFortyEighth,
50    /// A grace-note duration of no length.
51    Zero,
52}
53
54impl DurationType {
55    /// Every duration type, longest first, matching music21's ordering.
56    pub const ALL: [DurationType; 17] = [
57        Self::DuplexMaxima,
58        Self::Maxima,
59        Self::Longa,
60        Self::Breve,
61        Self::Whole,
62        Self::Half,
63        Self::Quarter,
64        Self::Eighth,
65        Self::Sixteenth,
66        Self::ThirtySecond,
67        Self::SixtyFourth,
68        Self::HundredTwentyEighth,
69        Self::TwoHundredFiftySixth,
70        Self::FiveHundredTwelfth,
71        Self::TenTwentyFourth,
72        Self::TwentyFortyEighth,
73        Self::Zero,
74    ];
75
76    /// Returns the music21 type name, such as `"whole"` or `"16th"`.
77    pub fn music21_name(self) -> &'static str {
78        match self {
79            Self::DuplexMaxima => "duplex-maxima",
80            Self::Maxima => "maxima",
81            Self::Longa => "longa",
82            Self::Breve => "breve",
83            Self::Whole => "whole",
84            Self::Half => "half",
85            Self::Quarter => "quarter",
86            Self::Eighth => "eighth",
87            Self::Sixteenth => "16th",
88            Self::ThirtySecond => "32nd",
89            Self::SixtyFourth => "64th",
90            Self::HundredTwentyEighth => "128th",
91            Self::TwoHundredFiftySixth => "256th",
92            Self::FiveHundredTwelfth => "512th",
93            Self::TenTwentyFourth => "1024th",
94            Self::TwentyFortyEighth => "2048th",
95            Self::Zero => "zero",
96        }
97    }
98
99    /// Returns the length of one undotted note of this type, in quarter lengths.
100    pub fn quarter_length(self) -> FloatType {
101        match self {
102            Self::DuplexMaxima => 64.0,
103            Self::Maxima => 32.0,
104            Self::Longa => 16.0,
105            Self::Breve => 8.0,
106            Self::Whole => 4.0,
107            Self::Half => 2.0,
108            Self::Quarter => 1.0,
109            Self::Eighth => 0.5,
110            Self::Sixteenth => 0.25,
111            Self::ThirtySecond => 0.125,
112            Self::SixtyFourth => 0.0625,
113            Self::HundredTwentyEighth => 0.03125,
114            Self::TwoHundredFiftySixth => 0.015625,
115            Self::FiveHundredTwelfth => 0.0078125,
116            Self::TenTwentyFourth => 0.00390625,
117            Self::TwentyFortyEighth => 0.001953125,
118            Self::Zero => 0.0,
119        }
120    }
121
122    /// Parses a music21 type name.
123    pub fn from_music21_name(name: &str) -> Option<Self> {
124        match name {
125            "duplex-maxima" => Some(Self::DuplexMaxima),
126            "maxima" => Some(Self::Maxima),
127            "longa" => Some(Self::Longa),
128            "breve" => Some(Self::Breve),
129            "whole" => Some(Self::Whole),
130            "half" => Some(Self::Half),
131            "quarter" => Some(Self::Quarter),
132            "eighth" => Some(Self::Eighth),
133            "16th" => Some(Self::Sixteenth),
134            "32nd" => Some(Self::ThirtySecond),
135            "64th" => Some(Self::SixtyFourth),
136            "128th" => Some(Self::HundredTwentyEighth),
137            "256th" => Some(Self::TwoHundredFiftySixth),
138            "512th" => Some(Self::FiveHundredTwelfth),
139            "1024th" => Some(Self::TenTwentyFourth),
140            "2048th" => Some(Self::TwentyFortyEighth),
141            "zero" => Some(Self::Zero),
142            _ => None,
143        }
144    }
145
146    /// Returns the type whose undotted length is exactly `quarter_length`.
147    pub fn from_quarter_length(quarter_length: FloatType) -> Option<Self> {
148        Self::ALL
149            .into_iter()
150            .find(|candidate| candidate.quarter_length() == quarter_length)
151    }
152
153    /// Returns the length of this type carrying `dots` augmentation dots.
154    ///
155    /// Each dot adds half of what came before, so a dotted half is `3.0` and a
156    /// double-dotted half is `3.5`.
157    pub fn quarter_length_with_dots(self, dots: u32) -> FloatType {
158        self.quarter_length() * (2.0 - (0.5 as FloatType).powi(dots as i32))
159    }
160}
161
162impl Display for DurationType {
163    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164        f.write_str(self.music21_name())
165    }
166}
167
168impl FromStr for DurationType {
169    type Err = Error;
170
171    fn from_str(value: &str) -> Result<Self> {
172        Self::from_music21_name(value)
173            .ok_or_else(|| Error::Ordinal(format!("unknown duration type {value:?}")))
174    }
175}
176
177#[derive(Clone, Debug)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179/// Rhythmic duration measured in quarter lengths.
180///
181/// A quarter note has a quarter length of `1.0`; an eighth note is `0.5`;
182/// a whole note is `4.0`.
183pub struct Duration {
184    quarter_length: FloatType,
185}
186
187impl Duration {
188    /// Creates a duration from a quarter-length value.
189    pub fn new(quarter_length: FloatType) -> Result<Self> {
190        if !quarter_length.is_finite() || quarter_length < 0.0 {
191            return Err(Error::Ordinal(format!(
192                "duration quarter length must be finite and non-negative, got {quarter_length}"
193            )));
194        }
195
196        Ok(Self { quarter_length })
197    }
198
199    /// Returns a quarter-note duration.
200    pub fn quarter() -> Self {
201        Self::default()
202    }
203
204    /// Returns a half-note duration.
205    pub fn half() -> Self {
206        Self::new(2.0).expect("constant duration is valid")
207    }
208
209    /// Returns a whole-note duration.
210    pub fn whole() -> Self {
211        Self::new(4.0).expect("constant duration is valid")
212    }
213
214    /// Returns an eighth-note duration.
215    pub fn eighth() -> Self {
216        Self::new(0.5).expect("constant duration is valid")
217    }
218
219    /// Creates a duration from a note-value type.
220    pub fn from_type(duration_type: DurationType) -> Self {
221        Self {
222            quarter_length: duration_type.quarter_length(),
223        }
224    }
225
226    /// Creates a duration from a note-value type carrying augmentation dots.
227    pub fn from_type_with_dots(duration_type: DurationType, dots: u32) -> Self {
228        Self {
229            quarter_length: duration_type.quarter_length_with_dots(dots),
230        }
231    }
232
233    /// Returns the note-value type whose undotted length this duration is.
234    ///
235    /// Returns `None` for a length that is not a plain note value, such as a
236    /// dotted or tuplet duration.
237    pub fn duration_type(&self) -> Option<DurationType> {
238        DurationType::from_quarter_length(self.quarter_length)
239    }
240
241    /// Returns the duration in quarter lengths.
242    pub fn quarter_length(&self) -> FloatType {
243        self.quarter_length
244    }
245
246    /// Updates the duration in quarter lengths.
247    pub fn set_quarter_length(&mut self, quarter_length: FloatType) -> Result<()> {
248        *self = Self::new(quarter_length)?;
249        Ok(())
250    }
251}
252
253impl Default for Duration {
254    fn default() -> Self {
255        Self {
256            quarter_length: 1.0,
257        }
258    }
259}
260
261impl PartialEq for Duration {
262    fn eq(&self, other: &Self) -> bool {
263        self.quarter_length == other.quarter_length
264    }
265}
266
267impl TryFrom<FloatType> for Duration {
268    type Error = Error;
269
270    fn try_from(value: FloatType) -> Result<Self> {
271        Self::new(value)
272    }
273}
274
275impl TryFrom<IntegerType> for Duration {
276    type Error = Error;
277
278    fn try_from(value: IntegerType) -> Result<Self> {
279        Self::new(value as FloatType)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    /// music21's `duration.typeToDuration`, verbatim.
288    const MUSIC21_TYPE_TO_DURATION: [(&str, FloatType); 17] = [
289        ("duplex-maxima", 64.0),
290        ("maxima", 32.0),
291        ("longa", 16.0),
292        ("breve", 8.0),
293        ("whole", 4.0),
294        ("half", 2.0),
295        ("quarter", 1.0),
296        ("eighth", 0.5),
297        ("16th", 0.25),
298        ("32nd", 0.125),
299        ("64th", 0.0625),
300        ("128th", 0.03125),
301        ("256th", 0.015625),
302        ("512th", 0.0078125),
303        ("1024th", 0.00390625),
304        ("2048th", 0.001953125),
305        ("zero", 0.0),
306    ];
307
308    #[test]
309    fn duration_types_match_music21s_table() {
310        assert_eq!(DurationType::ALL.len(), MUSIC21_TYPE_TO_DURATION.len());
311        for (duration_type, (name, quarter_length)) in
312            DurationType::ALL.into_iter().zip(MUSIC21_TYPE_TO_DURATION)
313        {
314            assert_eq!(duration_type.music21_name(), name);
315            assert_eq!(duration_type.quarter_length(), quarter_length, "{name}");
316            assert_eq!(DurationType::from_music21_name(name), Some(duration_type));
317        }
318    }
319
320    #[test]
321    fn duration_types_round_trip_through_their_names() {
322        for duration_type in DurationType::ALL {
323            let name = duration_type.music21_name();
324            assert_eq!(name.parse::<DurationType>().unwrap(), duration_type);
325            assert_eq!(duration_type.to_string(), name);
326        }
327        assert!("not-a-duration".parse::<DurationType>().is_err());
328    }
329
330    #[test]
331    fn each_type_is_half_the_one_before_it() {
332        // `zero` is the exception and is excluded.
333        let ordered = &DurationType::ALL[..DurationType::ALL.len() - 1];
334        for pair in ordered.windows(2) {
335            assert_eq!(
336                pair[1].quarter_length() * 2.0,
337                pair[0].quarter_length(),
338                "{} should be half of {}",
339                pair[1],
340                pair[0]
341            );
342        }
343    }
344
345    #[test]
346    fn dots_add_half_of_what_came_before() {
347        assert_eq!(DurationType::Half.quarter_length_with_dots(0), 2.0);
348        assert_eq!(DurationType::Half.quarter_length_with_dots(1), 3.0);
349        assert_eq!(DurationType::Half.quarter_length_with_dots(2), 3.5);
350        assert_eq!(DurationType::Half.quarter_length_with_dots(3), 3.75);
351        assert_eq!(DurationType::Quarter.quarter_length_with_dots(1), 1.5);
352    }
353
354    #[test]
355    fn durations_convert_to_and_from_note_values() {
356        assert_eq!(
357            Duration::from_type(DurationType::Whole).quarter_length(),
358            4.0
359        );
360        assert_eq!(
361            Duration::from_type(DurationType::Whole).duration_type(),
362            Some(DurationType::Whole)
363        );
364        assert_eq!(
365            Duration::from_type_with_dots(DurationType::Half, 1).quarter_length(),
366            3.0
367        );
368        // A dotted value is not itself a note value.
369        assert_eq!(
370            Duration::from_type_with_dots(DurationType::Half, 1).duration_type(),
371            None
372        );
373        // Nor is a triplet eighth.
374        assert_eq!(Duration::new(1.0 / 3.0).unwrap().duration_type(), None);
375    }
376
377    #[test]
378    fn the_named_helpers_agree_with_their_types() {
379        assert_eq!(
380            Duration::quarter(),
381            Duration::from_type(DurationType::Quarter)
382        );
383        assert_eq!(Duration::half(), Duration::from_type(DurationType::Half));
384        assert_eq!(Duration::whole(), Duration::from_type(DurationType::Whole));
385        assert_eq!(
386            Duration::eighth(),
387            Duration::from_type(DurationType::Eighth)
388        );
389    }
390
391    #[test]
392    fn duration_tracks_quarter_lengths() {
393        assert_eq!(Duration::quarter().quarter_length(), 1.0);
394        assert_eq!(Duration::half().quarter_length(), 2.0);
395        assert_eq!(Duration::whole().quarter_length(), 4.0);
396        assert_eq!(Duration::eighth().quarter_length(), 0.5);
397    }
398
399    #[test]
400    fn duration_rejects_invalid_values() {
401        assert!(Duration::new(-1.0).is_err());
402        assert!(Duration::new(FloatType::INFINITY).is_err());
403    }
404
405    #[test]
406    fn duration_supports_conversions_and_updates() {
407        let mut duration = Duration::try_from(3 as IntegerType).unwrap();
408        assert_eq!(duration.quarter_length(), 3.0);
409
410        duration.set_quarter_length(1.5).unwrap();
411        assert_eq!(duration, Duration::try_from(1.5).unwrap());
412        assert!(duration.set_quarter_length(FloatType::NAN).is_err());
413    }
414}