1 | use std::fmt; |
2 | use std::time::Duration; |
3 | |
4 | use number_prefix::NumberPrefix; |
5 | |
6 | const SECOND: Duration = Duration::from_secs(1); |
7 | const MINUTE: Duration = Duration::from_secs(60); |
8 | const HOUR: Duration = Duration::from_secs(60 * 60); |
9 | const DAY: Duration = Duration::from_secs(24 * 60 * 60); |
10 | const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60); |
11 | const YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60); |
12 | |
13 | /// Wraps an std duration for human basic formatting. |
14 | #[derive (Debug)] |
15 | pub struct FormattedDuration(pub Duration); |
16 | |
17 | /// Wraps an std duration for human readable formatting. |
18 | #[derive (Debug)] |
19 | pub struct HumanDuration(pub Duration); |
20 | |
21 | /// Formats bytes for human readability |
22 | #[derive (Debug)] |
23 | pub struct HumanBytes(pub u64); |
24 | |
25 | /// Formats bytes for human readability using SI prefixes |
26 | #[derive (Debug)] |
27 | pub struct DecimalBytes(pub u64); |
28 | |
29 | /// Formats bytes for human readability using ISO/IEC prefixes |
30 | #[derive (Debug)] |
31 | pub struct BinaryBytes(pub u64); |
32 | |
33 | /// Formats counts for human readability using commas |
34 | #[derive (Debug)] |
35 | pub struct HumanCount(pub u64); |
36 | |
37 | /// Formats counts for human readability using commas for floats |
38 | #[derive (Debug)] |
39 | pub struct HumanFloatCount(pub f64); |
40 | |
41 | impl fmt::Display for FormattedDuration { |
42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
43 | let mut t: u64 = self.0.as_secs(); |
44 | let seconds: u64 = t % 60; |
45 | t /= 60; |
46 | let minutes: u64 = t % 60; |
47 | t /= 60; |
48 | let hours: u64 = t % 24; |
49 | t /= 24; |
50 | if t > 0 { |
51 | let days: u64 = t; |
52 | write!(f, " {days}d {hours:02}: {minutes:02}: {seconds:02}" ) |
53 | } else { |
54 | write!(f, " {hours:02}: {minutes:02}: {seconds:02}" ) |
55 | } |
56 | } |
57 | } |
58 | |
59 | // `HumanDuration` should be as intuitively understandable as possible. |
60 | // So we want to round, not truncate: otherwise 1 hour and 59 minutes |
61 | // would display an ETA of "1 hour" which underestimates the time |
62 | // remaining by a factor 2. |
63 | // |
64 | // To make the precision more uniform, we avoid displaying "1 unit" |
65 | // (except for seconds), because it would be displayed for a relatively |
66 | // long duration compared to the unit itself. Instead, when we arrive |
67 | // around 1.5 unit, we change from "2 units" to the next smaller unit |
68 | // (e.g. "89 seconds"). |
69 | // |
70 | // Formally: |
71 | // * for n >= 2, we go from "n+1 units" to "n units" exactly at (n + 1/2) units |
72 | // * we switch from "2 units" to the next smaller unit at (1.5 unit minus half of the next smaller unit) |
73 | |
74 | impl fmt::Display for HumanDuration { |
75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
76 | let mut idx = 0; |
77 | for (i, &(cur, _, _)) in UNITS.iter().enumerate() { |
78 | idx = i; |
79 | match UNITS.get(i + 1) { |
80 | Some(&next) if self.0.saturating_add(next.0 / 2) >= cur + cur / 2 => break, |
81 | _ => continue, |
82 | } |
83 | } |
84 | |
85 | let (unit, name, alt) = UNITS[idx]; |
86 | // FIXME when `div_duration_f64` is stable |
87 | let mut t = (self.0.as_secs_f64() / unit.as_secs_f64()).round() as usize; |
88 | if idx < UNITS.len() - 1 { |
89 | t = Ord::max(t, 2); |
90 | } |
91 | |
92 | match (f.alternate(), t) { |
93 | (true, _) => write!(f, " {t}{alt}" ), |
94 | (false, 1) => write!(f, " {t} {name}" ), |
95 | (false, _) => write!(f, " {t} {name}s" ), |
96 | } |
97 | } |
98 | } |
99 | |
100 | const UNITS: &[(Duration, &str, &str)] = &[ |
101 | (YEAR, "year" , "y" ), |
102 | (WEEK, "week" , "w" ), |
103 | (DAY, "day" , "d" ), |
104 | (HOUR, "hour" , "h" ), |
105 | (MINUTE, "minute" , "m" ), |
106 | (SECOND, "second" , "s" ), |
107 | ]; |
108 | |
109 | impl fmt::Display for HumanBytes { |
110 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
111 | match NumberPrefix::binary(self.0 as f64) { |
112 | NumberPrefix::Standalone(number: f64) => write!(f, " {number:.0} B" ), |
113 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
114 | } |
115 | } |
116 | } |
117 | |
118 | impl fmt::Display for DecimalBytes { |
119 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
120 | match NumberPrefix::decimal(self.0 as f64) { |
121 | NumberPrefix::Standalone(number: f64) => write!(f, " {number:.0} B" ), |
122 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
123 | } |
124 | } |
125 | } |
126 | |
127 | impl fmt::Display for BinaryBytes { |
128 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
129 | match NumberPrefix::binary(self.0 as f64) { |
130 | NumberPrefix::Standalone(number: f64) => write!(f, " {number:.0} B" ), |
131 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
132 | } |
133 | } |
134 | } |
135 | |
136 | impl fmt::Display for HumanCount { |
137 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
138 | use fmt::Write; |
139 | |
140 | let num: String = self.0.to_string(); |
141 | let len: usize = num.len(); |
142 | for (idx: usize, c: char) in num.chars().enumerate() { |
143 | let pos: usize = len - idx - 1; |
144 | f.write_char(c)?; |
145 | if pos > 0 && pos % 3 == 0 { |
146 | f.write_char(',' )?; |
147 | } |
148 | } |
149 | Ok(()) |
150 | } |
151 | } |
152 | |
153 | impl fmt::Display for HumanFloatCount { |
154 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
155 | use fmt::Write; |
156 | |
157 | let num = format!(" {:.4}" , self.0); |
158 | let (int_part, frac_part) = match num.split_once('.' ) { |
159 | Some((int_str, fract_str)) => (int_str.to_string(), fract_str), |
160 | None => (self.0.trunc().to_string(), "" ), |
161 | }; |
162 | let len = int_part.len(); |
163 | for (idx, c) in int_part.chars().enumerate() { |
164 | let pos = len - idx - 1; |
165 | f.write_char(c)?; |
166 | if pos > 0 && pos % 3 == 0 { |
167 | f.write_char(',' )?; |
168 | } |
169 | } |
170 | let frac_trimmed = frac_part.trim_end_matches('0' ); |
171 | if !frac_trimmed.is_empty() { |
172 | f.write_char('.' )?; |
173 | f.write_str(frac_trimmed)?; |
174 | } |
175 | Ok(()) |
176 | } |
177 | } |
178 | |
179 | #[cfg (test)] |
180 | mod tests { |
181 | use super::*; |
182 | |
183 | const MILLI: Duration = Duration::from_millis(1); |
184 | |
185 | #[test ] |
186 | fn human_duration_alternate() { |
187 | for (unit, _, alt) in UNITS { |
188 | assert_eq!(format!("2 {alt}" ), format!(" {:#}" , HumanDuration(2 * *unit))); |
189 | } |
190 | } |
191 | |
192 | #[test ] |
193 | fn human_duration_less_than_one_second() { |
194 | assert_eq!( |
195 | "0 seconds" , |
196 | format!(" {}" , HumanDuration(Duration::from_secs(0))) |
197 | ); |
198 | assert_eq!("0 seconds" , format!(" {}" , HumanDuration(MILLI))); |
199 | assert_eq!("0 seconds" , format!(" {}" , HumanDuration(499 * MILLI))); |
200 | assert_eq!("1 second" , format!(" {}" , HumanDuration(500 * MILLI))); |
201 | assert_eq!("1 second" , format!(" {}" , HumanDuration(999 * MILLI))); |
202 | } |
203 | |
204 | #[test ] |
205 | fn human_duration_less_than_two_seconds() { |
206 | assert_eq!("1 second" , format!(" {}" , HumanDuration(1499 * MILLI))); |
207 | assert_eq!("2 seconds" , format!(" {}" , HumanDuration(1500 * MILLI))); |
208 | assert_eq!("2 seconds" , format!(" {}" , HumanDuration(1999 * MILLI))); |
209 | } |
210 | |
211 | #[test ] |
212 | fn human_duration_one_unit() { |
213 | assert_eq!("1 second" , format!(" {}" , HumanDuration(SECOND))); |
214 | assert_eq!("60 seconds" , format!(" {}" , HumanDuration(MINUTE))); |
215 | assert_eq!("60 minutes" , format!(" {}" , HumanDuration(HOUR))); |
216 | assert_eq!("24 hours" , format!(" {}" , HumanDuration(DAY))); |
217 | assert_eq!("7 days" , format!(" {}" , HumanDuration(WEEK))); |
218 | assert_eq!("52 weeks" , format!(" {}" , HumanDuration(YEAR))); |
219 | } |
220 | |
221 | #[test ] |
222 | fn human_duration_less_than_one_and_a_half_unit() { |
223 | // this one is actually done at 1.5 unit - half of the next smaller unit - epsilon |
224 | // and should display the next smaller unit |
225 | let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI); |
226 | assert_eq!("89 seconds" , format!(" {d}" )); |
227 | let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI); |
228 | assert_eq!("89 minutes" , format!(" {d}" )); |
229 | let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI); |
230 | assert_eq!("35 hours" , format!(" {d}" )); |
231 | let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI); |
232 | assert_eq!("10 days" , format!(" {d}" )); |
233 | let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI); |
234 | assert_eq!("78 weeks" , format!(" {d}" )); |
235 | } |
236 | |
237 | #[test ] |
238 | fn human_duration_one_and_a_half_unit() { |
239 | // this one is actually done at 1.5 unit - half of the next smaller unit |
240 | // and should still display "2 units" |
241 | let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2); |
242 | assert_eq!("2 minutes" , format!(" {d}" )); |
243 | let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2); |
244 | assert_eq!("2 hours" , format!(" {d}" )); |
245 | let d = HumanDuration(DAY + DAY / 2 - HOUR / 2); |
246 | assert_eq!("2 days" , format!(" {d}" )); |
247 | let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2); |
248 | assert_eq!("2 weeks" , format!(" {d}" )); |
249 | let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2); |
250 | assert_eq!("2 years" , format!(" {d}" )); |
251 | } |
252 | |
253 | #[test ] |
254 | fn human_duration_two_units() { |
255 | assert_eq!("2 seconds" , format!(" {}" , HumanDuration(2 * SECOND))); |
256 | assert_eq!("2 minutes" , format!(" {}" , HumanDuration(2 * MINUTE))); |
257 | assert_eq!("2 hours" , format!(" {}" , HumanDuration(2 * HOUR))); |
258 | assert_eq!("2 days" , format!(" {}" , HumanDuration(2 * DAY))); |
259 | assert_eq!("2 weeks" , format!(" {}" , HumanDuration(2 * WEEK))); |
260 | assert_eq!("2 years" , format!(" {}" , HumanDuration(2 * YEAR))); |
261 | } |
262 | |
263 | #[test ] |
264 | fn human_duration_less_than_two_and_a_half_units() { |
265 | let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI); |
266 | assert_eq!("2 seconds" , format!(" {d}" )); |
267 | let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI); |
268 | assert_eq!("2 minutes" , format!(" {d}" )); |
269 | let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI); |
270 | assert_eq!("2 hours" , format!(" {d}" )); |
271 | let d = HumanDuration(2 * DAY + DAY / 2 - MILLI); |
272 | assert_eq!("2 days" , format!(" {d}" )); |
273 | let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI); |
274 | assert_eq!("2 weeks" , format!(" {d}" )); |
275 | let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI); |
276 | assert_eq!("2 years" , format!(" {d}" )); |
277 | } |
278 | |
279 | #[test ] |
280 | fn human_duration_two_and_a_half_units() { |
281 | let d = HumanDuration(2 * SECOND + SECOND / 2); |
282 | assert_eq!("3 seconds" , format!(" {d}" )); |
283 | let d = HumanDuration(2 * MINUTE + MINUTE / 2); |
284 | assert_eq!("3 minutes" , format!(" {d}" )); |
285 | let d = HumanDuration(2 * HOUR + HOUR / 2); |
286 | assert_eq!("3 hours" , format!(" {d}" )); |
287 | let d = HumanDuration(2 * DAY + DAY / 2); |
288 | assert_eq!("3 days" , format!(" {d}" )); |
289 | let d = HumanDuration(2 * WEEK + WEEK / 2); |
290 | assert_eq!("3 weeks" , format!(" {d}" )); |
291 | let d = HumanDuration(2 * YEAR + YEAR / 2); |
292 | assert_eq!("3 years" , format!(" {d}" )); |
293 | } |
294 | |
295 | #[test ] |
296 | fn human_duration_three_units() { |
297 | assert_eq!("3 seconds" , format!(" {}" , HumanDuration(3 * SECOND))); |
298 | assert_eq!("3 minutes" , format!(" {}" , HumanDuration(3 * MINUTE))); |
299 | assert_eq!("3 hours" , format!(" {}" , HumanDuration(3 * HOUR))); |
300 | assert_eq!("3 days" , format!(" {}" , HumanDuration(3 * DAY))); |
301 | assert_eq!("3 weeks" , format!(" {}" , HumanDuration(3 * WEEK))); |
302 | assert_eq!("3 years" , format!(" {}" , HumanDuration(3 * YEAR))); |
303 | } |
304 | |
305 | #[test ] |
306 | fn human_count() { |
307 | assert_eq!("42" , format!(" {}" , HumanCount(42))); |
308 | assert_eq!("7,654" , format!(" {}" , HumanCount(7654))); |
309 | assert_eq!("12,345" , format!(" {}" , HumanCount(12345))); |
310 | assert_eq!("1,234,567,890" , format!(" {}" , HumanCount(1234567890))); |
311 | } |
312 | |
313 | #[test ] |
314 | fn human_float_count() { |
315 | assert_eq!("42" , format!(" {}" , HumanFloatCount(42.0))); |
316 | assert_eq!("7,654" , format!(" {}" , HumanFloatCount(7654.0))); |
317 | assert_eq!("12,345" , format!(" {}" , HumanFloatCount(12345.0))); |
318 | assert_eq!( |
319 | "1,234,567,890" , |
320 | format!(" {}" , HumanFloatCount(1234567890.0)) |
321 | ); |
322 | assert_eq!("42.5" , format!(" {}" , HumanFloatCount(42.5))); |
323 | assert_eq!("42.5" , format!(" {}" , HumanFloatCount(42.500012345))); |
324 | assert_eq!("42.502" , format!(" {}" , HumanFloatCount(42.502012345))); |
325 | assert_eq!("7,654.321" , format!(" {}" , HumanFloatCount(7654.321))); |
326 | assert_eq!("7,654.321" , format!(" {}" , HumanFloatCount(7654.3210123456))); |
327 | assert_eq!("12,345.6789" , format!(" {}" , HumanFloatCount(12345.6789))); |
328 | assert_eq!( |
329 | "1,234,567,890.1235" , |
330 | format!(" {}" , HumanFloatCount(1234567890.1234567)) |
331 | ); |
332 | assert_eq!( |
333 | "1,234,567,890.1234" , |
334 | format!(" {}" , HumanFloatCount(1234567890.1234321)) |
335 | ); |
336 | } |
337 | } |
338 | |