systemd_unit_edit/
timespan.rs1use std::time::Duration;
7
8pub fn parse_timespan(s: &str) -> Result<Duration, TimespanParseError> {
28 let s = s.trim();
29 if s.is_empty() {
30 return Err(TimespanParseError::Empty);
31 }
32
33 let mut total_micros: u128 = 0;
34 let mut current_number = String::new();
35 let mut chars = s.chars().peekable();
36
37 while let Some(ch) = chars.next() {
38 if ch.is_ascii_digit() {
39 current_number.push(ch);
40 } else if ch.is_whitespace() {
41 if !current_number.is_empty() {
43 if let Some(&next) = chars.peek() {
45 if next.is_ascii_alphabetic() {
46 continue;
48 } else if next.is_ascii_digit() {
49 let value: u64 = current_number
51 .parse()
52 .map_err(|_| TimespanParseError::InvalidNumber)?;
53 total_micros += value as u128 * 1_000_000;
54 current_number.clear();
55 }
56 } else {
57 let value: u64 = current_number
59 .parse()
60 .map_err(|_| TimespanParseError::InvalidNumber)?;
61 total_micros += value as u128 * 1_000_000;
62 current_number.clear();
63 }
64 }
65 } else if ch.is_ascii_alphabetic() {
66 let mut unit = String::from(ch);
68 while let Some(&next) = chars.peek() {
69 if next.is_ascii_alphabetic() {
70 unit.push(chars.next().unwrap());
71 } else {
72 break;
73 }
74 }
75
76 if current_number.is_empty() {
77 return Err(TimespanParseError::MissingNumber);
78 }
79
80 let value: u64 = current_number
81 .parse()
82 .map_err(|_| TimespanParseError::InvalidNumber)?;
83
84 let micros = match unit.as_str() {
85 "us" | "usec" => value as u128,
86 "ms" | "msec" => value as u128 * 1_000,
87 "s" | "sec" | "second" | "seconds" => value as u128 * 1_000_000,
88 "min" | "minute" | "minutes" => value as u128 * 60 * 1_000_000,
89 "h" | "hr" | "hour" | "hours" => value as u128 * 60 * 60 * 1_000_000,
90 "d" | "day" | "days" => value as u128 * 24 * 60 * 60 * 1_000_000,
91 "w" | "week" | "weeks" => value as u128 * 7 * 24 * 60 * 60 * 1_000_000,
92 _ => return Err(TimespanParseError::InvalidUnit(unit)),
93 };
94
95 total_micros += micros;
96 current_number.clear();
97 } else {
98 return Err(TimespanParseError::InvalidCharacter(ch));
99 }
100 }
101
102 if !current_number.is_empty() {
104 let value: u64 = current_number
105 .parse()
106 .map_err(|_| TimespanParseError::InvalidNumber)?;
107 total_micros += value as u128 * 1_000_000;
108 }
109
110 if total_micros == 0 {
111 return Err(TimespanParseError::Empty);
112 }
113
114 let secs = (total_micros / 1_000_000) as u64;
116 let nanos = ((total_micros % 1_000_000) * 1_000) as u32;
117
118 Ok(Duration::new(secs, nanos))
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum TimespanParseError {
124 Empty,
126 InvalidNumber,
128 MissingNumber,
130 InvalidUnit(String),
132 InvalidCharacter(char),
134}
135
136impl std::fmt::Display for TimespanParseError {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 match self {
139 TimespanParseError::Empty => write!(f, "empty timespan"),
140 TimespanParseError::InvalidNumber => write!(f, "invalid number format"),
141 TimespanParseError::MissingNumber => write!(f, "unit specified without a number"),
142 TimespanParseError::InvalidUnit(unit) => write!(f, "invalid time unit: {}", unit),
143 TimespanParseError::InvalidCharacter(ch) => {
144 write!(f, "invalid character: {}", ch)
145 }
146 }
147 }
148}
149
150impl std::error::Error for TimespanParseError {}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_parse_plain_number() {
158 assert_eq!(parse_timespan("30"), Ok(Duration::from_secs(30)));
159 assert_eq!(parse_timespan("0"), Err(TimespanParseError::Empty));
160 assert_eq!(parse_timespan("120"), Ok(Duration::from_secs(120)));
161 }
162
163 #[test]
164 fn test_parse_seconds() {
165 assert_eq!(parse_timespan("30s"), Ok(Duration::from_secs(30)));
166 assert_eq!(parse_timespan("1sec"), Ok(Duration::from_secs(1)));
167 assert_eq!(parse_timespan("5seconds"), Ok(Duration::from_secs(5)));
168 }
169
170 #[test]
171 fn test_parse_minutes() {
172 assert_eq!(parse_timespan("2min"), Ok(Duration::from_secs(120)));
173 assert_eq!(parse_timespan("1minute"), Ok(Duration::from_secs(60)));
174 assert_eq!(parse_timespan("5minutes"), Ok(Duration::from_secs(300)));
175 }
176
177 #[test]
178 fn test_parse_hours() {
179 assert_eq!(parse_timespan("1h"), Ok(Duration::from_secs(3600)));
180 assert_eq!(parse_timespan("2hr"), Ok(Duration::from_secs(7200)));
181 assert_eq!(parse_timespan("1hour"), Ok(Duration::from_secs(3600)));
182 assert_eq!(parse_timespan("3hours"), Ok(Duration::from_secs(10800)));
183 }
184
185 #[test]
186 fn test_parse_days() {
187 assert_eq!(parse_timespan("1d"), Ok(Duration::from_secs(86400)));
188 assert_eq!(parse_timespan("2days"), Ok(Duration::from_secs(172800)));
189 }
190
191 #[test]
192 fn test_parse_weeks() {
193 assert_eq!(parse_timespan("1w"), Ok(Duration::from_secs(604800)));
194 assert_eq!(parse_timespan("2weeks"), Ok(Duration::from_secs(1209600)));
195 }
196
197 #[test]
198 fn test_parse_milliseconds() {
199 assert_eq!(parse_timespan("500ms"), Ok(Duration::from_millis(500)));
200 assert_eq!(parse_timespan("1000msec"), Ok(Duration::from_millis(1000)));
201 }
202
203 #[test]
204 fn test_parse_microseconds() {
205 assert_eq!(parse_timespan("500us"), Ok(Duration::from_micros(500)));
206 assert_eq!(parse_timespan("1000usec"), Ok(Duration::from_micros(1000)));
207 }
208
209 #[test]
210 fn test_parse_combined() {
211 assert_eq!(parse_timespan("2min 30s"), Ok(Duration::from_secs(150)));
212 assert_eq!(parse_timespan("1h 30min"), Ok(Duration::from_secs(5400)));
213 assert_eq!(
214 parse_timespan("1d 2h 3min 4s"),
215 Ok(Duration::from_secs(93784))
216 );
217 }
218
219 #[test]
220 fn test_parse_combined_no_space() {
221 assert_eq!(parse_timespan("2min30s"), Ok(Duration::from_secs(150)));
222 assert_eq!(parse_timespan("1h30min"), Ok(Duration::from_secs(5400)));
223 }
224
225 #[test]
226 fn test_parse_with_extra_whitespace() {
227 assert_eq!(
228 parse_timespan(" 2min 30s "),
229 Ok(Duration::from_secs(150))
230 );
231 assert_eq!(parse_timespan("1h 30min"), Ok(Duration::from_secs(5400)));
232 }
233
234 #[test]
235 fn test_parse_errors() {
236 assert_eq!(parse_timespan(""), Err(TimespanParseError::Empty));
237 assert_eq!(parse_timespan(" "), Err(TimespanParseError::Empty));
238 assert!(parse_timespan("abc").is_err());
239 assert!(parse_timespan("10xyz").is_err());
240 }
241
242 #[test]
243 fn test_parse_subsecond_precision() {
244 let result = parse_timespan("1s 500ms").unwrap();
245 assert_eq!(result, Duration::from_millis(1500));
246
247 let result = parse_timespan("200ms").unwrap();
248 assert_eq!(result.as_millis(), 200);
249 }
250}