Skip to main content

music21_rs/
duration.rs

1use crate::{
2    common::objects::slottedobjectmixin::{SlottedObjectMixin, SlottedObjectMixinTrait},
3    defaults::{FloatType, IntegerType},
4    error::{Error, Result},
5    prebase::{ProtoM21Object, ProtoM21ObjectTrait},
6};
7
8#[derive(Clone, Debug)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10/// Rhythmic duration measured in quarter lengths.
11///
12/// A quarter note has a quarter length of `1.0`; an eighth note is `0.5`;
13/// a whole note is `4.0`.
14pub struct Duration {
15    proto: ProtoM21Object,
16    mixin: SlottedObjectMixin,
17    quarter_length: FloatType,
18}
19
20impl Duration {
21    /// Creates a duration from a quarter-length value.
22    pub fn new(quarter_length: FloatType) -> Result<Self> {
23        if !quarter_length.is_finite() || quarter_length < 0.0 {
24            return Err(Error::Ordinal(format!(
25                "duration quarter length must be finite and non-negative, got {quarter_length}"
26            )));
27        }
28
29        Ok(Self {
30            proto: ProtoM21Object::new(),
31            mixin: SlottedObjectMixin::new(),
32            quarter_length,
33        })
34    }
35
36    /// Returns a quarter-note duration.
37    pub fn quarter() -> Self {
38        Self::default()
39    }
40
41    /// Returns a half-note duration.
42    pub fn half() -> Self {
43        Self::new(2.0).expect("constant duration is valid")
44    }
45
46    /// Returns a whole-note duration.
47    pub fn whole() -> Self {
48        Self::new(4.0).expect("constant duration is valid")
49    }
50
51    /// Returns an eighth-note duration.
52    pub fn eighth() -> Self {
53        Self::new(0.5).expect("constant duration is valid")
54    }
55
56    /// Returns the duration in quarter lengths.
57    pub fn quarter_length(&self) -> FloatType {
58        self.quarter_length
59    }
60
61    /// Updates the duration in quarter lengths.
62    pub fn set_quarter_length(&mut self, quarter_length: FloatType) -> Result<()> {
63        *self = Self::new(quarter_length)?;
64        Ok(())
65    }
66}
67
68impl Default for Duration {
69    fn default() -> Self {
70        Self {
71            proto: ProtoM21Object::new(),
72            mixin: SlottedObjectMixin::new(),
73            quarter_length: 1.0,
74        }
75    }
76}
77
78impl PartialEq for Duration {
79    fn eq(&self, other: &Self) -> bool {
80        self.quarter_length == other.quarter_length
81    }
82}
83
84impl TryFrom<FloatType> for Duration {
85    type Error = Error;
86
87    fn try_from(value: FloatType) -> Result<Self> {
88        Self::new(value)
89    }
90}
91
92impl TryFrom<IntegerType> for Duration {
93    type Error = Error;
94
95    fn try_from(value: IntegerType) -> Result<Self> {
96        Self::new(value as FloatType)
97    }
98}
99
100impl ProtoM21ObjectTrait for Duration {}
101
102impl SlottedObjectMixinTrait for Duration {}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn duration_tracks_quarter_lengths() {
110        assert_eq!(Duration::quarter().quarter_length(), 1.0);
111        assert_eq!(Duration::half().quarter_length(), 2.0);
112        assert_eq!(Duration::whole().quarter_length(), 4.0);
113        assert_eq!(Duration::eighth().quarter_length(), 0.5);
114    }
115
116    #[test]
117    fn duration_rejects_invalid_values() {
118        assert!(Duration::new(-1.0).is_err());
119        assert!(Duration::new(FloatType::INFINITY).is_err());
120    }
121
122    #[test]
123    fn duration_supports_conversions_and_updates() {
124        let mut duration = Duration::try_from(3 as IntegerType).unwrap();
125        assert_eq!(duration.quarter_length(), 3.0);
126
127        duration.set_quarter_length(1.5).unwrap();
128        assert_eq!(duration, Duration::try_from(1.5).unwrap());
129        assert!(duration.set_quarter_length(FloatType::NAN).is_err());
130    }
131}