1use std::fmt;
2use std::time::Duration;
3
4use number_prefix::NumberPrefix;
5
6const SECOND: Duration = Duration::from_secs(1);
7const MINUTE: Duration = Duration::from_secs(60);
8const HOUR: Duration = Duration::from_secs(60 * 60);
9const DAY: Duration = Duration::from_secs(24 * 60 * 60);
10const WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
11const YEAR: Duration = Duration::from_secs(365 * 24 * 60 * 60);
12
13/// Wraps an std duration for human basic formatting.
14#[derive(Debug)]
15pub struct FormattedDuration(pub Duration);
16
17/// Wraps an std duration for human readable formatting.
18#[derive(Debug)]
19pub struct HumanDuration(pub Duration);
20
21/// Formats bytes for human readability
22#[derive(Debug)]
23pub struct HumanBytes(pub u64);
24
25/// Formats bytes for human readability using SI prefixes
26#[derive(Debug)]
27pub struct DecimalBytes(pub u64);
28
29/// Formats bytes for human readability using ISO/IEC prefixes
30#[derive(Debug)]
31pub struct BinaryBytes(pub u64);
32
33/// Formats counts for human readability using commas
34#[derive(Debug)]
35pub struct HumanCount(pub u64);
36
37/// Formats counts for human readability using commas for floats
38#[derive(Debug)]
39pub struct HumanFloatCount(pub f64);
40
41impl 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
74impl 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
100const 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
109impl 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
118impl 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
127impl 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
136impl 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
153impl 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)]
180mod 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