1#![allow(dead_code)]
6
7use anyhow::{anyhow, bail, Result};
8use std::time::Duration;
9
10pub const PRESIGN_MAX: Duration = Duration::from_secs(7 * 86_400);
14
15pub fn parse(s: &str) -> Result<Duration> {
18 let s = s.trim();
19 let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
20 let (num, unit) = s.split_at(split);
21 if num.is_empty() {
22 bail!("duration needs a number, e.g. 3d (got {s:?})");
23 }
24 let n: u64 = num
25 .parse()
26 .map_err(|_| anyhow!("duration number out of range in {s:?}"))?;
27 if n == 0 {
28 bail!("duration must be greater than zero (got {s:?})");
29 }
30 let secs = match unit {
31 "d" => n * 86_400,
32 "h" => n * 3_600,
33 "m" => n * 60,
34 "s" => n,
35 "" => bail!("duration needs a unit d/h/m/s, e.g. 3d (got {s:?})"),
36 other => bail!("unknown duration unit {other:?} — use d, h, m, or s"),
37 };
38 Ok(Duration::from_secs(secs))
39}
40
41pub fn within_presign_limit(d: Duration) -> bool {
43 d <= PRESIGN_MAX
44}
45
46pub fn human(d: Duration) -> String {
49 let s = d.as_secs();
50 if s == 0 {
51 return "0s".to_string();
52 }
53 if s.is_multiple_of(86_400) {
54 format!("{}d", s / 86_400)
55 } else if s.is_multiple_of(3_600) {
56 format!("{}h", s / 3_600)
57 } else if s.is_multiple_of(60) {
58 format!("{}m", s / 60)
59 } else {
60 format!("{s}s")
61 }
62}
63
64pub fn human_long(d: Duration) -> String {
67 let s = d.as_secs();
68 let (n, unit) = if s.is_multiple_of(86_400) {
69 (s / 86_400, "day")
70 } else if s.is_multiple_of(3_600) {
71 (s / 3_600, "hour")
72 } else if s.is_multiple_of(60) {
73 (s / 60, "minute")
74 } else {
75 (s, "second")
76 };
77 format!("{n} {unit}{}", if n == 1 { "" } else { "s" })
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn human_long_spells_it_out() {
86 assert_eq!(human_long(parse("2d").unwrap()), "2 days");
87 assert_eq!(human_long(parse("1d").unwrap()), "1 day");
88 assert_eq!(human_long(parse("12h").unwrap()), "12 hours");
89 }
90
91 #[test]
92 fn parses_each_unit() {
93 assert_eq!(parse("3d").unwrap(), Duration::from_secs(3 * 86_400));
94 assert_eq!(parse("12h").unwrap(), Duration::from_secs(12 * 3_600));
95 assert_eq!(parse("30m").unwrap(), Duration::from_secs(30 * 60));
96 assert_eq!(parse("90s").unwrap(), Duration::from_secs(90));
97 assert_eq!(parse(" 5d ").unwrap(), Duration::from_secs(5 * 86_400));
98 }
99
100 #[test]
101 fn rejects_bad_input() {
102 assert!(parse("0d").is_err()); assert!(parse("d").is_err()); assert!(parse("5").is_err()); assert!(parse("5x").is_err()); assert!(parse("5days").is_err()); assert!(parse("").is_err());
108 }
109
110 #[test]
111 fn presign_limit_is_seven_days() {
112 assert!(within_presign_limit(parse("7d").unwrap()));
113 assert!(within_presign_limit(parse("168h").unwrap())); assert!(!within_presign_limit(parse("8d").unwrap()));
115 assert!(!within_presign_limit(parse("169h").unwrap()));
116 }
117
118 #[test]
119 fn human_uses_largest_even_unit() {
120 assert_eq!(human(parse("3d").unwrap()), "3d");
121 assert_eq!(human(parse("12h").unwrap()), "12h");
122 assert_eq!(human(parse("90m").unwrap()), "90m");
123 assert_eq!(human(Duration::from_secs(90)), "90s");
124 }
125}