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}