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 | /// |
23 | /// # Examples |
24 | /// ```rust |
25 | /// # use indicatif::HumanBytes; |
26 | /// assert_eq!("15 B" , format!("{}" , HumanBytes(15))); |
27 | /// assert_eq!("1.46 KiB" , format!("{}" , HumanBytes(1_500))); |
28 | /// assert_eq!("1.43 MiB" , format!("{}" , HumanBytes(1_500_000))); |
29 | /// assert_eq!("1.40 GiB" , format!("{}" , HumanBytes(1_500_000_000))); |
30 | /// assert_eq!("1.36 TiB" , format!("{}" , HumanBytes(1_500_000_000_000))); |
31 | /// assert_eq!("1.33 PiB" , format!("{}" , HumanBytes(1_500_000_000_000_000))); |
32 | /// ``` |
33 | #[derive (Debug)] |
34 | pub struct HumanBytes(pub u64); |
35 | |
36 | /// Formats bytes for human readability using SI prefixes |
37 | /// |
38 | /// # Examples |
39 | /// ```rust |
40 | /// # use indicatif::DecimalBytes; |
41 | /// assert_eq!("15 B" , format!("{}" , DecimalBytes(15))); |
42 | /// assert_eq!("1.50 kB" , format!("{}" , DecimalBytes(1_500))); |
43 | /// assert_eq!("1.50 MB" , format!("{}" , DecimalBytes(1_500_000))); |
44 | /// assert_eq!("1.50 GB" , format!("{}" , DecimalBytes(1_500_000_000))); |
45 | /// assert_eq!("1.50 TB" , format!("{}" , DecimalBytes(1_500_000_000_000))); |
46 | /// assert_eq!("1.50 PB" , format!("{}" , DecimalBytes(1_500_000_000_000_000))); |
47 | /// ``` |
48 | #[derive (Debug)] |
49 | pub struct DecimalBytes(pub u64); |
50 | |
51 | /// Formats bytes for human readability using ISO/IEC prefixes |
52 | /// |
53 | /// # Examples |
54 | /// ```rust |
55 | /// # use indicatif::BinaryBytes; |
56 | /// assert_eq!("15 B" , format!("{}" , BinaryBytes(15))); |
57 | /// assert_eq!("1.46 KiB" , format!("{}" , BinaryBytes(1_500))); |
58 | /// assert_eq!("1.43 MiB" , format!("{}" , BinaryBytes(1_500_000))); |
59 | /// assert_eq!("1.40 GiB" , format!("{}" , BinaryBytes(1_500_000_000))); |
60 | /// assert_eq!("1.36 TiB" , format!("{}" , BinaryBytes(1_500_000_000_000))); |
61 | /// assert_eq!("1.33 PiB" , format!("{}" , BinaryBytes(1_500_000_000_000_000))); |
62 | /// ``` |
63 | #[derive (Debug)] |
64 | pub struct BinaryBytes(pub u64); |
65 | |
66 | /// Formats counts for human readability using commas |
67 | #[derive (Debug)] |
68 | pub struct HumanCount(pub u64); |
69 | |
70 | /// Formats counts for human readability using commas for floats |
71 | #[derive (Debug)] |
72 | pub struct HumanFloatCount(pub f64); |
73 | |
74 | impl fmt::Display for FormattedDuration { |
75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
76 | let mut t: u64 = self.0.as_secs(); |
77 | let seconds: u64 = t % 60; |
78 | t /= 60; |
79 | let minutes: u64 = t % 60; |
80 | t /= 60; |
81 | let hours: u64 = t % 24; |
82 | t /= 24; |
83 | if t > 0 { |
84 | let days: u64 = 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 | |
92 | // `HumanDuration` should be as intuitively understandable as possible. |
93 | // So we want to round, not truncate: otherwise 1 hour and 59 minutes |
94 | // would display an ETA of "1 hour" which underestimates the time |
95 | // remaining by a factor 2. |
96 | // |
97 | // To make the precision more uniform, we avoid displaying "1 unit" |
98 | // (except for seconds), because it would be displayed for a relatively |
99 | // long duration compared to the unit itself. Instead, when we arrive |
100 | // around 1.5 unit, we change from "2 units" to the next smaller unit |
101 | // (e.g. "89 seconds"). |
102 | // |
103 | // Formally: |
104 | // * for n >= 2, we go from "n+1 units" to "n units" exactly at (n + 1/2) units |
105 | // * we switch from "2 units" to the next smaller unit at (1.5 unit minus half of the next smaller unit) |
106 | |
107 | impl 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 | // FIXME when `div_duration_f64` is stable |
120 | 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 | |
133 | const 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 | |
142 | impl 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: f64) => write!(f, " {number:.0} B" ), |
146 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
147 | } |
148 | } |
149 | } |
150 | |
151 | impl 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: f64) => write!(f, " {number:.0} B" ), |
155 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
156 | } |
157 | } |
158 | } |
159 | |
160 | impl 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: f64) => write!(f, " {number:.0} B" ), |
164 | NumberPrefix::Prefixed(prefix: Prefix, number: f64) => write!(f, " {number:.2} {prefix}B" ), |
165 | } |
166 | } |
167 | } |
168 | |
169 | impl fmt::Display for HumanCount { |
170 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
171 | use fmt::Write; |
172 | |
173 | let num: String = self.0.to_string(); |
174 | let len: usize = num.len(); |
175 | for (idx: usize, c: char) in num.chars().enumerate() { |
176 | let pos: usize = 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 | |
186 | impl 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)] |
213 | mod 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 | // this one is actually done at 1.5 unit - half of the next smaller unit - epsilon |
257 | // and should display the next smaller unit |
258 | 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 | // this one is actually done at 1.5 unit - half of the next smaller unit |
273 | // and should still display "2 units" |
274 | 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 | } |
371 | |