wasmer_deploy_util/
pretty_duration.rs1#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
6pub struct PrettyDuration(pub std::time::Duration);
7
8impl PrettyDuration {
9 pub fn new(duration: std::time::Duration) -> Self {
10 Self(duration)
11 }
12
13 pub fn as_duration(&self) -> std::time::Duration {
14 self.0
15 }
16
17 pub fn from_secs(secs: u64) -> Self {
18 Self(std::time::Duration::from_secs(secs))
19 }
20
21 pub fn from_mins(mins: u64) -> Self {
22 Self(std::time::Duration::from_secs(mins * 60))
23 }
24
25 pub fn from_hours(hours: u64) -> Self {
26 Self(std::time::Duration::from_secs(hours * 60 * 60))
27 }
28}
29
30impl std::ops::Deref for PrettyDuration {
31 type Target = std::time::Duration;
32
33 fn deref(&self) -> &Self::Target {
34 &self.0
35 }
36}
37
38impl From<std::time::Duration> for PrettyDuration {
39 fn from(duration: std::time::Duration) -> Self {
40 Self::new(duration)
41 }
42}
43
44impl std::fmt::Display for PrettyDuration {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 const DAY: u64 = 60 * 60 * 24;
47 const HOUR: u64 = 60 * 60;
48
49 let secs = self.0.as_secs();
50
51 let days = secs / DAY;
52 if days > 0 {
53 write!(f, "{}d", days)?;
54 }
55 let secs = secs % DAY;
56
57 let hours = secs / HOUR;
58 if hours > 0 {
59 write!(f, "{}h", hours)?;
60 }
61 let secs = secs % HOUR;
62
63 let mins = secs / 60;
64 if mins > 0 {
65 write!(f, "{}m", mins)?;
66 }
67 let secs = secs % 60;
68 if secs > 0 {
69 write!(f, "{}s", secs)?;
70 }
71
72 Ok(())
73 }
74}
75
76#[derive(Debug)]
77pub struct PrettyDurationParseError {
78 value: String,
79 message: String,
80}
81
82impl std::fmt::Display for PrettyDurationParseError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "Invalid time spec '{}': {}", self.value, self.message)
85 }
86}
87
88impl std::error::Error for PrettyDurationParseError {}
89
90impl std::str::FromStr for PrettyDuration {
91 type Err = PrettyDurationParseError;
92
93 fn from_str(mut input: &str) -> Result<Self, Self::Err> {
94 let mut seconds = 0;
95
96 loop {
97 input = input.strip_prefix(' ').unwrap_or(input);
98 if input.is_empty() {
99 break;
100 }
101
102 let nums = input.chars().take_while(|x| x.is_ascii_digit()).count();
103 if nums < 1 {
104 return Err(PrettyDurationParseError {
105 message: "must start with a number".to_string(),
106 value: input.to_string(),
107 });
108 }
109
110 let number = &input[..nums]
111 .parse::<u64>()
112 .map_err(|e| PrettyDurationParseError {
113 message: format!("invalid number: {}", e),
114 value: input.to_string(),
115 })?;
116
117 input = &input[nums..];
118 let chars = input
119 .chars()
120 .take_while(|x| x.is_ascii_alphabetic())
121 .count();
122 let unit = &input[..chars];
123 input = &input[chars..];
124
125 let scale = match unit {
126 "s" | "sec" | "secs" | "seconds" => 1,
127 "m" | "min" | "mins" | "minutes" => 60,
128 "h" | "hour" | "hours" => 60 * 60,
129 "d" | "day" | "days" => 60 * 60 * 24,
130 _ => {
131 return Err(PrettyDurationParseError {
132 message: "unknown unit".to_string(),
133 value: input.to_string(),
134 });
135 }
136 };
137
138 seconds += number * scale;
139 }
140
141 let dur = std::time::Duration::from_secs(seconds);
142
143 Ok(Self(dur))
144 }
145}
146
147impl serde::Serialize for PrettyDuration {
148 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
149 where
150 S: serde::Serializer,
151 {
152 self.to_string().serialize(serializer)
153 }
154}
155
156impl<'de> serde::Deserialize<'de> for PrettyDuration {
157 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
158 where
159 D: serde::Deserializer<'de>,
160 {
161 let s = String::deserialize(deserializer)?;
162 s.parse().map_err(serde::de::Error::custom)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use std::time::Duration;
169
170 use super::*;
171
172 #[test]
173 fn test_pretty_duration_constructors() {
174 assert_eq!(
175 PrettyDuration::from_secs(1).as_duration(),
176 Duration::from_secs(1)
177 );
178 assert_eq!(
179 PrettyDuration::from_mins(1).as_duration(),
180 Duration::from_secs(60)
181 );
182 assert_eq!(
183 PrettyDuration::from_hours(1).as_duration(),
184 Duration::from_secs(60 * 60)
185 );
186 }
187
188 #[test]
189 fn test_pretty_duration_parse() {
190 let cases = &[
191 ("1s", Duration::from_secs(1), "1s"),
192 ("10s", Duration::from_secs(10), "10s"),
193 ("59s", Duration::from_secs(59), "59s"),
194 ("60s", Duration::from_secs(60), "1m"),
195 ("1m", Duration::from_secs(60), "1m"),
196 ("11m", Duration::from_secs(60) * 11, "11m"),
197 ("60m", Duration::from_secs(60) * 60, "1h"),
198 ("1h", Duration::from_secs(60) * 60, "1h"),
199 ("11h", Duration::from_secs(60) * 60 * 11, "11h"),
200 ("1h1m", Duration::from_secs(61) * 60, "1h1m"),
201 ("1h1m1s", Duration::from_secs(61 * 60 + 1), "1h1m1s"),
202 ];
203
204 for (index, (input, duration, output)) in cases.iter().enumerate() {
205 eprintln!("test case {index}: {input} => {duration:?} => {output}");
206 let p = input.parse::<PrettyDuration>().unwrap();
207 assert_eq!(p, PrettyDuration::new(*duration));
208 assert_eq!(p.to_string(), output.to_string());
209 }
210 }
211
212 #[test]
213 fn test_pretty_duration_serde() {
214 let dur = PrettyDuration::from_secs(1);
215 let json = serde_json::to_string(&dur).unwrap();
216 assert_eq!(json, "\"1s\"");
217
218 let dur2: PrettyDuration = serde_json::from_str(&json).unwrap();
219 assert_eq!(dur, dur2);
220 }
221}