ito_core/ralph/
duration.rs1use crate::errors::CoreResult;
2use std::time::Duration;
3
4pub fn parse_duration(s: &str) -> CoreResult<Duration> {
26 let s = s.trim();
27 if s.is_empty() {
28 return Err(crate::errors::CoreError::Parse(
29 "Duration string cannot be empty".into(),
30 ));
31 }
32
33 if let Ok(secs) = s.parse::<u64>() {
35 return Ok(Duration::from_secs(secs));
36 }
37
38 let mut total_secs: u64 = 0;
39 let mut current_num = String::new();
40
41 for c in s.chars() {
42 if c.is_ascii_digit() {
43 current_num.push(c);
44 } else {
45 let unit = c.to_ascii_lowercase();
46 if current_num.is_empty() {
47 return Err(crate::errors::CoreError::Parse(format!(
48 "Invalid duration format: missing number before '{unit}'"
49 )));
50 }
51 let num: u64 = current_num.parse().map_err(|_| {
52 crate::errors::CoreError::Parse(format!(
53 "Invalid number in duration: {current_num}"
54 ))
55 })?;
56 current_num.clear();
57
58 let multiplier = match unit {
59 's' => 1,
60 'm' => 60,
61 'h' => 3600,
62 _ => {
63 return Err(crate::errors::CoreError::Parse(format!(
64 "Invalid duration unit '{unit}'. Use 's', 'm', or 'h'"
65 )));
66 }
67 };
68
69 total_secs = total_secs
70 .checked_add(num.saturating_mul(multiplier))
71 .ok_or_else(|| crate::errors::CoreError::Parse("Duration overflow".into()))?;
72 }
73 }
74
75 if !current_num.is_empty() {
77 let num: u64 = current_num.parse().map_err(|_| {
78 crate::errors::CoreError::Parse(format!("Invalid number in duration: {current_num}"))
79 })?;
80 total_secs = total_secs
81 .checked_add(num)
82 .ok_or_else(|| crate::errors::CoreError::Parse("Duration overflow".into()))?;
83 }
84
85 if total_secs == 0 {
86 return Err(crate::errors::CoreError::Parse(
87 "Duration must be greater than 0".into(),
88 ));
89 }
90
91 Ok(Duration::from_secs(total_secs))
92}
93
94pub fn format_duration(d: Duration) -> String {
96 let total_secs = d.as_secs();
97 let hours = total_secs / 3600;
98 let minutes = (total_secs % 3600) / 60;
99 let seconds = total_secs % 60;
100
101 let mut parts = Vec::new();
102 if hours > 0 {
103 parts.push(format!("{hours}h"));
104 }
105 if minutes > 0 {
106 parts.push(format!("{minutes}m"));
107 }
108 if seconds > 0 || parts.is_empty() {
109 parts.push(format!("{seconds}s"));
110 }
111 parts.join("")
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_parse_seconds() {
120 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
121 assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
122 assert_eq!(parse_duration("120s").unwrap(), Duration::from_secs(120));
123 }
124
125 #[test]
126 fn test_parse_minutes() {
127 assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
128 assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
129 }
130
131 #[test]
132 fn test_parse_hours() {
133 assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
134 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
135 }
136
137 #[test]
138 fn test_parse_combined() {
139 assert_eq!(parse_duration("1m30s").unwrap(), Duration::from_secs(90));
140 assert_eq!(parse_duration("1h30m").unwrap(), Duration::from_secs(5400));
141 assert_eq!(
142 parse_duration("1h30m45s").unwrap(),
143 Duration::from_secs(5445)
144 );
145 }
146
147 #[test]
148 fn test_parse_bare_number() {
149 assert_eq!(parse_duration("90").unwrap(), Duration::from_secs(90));
150 assert_eq!(parse_duration("1").unwrap(), Duration::from_secs(1));
151 }
152
153 #[test]
154 fn test_parse_case_insensitive() {
155 assert_eq!(parse_duration("5M").unwrap(), Duration::from_secs(300));
156 assert_eq!(parse_duration("2H").unwrap(), Duration::from_secs(7200));
157 assert_eq!(parse_duration("30S").unwrap(), Duration::from_secs(30));
158 }
159
160 #[test]
161 fn test_parse_with_whitespace() {
162 assert_eq!(parse_duration(" 30s ").unwrap(), Duration::from_secs(30));
163 }
164
165 #[test]
166 fn test_parse_errors() {
167 assert!(parse_duration("").is_err());
168 assert!(parse_duration("abc").is_err());
169 assert!(parse_duration("5x").is_err());
170 assert!(parse_duration("m5").is_err());
171 }
172
173 #[test]
174 fn test_format_duration() {
175 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
176 assert_eq!(format_duration(Duration::from_secs(60)), "1m");
177 assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
178 assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
179 assert_eq!(format_duration(Duration::from_secs(3660)), "1h1m");
180 assert_eq!(format_duration(Duration::from_secs(3661)), "1h1m1s");
181 assert_eq!(format_duration(Duration::from_secs(0)), "0s");
182 }
183}