From 01d48ac0e46c6d3967e53284cd4e296c18828ac2 Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Sun, 11 Oct 2026 17:09:44 +0200 Subject: [PATCH] Fixed parsing of opening hours with a discount. Fixes #4 Co-Authored-By: Claude Opus 5.5 --- src/backend/data.rs | 45 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/backend/data.rs b/src/backend/data.rs index eb3d96a..fa0d287 100644 --- a/src/backend/data.rs +++ b/src/backend/data.rs @@ -309,7 +309,19 @@ impl TryFrom<&str> for DayHour { return Err(AppError::HourParseError) } - let times = value.split("-") + // Optional discount in parentheses after the interval: "08:00 - 12:00 (10)" + let (interval, discount) = match value.trim().strip_suffix(")") { + Some(rest) => { + let (interval, discount) = rest.rsplit_once("(").ok_or(AppError::HourParseError)?; + if discount.is_empty() || !discount.chars().all(|c| c.is_ascii_digit()) { + return Err(AppError::HourParseError) + } + (interval, Some(discount.parse::().map_err(|_| AppError::HourParseError)?)) + } + None => (value, None) + }; + + let times = interval.split("-") .map(|t| t.trim()).collect::>(); if times.len() != 2 { @@ -326,7 +338,7 @@ impl TryFrom<&str> for DayHour { Ok(DayHour { from: from.unwrap(), to: to.unwrap(), - discount: None + discount }) } } @@ -892,6 +904,24 @@ mod tests { } } + #[test] + fn day_hour_parses_discount() { + let expected = DayHour::new(time(8, 0), time(12, 30), Some(10)); + + assert_eq!(DayHour::try_from("08:00 - 12:30 (10)"), Ok(expected.clone())); + assert_eq!(DayHour::try_from("08:00-12:30(10)"), Ok(expected)); + assert_eq!(DayHour::try_from("08:00 - 12:30 (0)"), Ok(DayHour::new(time(8, 0), time(12, 30), Some(0)))); + } + + #[test] + fn day_hour_rejects_malformed_discount() { + for input in ["08:00 - 12:00 ()", "08:00 - 12:00 (x)", "08:00 - 12:00 (-5)", "08:00 - 12:00 (1.5)", "08:00 - 12:00 (10", + "08:00 - 12:00 10)", "(10) 08:00 - 12:00", "08:00 (10) - 12:00", "08:00 - 12:00 (10) (20)", "(10)", + "08:00 - 12:00 (99999999999)"] { + assert_eq!(DayHour::try_from(input), Err(AppError::HourParseError), "input: {:?}", input); + } + } + #[test] fn day_hours_empty_string_is_closed_day() { let hours = DayHours::try_new("").unwrap(); @@ -930,7 +960,8 @@ mod tests { fn day_hours_display_round_trip() { let hours = DayHours(vec![ DayHour::new(time(8, 0), time(12, 0), None), - DayHour::new(time(13, 0), time(17, 0), None) + DayHour::new(time(13, 0), time(17, 0), Some(10)), + DayHour::new(time(18, 0), time(20, 0), Some(0)) ]); let parsed = DayHours::try_new(&hours.to_string()).unwrap(); @@ -950,9 +981,11 @@ mod tests { #[test] fn week_hours_accepts_valid_hours() { - for input in ["", "08:00 - 12:00", "08:00-12:00", "08:00 - 12:00, 13:00 - 17:00"] { + for input in ["", "08:00 - 12:00", "08:00-12:00", "08:00 - 12:00, 13:00 - 17:00", + "08:00 - 12:00 (10)", "08:00 - 12:00 (10), 13:00 - 17:00", "08:00 - 12:00, 13:00 - 17:00(20)"] { let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() }; assert!(hours.validate().is_ok(), "input: {:?}", input); + assert!(DayHours::try_new(input).is_ok(), "input: {:?}", input); } } @@ -978,7 +1011,7 @@ mod tests { #[test] fn week_hours_validation_agrees_with_parser() { for input in ["", "08:00 - 12:00", "08:00-12:00", "08:00 - 12:00,13:00 - 17:00", "08:00 - 12:00 , 13:00 - 17:00", - "08:00 - 12:00, nonsense", "08:00 - 12:00,", "25:00 - 26:00", "08:00 - 12:00 garbage"] { + "08:00 - 12:00 (10)", "08:00 - 12:00 (10) , 13:00 - 17:00 (5)", "08:00 - 12:00 (99999999999)", "08:00 - 12:00, nonsense", "08:00 - 12:00,", "25:00 - 26:00", "08:00 - 12:00 garbage"] { let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() }; if hours.validate().is_ok() { assert!(DayHours::try_new(input).is_ok(), "input: {:?}", input); @@ -1003,7 +1036,7 @@ mod tests { #[test] fn week_hours_rejects_to_before_from() { - for input in ["12:00 - 08:00", "08:00 - 08:00", "08:00 - 12:00, 17:00 - 13:00"] { + for input in ["12:00 - 08:00", "08:00 - 08:00", "08:00 - 12:00, 17:00 - 13:00", "12:00 - 08:00 (10)"] { let hours = WeekHours { day: Weekday::Mon, hours: input.to_string() }; assert!(has_field_error(&hours, "__all__"), "input: {:?}", input); }