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.as_secs_f64() / unit.as_secs_f64()).round() as usize;
121 if idx < UNITS.len() - 1 {
122 t = Ord::max(t, 2);
123 }
124
125 match (f.alternate(), t) {
126 (true, _) => write!(f, "{t}{alt}"),
127 (false, 1) => write!(f, "{t} {name}"),
128 (false, _) => write!(f, "{t} {name}s"),
129 }
130 }
131}
132
133const UNITS: &[(Duration, &str, &str)] = &[
134 (YEAR, "year", "y"),
135 (WEEK, "week", "w"),
136 (DAY, "day", "d"),
137 (HOUR, "hour", "h"),
138 (MINUTE, "minute", "m"),
139 (SECOND, "second", "s"),
140];
141
142impl fmt::Display for HumanBytes {
143 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144 match NumberPrefix::binary(self.0 as f64) {
145 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
146 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
147 }
148 }
149}
150
151impl fmt::Display for DecimalBytes {
152 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153 match NumberPrefix::decimal(self.0 as f64) {
154 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
155 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
156 }
157 }
158}
159
160impl fmt::Display for BinaryBytes {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 match NumberPrefix::binary(self.0 as f64) {
163 NumberPrefix::Standalone(number) => write!(f, "{number:.0} B"),
164 NumberPrefix::Prefixed(prefix, number) => write!(f, "{number:.2} {prefix}B"),
165 }
166 }
167}
168
169impl fmt::Display for HumanCount {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 use fmt::Write;
172
173 let num = self.0.to_string();
174 let len = num.len();
175 for (idx, c) in num.chars().enumerate() {
176 let pos = len - idx - 1;
177 f.write_char(c)?;
178 if pos > 0 && pos % 3 == 0 {
179 f.write_char(',')?;
180 }
181 }
182 Ok(())
183 }
184}
185
186impl fmt::Display for HumanFloatCount {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 use fmt::Write;
189
190 let precision = f.precision().unwrap_or(4);
192 let num = format!("{:.*}", precision, self.0);
193
194 let (int_part, frac_part) = match num.split_once('.') {
195 Some((int_str, fract_str)) => (int_str.to_string(), fract_str),
196 None => (self.0.trunc().to_string(), ""),
197 };
198 let len = int_part.len();
199 for (idx, c) in int_part.chars().enumerate() {
200 let pos = len - idx - 1;
201 f.write_char(c)?;
202 if pos > 0 && pos % 3 == 0 {
203 f.write_char(',')?;
204 }
205 }
206 let frac_trimmed = frac_part.trim_end_matches('0');
207 if !frac_trimmed.is_empty() {
208 f.write_char('.')?;
209 f.write_str(frac_trimmed)?;
210 }
211 Ok(())
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 const MILLI: Duration = Duration::from_millis(1);
220
221 #[test]
222 fn human_duration_alternate() {
223 for (unit, _, alt) in UNITS {
224 assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit)));
225 }
226 }
227
228 #[test]
229 fn human_duration_less_than_one_second() {
230 assert_eq!(
231 "0 seconds",
232 format!("{}", HumanDuration(Duration::from_secs(0)))
233 );
234 assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI)));
235 assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI)));
236 assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI)));
237 assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI)));
238 }
239
240 #[test]
241 fn human_duration_less_than_two_seconds() {
242 assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI)));
243 assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI)));
244 assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI)));
245 }
246
247 #[test]
248 fn human_duration_one_unit() {
249 assert_eq!("1 second", format!("{}", HumanDuration(SECOND)));
250 assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE)));
251 assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR)));
252 assert_eq!("24 hours", format!("{}", HumanDuration(DAY)));
253 assert_eq!("7 days", format!("{}", HumanDuration(WEEK)));
254 assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR)));
255 }
256
257 #[test]
258 fn human_duration_less_than_one_and_a_half_unit() {
259 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI);
262 assert_eq!("89 seconds", format!("{d}"));
263 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI);
264 assert_eq!("89 minutes", format!("{d}"));
265 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI);
266 assert_eq!("35 hours", format!("{d}"));
267 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI);
268 assert_eq!("10 days", format!("{d}"));
269 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI);
270 assert_eq!("78 weeks", format!("{d}"));
271 }
272
273 #[test]
274 fn human_duration_one_and_a_half_unit() {
275 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2);
278 assert_eq!("2 minutes", format!("{d}"));
279 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2);
280 assert_eq!("2 hours", format!("{d}"));
281 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2);
282 assert_eq!("2 days", format!("{d}"));
283 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2);
284 assert_eq!("2 weeks", format!("{d}"));
285 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2);
286 assert_eq!("2 years", format!("{d}"));
287 }
288
289 #[test]
290 fn human_duration_two_units() {
291 assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND)));
292 assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE)));
293 assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR)));
294 assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY)));
295 assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK)));
296 assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR)));
297 }
298
299 #[test]
300 fn human_duration_less_than_two_and_a_half_units() {
301 let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI);
302 assert_eq!("2 seconds", format!("{d}"));
303 let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI);
304 assert_eq!("2 minutes", format!("{d}"));
305 let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI);
306 assert_eq!("2 hours", format!("{d}"));
307 let d = HumanDuration(2 * DAY + DAY / 2 - MILLI);
308 assert_eq!("2 days", format!("{d}"));
309 let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI);
310 assert_eq!("2 weeks", format!("{d}"));
311 let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI);
312 assert_eq!("2 years", format!("{d}"));
313 }
314
315 #[test]
316 fn human_duration_two_and_a_half_units() {
317 let d = HumanDuration(2 * SECOND + SECOND / 2);
318 assert_eq!("3 seconds", format!("{d}"));
319 let d = HumanDuration(2 * MINUTE + MINUTE / 2);
320 assert_eq!("3 minutes", format!("{d}"));
321 let d = HumanDuration(2 * HOUR + HOUR / 2);
322 assert_eq!("3 hours", format!("{d}"));
323 let d = HumanDuration(2 * DAY + DAY / 2);
324 assert_eq!("3 days", format!("{d}"));
325 let d = HumanDuration(2 * WEEK + WEEK / 2);
326 assert_eq!("3 weeks", format!("{d}"));
327 let d = HumanDuration(2 * YEAR + YEAR / 2);
328 assert_eq!("3 years", format!("{d}"));
329 }
330
331 #[test]
332 fn human_duration_three_units() {
333 assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND)));
334 assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE)));
335 assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR)));
336 assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY)));
337 assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK)));
338 assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR)));
339 }
340
341 #[test]
342 fn human_count() {
343 assert_eq!("42", format!("{}", HumanCount(42)));
344 assert_eq!("7,654", format!("{}", HumanCount(7654)));
345 assert_eq!("12,345", format!("{}", HumanCount(12345)));
346 assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));
347 }
348
349 #[test]
350 fn human_float_count() {
351 assert_eq!("42", format!("{}", HumanFloatCount(42.0)));
352 assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0)));
353 assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0)));
354 assert_eq!(
355 "1,234,567,890",
356 format!("{}", HumanFloatCount(1234567890.0))
357 );
358 assert_eq!("42.5", format!("{}", HumanFloatCount(42.5)));
359 assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345)));
360 assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345)));
361 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321)));
362 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456)));
363 assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789)));
364 assert_eq!(
365 "1,234,567,890.1235",
366 format!("{}", HumanFloatCount(1234567890.1234567))
367 );
368 assert_eq!(
369 "1,234,567,890.1234",
370 format!("{}", HumanFloatCount(1234567890.1234321))
371 );
372 assert_eq!("1,234", format!("{:.0}", HumanFloatCount(1234.1234321)));
373 assert_eq!("1,234.1", format!("{:.1}", HumanFloatCount(1234.1234321)));
374 assert_eq!("1,234.12", format!("{:.2}", HumanFloatCount(1234.1234321)));
375 assert_eq!("1,234.123", format!("{:.3}", HumanFloatCount(1234.1234321)));
376 assert_eq!(
377 "1,234.1234320999999454215867445",
378 format!("{:.25}", HumanFloatCount(1234.1234321))
379 );
380 }
381}