literal: round-half-even tie for normal-range float/complex repr (#8243)

to_string's normal-magnitude branch and complex::component_to_string
returned Rust's shortest formatting directly, which can pick the odd-digit
neighbour of a rounding tie where repr() picks the even one (e.g. bits
0x42e26687db6b9b04 formats as 161852602146008.13 vs repr's ...08.12).
Route both through prefer_cpython_tie_repr, generalized to fixed-notation
strings. Add unit tests and float/complex repr snippet tests.

Assisted-by: Claude
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-07-09 21:42:08 +09:00
1 parent 150990e866
commit 238bed66fb
4 files changed
+49 -13

No files matched your search

+2 -2
View File
@@ -19,9 +19,9 @@ fn component_to_string(value: f64) -> String {
if exponent < 16 && exponent > -5 {
// Normal magnitude — Rust's default Display emits "1" for 1.0,
// "1.5" for 1.5, "1000000000000000" for 1e15, etc.
value.to_string()
float::prefer_cpython_tie_repr(value.to_string(), value)
} else {
alloc::format!("{significand}e{exponent:+#03}")
float::prefer_cpython_tie_repr(alloc::format!("{significand}e{exponent:+#03}"), value)
}
} else {
// nan / inf / -inf — `format!("{x:e}")` produces e.g. "NaN" with no
+28 -11
View File
@@ -209,11 +209,13 @@ pub fn format_general(
}
}
fn prefer_cpython_tie_repr(s: String, value: f64) -> String {
let Some(exponent_pos) = s.find('e') else {
return s;
};
let Some(digit_pos) = s[..exponent_pos].bytes().rposition(|b| b.is_ascii_digit()) else {
pub(crate) fn prefer_cpython_tie_repr(s: String, value: f64) -> String {
// Rust's shortest float formatter can land on the odd-digit neighbour of a
// rounding tie where round-half-to-even (what `repr` uses) picks the even
// one. When the last significant digit is odd and its even neighbour still
// round-trips and is no further from the value, prefer the even neighbour.
let boundary = s.find('e').unwrap_or(s.len());
let Some(digit_pos) = s[..boundary].bytes().rposition(|b| b.is_ascii_digit()) else {
return s;
};
@@ -258,11 +260,11 @@ fn checked_pow_u128(base: u128, exp: u32) -> Option<u128> {
}
fn parse_decimal_rational(s: &str) -> Option<(u128, u32)> {
let exponent_pos = s.find('e')?;
let exponent = s[exponent_pos + 1..].parse::<i32>().ok()?;
let significand = s[..exponent_pos]
.strip_prefix('-')
.unwrap_or(&s[..exponent_pos]);
let (mantissa, exponent) = match s.find('e') {
Some(pos) => (&s[..pos], s[pos + 1..].parse::<i32>().ok()?),
None => (s, 0),
};
let significand = mantissa.strip_prefix('-').unwrap_or(mantissa);
let dot_pos = significand.find('.');
let frac_digits = dot_pos
.map(|pos| significand.len().saturating_sub(pos + 1))
@@ -325,7 +327,7 @@ pub fn to_string(value: f64) -> String {
if is_integer(value) {
format!("{value:.1?}")
} else {
value.to_string()
prefer_cpython_tie_repr(value.to_string(), value)
}
} else {
prefer_cpython_tie_repr(format!("{significand}e{exponent:+#03}"), value)
@@ -351,6 +353,21 @@ mod tests {
"6.1005353927612305e-05"
);
}
#[test]
fn repr_normal_range_uses_cpython_tie_digit() {
// Rust's shortest formatter yields "161852602146008.13" for this
// value; round-half-to-even (what `repr` uses) picks "…08.12".
assert_eq!(
to_string(f64::from_bits(0x42e26687db6b9b04)),
"161852602146008.12"
);
// Non-tie values are left untouched.
assert_eq!(to_string(1.5), "1.5");
assert_eq!(to_string(0.1), "0.1");
assert_eq!(to_string(12.34), "12.34");
assert_eq!(to_string(100.0), "100.0");
}
}
pub fn from_hex(s: &str) -> Option<f64> {
+7
View File
@@ -277,3 +277,10 @@ assert repr(float("inf") + 1j) == "(inf+1j)"
assert repr(float("-inf") + 1j) == "(-inf+1j)"
assert repr(complex(1, float("nan"))) == "(1+nanj)"
assert repr(complex(1, float("inf"))) == "(1+infj)"
# Round-half-to-even ties: Rust's shortest formatter can land on the
# odd-digit neighbour where repr()'s tie-breaking picks the even one.
assert repr(161852602146008.12 + 1j) == "(161852602146008.12+1j)"
assert repr(-788830060729777.2 + 2j) == "(-788830060729777.2+2j)"
assert repr(complex(0.0, 1959276370239205.2)) == "1959276370239205.2j"
assert repr(complex(-1818262230632059.2, 0.0)) == "(-1818262230632059.2+0j)"
+12
View File
@@ -549,3 +549,15 @@ _check_msg(
lambda: INF.__int__(), OverflowError, "cannot convert float infinity to integer"
)
_check_msg(lambda: NAN.__floor__(), ValueError, "cannot convert float NaN to integer")
# repr round-half-to-even ties: Rust's shortest formatter can land on the
# odd-digit neighbour where repr()'s tie-breaking picks the even one.
assert repr(161852602146008.12) == "161852602146008.12"
assert repr(-788830060729777.2) == "-788830060729777.2"
assert repr(1959276370239205.2) == "1959276370239205.2"
assert repr(-1818262230632059.2) == "-1818262230632059.2"
assert str(161852602146008.12) == "161852602146008.12"
# non-tie values are unaffected
assert repr(1.5) == "1.5"
assert repr(0.1) == "0.1"
assert repr(100.0) == "100.0"