Skip to content
Merged
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
76 changes: 54 additions & 22 deletions src/uucore/src/lib/features/format/num_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ fn zero_pad_to(s: &str, width: usize) -> String {
}
}

/// Like `format!("{:.precision$}", 0.0)`, which panics past `u16::MAX`.
fn zero_with_fraction_digits(precision: usize) -> String {
if precision == 0 {
return String::from("0");
}
let mut s = String::from("0.");
s.extend(std::iter::repeat_n('0', precision));
s
}

fn get_sign_indicator(sign: PositiveSign, negative: bool) -> String {
if negative {
String::from("-")
Expand Down Expand Up @@ -418,6 +428,24 @@ fn format_float_non_finite(e: &ExtendedBigDecimal, case: Case) -> String {
/// Largest precision `format!` accepts; beyond it the formatter panics.
const MAX_FMT_PRECISION: usize = u16::MAX as usize;

/// `{bd:.0}` switches to exponent notation past 1000 zeros, so write integers
/// out ourselves, up to `MAX_FORMAT_WIDTH` digits.
fn integer_digits(bd: &BigDecimal) -> Option<String> {
let (bi, scale) = bd.as_bigint_and_scale();
if scale > 0 {
return None;
}
if bi.is_zero() {
return Some(String::from("0"));
}
let zeros = usize::try_from(scale.unsigned_abs())
.ok()
.filter(|zeros| *zeros <= super::MAX_FORMAT_WIDTH)?;
let mut s = bi.to_str_radix(10);
Comment on lines +443 to +444
s.extend(std::iter::repeat_n('0', zeros));
Some(s)
}

fn format_float_decimal(
bd: &BigDecimal,
precision: Option<usize>,
Expand All @@ -426,23 +454,26 @@ fn format_float_decimal(
debug_assert!(!bd.is_negative());
let precision = precision.unwrap_or(6); // Default %f precision (C standard)
if precision == 0 {
let (bi, scale) = bd.as_bigint_and_scale();
if scale == 0 && force_decimal != ForceDecimal::Yes {
// Optimization when printing integers.
return bi.to_str_radix(10);
} else if force_decimal == ForceDecimal::Yes {
return format!("{bd:.0}.");
}
let s = integer_digits(bd).unwrap_or_else(|| format!("{bd:.0}"));
return if force_decimal == ForceDecimal::Yes {
format!("{s}.")
} else {
s
};
Comment on lines +457 to +462
}
// `format!` stores width and precision as `u16` and panics above that, so
// write the digits we have and pad the remainder with zeros ourselves.
if precision > MAX_FMT_PRECISION {
let digits = usize::try_from(bd.fractional_digit_count().max(0)).unwrap_or(usize::MAX);
let written = digits.min(MAX_FMT_PRECISION);
let mut s = format!("{bd:.written$}");
if written == 0 {
s.push('.');
}
// `format!` panics past `MAX_FMT_PRECISION` and `bigdecimal` stops padding
// past 1000 zeros, so pad the rest ourselves.
let digits = usize::try_from(bd.fractional_digit_count().max(0)).unwrap_or(usize::MAX);
let written = digits.min(MAX_FMT_PRECISION);
if precision > written {
let mut s = if written > 0 {
format!("{bd:.written$}")
} else if let Some(s) = integer_digits(bd) {
s + "."
} else {
// Past `MAX_FORMAT_WIDTH` zeros, print exponent form and skip the precision and `.`.
return format!("{bd:.0}");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

here we silently drop the requested precision (and the .), is that intended? if so, could you please add a short comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It only kicks in past MAX_FORMAT_WIDTH zeros (%.2f 1e2000000), where main already printed 1e+2000000 and GNU prints inf with an error, so I kept that rather than writing out all the digits. Added a comment in a7ab2b1.

};
s.extend(std::iter::repeat_n('0', precision - written));
return s;
}
Comment on lines +466 to 479
Expand Down Expand Up @@ -502,7 +533,7 @@ fn format_float_scientific(
return if force_decimal == ForceDecimal::Yes && precision == 0 {
format!("0.{exp_char}+00")
} else {
format!("{:.precision$}{exp_char}+00", 0.0)
format!("{}{exp_char}+00", zero_with_fraction_digits(precision))
};
}

Expand Down Expand Up @@ -539,9 +570,7 @@ fn format_float_shortest(
if BigDecimal::zero().eq(bd) {
return match (force_decimal, precision) {
(ForceDecimal::Yes, 1) => "0.".into(),
(ForceDecimal::Yes, _) => {
format!("{:.*}", precision - 1, 0.0)
}
(ForceDecimal::Yes, _) => zero_with_fraction_digits(precision - 1),
(ForceDecimal::No, _) => "0".into(),
};
}
Expand Down Expand Up @@ -640,9 +669,12 @@ fn format_float_hexadecimal(
// To print 0, we don't ever need any digits after the decimal point, so default to
// that if precision is not specified.
return if force_decimal == ForceDecimal::Yes && precision.unwrap_or(0) == 0 {
format!("0x0.{exp_char}+0")
format!("{prefix}0.{exp_char}+0")
} else {
format!("0x{:.*}{exp_char}+0", precision.unwrap_or(0), 0.0)
format!(
"{prefix}{}{exp_char}+0",
zero_with_fraction_digits(precision.unwrap_or(0))
)
};
}

Expand Down
33 changes: 33 additions & 0 deletions tests/by-util/test_printf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,3 +1891,36 @@ fn test_precision_above_formatter_limit() {
assert!(out.starts_with("3.25"));
assert!(out[4..].bytes().all(|b| b == b'0'));
}

#[test]
fn test_integer_precision_above_padding_limit() {
new_ucmd!()
.args(&["%.1500f", "1"])
.succeeds()
.stdout_only(format!("1.{}", "0".repeat(1500)));
new_ucmd!()
.args(&["%f", "1e1001"])
.succeeds()
.stdout_only(format!("1{}.000000", "0".repeat(1001)));
new_ucmd!()
.args(&["%#.0f", "1e1001"])
.succeeds()
.stdout_only(format!("1{}.", "0".repeat(1001)));
}

#[test]
fn test_zero_precision_above_formatter_limit() {
let zeros = "0".repeat(70000);
new_ucmd!()
.args(&["%.70000e", "0"])
.succeeds()
.stdout_only(format!("0.{zeros}e+00"));
Comment on lines +1912 to +1917
new_ucmd!()
.args(&["%.70000A", "0"])
.succeeds()
.stdout_only(format!("0X0.{zeros}P+0"));
new_ucmd!()
.args(&["%#.70000g", "0"])
.succeeds()
.stdout_only(format!("0.{}", &zeros[1..]));
}
8 changes: 8 additions & 0 deletions tests/by-util/test_seq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,3 +1245,11 @@ fn test_format_precision_above_formatter_limit() {
assert!(out.starts_with("4."));
assert!(out[2..].trim_end().bytes().all(|b| b == b'0'));
}

#[test]
fn test_format_zero_precision_above_formatter_limit() {
new_ucmd!()
.args(&["-f", "%.70000e", "0", "0"])
.succeeds()
.stdout_only(format!("0.{}e+00\n", "0".repeat(70000)));
}
Loading