Skip to main content

use_notation/
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        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}