Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 147 additions & 27 deletions src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,48 +170,122 @@ impl fmt::Display for HumanCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;

let num = self.0.to_string();
let len = num.len();
for (idx, c) in num.chars().enumerate() {
let pos = len - idx - 1;
f.write_char(c)?;
if pos > 0 && pos % 3 == 0 {
f.write_char(',')?;
if f.alternate() {
// Find appropriate suffix
let (divisor, suffix) = HUMAN_COUNT_THRESHOLDS
.iter()
.find(|(threshold, _)| self.0 >= *threshold)
.unwrap();

if *divisor == 0 || suffix.is_empty() {
// No need for decimal places when no suffix
write!(f, "{}", self.0)?;
} else {
// Scale the number appropriately - convert to f64 for proper division
let scaled_value = self.0 as f64 / *divisor as f64;

// If precision is manually set, use it
// Otherwise, calculate precision to show exactly 3 significant digits
let precision = f.precision().unwrap_or_else(|| {
if scaled_value.abs() < 10.0 {
2
} else if scaled_value.abs() < 100.0 {
1
} else {
0
}
});

write!(f, "{:.*}{}", precision, scaled_value, suffix)?;
}
} else {
let num = self.0.to_string();
let len = num.len();
for (idx, c) in num.chars().enumerate() {
let pos = len - idx - 1;
f.write_char(c)?;
if pos > 0 && pos % 3 == 0 {
f.write_char(',')?;
}
}
}
Ok(())
}
}

const HUMAN_COUNT_THRESHOLDS: [(u64, &str); 5] = [
(1_000_000_000_000, "T"), // trillion
(1_000_000_000, "B"), // billion
(1_000_000, "M"), // million
(1_000, "k"), // thousand
(0, ""), // no suffix
];

impl fmt::Display for HumanFloatCount {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;

// Use formatter's precision if provided, otherwise default to 4
let precision = f.precision().unwrap_or(4);
let num = format!("{:.*}", precision, self.0);

let (int_part, frac_part) = match num.split_once('.') {
Some((int_str, fract_str)) => (int_str.to_string(), fract_str),
None => (self.0.trunc().to_string(), ""),
};
let len = int_part.len();
for (idx, c) in int_part.chars().enumerate() {
let pos = len - idx - 1;
f.write_char(c)?;
if pos > 0 && pos % 3 == 0 {
f.write_char(',')?;
if f.alternate() {
// Find appropriate suffix
let (divisor, suffix) = HUMAN_FLOAT_COUNT_THRESHOLDS
.iter()
.find(|(threshold, _)| self.0.abs() >= *threshold)
.unwrap();

// Scale the number appropriately
let scaled_value = if *divisor > 0.0 {
self.0 / divisor
} else {
self.0
};

// If precision is manually set, use it
// Otherwise, calculate precision to show exactly 3 significant digits
let precision = f.precision().unwrap_or_else(|| {
if scaled_value.abs() < 10.0 {
2
} else if scaled_value.abs() < 100.0 {
1
} else {
0
}
});

write!(f, "{:.*}{}", precision, scaled_value, suffix)?;
} else {
// Use formatter's precision if provided, otherwise default to 4
let precision = f.precision().unwrap_or(4);
let num = format!("{:.*}", precision, self.0);

let (int_part, frac_part) = match num.split_once('.') {
Some((int_str, fract_str)) => (int_str.to_string(), fract_str),
None => (self.0.trunc().to_string(), ""),
};
let len = int_part.len();
for (idx, c) in int_part.chars().enumerate() {
let pos = len - idx - 1;
f.write_char(c)?;
if pos > 0 && pos % 3 == 0 {
f.write_char(',')?;
}
}
let frac_trimmed = frac_part.trim_end_matches('0');
if !frac_trimmed.is_empty() {
f.write_char('.')?;
f.write_str(frac_trimmed)?;
}
}
let frac_trimmed = frac_part.trim_end_matches('0');
if !frac_trimmed.is_empty() {
f.write_char('.')?;
f.write_str(frac_trimmed)?;
}
Ok(())
}
}

const HUMAN_FLOAT_COUNT_THRESHOLDS: [(f64, &str); 5] = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need both this and HUMAN_COUNT_TRESHOLDS, presumably we can drive the float value from the integer one?

Copy link
Contributor Author

@ReagentX ReagentX Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The types are different, so they have to be defined separately unless I want to cast them every time the formatter runs. I could do something like either of these:

macro_rules! define_thresholds {
    ($(($num:expr, $suffix:expr)),*) => {
        const HUMAN_COUNT_THRESHOLDS: [(u64, &str); 5] = [
            $(($num, $suffix),)*
        ];

        const HUMAN_FLOAT_COUNT_THRESHOLDS: [(f64, &str); 5] = [
            $(($num as f64, $suffix),)*
        ];
    };
}

// Explicit type for 1T because Rust infers numeric literals to be i32 by default
define_thresholds!(
    (1_000_000_000_000i64, "T"),
    (1_000_000_000, "B"),
    (1_000_000, "M"),
    (1_000, "k"),
    (0, "")
);

or

const TRILLION: u64 = 1_000_000_000_000;
const BILLION: u64 = 1_000_000_000;
const MILLION: u64 = 1_000_000;
const THOUSAND: u64 = 1_000;
const ZERO: u64 = 0;

const HUMAN_COUNT_THRESHOLDS: [(u64, &str); 5] = [
    (TRILLION, "T"),
    (BILLION, "B"),
    (MILLION, "M"),
    (THOUSAND, "k"),
    (ZERO, ""),
];

const HUMAN_FLOAT_COUNT_THRESHOLDS: [(f64, &str); 5] = [
    (TRILLION as f64, "T"),
    (BILLION as f64, "B"),
    (MILLION as f64, "M"),
    (THOUSAND as f64, "k"),
    (ZERO as f64, ""),
];

but they both feel a lot less maintainable than just having two arrays.

(1_000_000_000_000., "T"), // trillion
(1_000_000_000., "B"), // billion
(1_000_000., "M"), // million
(1_000., "k"), // thousand
(0., ""), // no suffix
];

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -344,6 +418,19 @@ mod tests {
assert_eq!("7,654", format!("{}", HumanCount(7654)));
assert_eq!("12,345", format!("{}", HumanCount(12345)));
assert_eq!("1,234,567,890", format!("{}", HumanCount(1234567890)));

assert_eq!("42", format!("{:#}", HumanCount(42)));
assert_eq!("7.65k", format!("{:#}", HumanCount(7654)));
assert_eq!("12.3k", format!("{:#}", HumanCount(12345)));
assert_eq!("1.23M", format!("{:#}", HumanCount(1234567)));
assert_eq!("1.23B", format!("{:#}", HumanCount(1234567890)));
assert_eq!("1.23T", format!("{:#}", HumanCount(1234567890000)));

assert_eq!("7.65k", format!("{:#.2}", HumanCount(7654)));
assert_eq!("12.35k", format!("{:#.2}", HumanCount(12345)));
assert_eq!("1.23M", format!("{:#.2}", HumanCount(1234567)));
assert_eq!("1.23B", format!("{:#.2}", HumanCount(1234567890)));
assert_eq!("1.23T", format!("{:#.2}", HumanCount(1234567890000)));
}

#[test]
Expand Down Expand Up @@ -377,5 +464,38 @@ mod tests {
"1,234.1234320999999454215867445",
format!("{:.25}", HumanFloatCount(1234.1234321))
);

assert_eq!("42.5", format!("{:#}", HumanFloatCount(42.5)));
assert_eq!("42.5", format!("{:#}", HumanFloatCount(42.500012345)));
assert_eq!("42.5", format!("{:#}", HumanFloatCount(42.502012345)));
assert_eq!("7.65k", format!("{:#}", HumanFloatCount(7654.321)));
assert_eq!("7.65k", format!("{:#}", HumanFloatCount(7654.3210123456)));
assert_eq!("12.3k", format!("{:#}", HumanFloatCount(12345.6789)));
assert_eq!(
"1.23B",
format!("{:#}", HumanFloatCount(1234567890.1234567))
);
assert_eq!(
"1.23B",
format!("{:#}", HumanFloatCount(1234567890.1234321))
);

assert_eq!("42.500", format!("{:#.3}", HumanFloatCount(42.5)));
assert_eq!("42.500", format!("{:#.3}", HumanFloatCount(42.500012345)));
assert_eq!("42.502", format!("{:#.3}", HumanFloatCount(42.502012345)));
assert_eq!("7.654k", format!("{:#.3}", HumanFloatCount(7654.321)));
assert_eq!(
"7.654k",
format!("{:#.3}", HumanFloatCount(7654.3210123456))
);
assert_eq!("12.346k", format!("{:#.3}", HumanFloatCount(12345.6789)));
assert_eq!(
"1.235B",
format!("{:#.3}", HumanFloatCount(1234567890.1234567))
);
assert_eq!(
"1.235B",
format!("{:#.3}", HumanFloatCount(1234567890.1234321))
);
}
}
12 changes: 9 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,15 @@
//! * `wide_msg`: like `msg` but always fills the remaining space and truncates. It should not be used
//! with `wide_bar`.
//! * `pos`: renders the current position of the bar as integer
//! * `human_pos`: renders the current position of the bar as an integer, with commas as the
//! * `human_pos_formatted`: renders the current position of the bar as an integer, with commas as the
//! thousands separator.
//! * `human_pos`: renders the current position of the bar as a number with shorthand notation
//! for large values (e.g., "1.5k" for 1,500 or "1.2M" for 1,200,000).
//! * `len`: renders the amount of work to be done as an integer
//! * `human_len`: renders the total length of the bar as an integer, with commas as the thousands
//! * `human_len_formatted`: renders the total length of the bar as an integer, with commas as the thousands
//! separator.
//! * `human_len`: renders the total length of the bar as a number with shorthand notation
//! for large values (e.g., "1.5k" for 1,500 or "1.2M" for 1,200,000).
//! * `percent`: renders the current position of the bar as a percentage of the total length (as an integer).
//! * `percent_precise`: renders the current position of the bar as a percentage of the total length (with 3 fraction digits).
//! * `bytes`: renders the current position of the bar as bytes (alias of `binary_bytes`).
Expand All @@ -203,7 +207,9 @@
//! power-of-two units, i.e. `MiB`, `KiB`, etc.
//! * `elapsed_precise`: renders the elapsed time as `HH:MM:SS`.
//! * `elapsed`: renders the elapsed time as `42s`, `1m` etc.
//! * `per_sec`: renders the speed in steps per second.
//! * `per_sec_formatted`: renders the speed in steps per second.
//! * `per_sec`: renders the speed in steps per second as a number with shorthand notation
//! for large values (e.g., "1.5k" for 1,500 or "1.2M" for 1,200,000).
//! * `bytes_per_sec`: renders the speed in bytes per second (alias of `binary_bytes_per_sec`).
//! * `decimal_bytes_per_sec`: renders the speed in bytes per second using
//! power-of-10 units, i.e. `MB`, `kB`, etc.
Expand Down
86 changes: 83 additions & 3 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,39 @@ impl ProgressStyle {
"msg" => buf.push_str(state.message.expanded()),
"prefix" => buf.push_str(state.prefix.expanded()),
"pos" => buf.write_fmt(format_args!("{pos}")).unwrap(),
"human_pos" => {
"human_pos_formatted" => {
buf.write_fmt(format_args!("{}", HumanCount(pos))).unwrap();
}
"human_pos" => {
if let Some(width) = width {
buf.write_fmt(format_args!(
"{:#.1$}",
HumanCount(pos),
*width as usize
))
.unwrap();
} else {
buf.write_fmt(format_args!("{:#}", HumanCount(pos)))
.unwrap();
}
}
"len" => buf.write_fmt(format_args!("{len}")).unwrap(),
"human_len" => {
"human_len_formatted" => {
buf.write_fmt(format_args!("{}", HumanCount(len))).unwrap();
}
"human_len" => {
if let Some(width) = width {
buf.write_fmt(format_args!(
"{:#.1$}",
HumanCount(len),
*width as usize
))
.unwrap();
} else {
buf.write_fmt(format_args!("{:#}", HumanCount(len)))
.unwrap();
}
}
"percent" => buf
.write_fmt(format_args!("{:.*}", 0, state.fraction() * 100f32))
.unwrap(),
Expand Down Expand Up @@ -331,7 +357,7 @@ impl ProgressStyle {
"elapsed" => buf
.write_fmt(format_args!("{:#}", HumanDuration(state.elapsed())))
.unwrap(),
"per_sec" => {
"per_sec_formatted" => {
if let Some(width) = width {
buf.write_fmt(format_args!(
"{:.1$}/s",
Expand All @@ -347,6 +373,22 @@ impl ProgressStyle {
.unwrap();
}
}
"per_sec" => {
if let Some(width) = width {
buf.write_fmt(format_args!(
"{:#.1$}/s",
HumanFloatCount(state.per_sec()),
*width as usize
))
.unwrap();
} else {
buf.write_fmt(format_args!(
"{:#}/s",
HumanFloatCount(state.per_sec())
))
.unwrap();
}
}
"bytes_per_sec" => buf
.write_fmt(format_args!("{}/s", HumanBytes(state.per_sec() as u64)))
.unwrap(),
Expand Down Expand Up @@ -1178,4 +1220,42 @@ mod tests {
assert_eq!(&buf[2], "bar");
assert_eq!(&buf[3], "baz");
}

#[test]
fn human_count_handling() {
const WIDTH: u16 = 80;
let pos = Arc::new(AtomicPosition::new());
pos.set(543_234);
let state = ProgressState::new(Some(1_000_000), pos);
let mut buf = Vec::new();

let mut style = ProgressStyle::default_bar();
style.template = Template::from_str("{pos} / {len}").unwrap();
style.format_state(&state, &mut buf, WIDTH);

assert_eq!(buf.len(), 1);
assert_eq!(&buf[0], "543234 / 1000000");

buf.clear();
style.template =
Template::from_str("{human_pos_formatted} / {human_len_formatted}").unwrap();
style.format_state(&state, &mut buf, WIDTH);

assert_eq!(buf.len(), 1);
assert_eq!(&buf[0], "543,234 / 1,000,000");

buf.clear();
style.template = Template::from_str("{human_pos} / {human_len}").unwrap();
style.format_state(&state, &mut buf, WIDTH);

assert_eq!(buf.len(), 1);
assert_eq!(&buf[0], "543k / 1.00M");

buf.clear();
style.template = Template::from_str("{human_pos:3} / {human_len:3}").unwrap();
style.format_state(&state, &mut buf, WIDTH);

assert_eq!(buf.len(), 1);
assert_eq!(&buf[0], "543.234k / 1.000M");
}
}