#[cfg(test)]
mod tests {
use super::*;
fn strs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn parse_slots_valid_hh_mm_normalises_and_returns_times() {
let result = parse_slots(&strs(&["8:5"])).unwrap();
assert_eq!(result.normalized, vec!["08:05"]);
assert_eq!(result.times, vec![(8, 5)]);
}
#[test]
fn parse_slots_skips_empty_strings() {
let result = parse_slots(&strs(&["", "20:00"])).unwrap();
assert_eq!(result.times, vec![(20, 0)]);
}
#[test]
fn parse_slots_returns_invalid_format_for_non_numeric_input() {
let err = parse_slots(&strs(&["ab:cd"])).unwrap_err();
assert_eq!(err, SlotParseError::InvalidFormat { slot_index: 0 });
}
#[test]
fn parse_slots_returns_invalid_format_when_minute_missing() {
let err = parse_slots(&strs(&["08"])).unwrap_err();
assert_eq!(err, SlotParseError::InvalidFormat { slot_index: 0 });
}
#[test]
fn parse_slots_returns_out_of_range_for_hour_above_23() {
let err = parse_slots(&strs(&["24:00"])).unwrap_err();
assert_eq!(err, SlotParseError::OutOfRange { slot_index: 0 });
}
#[test]
fn parse_slots_returns_out_of_range_for_minute_above_59() {
let err = parse_slots(&strs(&["08:60"])).unwrap_err();
assert_eq!(err, SlotParseError::OutOfRange { slot_index: 0 });
}
#[test]
fn parse_slots_error_reports_correct_slot_index() {
let err = parse_slots(&strs(&["08:00", "bad"])).unwrap_err();
assert_eq!(err, SlotParseError::InvalidFormat { slot_index: 1 });
}
#[test]
fn validate_slot_count_once_daily_accepts_one() {
assert!(validate_slot_count(0, 1).is_ok());
}
#[test]
fn validate_slot_count_once_daily_rejects_two() {
assert!(validate_slot_count(0, 2).is_err());
}
#[test]
fn validate_slot_count_twice_daily_accepts_two() {
assert!(validate_slot_count(1, 2).is_ok());
}
#[test]
fn validate_slot_count_thrice_daily_accepts_three() {
assert!(validate_slot_count(2, 3).is_ok());
}
#[test]
fn validate_slot_count_custom_accepts_any_nonzero_count() {
assert!(validate_slot_count(3, 1).is_ok());
assert!(validate_slot_count(3, 5).is_ok());
}
#[test]
fn validate_slot_count_custom_rejects_zero() {
assert!(validate_slot_count(3, 0).is_err());
}
#[test]
fn validate_slot_count_error_message_includes_expected_count_for_fixed_frequency() {
let err = validate_slot_count(1, 1).unwrap_err();
assert!(err.contains('2'));
}
#[test]
fn frequency_str_maps_indices_to_expected_strings() {
assert_eq!(frequency_str(0), "OnceDaily");
assert_eq!(frequency_str(1), "TwiceDaily");
assert_eq!(frequency_str(2), "ThriceDaily");
assert_eq!(frequency_str(3), "Custom");
}
}
#[derive(Debug, PartialEq)]
pub struct ParsedSlots {
pub normalized: Vec<String>,
pub times: Vec<(u32, u32)>,
}
#[derive(Debug, PartialEq)]
pub enum SlotParseError {
InvalidFormat { slot_index: usize },
OutOfRange { slot_index: usize },
}
impl std::fmt::Display for SlotParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFormat { slot_index } => {
write!(
f,
"Invalid time in slot {}: expected HH:MM or HH:MM:SS",
slot_index + 1
)
}
Self::OutOfRange { slot_index } => {
write!(f, "Time out of range in slot {}", slot_index + 1)
}
}
}
}
pub fn parse_slots(raw: &[String]) -> Result<ParsedSlots, SlotParseError> {
let mut normalized = raw.to_vec();
let mut times = Vec::new();
for (i, slot) in normalized.iter_mut().enumerate() {
let part = slot.trim();
if part.is_empty() {
continue;
}
let mut iter = part.split(':');
let h = iter
.next()
.and_then(|s| s.parse::<u32>().ok())
.ok_or(SlotParseError::InvalidFormat { slot_index: i })?;
let m = iter
.next()
.and_then(|s| s.parse::<u32>().ok())
.ok_or(SlotParseError::InvalidFormat { slot_index: i })?;
if h > 23 || m > 59 {
return Err(SlotParseError::OutOfRange { slot_index: i });
}
*slot = format!("{:02}:{:02}", h, m);
times.push((h, m));
}
Ok(ParsedSlots { normalized, times })
}
pub fn validate_slot_count(selected_frequency: usize, count: usize) -> Result<(), String> {
let valid = match selected_frequency {
0 => count == 1,
1 => count == 2,
2 => count == 3,
_ => count >= 1,
};
if valid {
Ok(())
} else {
let msg = match selected_frequency {
3 => "Please provide at least 1 scheduled time".into(),
n => format!("Please provide {} scheduled time(s)", n + 1),
};
Err(msg)
}
}
pub fn frequency_str(selected_frequency: usize) -> &'static str {
match selected_frequency {
0 => "OnceDaily",
1 => "TwiceDaily",
2 => "ThriceDaily",
_ => "Custom",
}
}