Skip to main content

use_dynamics/
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        AccentDynamicKind, DynamicChangeKind, DynamicLevel, DynamicMarking, DynamicsError,
10        ExpressionMarking, HairpinKind,
11    };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ExpressionMarking(String);
15
16impl ExpressionMarking {
17    pub fn new(value: impl AsRef<str>) -> Result<Self, DynamicsError> {
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 ExpressionMarking {
35    fn as_ref(&self) -> &str {
36        self.as_str()
37    }
38}
39
40impl fmt::Display for ExpressionMarking {
41    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42        formatter.write_str(self.as_str())
43    }
44}
45
46impl FromStr for ExpressionMarking {
47    type Err = DynamicsError;
48
49    fn from_str(value: &str) -> Result<Self, Self::Err> {
50        Self::new(value)
51    }
52}
53
54impl TryFrom<&str> for ExpressionMarking {
55    type Error = DynamicsError;
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 struct DynamicLevel(u8);
63
64impl DynamicLevel {
65    pub fn new(value: u8) -> Result<Self, DynamicsError> {
66        if !(0..=127).contains(&value) {
67            return Err(DynamicsError::OutOfRange);
68        }
69
70        Ok(Self(value))
71    }
72
73    pub const fn value(self) -> u8 {
74        self.0
75    }
76}
77
78impl fmt::Display for DynamicLevel {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        self.0.fmt(formatter)
81    }
82}
83
84impl FromStr for DynamicLevel {
85    type Err = DynamicsError;
86
87    fn from_str(value: &str) -> Result<Self, Self::Err> {
88        let parsed = value
89            .trim()
90            .parse::<u8>()
91            .map_err(|_| DynamicsError::InvalidFormat)?;
92        Self::new(parsed)
93    }
94}
95
96impl TryFrom<u8> for DynamicLevel {
97    type Error = DynamicsError;
98
99    fn try_from(value: u8) -> Result<Self, Self::Error> {
100        Self::new(value)
101    }
102}
103#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
104pub enum DynamicMarking {
105    Pppp,
106    Ppp,
107    Pp,
108    P,
109    Mp,
110    Mf,
111    F,
112    Ff,
113    Fff,
114    Ffff,
115    Sfz,
116    Fp,
117    Custom,
118}
119
120impl DynamicMarking {
121    pub const ALL: &'static [Self] = &[
122        Self::Pppp,
123        Self::Ppp,
124        Self::Pp,
125        Self::P,
126        Self::Mp,
127        Self::Mf,
128        Self::F,
129        Self::Ff,
130        Self::Fff,
131        Self::Ffff,
132        Self::Sfz,
133        Self::Fp,
134        Self::Custom,
135    ];
136
137    pub const fn as_str(self) -> &'static str {
138        match self {
139            Self::Pppp => "pppp",
140            Self::Ppp => "ppp",
141            Self::Pp => "pp",
142            Self::P => "p",
143            Self::Mp => "mp",
144            Self::Mf => "mf",
145            Self::F => "f",
146            Self::Ff => "ff",
147            Self::Fff => "fff",
148            Self::Ffff => "ffff",
149            Self::Sfz => "sfz",
150            Self::Fp => "fp",
151            Self::Custom => "custom",
152        }
153    }
154}
155
156impl fmt::Display for DynamicMarking {
157    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158        formatter.write_str(self.as_str())
159    }
160}
161
162impl FromStr for DynamicMarking {
163    type Err = DynamicsError;
164
165    fn from_str(value: &str) -> Result<Self, Self::Err> {
166        match normalized_label(value)?.as_str() {
167            "pppp" => Ok(Self::Pppp),
168            "ppp" => Ok(Self::Ppp),
169            "pp" => Ok(Self::Pp),
170            "p" => Ok(Self::P),
171            "mp" => Ok(Self::Mp),
172            "mf" => Ok(Self::Mf),
173            "f" => Ok(Self::F),
174            "ff" => Ok(Self::Ff),
175            "fff" => Ok(Self::Fff),
176            "ffff" => Ok(Self::Ffff),
177            "sfz" => Ok(Self::Sfz),
178            "fp" => Ok(Self::Fp),
179            "custom" => Ok(Self::Custom),
180            _ => Err(DynamicsError::UnknownLabel),
181        }
182    }
183}
184#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
185pub enum DynamicChangeKind {
186    Crescendo,
187    Decrescendo,
188    Diminuendo,
189    Subito,
190    Gradual,
191    Unknown,
192}
193
194impl DynamicChangeKind {
195    pub const ALL: &'static [Self] = &[
196        Self::Crescendo,
197        Self::Decrescendo,
198        Self::Diminuendo,
199        Self::Subito,
200        Self::Gradual,
201        Self::Unknown,
202    ];
203
204    pub const fn as_str(self) -> &'static str {
205        match self {
206            Self::Crescendo => "crescendo",
207            Self::Decrescendo => "decrescendo",
208            Self::Diminuendo => "diminuendo",
209            Self::Subito => "subito",
210            Self::Gradual => "gradual",
211            Self::Unknown => "unknown",
212        }
213    }
214}
215
216impl fmt::Display for DynamicChangeKind {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        formatter.write_str(self.as_str())
219    }
220}
221
222impl FromStr for DynamicChangeKind {
223    type Err = DynamicsError;
224
225    fn from_str(value: &str) -> Result<Self, Self::Err> {
226        match normalized_label(value)?.as_str() {
227            "crescendo" => Ok(Self::Crescendo),
228            "decrescendo" => Ok(Self::Decrescendo),
229            "diminuendo" => Ok(Self::Diminuendo),
230            "subito" => Ok(Self::Subito),
231            "gradual" => Ok(Self::Gradual),
232            "unknown" => Ok(Self::Unknown),
233            _ => Err(DynamicsError::UnknownLabel),
234        }
235    }
236}
237#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
238pub enum HairpinKind {
239    Crescendo,
240    Decrescendo,
241}
242
243impl HairpinKind {
244    pub const ALL: &'static [Self] = &[Self::Crescendo, Self::Decrescendo];
245
246    pub const fn as_str(self) -> &'static str {
247        match self {
248            Self::Crescendo => "crescendo",
249            Self::Decrescendo => "decrescendo",
250        }
251    }
252}
253
254impl fmt::Display for HairpinKind {
255    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
256        formatter.write_str(self.as_str())
257    }
258}
259
260impl FromStr for HairpinKind {
261    type Err = DynamicsError;
262
263    fn from_str(value: &str) -> Result<Self, Self::Err> {
264        match normalized_label(value)?.as_str() {
265            "crescendo" => Ok(Self::Crescendo),
266            "decrescendo" => Ok(Self::Decrescendo),
267            _ => Err(DynamicsError::UnknownLabel),
268        }
269    }
270}
271#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
272pub enum AccentDynamicKind {
273    Accent,
274    Marcato,
275    Sforzando,
276    Rinforzando,
277    Custom,
278}
279
280impl AccentDynamicKind {
281    pub const ALL: &'static [Self] = &[
282        Self::Accent,
283        Self::Marcato,
284        Self::Sforzando,
285        Self::Rinforzando,
286        Self::Custom,
287    ];
288
289    pub const fn as_str(self) -> &'static str {
290        match self {
291            Self::Accent => "accent",
292            Self::Marcato => "marcato",
293            Self::Sforzando => "sforzando",
294            Self::Rinforzando => "rinforzando",
295            Self::Custom => "custom",
296        }
297    }
298}
299
300impl fmt::Display for AccentDynamicKind {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        formatter.write_str(self.as_str())
303    }
304}
305
306impl FromStr for AccentDynamicKind {
307    type Err = DynamicsError;
308
309    fn from_str(value: &str) -> Result<Self, Self::Err> {
310        match normalized_label(value)?.as_str() {
311            "accent" => Ok(Self::Accent),
312            "marcato" => Ok(Self::Marcato),
313            "sforzando" => Ok(Self::Sforzando),
314            "rinforzando" => Ok(Self::Rinforzando),
315            "custom" => Ok(Self::Custom),
316            _ => Err(DynamicsError::UnknownLabel),
317        }
318    }
319}
320
321#[derive(Clone, Copy, Debug, Eq, PartialEq)]
322pub enum DynamicsError {
323    Empty,
324    InvalidFormat,
325    OutOfRange,
326    NonFinite,
327    NonPositive,
328    UnknownLabel,
329}
330
331impl fmt::Display for DynamicsError {
332    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
333        match self {
334            Self::Empty => formatter.write_str("dynamics metadata text cannot be empty"),
335            Self::InvalidFormat => formatter.write_str("dynamics metadata has an invalid format"),
336            Self::OutOfRange => formatter.write_str("dynamics metadata value is out of range"),
337            Self::NonFinite => formatter.write_str("dynamics metadata value must be finite"),
338            Self::NonPositive => formatter.write_str("dynamics metadata value must be positive"),
339            Self::UnknownLabel => formatter.write_str("unknown dynamics metadata label"),
340        }
341    }
342}
343
344impl Error for DynamicsError {}
345
346#[allow(dead_code)]
347fn non_empty_text(value: impl AsRef<str>) -> Result<String, DynamicsError> {
348    let trimmed = value.as_ref().trim();
349    if trimmed.is_empty() {
350        Err(DynamicsError::Empty)
351    } else {
352        Ok(trimmed.to_string())
353    }
354}
355
356fn normalized_label(value: &str) -> Result<String, DynamicsError> {
357    let trimmed = value.trim();
358    if trimmed.is_empty() {
359        Err(DynamicsError::Empty)
360    } else {
361        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
362    }
363}
364#[cfg(test)]
365#[allow(
366    unused_imports,
367    clippy::unnecessary_wraps,
368    clippy::assertions_on_constants
369)]
370mod tests {
371    use super::{
372        AccentDynamicKind, DynamicChangeKind, DynamicLevel, DynamicMarking, DynamicsError,
373        ExpressionMarking, HairpinKind,
374    };
375    use core::{fmt, str::FromStr};
376
377    fn assert_enum_family<T>(variants: &[T]) -> Result<(), DynamicsError>
378    where
379        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = DynamicsError>,
380    {
381        for variant in variants {
382            let label = variant.to_string();
383            assert_eq!(label.parse::<T>()?, *variant);
384            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
385            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
386        }
387        Ok(())
388    }
389
390    #[test]
391    fn validates_text_newtypes() -> Result<(), DynamicsError> {
392        let value = ExpressionMarking::new(" example-value ")?;
393        assert_eq!(value.as_str(), "example-value");
394        assert_eq!(value.value(), "example-value");
395        assert_eq!(value.to_string(), "example-value");
396        assert_eq!(
397            <ExpressionMarking as TryFrom<&str>>::try_from("example-value")?,
398            value
399        );
400        Ok(())
401    }
402
403    #[test]
404    fn validates_numeric_newtypes() -> Result<(), DynamicsError> {
405        let value = DynamicLevel::new(0)?;
406        assert_eq!(value.value(), 0);
407        assert_eq!("0".parse::<DynamicLevel>()?, value);
408        assert_eq!(DynamicLevel::new(128), Err(DynamicsError::OutOfRange));
409        Ok(())
410    }
411
412    #[test]
413    fn displays_and_parses_enums() -> Result<(), DynamicsError> {
414        assert_enum_family(DynamicMarking::ALL)?;
415        assert_enum_family(DynamicChangeKind::ALL)?;
416        assert_enum_family(HairpinKind::ALL)?;
417        assert_enum_family(AccentDynamicKind::ALL)?;
418        Ok(())
419    }
420}