1use std::fmt;
2use std::time::Duration;
3
4use unit_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#[derive(Debug)]
15pub struct FormattedDuration(pub Duration);
16
17#[derive(Debug)]
19pub struct HumanDuration(pub Duration);
20
21#[derive(Debug)]
34pub struct HumanBytes(pub u64);
35
36#[derive(Debug)]
49pub struct DecimalBytes(pub u64);
50
51#[derive(Debug)]
64pub struct BinaryBytes(pub u64);
65
66#[derive(Debug)]
68pub struct HumanCount(pub u64);
69
70#[derive(Debug)]
72pub struct HumanFloatCount(pub f64);
73
74impl fmt::Display for FormattedDuration {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 let mut t = self.0.as_secs();
77 let seconds = t % 60;
78 t /= 60;
79 let minutes = t % 60;
80 t /= 60;
81 let hours = t % 24;
82 t /= 24;
83 if t > 0 {
84 let days = t;
85 write!(f, "{days}d {hours:02}:{minutes:02}:{seconds:02}")
86 } else {
87 write!(f, "{hours:02}:{minutes:02}:{seconds:02}")
88 }
89 }
90}
91
92impl fmt::Display for HumanDuration {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 let mut idx = 0;
110 for (i, &(cur, _, _)) in UNITS.iter().enumerate() {
111 idx = i;
112 match UNITS.get(i + 1) {
113 Some(&next) if self.0.saturating_add(next.0 / 2) >= cur + cur / 2 => break,
114 _ => continue,
115 }
116 }
117
118 let (unit, name, alt) = UNITS[idx];
119 let mut t = self.0.div_duration_f64(unit).round() as usize;
120 if idx < UNITS.len() - 1 {
121 t = Ord::max(t, 2);
122 }
123
124 match (f.alternate(), t) {
125 (true, _) => write!(f, "{t}{alt}"),
126 (false, 1) => write!(f, "{t} {name}"),
127 (false, _) => write!(f, "{t} {name}s"),
128 }
129 }
130}
131
132const UNITS: &[(Duration, &str, &str)] = &[
133 (YEAR, "year", "y"),
134 (WEEK, "week", "w"),
135 (DAY, "day", "d"),
136 (HOUR, "hour", "h"),
137 (MINUTE, "minute", "m"),
138 (SECOND, "second", "s"),
139];
140
141impl fmt::Display for HumanBytes {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match NumberPrefix::binary(self.0 as f64) {
144 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
145 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
146 }
147 }
148}
149
150impl fmt::Display for DecimalBytes {
151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152 match NumberPrefix::decimal(self.0 as f64) {
153 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
154 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
155 }
156 }
157}
158
159impl fmt::Display for BinaryBytes {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match NumberPrefix::binary(self.0 as f64) {
162 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
163 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
164 }
165 }
166}
167
168impl fmt::Display for HumanCount {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 use fmt::Write;
171
172 let num = self.0.to_string();
173 let len = num.len();
174 for (idx, c) in num.chars().enumerate() {
175 let pos = len - idx - 1;
176 f.write_char(c)?;
177 if pos > 0 && pos % 3 == 0 {
178 f.write_char(',')?;
179 }
180 }
181 Ok(())
182 }
183}
184
185impl fmt::Display for HumanFloatCount {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 use fmt::Write;
188
189 let precision = f.precision().unwrap_or(4);
191 let num = format!("{:.*}", precision, self.0);
192
193 let (int_part, frac_part) = match num.split_once('.') {
194 Some((int_str, fract_str)) => (int_str, fract_str),
195 None => (num.as_str(), ""),
200 };
201 let (sign, digits) = match int_part.strip_prefix('-') {
205 Some(digits) => ("-", digits),
206 None => ("", int_part),
207 };
208 f.write_str(sign)?;
209 let len = digits.len();
210 for (idx, c) in digits.chars().enumerate() {
211 let pos = len - idx - 1;
212 f.write_char(c)?;
213 if pos > 0 && pos % 3 == 0 {
214 f.write_char(',')?;
215 }
216 }
217 let frac_trimmed = frac_part.trim_end_matches('0');
218 if !frac_trimmed.is_empty() {
219 f.write_char('.')?;
220 f.write_str(frac_trimmed)?;
221 }
222 Ok(())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 const MILLI: Duration = Duration::from_millis(1);
231
232 #[test]
233 fn human_duration_alternate() {
234 for (unit, _, alt) in UNITS {
235 assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit)));
236 }
237 }
238
239 #[test]
240 fn human_duration_less_than_one_second() {
241 assert_eq!(
242 "0 seconds",
243 format!("{}", HumanDuration(Duration::from_secs(0)))
244 );
245 assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI)));
246 assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI)));
247 assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI)));
248 assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI)));
249 }
250
251 #[test]
252 fn human_duration_less_than_two_seconds() {
253 assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI)));
254 assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI)));
255 assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI)));
256 }
257
258 #[test]
259 fn human_duration_one_unit() {
260 assert_eq!("1 second", format!("{}", HumanDuration(SECOND)));
261 assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE)));
262 assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR)));
263 assert_eq!("24 hours", format!("{}", HumanDuration(DAY)));
264 assert_eq!("7 days", format!("{}", HumanDuration(WEEK)));
265 assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR)));
266 }
267
268 #[test]
269 fn human_duration_less_than_one_and_a_half_unit() {
270 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI);
273 assert_eq!("89 seconds", format!("{d}"));
274 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI);
275 assert_eq!("89 minutes", format!("{d}"));
276 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI);
277 assert_eq!("35 hours", format!("{d}"));
278 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI);
279 assert_eq!("10 days", format!("{d}"));
280 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI);
281 assert_eq!("78 weeks", format!("{d}"));
282 }
283
284 #[test]
285 fn human_duration_one_and_a_half_unit() {
286 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2);
289 assert_eq!("2 minutes", format!("{d}"));
290 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2);
291 assert_eq!("2 hours", format!("{d}"));
292 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2);
293 assert_eq!("2 days", format!("{d}"));
294 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2);
295 assert_eq!("2 weeks", format!("{d}"));
296 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2);
297 assert_eq!("2 years", format!("{d}"));
298 }
299
300 #[test]
301 fn human_duration_two_units() {
302 assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND)));
303 assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE)));
304 assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR)));
305 assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY)));
306 assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK)));
307 assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR)));
308 }
309
310 #[test]
311 fn human_duration_less_than_two_and_a_half_units() {
312 let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI);
313 assert_eq!("2 seconds", format!("{d}"));
314 let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI);
315 assert_eq!("2 minutes", format!("{d}"));
316 let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI);
317 assert_eq!("2 hours", format!("{d}"));
318 let d = HumanDuration(2 * DAY + DAY / 2 - MILLI);
319 assert_eq!("2 days", format!("{d}"));
320 let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI);
321 assert_eq!("2 weeks", format!("{d}"));
322 let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI);
323 assert_eq!("2 years", format!("{d}"));
324 }
325
326 #[test]
327 fn human_duration_two_and_a_half_units() {
328 let d = HumanDuration(2 * SECOND + SECOND / 2);
329 assert_eq!("3 seconds", format!("{d}"));
330 let d = HumanDuration(2 * MINUTE + MINUTE / 2);
331 assert_eq!("3 minutes", format!("{d}"));
332 let d = HumanDuration(2 * HOUR + HOUR / 2);
333 assert_eq!("3 hours", format!("{d}"));
334 let d = HumanDuration(2 * DAY + DAY / 2);
335 assert_eq!("3 days", format!("{d}"));
336 let d = HumanDuration(2 * WEEK + WEEK / 2);
337 assert_eq!("3 weeks", format!("{d}"));
338 let d = HumanDuration(2 * YEAR + YEAR / 2);
339 assert_eq!("3 years", format!("{d}"));
340 }
341
342 #[test]
343 fn human_duration_three_units() {
344 assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND)));
345 assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE)));
346 assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR)));
347 assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY)));
348 assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK)));
349 assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR)));
350 }
351
352 #[test]
353 fn human_count() {
354 assert_eq!("42", format!("{}", HumanCount(42)));
355 assert_eq!("7,654", format!("{}", HumanCount(7654)));
356 assert_eq!("12,345", format!("{}", HumanCount(12345)));
357 assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));
358 }
359
360 #[test]
361 fn human_float_count() {
362 assert_eq!("42", format!("{}", HumanFloatCount(42.0)));
363 assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0)));
364 assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0)));
365 assert_eq!(
366 "1,234,567,890",
367 format!("{}", HumanFloatCount(1234567890.0))
368 );
369 assert_eq!("42.5", format!("{}", HumanFloatCount(42.5)));
370 assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345)));
371 assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345)));
372 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321)));
373 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456)));
374 assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789)));
375 assert_eq!(
376 "1,234,567,890.1235",
377 format!("{}", HumanFloatCount(1234567890.1234567))
378 );
379 assert_eq!(
380 "1,234,567,890.1234",
381 format!("{}", HumanFloatCount(1234567890.1234321))
382 );
383 assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.1234321)));
384 assert_eq!("1,234.1", format!("{:.1}", HumanFloatCount(1234.1234321)));
385 assert_eq!("1,234.12", format!("{:.2}", HumanFloatCount(1234.1234321)));
386 assert_eq!("1,234.123", format!("{:.3}", HumanFloatCount(1234.1234321)));
387 assert_eq!(
388 "1,234.1234320999999454215867445",
389 format!("{:.25}", HumanFloatCount(1234.1234321))
390 );
391 }
392
393 #[test]
394 fn human_float_count_negative() {
395 assert_eq!("-100", format!("{}", HumanFloatCount(-100.0)));
398 assert_eq!("-100,000", format!("{}", HumanFloatCount(-100000.0)));
399 assert_eq!("-1,000", format!("{}", HumanFloatCount(-1000.0)));
400 assert_eq!("-42.5", format!("{}", HumanFloatCount(-42.5)));
401 assert_eq!("-inf", format!("{}", HumanFloatCount(f64::NEG_INFINITY)));
402 }
403
404 #[test]
405 fn human_float_count_zero_precision_rounds() {
406 assert_eq!("1,235", format!("{:.0}", HumanFloatCount(1234.9)));
410 assert_eq!("2,000", format!("{:.0}", HumanFloatCount(1999.6)));
411 assert_eq!("1,000", format!("{:.0}", HumanFloatCount(999.5)));
412 assert_eq!("-1,235", format!("{:.0}", HumanFloatCount(-1234.9)));
413 assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.4)));
415 }
416}