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#[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 num = format!("{:.4}", self.0);
191 let (int_part, frac_part) = match num.split_once('.') {
192 Some((int_str, fract_str)) => (int_str.to_string(), fract_str),
193 None => (self.0.trunc().to_string(), ""),
194 };
195 let len = int_part.len();
196 for (idx, c) in int_part.chars().enumerate() {
197 let pos = len - idx - 1;
198 f.write_char(c)?;
199 if pos > 0 && pos % 3 == 0 {
200 f.write_char(',')?;
201 }
202 }
203 let frac_trimmed = frac_part.trim_end_matches('0');
204 if !frac_trimmed.is_empty() {
205 f.write_char('.')?;
206 f.write_str(frac_trimmed)?;
207 }
208 Ok(())
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 const MILLI: Duration = Duration::from_millis(1);
217
218 #[test]
219 fn human_duration_alternate() {
220 for (unit, _, alt) in UNITS {
221 assert_eq!(format!("2{alt}"), format!("{:#}", HumanDuration(2 * *unit)));
222 }
223 }
224
225 #[test]
226 fn human_duration_less_than_one_second() {
227 assert_eq!(
228 "0 seconds",
229 format!("{}", HumanDuration(Duration::from_secs(0)))
230 );
231 assert_eq!("0 seconds", format!("{}", HumanDuration(MILLI)));
232 assert_eq!("0 seconds", format!("{}", HumanDuration(499 * MILLI)));
233 assert_eq!("1 second", format!("{}", HumanDuration(500 * MILLI)));
234 assert_eq!("1 second", format!("{}", HumanDuration(999 * MILLI)));
235 }
236
237 #[test]
238 fn human_duration_less_than_two_seconds() {
239 assert_eq!("1 second", format!("{}", HumanDuration(1499 * MILLI)));
240 assert_eq!("2 seconds", format!("{}", HumanDuration(1500 * MILLI)));
241 assert_eq!("2 seconds", format!("{}", HumanDuration(1999 * MILLI)));
242 }
243
244 #[test]
245 fn human_duration_one_unit() {
246 assert_eq!("1 second", format!("{}", HumanDuration(SECOND)));
247 assert_eq!("60 seconds", format!("{}", HumanDuration(MINUTE)));
248 assert_eq!("60 minutes", format!("{}", HumanDuration(HOUR)));
249 assert_eq!("24 hours", format!("{}", HumanDuration(DAY)));
250 assert_eq!("7 days", format!("{}", HumanDuration(WEEK)));
251 assert_eq!("52 weeks", format!("{}", HumanDuration(YEAR)));
252 }
253
254 #[test]
255 fn human_duration_less_than_one_and_a_half_unit() {
256 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2 - MILLI);
259 assert_eq!("89 seconds", format!("{d}"));
260 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2 - MILLI);
261 assert_eq!("89 minutes", format!("{d}"));
262 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2 - MILLI);
263 assert_eq!("35 hours", format!("{d}"));
264 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2 - MILLI);
265 assert_eq!("10 days", format!("{d}"));
266 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2 - MILLI);
267 assert_eq!("78 weeks", format!("{d}"));
268 }
269
270 #[test]
271 fn human_duration_one_and_a_half_unit() {
272 let d = HumanDuration(MINUTE + MINUTE / 2 - SECOND / 2);
275 assert_eq!("2 minutes", format!("{d}"));
276 let d = HumanDuration(HOUR + HOUR / 2 - MINUTE / 2);
277 assert_eq!("2 hours", format!("{d}"));
278 let d = HumanDuration(DAY + DAY / 2 - HOUR / 2);
279 assert_eq!("2 days", format!("{d}"));
280 let d = HumanDuration(WEEK + WEEK / 2 - DAY / 2);
281 assert_eq!("2 weeks", format!("{d}"));
282 let d = HumanDuration(YEAR + YEAR / 2 - WEEK / 2);
283 assert_eq!("2 years", format!("{d}"));
284 }
285
286 #[test]
287 fn human_duration_two_units() {
288 assert_eq!("2 seconds", format!("{}", HumanDuration(2 * SECOND)));
289 assert_eq!("2 minutes", format!("{}", HumanDuration(2 * MINUTE)));
290 assert_eq!("2 hours", format!("{}", HumanDuration(2 * HOUR)));
291 assert_eq!("2 days", format!("{}", HumanDuration(2 * DAY)));
292 assert_eq!("2 weeks", format!("{}", HumanDuration(2 * WEEK)));
293 assert_eq!("2 years", format!("{}", HumanDuration(2 * YEAR)));
294 }
295
296 #[test]
297 fn human_duration_less_than_two_and_a_half_units() {
298 let d = HumanDuration(2 * SECOND + SECOND / 2 - MILLI);
299 assert_eq!("2 seconds", format!("{d}"));
300 let d = HumanDuration(2 * MINUTE + MINUTE / 2 - MILLI);
301 assert_eq!("2 minutes", format!("{d}"));
302 let d = HumanDuration(2 * HOUR + HOUR / 2 - MILLI);
303 assert_eq!("2 hours", format!("{d}"));
304 let d = HumanDuration(2 * DAY + DAY / 2 - MILLI);
305 assert_eq!("2 days", format!("{d}"));
306 let d = HumanDuration(2 * WEEK + WEEK / 2 - MILLI);
307 assert_eq!("2 weeks", format!("{d}"));
308 let d = HumanDuration(2 * YEAR + YEAR / 2 - MILLI);
309 assert_eq!("2 years", format!("{d}"));
310 }
311
312 #[test]
313 fn human_duration_two_and_a_half_units() {
314 let d = HumanDuration(2 * SECOND + SECOND / 2);
315 assert_eq!("3 seconds", format!("{d}"));
316 let d = HumanDuration(2 * MINUTE + MINUTE / 2);
317 assert_eq!("3 minutes", format!("{d}"));
318 let d = HumanDuration(2 * HOUR + HOUR / 2);
319 assert_eq!("3 hours", format!("{d}"));
320 let d = HumanDuration(2 * DAY + DAY / 2);
321 assert_eq!("3 days", format!("{d}"));
322 let d = HumanDuration(2 * WEEK + WEEK / 2);
323 assert_eq!("3 weeks", format!("{d}"));
324 let d = HumanDuration(2 * YEAR + YEAR / 2);
325 assert_eq!("3 years", format!("{d}"));
326 }
327
328 #[test]
329 fn human_duration_three_units() {
330 assert_eq!("3 seconds", format!("{}", HumanDuration(3 * SECOND)));
331 assert_eq!("3 minutes", format!("{}", HumanDuration(3 * MINUTE)));
332 assert_eq!("3 hours", format!("{}", HumanDuration(3 * HOUR)));
333 assert_eq!("3 days", format!("{}", HumanDuration(3 * DAY)));
334 assert_eq!("3 weeks", format!("{}", HumanDuration(3 * WEEK)));
335 assert_eq!("3 years", format!("{}", HumanDuration(3 * YEAR)));
336 }
337
338 #[test]
339 fn human_count() {
340 assert_eq!("42", format!("{}", HumanCount(42)));
341 assert_eq!("7,654", format!("{}", HumanCount(7654)));
342 assert_eq!("12,345", format!("{}", HumanCount(12345)));
343 assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));
344 }
345
346 #[test]
347 fn human_float_count() {
348 assert_eq!("42", format!("{}", HumanFloatCount(42.0)));
349 assert_eq!("7,654", format!("{}", HumanFloatCount(7654.0)));
350 assert_eq!("12,345", format!("{}", HumanFloatCount(12345.0)));
351 assert_eq!(
352 "1,234,567,890",
353 format!("{}", HumanFloatCount(1234567890.0))
354 );
355 assert_eq!("42.5", format!("{}", HumanFloatCount(42.5)));
356 assert_eq!("42.5", format!("{}", HumanFloatCount(42.500012345)));
357 assert_eq!("42.502", format!("{}", HumanFloatCount(42.502012345)));
358 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.321)));
359 assert_eq!("7,654.321", format!("{}", HumanFloatCount(7654.3210123456)));
360 assert_eq!("12,345.6789", format!("{}", HumanFloatCount(12345.6789)));
361 assert_eq!(
362 "1,234,567,890.1235",
363 format!("{}", HumanFloatCount(1234567890.1234567))
364 );
365 assert_eq!(
366 "1,234,567,890.1234",
367 format!("{}", HumanFloatCount(1234567890.1234321))
368 );
369 }
370}