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