torii_lib/util/
duration.rs1use crate::error::{Result, ToriiError};
2
3pub fn parse_duration(s: &str) -> Result<u32> {
5 let s = s.trim();
6
7 if s.is_empty() {
8 return Err(ToriiError::InvalidConfig("Empty duration string".to_string()));
9 }
10
11 let mut total_minutes: u32 = 0;
12 let mut current_number = String::new();
13 let mut chars = s.chars().peekable();
14
15 while let Some(ch) = chars.next() {
16 if ch.is_ascii_digit() {
17 current_number.push(ch);
18 } else if ch.is_alphabetic() {
19 if current_number.is_empty() {
21 return Err(ToriiError::InvalidConfig(
22 format!("Invalid duration format: '{}'. Expected number before unit", s)
23 ));
24 }
25
26 let number: u32 = current_number.parse()
27 .map_err(|_| ToriiError::InvalidConfig(
28 format!("Invalid number in duration: '{}'", current_number)
29 ))?;
30
31 let mut unit = String::from(ch);
33 while let Some(&next_ch) = chars.peek() {
34 if next_ch.is_alphabetic() {
35 unit.push(chars.next().unwrap());
36 } else {
37 break;
38 }
39 }
40
41 let unit_lower = unit.to_lowercase();
42 let minutes = match unit_lower.as_str() {
43 "s" | "sec" | "second" | "seconds" => {
44 if number < 60 && total_minutes == 0 {
45 return Err(ToriiError::InvalidConfig(
46 "Duration must be at least 1 minute (60 seconds)".to_string()
47 ));
48 }
49 number / 60
50 }
51 "m" | "min" | "minute" | "minutes" => number,
52 "h" | "hr" | "hour" | "hours" => number * 60,
53 "d" | "day" | "days" => number * 60 * 24,
54 _ => {
55 return Err(ToriiError::InvalidConfig(
56 format!("Unknown time unit: '{}'. Use: s (seconds), m (minutes), h (hours), d (days)", unit)
57 ));
58 }
59 };
60
61 total_minutes += minutes;
62 current_number.clear();
63 } else if !ch.is_whitespace() {
64 return Err(ToriiError::InvalidConfig(
65 format!("Invalid character in duration: '{}'", ch)
66 ));
67 }
68 }
69
70 if !current_number.is_empty() {
72 let number: u32 = current_number.parse()
73 .map_err(|_| ToriiError::InvalidConfig(
74 format!("Invalid number in duration: '{}'", current_number)
75 ))?;
76 total_minutes += number;
77 }
78
79 if total_minutes == 0 {
80 return Err(ToriiError::InvalidConfig(
81 "Duration must be at least 1 minute".to_string()
82 ));
83 }
84
85 Ok(total_minutes)
86}
87
88pub fn format_duration(minutes: u32) -> String {
90 if minutes < 60 {
91 format!("{} minute{}", minutes, if minutes == 1 { "" } else { "s" })
92 } else if minutes < 60 * 24 {
93 let hours = minutes / 60;
94 let mins = minutes % 60;
95 if mins == 0 {
96 format!("{} hour{}", hours, if hours == 1 { "" } else { "s" })
97 } else {
98 format!("{} hour{} {} minute{}",
99 hours, if hours == 1 { "" } else { "s" },
100 mins, if mins == 1 { "" } else { "s" })
101 }
102 } else {
103 let days = minutes / (60 * 24);
104 let hours = (minutes % (60 * 24)) / 60;
105 if hours == 0 {
106 format!("{} day{}", days, if days == 1 { "" } else { "s" })
107 } else {
108 format!("{} day{} {} hour{}",
109 days, if days == 1 { "" } else { "s" },
110 hours, if hours == 1 { "" } else { "s" })
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_parse_duration() {
121 assert_eq!(parse_duration("10m").unwrap(), 10);
123 assert_eq!(parse_duration("120s").unwrap(), 2);
124 assert_eq!(parse_duration("2h").unwrap(), 120);
125 assert_eq!(parse_duration("1d").unwrap(), 1440);
126 assert_eq!(parse_duration("10").unwrap(), 10); assert_eq!(parse_duration("1h30m").unwrap(), 90);
130 assert_eq!(parse_duration("2h15m").unwrap(), 135);
131 assert_eq!(parse_duration("1d12h").unwrap(), 2160); assert_eq!(parse_duration("1d12h30m").unwrap(), 2190); assert_eq!(parse_duration("2d6h30m").unwrap(), 3270); assert_eq!(parse_duration("1h 30m").unwrap(), 90);
137 assert_eq!(parse_duration("2d12h30m").unwrap(), 3630);
141 }
142
143 #[test]
144 fn test_format_duration() {
145 assert_eq!(format_duration(10), "10 minutes");
146 assert_eq!(format_duration(1), "1 minute");
147 assert_eq!(format_duration(60), "1 hour");
148 assert_eq!(format_duration(90), "1 hour 30 minutes");
149 assert_eq!(format_duration(1440), "1 day");
150 assert_eq!(format_duration(1500), "1 day 1 hour");
151 }
152}