diff --git a/library/core/src/fmt/float.rs b/library/core/src/fmt/float.rs index 87ce27b0d86d..bb70e59f960e 100644 --- a/library/core/src/fmt/float.rs +++ b/library/core/src/fmt/float.rs @@ -171,8 +171,7 @@ fn float_to_exponential_common(fmt: &mut Formatter<'_>, num: &T, upper: bool) }; if let Some(precision) = fmt.options.get_precision() { - // 1 integral digit + `precision` fractional digits = `precision + 1` total digits - float_to_exponential_common_exact(fmt, num, sign, precision + 1, upper) + float_to_exponential_common_exact(fmt, num, sign, precision, upper) } else { float_to_exponential_common_shortest(fmt, num, sign, upper) } diff --git a/library/core/src/fmt/mod.rs b/library/core/src/fmt/mod.rs index 05d8e0d84f05..cd2b6cc86653 100644 --- a/library/core/src/fmt/mod.rs +++ b/library/core/src/fmt/mod.rs @@ -2006,7 +2006,13 @@ unsafe fn pad_formatted_parts(&mut self, formatted: &numfmt::Formatted<'_>) -> R // SAFETY: Per the precondition. unsafe { self.write_formatted_parts(&formatted) } } else { - let post_padding = self.padding(width - len as u16, Alignment::Right)?; + // Padding widths are capped at `u16`, so reaching this branch means + // the formatted output is also shorter than `u16::MAX`. + let len = match u16::try_from(len) { + Ok(len) => len, + Err(_) => unreachable!(), + }; + let post_padding = self.padding(width - len, Alignment::Right)?; // SAFETY: Per the precondition. unsafe { self.write_formatted_parts(&formatted)?; diff --git a/library/core/src/num/imp/flt2dec/mod.rs b/library/core/src/num/imp/flt2dec/mod.rs index e79a00a86596..cf895b3e89e6 100644 --- a/library/core/src/num/imp/flt2dec/mod.rs +++ b/library/core/src/num/imp/flt2dec/mod.rs @@ -244,18 +244,21 @@ fn digits_to_dec_str<'a>( } /// Formats the given decimal digits `0.<...buf...> * 10^exp` into the exponential -/// form with at least the given number of significant digits. When `upper` is `true`, +/// form with at least the given number of fractional digits. When `upper` is `true`, /// the exponent will be prefixed by `E`; otherwise that's `e`. The result is /// stored to the supplied parts array and a slice of written parts is returned. /// -/// `min_digits` can be less than the number of actual significant digits in `buf`; +/// `frac_digits` can be less than the number of actual fractional digits in `buf`; /// it will be ignored and full digits will be printed. It is only used to print -/// additional zeroes after rendered digits. Thus, `min_digits == 0` means that +/// additional zeroes after rendered digits. Thus, `frac_digits == 0` means that /// it will only print the given digits and nothing else. +/// +/// For example, `buf = b"123", exp = 3, frac_digits = 4` yields the parts for +/// `1.2300e2`. fn digits_to_exp_str<'a>( buf: &'a [u8], exp: i16, - min_ndigits: usize, + frac_digits: usize, upper: bool, parts: &'a mut [MaybeUninit>], ) -> &'a [Part<'a>] { @@ -268,12 +271,22 @@ fn digits_to_exp_str<'a>( parts[n] = MaybeUninit::new(Part::Copy(&buf[..1])); n += 1; - if buf.len() > 1 || min_ndigits > 1 { + // The first generated digit becomes the integral digit, anything after that is already part of + // the fractional portion we can emit verbatim. + let actual_frac_digits = buf.len() - 1; + if actual_frac_digits > 0 || frac_digits > 0 { + // Emit a decimal point either when we already have fractional digits or when the requested + // precision needs trailing zeroes after the radix point. parts[n] = MaybeUninit::new(Part::Copy(b".")); - parts[n + 1] = MaybeUninit::new(Part::Copy(&buf[1..])); - n += 2; - if min_ndigits > buf.len() { - parts[n] = MaybeUninit::new(Part::Zero(min_ndigits - buf.len())); + n += 1; + if actual_frac_digits > 0 { + parts[n] = MaybeUninit::new(Part::Copy(&buf[1..])); + n += 1; + } + if frac_digits > actual_frac_digits { + // format_exact exhausted the meaningful digits, so extend the fractional part with + // zeroes up to the requested precision. + parts[n] = MaybeUninit::new(Part::Zero(frac_digits - actual_frac_digits)); n += 1; } } @@ -492,7 +505,7 @@ fn estimate_max_buf_len(exp: i16) -> usize { } /// Formats given floating point number into the exponential form with -/// exactly given number of significant digits. The result is stored to +/// exactly given number of fractional digits. The result is stored to /// the supplied parts array while utilizing given byte buffer as a scratch. /// `upper` is used to determine the case of the exponent prefix (`e` or `E`). /// The first part to be rendered is always a `Part::Sign` (which can be @@ -502,8 +515,10 @@ fn estimate_max_buf_len(exp: i16) -> usize { /// It should return the part of the buffer that it initialized. /// You probably would want `strategy::grisu::format_exact` for this. /// -/// The byte buffer should be at least `ndigits` bytes long unless `ndigits` is -/// so large that only the fixed number of digits will be ever written. +/// The returned format is `[sign][digit][.fraction?][zero padding?][e|E][exponent]`. +/// +/// The byte buffer should be at least `frac_digits + 1` bytes long unless +/// `frac_digits` is so large that only the fixed number of digits will be ever written. /// (The tipping point for `f64` is about 800, so 1000 bytes should be enough.) /// There should be at least 6 parts available, due to the worst case like /// `[+][1][.][2345][e][-][6]`. @@ -511,7 +526,7 @@ pub fn to_exact_exp_str<'a, T, F>( mut format_exact: F, v: T, sign: Sign, - ndigits: usize, + frac_digits: usize, upper: bool, buf: &'a mut [MaybeUninit], parts: &'a mut [MaybeUninit>], @@ -521,7 +536,6 @@ pub fn to_exact_exp_str<'a, T, F>( F: FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), { assert!(parts.len() >= 6); - assert!(ndigits > 0); let (negative, full_decoded) = decode(v); let sign = determine_sign(sign, &full_decoded, negative); @@ -537,10 +551,10 @@ pub fn to_exact_exp_str<'a, T, F>( Formatted { sign, parts: unsafe { parts[..1].assume_init_ref() } } } FullDecoded::Zero => { - if ndigits > 1 { + if frac_digits > 0 { // [0.][0000][e0] parts[0] = MaybeUninit::new(Part::Copy(b"0.")); - parts[1] = MaybeUninit::new(Part::Zero(ndigits - 1)); + parts[1] = MaybeUninit::new(Part::Zero(frac_digits)); parts[2] = MaybeUninit::new(Part::Copy(if upper { b"E0" } else { b"e0" })); Formatted { sign, @@ -558,11 +572,14 @@ pub fn to_exact_exp_str<'a, T, F>( } FullDecoded::Finite(ref decoded) => { let maxlen = estimate_max_buf_len(decoded.exp); - assert!(buf.len() >= ndigits || buf.len() >= maxlen); + // Scratch space is only needed for the significant digits that `format_exact` can + // actually generate. Any remaining requested fractional precision becomes a trailing + // `Part::Zero`. + let sig_digits = if frac_digits < maxlen { frac_digits + 1 } else { maxlen }; + assert!(buf.len() >= sig_digits); - let trunc = if ndigits < maxlen { ndigits } else { maxlen }; - let (buf, exp) = format_exact(decoded, &mut buf[..trunc], i16::MIN); - Formatted { sign, parts: digits_to_exp_str(buf, exp, ndigits, upper, parts) } + let (buf, exp) = format_exact(decoded, &mut buf[..sig_digits], i16::MIN); + Formatted { sign, parts: digits_to_exp_str(buf, exp, frac_digits, upper, parts) } } } } diff --git a/library/core/src/num/imp/fmt.rs b/library/core/src/num/imp/fmt.rs index 0e4b2844d819..db5dfb37ed42 100644 --- a/library/core/src/num/imp/fmt.rs +++ b/library/core/src/num/imp/fmt.rs @@ -68,9 +68,14 @@ pub struct Formatted<'a> { } impl<'a> Formatted<'a> { - /// Returns the exact byte length of combined formatted result. + /// Returns the byte length of combined formatted result. + /// + /// Saturates at `usize::MAX` if the actual length is larger. + /// + /// This matters on 16-bit targets, where exponential formatting can exceed + /// `usize::MAX` by emitting `u16::MAX` trailing zeroes plus `"1."` / `"e0"`. pub fn len(&self) -> usize { - self.sign.len() + self.parts.iter().map(|part| part.len()).sum::() + self.parts.iter().fold(self.sign.len(), |len, part| len.saturating_add(part.len())) } /// Writes all formatted parts into the supplied buffer. diff --git a/library/coretests/tests/fmt/float.rs b/library/coretests/tests/fmt/float.rs index 003782f34dc9..4f7357761744 100644 --- a/library/coretests/tests/fmt/float.rs +++ b/library/coretests/tests/fmt/float.rs @@ -1,3 +1,5 @@ +use core::fmt::{self, Write}; + #[test] fn test_format_f64() { assert_eq!("1", format!("{:.0}", 1.0f64)); @@ -170,6 +172,72 @@ fn test_format_f32_rounds_ties_to_even() { assert_eq!("-1.28E2", format!("{:.2E}", -128.5f32)); } +#[test] +fn test_format_f64_max_precision_exponential() { + struct ExactExpWriter { + prefix: &'static [u8], + zeroes_remaining: u32, + suffix: &'static [u8], + prefix_pos: usize, + suffix_pos: usize, + total_len: u32, + } + + impl ExactExpWriter { + fn new(prefix: &'static str, suffix: &'static str) -> Self { + Self { + prefix: prefix.as_bytes(), + zeroes_remaining: u16::MAX.into(), + suffix: suffix.as_bytes(), + prefix_pos: 0, + suffix_pos: 0, + total_len: 0, + } + } + + fn finish(self) { + assert_eq!(self.prefix_pos, self.prefix.len()); + assert_eq!(self.zeroes_remaining, 0); + assert_eq!(self.suffix_pos, self.suffix.len()); + assert_eq!(self.total_len, u32::from(u16::MAX) + 4); + } + } + + impl Write for ExactExpWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + for byte in s.bytes() { + self.total_len += 1; + + if self.prefix_pos < self.prefix.len() { + assert_eq!(byte, self.prefix[self.prefix_pos]); + self.prefix_pos += 1; + } else if self.zeroes_remaining > 0 { + assert_eq!(byte, b'0'); + self.zeroes_remaining -= 1; + } else { + assert!(self.suffix_pos < self.suffix.len()); + assert_eq!(byte, self.suffix[self.suffix_pos]); + self.suffix_pos += 1; + } + } + + Ok(()) + } + } + + fn assert_exact_exp(args: fmt::Arguments<'_>, prefix: &'static str, suffix: &'static str) { + let mut writer = ExactExpWriter::new(prefix, suffix); + fmt::write(&mut writer, args).unwrap(); + writer.finish(); + } + + assert_exact_exp(format_args!("{:.65535e}", 0.0f64), "0.", "e0"); + assert_exact_exp(format_args!("{:.65535e}", 1.0f64), "1.", "e0"); + assert_exact_exp(format_args!("{:.65535E}", 0.0f64), "0.", "E0"); + assert_exact_exp(format_args!("{:.65535E}", 1.0f64), "1.", "E0"); + assert_exact_exp(format_args!("{:65535.65535e}", 1.0f64), "1.", "e0"); +} + fn is_exponential(s: &str) -> bool { s.contains("e") || s.contains("E") } diff --git a/library/coretests/tests/num/flt2dec/mod.rs b/library/coretests/tests/num/flt2dec/mod.rs index 4888d9d1de06..02a5d31553da 100644 --- a/library/coretests/tests/num/flt2dec/mod.rs +++ b/library/coretests/tests/num/flt2dec/mod.rs @@ -793,7 +793,15 @@ fn to_string(f: &mut F, v: T, sign: Sign, ndigits: usize, upper: bool) -> F: for<'a> FnMut(&Decoded, &'a mut [MaybeUninit], i16) -> (&'a [u8], i16), { to_string_with_parts(|buf, parts| { - to_exact_exp_str(|d, b, l| f(d, b, l), v, sign, ndigits, upper, buf, parts) + to_exact_exp_str( + |d, b, l| f(d, b, l), + v, + sign, + ndigits.saturating_sub(1), + upper, + buf, + parts, + ) }) }