| 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 | |