Skip to main content

use_rhythm/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8    pub use crate::{
9        BeatDivision, DottedDuration, DurationValue, NoteDuration, RestDuration, RhythmError,
10        RhythmPatternName, RhythmicPosition, SyncopationKind, TupletRatio,
11    };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct RhythmPatternName(String);
15
16impl RhythmPatternName {
17    pub fn new(value: impl AsRef<str>) -> Result<Self, RhythmError> {
18        non_empty_text(value).map(Self)
19    }
20
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24
25    pub fn value(&self) -> &str {
26        self.as_str()
27    }
28
29    pub fn into_string(self) -> String {
30        self.0
31    }
32}
33
34impl AsRef<str> for RhythmPatternName {
35    fn as_ref(&self) -> &str {
36        self.as_str()
37    }
38}
39
40impl fmt::Display for RhythmPatternName {
41    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42        formatter.write_str(self.as_str())
43    }
44}
45
46impl FromStr for RhythmPatternName {
47    type Err = RhythmError;
48
49    fn from_str(value: &str) -> Result<Self, Self::Err> {
50        Self::new(value)
51    }
52}
53
54impl TryFrom<&str> for RhythmPatternName {
55    type Error = RhythmError;
56
57    fn try_from(value: &str) -> Result<Self, Self::Error> {
58        Self::new(value)
59    }
60}
61#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub enum DurationValue {
63    DoubleWhole,
64    Whole,
65    Half,
66    Quarter,
67    Eighth,
68    Sixteenth,
69    ThirtySecond,
70    SixtyFourth,
71    OneTwentyEighth,
72}
73
74impl DurationValue {
75    pub const ALL: &'static [Self] = &[
76        Self::DoubleWhole,
77        Self::Whole,
78        Self::Half,
79        Self::Quarter,
80        Self::Eighth,
81        Self::Sixteenth,
82        Self::ThirtySecond,
83        Self::SixtyFourth,
84        Self::OneTwentyEighth,
85    ];
86
87    pub const fn as_str(self) -> &'static str {
88        match self {
89            Self::DoubleWhole => "double-whole",
90            Self::Whole => "whole",
91            Self::Half => "half",
92            Self::Quarter => "quarter",
93            Self::Eighth => "eighth",
94            Self::Sixteenth => "sixteenth",
95            Self::ThirtySecond => "thirty-second",
96            Self::SixtyFourth => "sixty-fourth",
97            Self::OneTwentyEighth => "one-twenty-eighth",
98        }
99    }
100}
101
102impl fmt::Display for DurationValue {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        formatter.write_str(self.as_str())
105    }
106}
107
108impl FromStr for DurationValue {
109    type Err = RhythmError;
110
111    fn from_str(value: &str) -> Result<Self, Self::Err> {
112        match normalized_label(value)?.as_str() {
113            "double-whole" => Ok(Self::DoubleWhole),
114            "whole" => Ok(Self::Whole),
115            "half" => Ok(Self::Half),
116            "quarter" => Ok(Self::Quarter),
117            "eighth" => Ok(Self::Eighth),
118            "sixteenth" => Ok(Self::Sixteenth),
119            "thirty-second" => Ok(Self::ThirtySecond),
120            "sixty-fourth" => Ok(Self::SixtyFourth),
121            "one-twenty-eighth" => Ok(Self::OneTwentyEighth),
122            _ => Err(RhythmError::UnknownLabel),
123        }
124    }
125}
126#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
127pub enum BeatDivision {
128    Duple,
129    Triple,
130    Quadruple,
131    Quintuple,
132    Septuple,
133    Custom,
134}
135
136impl BeatDivision {
137    pub const ALL: &'static [Self] = &[
138        Self::Duple,
139        Self::Triple,
140        Self::Quadruple,
141        Self::Quintuple,
142        Self::Septuple,
143        Self::Custom,
144    ];
145
146    pub const fn as_str(self) -> &'static str {
147        match self {
148            Self::Duple => "duple",
149            Self::Triple => "triple",
150            Self::Quadruple => "quadruple",
151            Self::Quintuple => "quintuple",
152            Self::Septuple => "septuple",
153            Self::Custom => "custom",
154        }
155    }
156}
157
158impl fmt::Display for BeatDivision {
159    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
160        formatter.write_str(self.as_str())
161    }
162}
163
164impl FromStr for BeatDivision {
165    type Err = RhythmError;
166
167    fn from_str(value: &str) -> Result<Self, Self::Err> {
168        match normalized_label(value)?.as_str() {
169            "duple" => Ok(Self::Duple),
170            "triple" => Ok(Self::Triple),
171            "quadruple" => Ok(Self::Quadruple),
172            "quintuple" => Ok(Self::Quintuple),
173            "septuple" => Ok(Self::Septuple),
174            "custom" => Ok(Self::Custom),
175            _ => Err(RhythmError::UnknownLabel),
176        }
177    }
178}
179#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
180pub enum SyncopationKind {
181    None,
182    WeakBeatAccent,
183    OffBeat,
184    Anticipation,
185    Suspension,
186    Unknown,
187}
188
189impl SyncopationKind {
190    pub const ALL: &'static [Self] = &[
191        Self::None,
192        Self::WeakBeatAccent,
193        Self::OffBeat,
194        Self::Anticipation,
195        Self::Suspension,
196        Self::Unknown,
197    ];
198
199    pub const fn as_str(self) -> &'static str {
200        match self {
201            Self::None => "none",
202            Self::WeakBeatAccent => "weak-beat-accent",
203            Self::OffBeat => "off-beat",
204            Self::Anticipation => "anticipation",
205            Self::Suspension => "suspension",
206            Self::Unknown => "unknown",
207        }
208    }
209}
210
211impl fmt::Display for SyncopationKind {
212    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213        formatter.write_str(self.as_str())
214    }
215}
216
217impl FromStr for SyncopationKind {
218    type Err = RhythmError;
219
220    fn from_str(value: &str) -> Result<Self, Self::Err> {
221        match normalized_label(value)?.as_str() {
222            "none" => Ok(Self::None),
223            "weak-beat-accent" => Ok(Self::WeakBeatAccent),
224            "off-beat" => Ok(Self::OffBeat),
225            "anticipation" => Ok(Self::Anticipation),
226            "suspension" => Ok(Self::Suspension),
227            "unknown" => Ok(Self::Unknown),
228            _ => Err(RhythmError::UnknownLabel),
229        }
230    }
231}
232#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
233pub struct NoteDuration {
234    value: DurationValue,
235}
236
237impl NoteDuration {
238    pub const fn new(value: DurationValue) -> Self {
239        Self { value }
240    }
241    pub const fn value(self) -> DurationValue {
242        self.value
243    }
244    pub const fn is_rest(self) -> bool {
245        false
246    }
247    pub const fn is_shorter_than_quarter_like(self) -> bool {
248        self.value.is_shorter_than_quarter_like()
249    }
250}
251
252#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
253pub struct RestDuration {
254    value: DurationValue,
255}
256
257impl RestDuration {
258    pub const fn new(value: DurationValue) -> Self {
259        Self { value }
260    }
261    pub const fn value(self) -> DurationValue {
262        self.value
263    }
264    pub const fn is_rest(self) -> bool {
265        true
266    }
267}
268
269#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
270pub struct TupletRatio {
271    actual: u8,
272    normal: u8,
273}
274
275impl TupletRatio {
276    pub fn new(actual: u8, normal: u8) -> Result<Self, RhythmError> {
277        if actual == 0 || normal == 0 {
278            return Err(RhythmError::OutOfRange);
279        }
280        Ok(Self { actual, normal })
281    }
282    pub const fn actual(self) -> u8 {
283        self.actual
284    }
285    pub const fn normal(self) -> u8 {
286        self.normal
287    }
288}
289
290#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
291pub struct DottedDuration {
292    value: DurationValue,
293    dots: u8,
294}
295
296impl DottedDuration {
297    pub const fn new(value: DurationValue, dots: u8) -> Self {
298        Self { value, dots }
299    }
300    pub const fn value(self) -> DurationValue {
301        self.value
302    }
303    pub const fn dot_count(self) -> u8 {
304        self.dots
305    }
306}
307
308#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
309pub struct RhythmicPosition {
310    beat: u16,
311    subdivision: u16,
312}
313
314impl RhythmicPosition {
315    pub const fn new(beat: u16, subdivision: u16) -> Self {
316        Self { beat, subdivision }
317    }
318    pub const fn beat(self) -> u16 {
319        self.beat
320    }
321    pub const fn subdivision(self) -> u16 {
322        self.subdivision
323    }
324}
325
326impl DurationValue {
327    pub const fn is_shorter_than_quarter_like(self) -> bool {
328        matches!(
329            self,
330            Self::Eighth
331                | Self::Sixteenth
332                | Self::ThirtySecond
333                | Self::SixtyFourth
334                | Self::OneTwentyEighth
335        )
336    }
337}
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub enum RhythmError {
340    Empty,
341    InvalidFormat,
342    OutOfRange,
343    NonFinite,
344    NonPositive,
345    UnknownLabel,
346}
347
348impl fmt::Display for RhythmError {
349    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
350        match self {
351            Self::Empty => formatter.write_str("rhythm metadata text cannot be empty"),
352            Self::InvalidFormat => formatter.write_str("rhythm metadata has an invalid format"),
353            Self::OutOfRange => formatter.write_str("rhythm metadata value is out of range"),
354            Self::NonFinite => formatter.write_str("rhythm metadata value must be finite"),
355            Self::NonPositive => formatter.write_str("rhythm metadata value must be positive"),
356            Self::UnknownLabel => formatter.write_str("unknown rhythm metadata label"),
357        }
358    }
359}
360
361impl Error for RhythmError {}
362
363#[allow(dead_code)]
364fn non_empty_text(value: impl AsRef<str>) -> Result<String, RhythmError> {
365    let trimmed = value.as_ref().trim();
366    if trimmed.is_empty() {
367        Err(RhythmError::Empty)
368    } else {
369        Ok(trimmed.to_string())
370    }
371}
372
373fn normalized_label(value: &str) -> Result<String, RhythmError> {
374    let trimmed = value.trim();
375    if trimmed.is_empty() {
376        Err(RhythmError::Empty)
377    } else {
378        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
379    }
380}
381#[cfg(test)]
382#[allow(
383    unused_imports,
384    clippy::unnecessary_wraps,
385    clippy::assertions_on_constants
386)]
387mod tests {
388    use super::{
389        BeatDivision, DottedDuration, DurationValue, NoteDuration, RestDuration, RhythmError,
390        RhythmPatternName, RhythmicPosition, SyncopationKind, TupletRatio,
391    };
392    use core::{fmt, str::FromStr};
393
394    fn assert_enum_family<T>(variants: &[T]) -> Result<(), RhythmError>
395    where
396        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = RhythmError>,
397    {
398        for variant in variants {
399            let label = variant.to_string();
400            assert_eq!(label.parse::<T>()?, *variant);
401            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
402            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
403        }
404        Ok(())
405    }
406
407    #[test]
408    fn validates_text_newtypes() -> Result<(), RhythmError> {
409        let value = RhythmPatternName::new(" example-value ")?;
410        assert_eq!(value.as_str(), "example-value");
411        assert_eq!(value.value(), "example-value");
412        assert_eq!(value.to_string(), "example-value");
413        assert_eq!(
414            <RhythmPatternName as TryFrom<&str>>::try_from("example-value")?,
415            value
416        );
417        Ok(())
418    }
419
420    #[test]
421    fn validates_numeric_newtypes() -> Result<(), RhythmError> {
422        assert!(true);
423        Ok(())
424    }
425
426    #[test]
427    fn displays_and_parses_enums() -> Result<(), RhythmError> {
428        assert_enum_family(DurationValue::ALL)?;
429        assert_enum_family(BeatDivision::ALL)?;
430        assert_enum_family(SyncopationKind::ALL)?;
431        Ok(())
432    }
433
434    #[test]
435    fn models_symbolic_durations() -> Result<(), RhythmError> {
436        let note = NoteDuration::new(DurationValue::Eighth);
437        let rest = RestDuration::new(DurationValue::Quarter);
438        let dotted = DottedDuration::new(DurationValue::Half, 2);
439        let tuplet = TupletRatio::new(3, 2)?;
440        assert!(note.is_shorter_than_quarter_like());
441        assert!(!note.is_rest());
442        assert!(rest.is_rest());
443        assert_eq!(dotted.dot_count(), 2);
444        assert_eq!(tuplet.actual(), 3);
445        Ok(())
446    }
447}