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
This commit is contained in:
Dylan Knutson
2025-08-30 00:17:24 +00:00
parent 374abf8c42
commit 8fb51d12a7

View File

@@ -51,12 +51,52 @@ pub fn validate_slots(text: &str) -> Result<i32, String> {
} }
pub fn validate_duration(text: &str) -> Result<ListingDuration, String> { pub fn validate_duration(text: &str) -> Result<ListingDuration, String> {
match text.parse::<i32>() { let text = text.trim().to_lowercase();
Ok(hours) if (1..=720).contains(&hours) => Ok(ListingDuration::hours(hours)), // 1 hour to 30 days
Ok(_) => Err( // Try to parse as plain number first (backwards compatibility)
"❌ Duration must be between 1 and 720 hours. Please enter a valid number:".to_string(), if let Ok(hours) = text.parse::<i32>() {
), if (1..=720).contains(&hours) {
Err(_) => Err("❌ Invalid number. Please enter number of hours (1-720):".to_string()), 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::<i32>() {
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<ListingDuration, String> {
), ),
} }
} }
#[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());
}
}