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}