diff --git a/src/languages/german.rs b/src/languages/german.rs new file mode 100644 index 0000000..138887d --- /dev/null +++ b/src/languages/german.rs @@ -0,0 +1,255 @@ +//! A module for German LilyPond note input. + +use super::common; +use crate::{ + lilypond_objects::lilypond_note::LilyPondNote, + notation::{ + note::Note, + pitch::{Accidental, NoteName, Pitch}, + rhythm::DurationType, + }, +}; + +pub static LANGUAGE_STR: &str = "german"; + +pub static NOTE_REGEX_STR: &str = r"(?x-u) + # Flags: x = whitespace allowed, -u = no unicode support + ^(?P[a-gr]) # note name or rest + (?P(?:is){0,2}|(?:es){0,2}) # accidental + (?P(?:(?:,{0,3})|(?:'{0,6}))?) # octave transposition characters + (?P(?:1|2|4|8|(?:16)|(?:32)|(?:64)|(?:128))?) # Durations + (?P\.{0,255})$ # optional dot and end of line + "; + +pub fn lilypond_from_note(note: &Note) -> String { + // We cover the special case in German that the English b flat is covered by a simple B. + if (note.pitch.note_name == NoteName::B) && (note.pitch.accidental == Accidental::Flat) { + return format!( + "b{}{}{}", + note.pitch.octave, + note.rhythm.length, + note.rhythm.dots, + ) + } + format!( + "{}{}{}{}{}", + lilypond_from_note_name(note), + lilypond_from_accidental(note), + note.pitch.octave, + note.rhythm.length, + note.rhythm.dots, + ) +} + +fn lilypond_from_note_name(note: &Note) -> &str { + match note.rhythm.duration_type { + DurationType::Rest => "r", + DurationType::Note => match note.pitch.note_name { + NoteName::A => "a", + NoteName::B => "h", + NoteName::C => "c", + NoteName::D => "d", + NoteName::E => "e", + NoteName::F => "f", + NoteName::G => "g", + NoteName::None => "r", + }, + } +} + +fn lilypond_from_accidental(note: &Note) -> &str { + match note.pitch.accidental { + Accidental::None => "", + Accidental::Flat => "es", + Accidental::DoubleFlat => "eses", + Accidental::Sharp => "is", + Accidental::DoubleSharp => "isis", + } +} + +pub fn note_from_lilypond(note: &LilyPondNote) -> Result { + Ok(Note { + pitch: Pitch { + note_name: note_name_from_lilypond(note)?, + accidental: accidental_from_lilypond(note)?, + octave: common::octave_from_lilypond(note)?, + }, + rhythm: common::rhythm_from_lilypond(note)?, + }) +} + +fn note_name_from_lilypond(note: &LilyPondNote) -> Result { + match common::duration_type_from_lilypond(note) { + DurationType::Rest => Ok(NoteName::None), + DurationType::Note => match note.get_capture("note_name").as_str() { + "a" => Ok(NoteName::A), + "h" => Ok(NoteName::B), + "c" => Ok(NoteName::C), + "d" => Ok(NoteName::D), + "e" => Ok(NoteName::E), + "f" => Ok(NoteName::F), + "g" => Ok(NoteName::G), + "b" => Ok(NoteName::B), + e => Err(format!("Invalid note name '{}'.", e)), + }, + } +} + +fn accidental_from_lilypond(note: &LilyPondNote) -> Result { + + // The German special case German b = English b flat + if note.get_capture("note_name").as_str().eq("b") { + return Ok(Accidental::Flat); + }; + + match common::duration_type_from_lilypond(note) { + DurationType::Rest => Ok(Accidental::None), + DurationType::Note => match note.get_capture("accidental").as_str() { + "" => Ok(Accidental::None), + "is" => Ok(Accidental::Sharp), + "isis" => Ok(Accidental::DoubleSharp), + "es" => Ok(Accidental::Flat), + "eses" => Ok(Accidental::DoubleFlat), + e => Err(format!("Invalid accidental '{}'.", e)), + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + use lazy_static::lazy_static; + use regex::Regex; + lazy_static! { + static ref LILYPOND_NOTE_REGEX: Regex = Regex::new(NOTE_REGEX_STR).unwrap(); + } + #[test] + fn test_note_name() { + let note = Note::new(NoteName::A); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("a", note_name_str); + let note = Note::new(NoteName::B); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("h", note_name_str); + let note = Note::new(NoteName::C); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("c", note_name_str); + let note = Note::new(NoteName::D); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("d", note_name_str); + let note = Note::new(NoteName::E); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("e", note_name_str); + let note = Note::new(NoteName::F); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("f", note_name_str); + let note = Note::new(NoteName::G); + let note_name_str = lilypond_from_note_name(¬e); + assert_eq!("g", note_name_str); + } + #[test] + fn test_accidental() { + let mut note = Note::new(NoteName::A); + let accidental_str = lilypond_from_accidental(¬e); + assert_eq!("", accidental_str); + note.pitch.accidental(Accidental::Flat); + let accidental_str = lilypond_from_accidental(¬e); + assert_eq!("es", accidental_str); + note.pitch.accidental(Accidental::Sharp); + let accidental_str = lilypond_from_accidental(¬e); + assert_eq!("is", accidental_str); + note.pitch.accidental(Accidental::DoubleFlat); + let accidental_str = lilypond_from_accidental(¬e); + assert_eq!("eses", accidental_str); + note.pitch.accidental(Accidental::DoubleSharp); + let accidental_str = lilypond_from_accidental(¬e); + assert_eq!("isis", accidental_str); + } + fn test_regex_case(note: &str) { + assert!(LILYPOND_NOTE_REGEX.is_match(note)); + } + #[test] + fn test_regex() { + let notes = [ + "r", + "a", + "b", + "cis", + "d,", + "ees'", + "fis", + "g,,", + "aes''1", + "bis2", + "c,,,4", + "des'''8", + "eis16", + "f32", + "ges''''64", + "ais128", + "b,", + "ces'''''1.", + "dis,,2.", + "e4.", + "fes,,,8.", + "feses,,,8.", + "gis''''''16.", + ]; + for note in notes { + test_regex_case(note); + println!("{}", note); + } + } + // not currently testing these two because I can't change the value of + // NOTE_NAME_LANGUAGE to change the LILYPOND_NOTE_REGEX. + #[allow(dead_code)] + fn test_accidental_from_lilypond() { + let ly_note = LilyPondNote::new("r8").unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(accidental_type, Accidental::None); + + let ly_note = LilyPondNote::new("fis").unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(accidental_type, Accidental::Sharp); + + let ly_note = LilyPondNote::new("ees").unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(accidental_type, Accidental::Flat); + + let ly_note = LilyPondNote::new("gisis").unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(accidental_type, Accidental::DoubleSharp); + + let ly_note = LilyPondNote::new("aeses").unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(accidental_type, Accidental::DoubleFlat); + } + #[allow(dead_code)] + fn test_note_name_from_lilypond() { + let ly_note = LilyPondNote::new("r8").unwrap(); + let note_name = note_name_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::None); + + let ly_note = LilyPondNote::new("fis").unwrap(); + let note_name = note_name_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::F); + + let ly_note = LilyPondNote::new("ees").unwrap(); + let note_name = note_name_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::E); + + let ly_note = LilyPondNote::new("gisis").unwrap(); + let note_name = note_name_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::G); + + let ly_note = LilyPondNote::new("aeses").unwrap(); + let note_name = note_name_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::A); + + let ly_note: LilyPondNote = LilyPondNote::new("b").unwrap(); + let note_name: NoteName = note_name_from_lilypond(&ly_note).unwrap(); + let accidental_type = accidental_from_lilypond(&ly_note).unwrap(); + assert_eq!(note_name, NoteName::B); + assert_eq!(accidental_type, Accidental::Flat); + } +} diff --git a/src/languages/mod.rs b/src/languages/mod.rs index 54ac4a7..3975b64 100644 --- a/src/languages/mod.rs +++ b/src/languages/mod.rs @@ -23,6 +23,7 @@ use lazy_static::lazy_static; mod common; mod english; mod nederlands; +mod german; lazy_static! { /// The string to be used by LilyPond to select the language, @@ -33,6 +34,7 @@ lazy_static! { pub static ref LANGUAGE_STR: &'static str = match *crate::NOTE_NAME_LANGUAGE { NoteNameLanguage::English => english::LANGUAGE_STR, NoteNameLanguage::Nederlands => nederlands::LANGUAGE_STR, + NoteNameLanguage::German => german::LANGUAGE_STR, }; /// A regular expression string which is used to match and parse LilyPond /// notes. @@ -46,6 +48,7 @@ lazy_static! { pub static ref NOTE_REGEX_STR: &'static str = match *crate::NOTE_NAME_LANGUAGE { NoteNameLanguage::English => english::NOTE_REGEX_STR, NoteNameLanguage::Nederlands => nederlands::NOTE_REGEX_STR, + NoteNameLanguage::German => german::NOTE_REGEX_STR, }; } @@ -55,6 +58,7 @@ pub fn lilypond_from_note(note: &Note) -> String { match *crate::NOTE_NAME_LANGUAGE { NoteNameLanguage::English => english::lilypond_from_note(note), NoteNameLanguage::Nederlands => nederlands::lilypond_from_note(note), + NoteNameLanguage::German => german::lilypond_from_note(note), } } @@ -70,5 +74,6 @@ pub fn note_from_lilypond(note: &LilyPondNote) -> Result { match *crate::NOTE_NAME_LANGUAGE { NoteNameLanguage::English => english::note_from_lilypond(note), NoteNameLanguage::Nederlands => nederlands::note_from_lilypond(note), + NoteNameLanguage::German => german::note_from_lilypond(note), } } diff --git a/src/lib.rs b/src/lib.rs index d172e69..3eddd02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,6 +171,11 @@ pub enum NoteNameLanguage { /// - Note names: `c`, `d`, `e`, `f`, `g`, `a`, `b` /// - Accidentals: `is`, `es`, `isis`, `eses` Nederlands, + /// German note names and accidentals. + /// + /// - Note names: `c`, `d`, `e`, `f`, `g`, `a`, `h`, `c` + /// - Accidentals: `is`, `es`, `b` + German, } impl Default for NoteNameLanguage {