From 8fb51d12a76544ae82d7762d18a9985e63b224de Mon Sep 17 00:00:00 2001 From: Dylan Knutson Date: Sat, 30 Aug 2025 00:17:24 +0000 Subject: [PATCH] feat: Add natural language duration parsing to validate_duration - Support parsing strings like '1 hour', '7 days' in addition to plain numbers - Maintain backwards compatibility with numeric input (e.g., '24') - Support units: hour(s), hr(s), day(s) - Add comprehensive test suite using rstest - Enforce 1-720 hour limit (1 hour to 30 days) - Provide clear error messages for invalid input --- src/commands/new_listing/validations.rs | 83 +++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/src/commands/new_listing/validations.rs b/src/commands/new_listing/validations.rs index ef63b93..88ee91e 100644 --- a/src/commands/new_listing/validations.rs +++ b/src/commands/new_listing/validations.rs @@ -51,12 +51,52 @@ pub fn validate_slots(text: &str) -> Result { } pub fn validate_duration(text: &str) -> Result { - match text.parse::() { - Ok(hours) if (1..=720).contains(&hours) => Ok(ListingDuration::hours(hours)), // 1 hour to 30 days - Ok(_) => Err( - "❌ Duration must be between 1 and 720 hours. Please enter a valid number:".to_string(), - ), - Err(_) => Err("❌ Invalid number. Please enter number of hours (1-720):".to_string()), + let text = text.trim().to_lowercase(); + + // Try to parse as plain number first (backwards compatibility) + if let Ok(hours) = text.parse::() { + if (1..=720).contains(&hours) { + return Ok(ListingDuration::hours(hours)); + } else { + return Err("❌ Duration must be between 1 hour and 30 days (720 hours). Please enter a valid duration:".to_string()); + } + } + + // Parse natural language duration + let parts: Vec<&str> = text.split_whitespace().collect(); + if parts.len() != 2 { + return Err( + "❌ Please enter duration like '1 hour', '7 days', or just hours (1-720):".to_string(), + ); + } + + let number_str = parts[0]; + let unit = parts[1]; + + let number = match number_str.parse::() { + Ok(n) if n > 0 => n, + _ => { + return Err( + "❌ Duration number must be a positive integer. Please enter a valid duration:" + .to_string(), + ) + } + }; + + let hours = match unit { + "hour" | "hours" | "hr" | "hrs" => number, + "day" | "days" => number * 24, + _ => { + return Err( + "❌ Supported units: hour(s), day(s). Please enter a valid duration:".to_string(), + ) + } + }; + + if (1..=720).contains(&hours) { + Ok(ListingDuration::hours(hours)) + } else { + Err("❌ Duration must be between 1 hour and 30 days (720 hours). Please enter a valid duration:".to_string()) } } @@ -72,3 +112,34 @@ pub fn validate_start_time(text: &str) -> Result { ), } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("24", ListingDuration::hours(24))] // Plain number + #[case("1 hour", ListingDuration::hours(1))] + #[case("2 hours", ListingDuration::hours(2))] + #[case("1 day", ListingDuration::hours(24))] + #[case("7 days", ListingDuration::hours(168))] + #[case("30 days", ListingDuration::hours(720))] // Max 30 days + fn test_validate_duration_valid(#[case] input: &str, #[case] expected: ListingDuration) { + let result = validate_duration(input).unwrap(); + assert_eq!(result, expected); + } + + #[rstest] + #[case("0")] + #[case("0 hours")] + #[case("721")] // Over limit + #[case("1 week")] // Unsupported unit + #[case("1 month")] // Unsupported unit + #[case("1 year")] // Unsupported unit + #[case("abc")] // Invalid text + #[case("-1 hour")] // Negative + fn test_validate_duration_invalid(#[case] input: &str) { + assert!(validate_duration(input).is_err()); + } +}