// SPDX-License-Identifier: AGPL-3.0-or-later //! Nom parsers used within the parsing steps. use nom::{ branch::alt, bytes::complete::{tag, take, take_till, take_while}, character::complete::{char, multispace0}, combinator::{map_res, rest}, sequence::{delimited, pair, preceded, separated_pair}, IResult, }; use crate::{CourseOffering, Trimester}; /// Determines if the provided character is an ascii digit. const fn is_decimal_digit(c: char) -> bool { c.is_ascii_digit() } /// Parses a string slice into a [`u16`]. /// /// # Errors /// /// This function will return an error if the string cannot be parsed. fn from_decimal(input: &str) -> Result { input.parse::() } /// Retrieves all the digits from a CRN and maps them to a [`u16`]. /// /// # Errors /// /// This function will return an error if nom cannot parse the input. fn crn_digits(input: &str) -> IResult<&str, u16> { map_res(take_while(is_decimal_digit), from_decimal)(input) } /// Parses a course reference number. /// /// # Errors /// /// This function will return an error if nom cannot parse the input. pub fn course_reference_number(input: &str) -> IResult<&str, u16> { preceded(pair(tag("CRN"), multispace0), crn_digits)(input) } /// Parser that parses a timetable separator. /// /// # Errors /// /// This function will return an error if input does not match expected format by nom. pub fn timetable_separator(input: &str) -> IResult<&str, char> { delimited(multispace0, char('\u{2022}'), multispace0)(input) } /// Parses a trimester from an input. /// /// # Errors /// /// This function will return an error if input does not match expected format by nom. pub fn trimester(input: &str) -> IResult<&str, Trimester> { map_res( alt(( tag("block dates/3"), tag("part year/3"), tag("full year"), take_till(char::is_whitespace), )), Trimester::try_from, )(input) } /// Parses a course offering from an input. /// /// # Errors /// /// This function will return an error if . pub fn offering(input: &str) -> IResult<&str, CourseOffering> { let (input, (trimester, crn)) = separated_pair(trimester, timetable_separator, course_reference_number)(input)?; Ok((input, CourseOffering::new(crn, trimester))) } /// Parses a course title from an input. /// /// # Errors /// /// This function will return an error if nom is unable to parse the input. This should only happen /// if the input is less than 8 characters. pub fn title(input: &str) -> IResult<&str, &str> { take(8usize)(input) } /// Parses a course subtitle from an input. /// /// # Errors /// /// This function will return an error if the subtitle is not preceded by "`\u{2013}` ". pub fn subtitle(input: &str) -> IResult<&str, &str> { preceded(tag("\u{2013} "), rest)(input) } #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; #[test] fn crn_parser_basic() { assert_eq!(course_reference_number("CRN 5912").unwrap().1, 5912); assert_eq!(course_reference_number("CRN 17146").unwrap().1, 17146); } #[test] fn crn_parser_postfix() { assert_eq!( course_reference_number("CRN 331 [Distance]").unwrap().1, 331 ); } #[test] fn crn_parser_extra_whitespace() { assert_eq!(course_reference_number("CRN 8913 ").unwrap().1, 8913); assert_eq!(course_reference_number("CRN 61151").unwrap().1, 61151); } #[test] fn crn_parser_no_whitespace() { assert_eq!(course_reference_number("CRN615").unwrap().1, 615); } #[test] fn offering_parser_alphabetic_durations() { let block_dates = offering("block dates/3 \u{2022} CRN 25341").unwrap(); assert_eq!(block_dates.1.course_reference_number, 25341); assert_eq!(block_dates.1.trimester, Trimester::BlockDates); let part_year = offering("part year/3 \u{2022} CRN 1816").unwrap(); assert_eq!(part_year.1.course_reference_number, 1816); assert_eq!(part_year.1.trimester, Trimester::PartYear); let full_year = offering("full year \u{2022} CRN 19175").unwrap(); assert_eq!(full_year.1.course_reference_number, 19175); assert_eq!(full_year.1.trimester, Trimester::FullYear); } #[test] fn title_parser() { assert_eq!(title("HELT 502 ").unwrap().1, "HELT 502"); } #[test] fn subtitle_parser() { let parsed_subtitle = subtitle("\u{2013} Identification, Assessment and Control of Hazards and Risks") .unwrap(); // The dash and space were parsed, and are not remaining data. assert_eq!(parsed_subtitle.0, ""); // The actual subtitle. assert_eq!( parsed_subtitle.1, "Identification, Assessment and Control of Hazards and Risks" ); } }