1use chrono::Utc;
48use clap::Parser;
49use dateparser::DateTimeUtc;
50use miette::{miette, Diagnostic, Result};
51use thiserror::Error;
52
53#[derive(Error, Debug, Diagnostic)]
54#[error("invalid time interval '{origin}'")]
55#[diagnostic(
56 code(invalid::time),
57 help("Try `sleep-progress --help` for more informations.")
58)]
59pub(crate) struct InvalidTimeInterval {
60 origin: String,
61}
62
63#[derive(Parser, Debug)]
64#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
65#[command(author, version, about, long_about = None)]
66#[doc(hidden)]
67pub struct Args {
68 #[arg(required_unless_present("timespec"))]
70 number: Vec<String>,
71
72 #[arg(short('u'), long("until"), conflicts_with("number"))]
74 pub timespec: Option<DateTimeUtc>,
75
76 #[arg(short, long)]
78 pub progress: bool,
79}
80
81#[doc(hidden)]
82pub fn parse_interval(args: &Args) -> Result<u64> {
83 match &args.timespec {
84 Some(timespec) => {
85 let millis = (timespec.0 - Utc::now()).num_milliseconds();
86 if millis < 0 {
87 Err(miette!(
88 "Can't wait a past time: {}",
89 timespec.0.to_rfc2822()
90 ))
91 } else {
92 Ok(millis as u64)
93 }
94 }
95 None => {
96 let mut sum = 0.0;
97 for duration_spec in args.number.iter() {
98 let (value, multipliers) = if let Some(seconds) = duration_spec.strip_suffix('s') {
99 (seconds, 1000.0)
100 } else if let Some(minutes) = duration_spec.strip_suffix('m') {
101 (minutes, 60.0 * 1000.0)
102 } else if let Some(hours) = duration_spec.strip_suffix('h') {
103 (hours, 60.0 * 60.0 * 1000.0)
104 } else if let Some(days) = duration_spec.strip_suffix('d') {
105 (days, 24.0 * 60.0 * 60.0 * 1000.0)
106 } else {
107 (duration_spec.as_str(), 1000.0)
108 };
109 sum += multipliers
110 * value.parse::<f64>().map_err(|_| InvalidTimeInterval {
111 origin: duration_spec.to_string(),
112 })?
113 }
114 Ok(sum.round() as u64)
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn parse_help() {
125 let result = Args::try_parse_from([" ", "-h"]);
126 assert!(result.is_err());
127 }
128
129 #[test]
130 fn parse_long_help() {
131 let result = Args::try_parse_from([" ", "--help"]);
132 assert!(result.is_err());
133 }
134
135 #[test]
136 fn parse_unknown_args() {
137 let result = Args::try_parse_from([" ", "-t"]);
138 assert!(result.is_err());
140 }
141
142 #[test]
143 fn parse_cli_arg() {
144 let result = Args::try_parse_from([" ", "34"]);
145 assert!(result.is_ok());
147 }
148
149 #[test]
150 fn parse_cli_args() {
151 let result = Args::try_parse_from([" ", "34", "6.4"]);
152 assert!(result.is_ok());
154 }
155 #[test]
156 fn parse_cli_arg_progress() {
157 let result = Args::try_parse_from([" ", "34", "-p"]);
158 assert!(result.is_ok());
160 }
161
162 #[test]
163 fn parse_cli_args_progress() {
164 let result = Args::try_parse_from([" ", "34", "6.4", "-p"]);
165 assert!(result.is_ok());
167 }
168
169 #[test]
170 fn parse_cli_arg_unknown_args() {
171 let result = Args::try_parse_from([" ", "34", " ", "-t"]);
172 assert!(result.is_err());
174 }
175
176 #[test]
177 fn parse_cli_args_unknown_args() {
178 let result = Args::try_parse_from([" ", "34", "6.4", " ", "-t"]);
179 assert!(result.is_err());
181 }
182 #[test]
183 fn parse_cli_arg_progress_unknown_args() {
184 let result = Args::try_parse_from([" ", "34", "-p", " ", "-t"]);
185 assert!(result.is_err());
187 }
188
189 #[test]
190 fn parse_cli_args_progress_unknown_args() {
191 let result = Args::try_parse_from([" ", "34", "6.4", "-p", " ", "-t"]);
192 assert!(result.is_err());
194 }
195
196 #[test]
197 fn parse_interval_1() {
198 let result = parse_interval(&Args {
199 number: vec!["1".into()],
200 timespec: None,
201 progress: false,
202 });
203 assert_eq!(result.ok(), Some(1000));
204 }
205
206 #[test]
207 fn parse_interval_1p() {
208 let result = parse_interval(&Args {
209 number: vec!["1".into()],
210 timespec: None,
211 progress: true,
212 });
213 assert_eq!(result.ok(), Some(1000));
214 }
215
216 #[test]
217 fn parse_interval_0_5() {
218 let result = parse_interval(&Args {
219 number: vec!["0.5".into()],
220 timespec: None,
221 progress: false,
222 });
223 assert_eq!(result.ok(), Some(500));
224 }
225
226 #[test]
227 fn parse_interval_1s() {
228 let result = parse_interval(&Args {
229 number: vec!["1s".into()],
230 timespec: None,
231 progress: false,
232 });
233 assert_eq!(result.ok(), Some(1000));
234 }
235
236 #[test]
237 fn parse_interval_1m() {
238 let result = parse_interval(&Args {
239 number: vec!["1m".into()],
240 timespec: None,
241 progress: false,
242 });
243 assert_eq!(result.ok(), Some(60000));
244 }
245
246 #[test]
247 fn parse_interval_1h() {
248 let result = parse_interval(&Args {
249 number: vec!["1h".into()],
250 timespec: None,
251 progress: false,
252 });
253 assert_eq!(result.ok(), Some(3600000));
254 }
255
256 #[test]
257 fn parse_interval_1d() {
258 let result = parse_interval(&Args {
259 number: vec!["1d".into()],
260 timespec: None,
261 progress: false,
262 });
263 assert_eq!(result.ok(), Some(86400000));
264 }
265
266 #[test]
267 fn parse_interval_multiple() {
268 let result = parse_interval(&Args {
269 number: vec![
270 "1.023".into(),
271 "1s".into(),
272 "1m".into(),
273 "1h".into(),
274 "1d".into(),
275 ],
276 timespec: None,
277 progress: false,
278 });
279 assert_eq!(result.ok(), Some(90062023));
280 }
281
282 #[test]
283 fn parse_interval_err() {
284 let result = parse_interval(&Args {
285 number: vec!["1z".into()],
286 timespec: None,
287 progress: false,
288 });
289
290 assert_eq!(
291 result.err().unwrap().to_string(),
292 "invalid time interval '1z'"
293 );
294 }
295
296 #[test]
297 fn parse_interval_err_2() {
298 let result = parse_interval(&Args {
299 number: vec![
300 "1".into(),
301 "2".into(),
302 "3e".into(),
303 "4".into(),
304 "5".into(),
305 "6".into(),
306 ],
307 timespec: None,
308 progress: false,
309 });
310
311 assert_eq!(
312 result.err().unwrap().to_string(),
313 "invalid time interval '3e'"
314 );
315 }
316
317 #[test]
318 fn parse_interval_err_3() {
319 let result = parse_interval(&Args {
320 number: vec!["one".into()],
321 timespec: None,
322 progress: false,
323 });
324
325 assert_eq!(
326 result.err().unwrap().to_string(),
327 "invalid time interval 'one'"
328 );
329 }
330}