diff --git a/src/parser.rs b/src/parser.rs index 63d4a8a..40590b2 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,7 +1,7 @@ use crate::{arrangement::Line, pitch::Pitch}; use anyhow::{anyhow, Result}; use itertools::Itertools; -use regex::Regex; +use regex::RegexBuilder; use std::{collections::HashSet, str::FromStr}; use strum_macros::EnumString; @@ -59,10 +59,16 @@ fn parse_measure_break(input_line: &str) -> Option>> { None } +/// Parses input line to extract valid musical pitches, returning an error if any part of the +/// input line cannot be parsed into a pitch. fn parse_pitch(input_index: usize, input_line: &str) -> Result>> { - let pattern = r"(?P[a-gA-G][#|♯|b|♭][0-9])|(?P[a-gA-G][0-9])"; - let re = Regex::new(pattern).unwrap(); - let (each_matched_indices, matched_pitches): (Vec>, Vec) = re + let pattern = r"(?P[A-G][#|♯|b|♭][0-9])|(?P[A-G][0-9])"; + // let re = Regex::new(pattern); + let re = RegexBuilder::new(pattern) + .case_insensitive(true) + .build() + .expect("Regex pattern should be valid"); + let (matched_index_ranges, matched_pitches): (Vec>, Vec) = re .find_iter(input_line) .filter_map(|regex_match| { if let Ok(pitch) = Pitch::from_str(regex_match.as_str()) { @@ -72,7 +78,7 @@ fn parse_pitch(input_index: usize, input_line: &str) -> Result>> }) .unzip(); - let matched_indices: HashSet = each_matched_indices.into_iter().flatten().collect(); + let matched_indices: HashSet = matched_index_ranges.into_iter().flatten().collect(); let input_indices: HashSet = (0..input_line.len()).collect(); let unmatched_indices: Vec = input_indices @@ -104,6 +110,77 @@ fn parse_pitch(input_index: usize, input_line: &str) -> Result>> Ok(Line::Playable(matched_pitches)) } +#[cfg(test)] +mod test_parse_pitch { + use super::*; + + #[test] + fn single_natural_pitch() -> Result<()> { + assert_eq!(parse_pitch(0, "A0")?, Line::Playable(vec![Pitch::A0])); + assert_eq!(parse_pitch(0, "E6")?, Line::Playable(vec![Pitch::E6])); + Ok(()) + } + #[test] + fn single_sharp_pitch() { + assert_eq!( + parse_pitch(0, "D#2").unwrap(), + Line::Playable(vec![Pitch::DSharpEFlat2]) + ); + } + #[test] + fn single_flat_pitch() { + assert_eq!( + parse_pitch(0, "Db2").unwrap(), + Line::Playable(vec![Pitch::CSharpDFlat2]) + ); + assert_eq!( + parse_pitch(0, "Bb2").unwrap(), + Line::Playable(vec![Pitch::ASharpBFlat2]) + ); + } + #[test] + fn case_insensitivity() { + assert_eq!( + parse_pitch(0, "A3").unwrap(), + Line::Playable(vec![Pitch::A3]) + ); + assert_eq!( + parse_pitch(0, "a3").unwrap(), + Line::Playable(vec![Pitch::A3]) + ); + assert_eq!( + parse_pitch(0, "Bb2").unwrap(), + Line::Playable(vec![Pitch::ASharpBFlat2]) + ); + assert_eq!( + parse_pitch(0, "bB2").unwrap(), + Line::Playable(vec![Pitch::ASharpBFlat2]) + ); + assert_eq!( + parse_pitch(0, "bb2").unwrap(), + Line::Playable(vec![Pitch::ASharpBFlat2]) + ); + } + #[test] + fn multiple_pitches() { + assert_eq!( + parse_pitch(0, "C3G2A#1F8").unwrap(), + Line::Playable(vec![Pitch::C3, Pitch::G2, Pitch::ASharpBFlat1, Pitch::F8]) + ); + } + #[test] + fn invalid_typo() { + let error_msg = format!("{}", parse_pitch(12, "ZA2G#444B3").unwrap_err()); + let expected_error_msg = "Input 'Z' on line 13 could not be parsed into a pitch.\nInput '44' on line 13 could not be parsed into a pitch."; + assert_eq!(error_msg, expected_error_msg); + } + #[test] + fn invalid_random() { + let error_msg = format!("{}", parse_pitch(0, "baS3Q-hNr").unwrap_err()); + let expected_error_msg = "Input 'baS3Q-hNr' on line 1 could not be parsed into a pitch."; + assert_eq!(error_msg, expected_error_msg); + } +} /// Returns a vector of consecutive slices of the input numbers. ///