Skip to content
Draft
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
4 changes: 2 additions & 2 deletions crates/literal/src/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 28 additions & 11 deletions crates/literal/src/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -351,6 +353,21 @@ mod tests {
"6.1005353927612305e-05"
);
}

#[test]
fn repr_normal_range_uses_cpython_tie_digit() {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Add also extra_tests

// 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(3.14), "3.14");
assert_eq!(to_string(100.0), "100.0");
}
}

pub fn from_hex(s: &str) -> Option<f64> {
Expand Down
7 changes: 7 additions & 0 deletions extra_tests/snippets/builtin_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,3 +268,10 @@ class complex_subclass(complex):
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 changes: 12 additions & 0 deletions extra_tests/snippets/builtin_float.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,3 +549,15 @@ def _check_msg(call, exc_type, expected_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"
Loading