mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
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:
1 parent
c93d63a759
commit
8e30b94eef
4 files changed
+49
-13
No files matched your search
@@ -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
@@ -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> {
|
||||
|
||||
@@ -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)"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user