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 ClefKind, EndingKind, MeasurePosition, MusicDocumentKind, NotationError, NotationFormat,
10 NotationSymbolKind, RepeatMarkKind, ScorePartName, StaffKind, StaffLineCount,
11 };
12}
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct ScorePartName(String);
15
16impl ScorePartName {
17 pub fn new(value: impl AsRef<str>) -> Result<Self, NotationError> {
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 ScorePartName {
35 fn as_ref(&self) -> &str {
36 self.as_str()
37 }
38}
39
40impl fmt::Display for ScorePartName {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 formatter.write_str(self.as_str())
43 }
44}
45
46impl FromStr for ScorePartName {
47 type Err = NotationError;
48
49 fn from_str(value: &str) -> Result<Self, Self::Err> {
50 Self::new(value)
51 }
52}
53
54impl TryFrom<&str> for ScorePartName {
55 type Error = NotationError;
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 StaffLineCount(u8);
63
64impl StaffLineCount {
65 pub fn new(value: u8) -> Result<Self, NotationError> {
66 if !(1..=16).contains(&value) {
67 return Err(NotationError::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 StaffLineCount {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 self.0.fmt(formatter)
81 }
82}
83
84impl FromStr for StaffLineCount {
85 type Err = NotationError;
86
87 fn from_str(value: &str) -> Result<Self, Self::Err> {
88 let parsed = value
89 .trim()
90 .parse::<u8>()
91 .map_err(|_| NotationError::InvalidFormat)?;
92 Self::new(parsed)
93 }
94}
95
96impl TryFrom<u8> for StaffLineCount {
97 type Error = NotationError;
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 struct MeasurePosition(u8);
105
106impl MeasurePosition {
107 pub fn new(value: u8) -> Result<Self, NotationError> {
108 if !(1..=255).contains(&value) {
109 return Err(NotationError::OutOfRange);
110 }
111
112 Ok(Self(value))
113 }
114
115 pub const fn value(self) -> u8 {
116 self.0
117 }
118}
119
120impl fmt::Display for MeasurePosition {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 self.0.fmt(formatter)
123 }
124}
125
126impl FromStr for MeasurePosition {
127 type Err = NotationError;
128
129 fn from_str(value: &str) -> Result<Self, Self::Err> {
130 let parsed = value
131 .trim()
132 .parse::<u8>()
133 .map_err(|_| NotationError::InvalidFormat)?;
134 Self::new(parsed)
135 }
136}
137
138impl TryFrom<u8> for MeasurePosition {
139 type Error = NotationError;
140
141 fn try_from(value: u8) -> Result<Self, Self::Error> {
142 Self::new(value)
143 }
144}
145#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
146pub enum ClefKind {
147 Treble,
148 Bass,
149 Alto,
150 Tenor,
151 Soprano,
152 MezzoSoprano,
153 Baritone,
154 Percussion,
155 Tab,
156 Neutral,
157}
158
159impl ClefKind {
160 pub const ALL: &'static [Self] = &[
161 Self::Treble,
162 Self::Bass,
163 Self::Alto,
164 Self::Tenor,
165 Self::Soprano,
166 Self::MezzoSoprano,
167 Self::Baritone,
168 Self::Percussion,
169 Self::Tab,
170 Self::Neutral,
171 ];
172
173 pub const fn as_str(self) -> &'static str {
174 match self {
175 Self::Treble => "treble",
176 Self::Bass => "bass",
177 Self::Alto => "alto",
178 Self::Tenor => "tenor",
179 Self::Soprano => "soprano",
180 Self::MezzoSoprano => "mezzo-soprano",
181 Self::Baritone => "baritone",
182 Self::Percussion => "percussion",
183 Self::Tab => "tab",
184 Self::Neutral => "neutral",
185 }
186 }
187}
188
189impl fmt::Display for ClefKind {
190 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
191 formatter.write_str(self.as_str())
192 }
193}
194
195impl FromStr for ClefKind {
196 type Err = NotationError;
197
198 fn from_str(value: &str) -> Result<Self, Self::Err> {
199 match normalized_label(value)?.as_str() {
200 "treble" => Ok(Self::Treble),
201 "bass" => Ok(Self::Bass),
202 "alto" => Ok(Self::Alto),
203 "tenor" => Ok(Self::Tenor),
204 "soprano" => Ok(Self::Soprano),
205 "mezzo-soprano" => Ok(Self::MezzoSoprano),
206 "baritone" => Ok(Self::Baritone),
207 "percussion" => Ok(Self::Percussion),
208 "tab" => Ok(Self::Tab),
209 "neutral" => Ok(Self::Neutral),
210 _ => Err(NotationError::UnknownLabel),
211 }
212 }
213}
214#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
215pub enum StaffKind {
216 Standard,
217 Grand,
218 Percussion,
219 Tablature,
220 LeadSheet,
221 ChordChart,
222}
223
224impl StaffKind {
225 pub const ALL: &'static [Self] = &[
226 Self::Standard,
227 Self::Grand,
228 Self::Percussion,
229 Self::Tablature,
230 Self::LeadSheet,
231 Self::ChordChart,
232 ];
233
234 pub const fn as_str(self) -> &'static str {
235 match self {
236 Self::Standard => "standard",
237 Self::Grand => "grand",
238 Self::Percussion => "percussion",
239 Self::Tablature => "tablature",
240 Self::LeadSheet => "lead-sheet",
241 Self::ChordChart => "chord-chart",
242 }
243 }
244}
245
246impl fmt::Display for StaffKind {
247 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
248 formatter.write_str(self.as_str())
249 }
250}
251
252impl FromStr for StaffKind {
253 type Err = NotationError;
254
255 fn from_str(value: &str) -> Result<Self, Self::Err> {
256 match normalized_label(value)?.as_str() {
257 "standard" => Ok(Self::Standard),
258 "grand" => Ok(Self::Grand),
259 "percussion" => Ok(Self::Percussion),
260 "tablature" => Ok(Self::Tablature),
261 "lead-sheet" => Ok(Self::LeadSheet),
262 "chord-chart" => Ok(Self::ChordChart),
263 _ => Err(NotationError::UnknownLabel),
264 }
265 }
266}
267#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
268pub enum NotationSymbolKind {
269 Note,
270 Rest,
271 Clef,
272 KeySignature,
273 TimeSignature,
274 Dynamic,
275 Articulation,
276 Repeat,
277 Text,
278 Custom,
279}
280
281impl NotationSymbolKind {
282 pub const ALL: &'static [Self] = &[
283 Self::Note,
284 Self::Rest,
285 Self::Clef,
286 Self::KeySignature,
287 Self::TimeSignature,
288 Self::Dynamic,
289 Self::Articulation,
290 Self::Repeat,
291 Self::Text,
292 Self::Custom,
293 ];
294
295 pub const fn as_str(self) -> &'static str {
296 match self {
297 Self::Note => "note",
298 Self::Rest => "rest",
299 Self::Clef => "clef",
300 Self::KeySignature => "key-signature",
301 Self::TimeSignature => "time-signature",
302 Self::Dynamic => "dynamic",
303 Self::Articulation => "articulation",
304 Self::Repeat => "repeat",
305 Self::Text => "text",
306 Self::Custom => "custom",
307 }
308 }
309}
310
311impl fmt::Display for NotationSymbolKind {
312 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
313 formatter.write_str(self.as_str())
314 }
315}
316
317impl FromStr for NotationSymbolKind {
318 type Err = NotationError;
319
320 fn from_str(value: &str) -> Result<Self, Self::Err> {
321 match normalized_label(value)?.as_str() {
322 "note" => Ok(Self::Note),
323 "rest" => Ok(Self::Rest),
324 "clef" => Ok(Self::Clef),
325 "key-signature" => Ok(Self::KeySignature),
326 "time-signature" => Ok(Self::TimeSignature),
327 "dynamic" => Ok(Self::Dynamic),
328 "articulation" => Ok(Self::Articulation),
329 "repeat" => Ok(Self::Repeat),
330 "text" => Ok(Self::Text),
331 "custom" => Ok(Self::Custom),
332 _ => Err(NotationError::UnknownLabel),
333 }
334 }
335}
336#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
337pub enum NotationFormat {
338 MusicXml,
339 Midi,
340 Abc,
341 LilyPond,
342 MuseScore,
343 GuitarPro,
344 PlainText,
345 Custom,
346}
347
348impl NotationFormat {
349 pub const ALL: &'static [Self] = &[
350 Self::MusicXml,
351 Self::Midi,
352 Self::Abc,
353 Self::LilyPond,
354 Self::MuseScore,
355 Self::GuitarPro,
356 Self::PlainText,
357 Self::Custom,
358 ];
359
360 pub const fn as_str(self) -> &'static str {
361 match self {
362 Self::MusicXml => "music-xml",
363 Self::Midi => "midi",
364 Self::Abc => "abc",
365 Self::LilyPond => "lily-pond",
366 Self::MuseScore => "muse-score",
367 Self::GuitarPro => "guitar-pro",
368 Self::PlainText => "plain-text",
369 Self::Custom => "custom",
370 }
371 }
372}
373
374impl fmt::Display for NotationFormat {
375 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376 formatter.write_str(self.as_str())
377 }
378}
379
380impl FromStr for NotationFormat {
381 type Err = NotationError;
382
383 fn from_str(value: &str) -> Result<Self, Self::Err> {
384 match normalized_label(value)?.as_str() {
385 "music-xml" => Ok(Self::MusicXml),
386 "midi" => Ok(Self::Midi),
387 "abc" => Ok(Self::Abc),
388 "lily-pond" => Ok(Self::LilyPond),
389 "muse-score" => Ok(Self::MuseScore),
390 "guitar-pro" => Ok(Self::GuitarPro),
391 "plain-text" => Ok(Self::PlainText),
392 "custom" => Ok(Self::Custom),
393 _ => Err(NotationError::UnknownLabel),
394 }
395 }
396}
397#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
398pub enum MusicDocumentKind {
399 Score,
400 Part,
401 LeadSheet,
402 ChordChart,
403 Tablature,
404 FakeBook,
405 Exercise,
406 Custom,
407}
408
409impl MusicDocumentKind {
410 pub const ALL: &'static [Self] = &[
411 Self::Score,
412 Self::Part,
413 Self::LeadSheet,
414 Self::ChordChart,
415 Self::Tablature,
416 Self::FakeBook,
417 Self::Exercise,
418 Self::Custom,
419 ];
420
421 pub const fn as_str(self) -> &'static str {
422 match self {
423 Self::Score => "score",
424 Self::Part => "part",
425 Self::LeadSheet => "lead-sheet",
426 Self::ChordChart => "chord-chart",
427 Self::Tablature => "tablature",
428 Self::FakeBook => "fake-book",
429 Self::Exercise => "exercise",
430 Self::Custom => "custom",
431 }
432 }
433}
434
435impl fmt::Display for MusicDocumentKind {
436 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
437 formatter.write_str(self.as_str())
438 }
439}
440
441impl FromStr for MusicDocumentKind {
442 type Err = NotationError;
443
444 fn from_str(value: &str) -> Result<Self, Self::Err> {
445 match normalized_label(value)?.as_str() {
446 "score" => Ok(Self::Score),
447 "part" => Ok(Self::Part),
448 "lead-sheet" => Ok(Self::LeadSheet),
449 "chord-chart" => Ok(Self::ChordChart),
450 "tablature" => Ok(Self::Tablature),
451 "fake-book" => Ok(Self::FakeBook),
452 "exercise" => Ok(Self::Exercise),
453 "custom" => Ok(Self::Custom),
454 _ => Err(NotationError::UnknownLabel),
455 }
456 }
457}
458#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
459pub enum RepeatMarkKind {
460 RepeatStart,
461 RepeatEnd,
462 RepeatBoth,
463 DalSegno,
464 DaCapo,
465 Coda,
466 Fine,
467}
468
469impl RepeatMarkKind {
470 pub const ALL: &'static [Self] = &[
471 Self::RepeatStart,
472 Self::RepeatEnd,
473 Self::RepeatBoth,
474 Self::DalSegno,
475 Self::DaCapo,
476 Self::Coda,
477 Self::Fine,
478 ];
479
480 pub const fn as_str(self) -> &'static str {
481 match self {
482 Self::RepeatStart => "repeat-start",
483 Self::RepeatEnd => "repeat-end",
484 Self::RepeatBoth => "repeat-both",
485 Self::DalSegno => "dal-segno",
486 Self::DaCapo => "da-capo",
487 Self::Coda => "coda",
488 Self::Fine => "fine",
489 }
490 }
491}
492
493impl fmt::Display for RepeatMarkKind {
494 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
495 formatter.write_str(self.as_str())
496 }
497}
498
499impl FromStr for RepeatMarkKind {
500 type Err = NotationError;
501
502 fn from_str(value: &str) -> Result<Self, Self::Err> {
503 match normalized_label(value)?.as_str() {
504 "repeat-start" => Ok(Self::RepeatStart),
505 "repeat-end" => Ok(Self::RepeatEnd),
506 "repeat-both" => Ok(Self::RepeatBoth),
507 "dal-segno" => Ok(Self::DalSegno),
508 "da-capo" => Ok(Self::DaCapo),
509 "coda" => Ok(Self::Coda),
510 "fine" => Ok(Self::Fine),
511 _ => Err(NotationError::UnknownLabel),
512 }
513 }
514}
515#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
516pub enum EndingKind {
517 FirstEnding,
518 SecondEnding,
519 ThirdEnding,
520 Custom,
521}
522
523impl EndingKind {
524 pub const ALL: &'static [Self] = &[
525 Self::FirstEnding,
526 Self::SecondEnding,
527 Self::ThirdEnding,
528 Self::Custom,
529 ];
530
531 pub const fn as_str(self) -> &'static str {
532 match self {
533 Self::FirstEnding => "first-ending",
534 Self::SecondEnding => "second-ending",
535 Self::ThirdEnding => "third-ending",
536 Self::Custom => "custom",
537 }
538 }
539}
540
541impl fmt::Display for EndingKind {
542 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
543 formatter.write_str(self.as_str())
544 }
545}
546
547impl FromStr for EndingKind {
548 type Err = NotationError;
549
550 fn from_str(value: &str) -> Result<Self, Self::Err> {
551 match normalized_label(value)?.as_str() {
552 "first-ending" => Ok(Self::FirstEnding),
553 "second-ending" => Ok(Self::SecondEnding),
554 "third-ending" => Ok(Self::ThirdEnding),
555 "custom" => Ok(Self::Custom),
556 _ => Err(NotationError::UnknownLabel),
557 }
558 }
559}
560
561#[derive(Clone, Copy, Debug, Eq, PartialEq)]
562pub enum NotationError {
563 Empty,
564 InvalidFormat,
565 OutOfRange,
566 NonFinite,
567 NonPositive,
568 UnknownLabel,
569}
570
571impl fmt::Display for NotationError {
572 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
573 match self {
574 Self::Empty => formatter.write_str("notation metadata text cannot be empty"),
575 Self::InvalidFormat => formatter.write_str("notation metadata has an invalid format"),
576 Self::OutOfRange => formatter.write_str("notation metadata value is out of range"),
577 Self::NonFinite => formatter.write_str("notation metadata value must be finite"),
578 Self::NonPositive => formatter.write_str("notation metadata value must be positive"),
579 Self::UnknownLabel => formatter.write_str("unknown notation metadata label"),
580 }
581 }
582}
583
584impl Error for NotationError {}
585
586#[allow(dead_code)]
587fn non_empty_text(value: impl AsRef<str>) -> Result<String, NotationError> {
588 let trimmed = value.as_ref().trim();
589 if trimmed.is_empty() {
590 Err(NotationError::Empty)
591 } else {
592 Ok(trimmed.to_string())
593 }
594}
595
596fn normalized_label(value: &str) -> Result<String, NotationError> {
597 let trimmed = value.trim();
598 if trimmed.is_empty() {
599 Err(NotationError::Empty)
600 } else {
601 Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
602 }
603}
604#[cfg(test)]
605#[allow(
606 unused_imports,
607 clippy::unnecessary_wraps,
608 clippy::assertions_on_constants
609)]
610mod tests {
611 use super::{
612 ClefKind, EndingKind, MeasurePosition, MusicDocumentKind, NotationError, NotationFormat,
613 NotationSymbolKind, RepeatMarkKind, ScorePartName, StaffKind, StaffLineCount,
614 };
615 use core::{fmt, str::FromStr};
616
617 fn assert_enum_family<T>(variants: &[T]) -> Result<(), NotationError>
618 where
619 T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = NotationError>,
620 {
621 for variant in variants {
622 let label = variant.to_string();
623 assert_eq!(label.parse::<T>()?, *variant);
624 assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
625 assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
626 }
627 Ok(())
628 }
629
630 #[test]
631 fn validates_text_newtypes() -> Result<(), NotationError> {
632 let value = ScorePartName::new(" example-value ")?;
633 assert_eq!(value.as_str(), "example-value");
634 assert_eq!(value.value(), "example-value");
635 assert_eq!(value.to_string(), "example-value");
636 assert_eq!(
637 <ScorePartName as TryFrom<&str>>::try_from("example-value")?,
638 value
639 );
640 Ok(())
641 }
642
643 #[test]
644 fn validates_numeric_newtypes() -> Result<(), NotationError> {
645 let value = StaffLineCount::new(1)?;
646 assert_eq!(value.value(), 1);
647 assert_eq!("1".parse::<StaffLineCount>()?, value);
648 assert_eq!(StaffLineCount::new(17), Err(NotationError::OutOfRange));
649 let value = MeasurePosition::new(1)?;
650 assert_eq!(value.value(), 1);
651 assert_eq!("1".parse::<MeasurePosition>()?, value);
652 assert_eq!(MeasurePosition::new(0), Err(NotationError::OutOfRange));
653 Ok(())
654 }
655
656 #[test]
657 fn displays_and_parses_enums() -> Result<(), NotationError> {
658 assert_enum_family(ClefKind::ALL)?;
659 assert_enum_family(StaffKind::ALL)?;
660 assert_enum_family(NotationSymbolKind::ALL)?;
661 assert_enum_family(NotationFormat::ALL)?;
662 assert_enum_family(MusicDocumentKind::ALL)?;
663 assert_enum_family(RepeatMarkKind::ALL)?;
664 assert_enum_family(EndingKind::ALL)?;
665 Ok(())
666 }
667}