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
41 changes: 32 additions & 9 deletions crates/literal/src/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,39 @@ use crate::float;
use alloc::borrow::ToOwned;
use alloc::string::{String, ToString};

/// Format a single complex component (real or imag) for `repr`.
/// Uses scientific notation when `|value| < 1e-4` or `|value| >= 1e16`
/// (matching CPython's `PyOS_double_to_string(format='r')`), otherwise
/// Rust's default `Display`, which drops the trailing `.0` for
/// integer-valued floats.
///
/// This differs from `float::to_string` only in that integer values in
/// the normal range render as `"1"` rather than `"1.0"` — complex repr
/// formats `1+2j` as `"(1+2j)"`, not `"(1.0+2.0j)"`.
fn component_to_string(value: f64) -> String {
let lit = alloc::format!("{value:e}");
if let Some(position) = lit.find('e') {
let significand = &lit[..position];
let exponent = lit[position + 1..].parse::<i32>().unwrap();
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()
} else {
alloc::format!("{significand}e{exponent:+#03}")
}
} else {
// nan / inf / -inf — `format!("{x:e}")` produces e.g. "NaN" with no
// exponent marker; lowercase to match Python.
let mut s = value.to_string();
s.make_ascii_lowercase();
s
}
}

/// Convert a complex number to a string.
pub fn to_string(re: f64, im: f64) -> String {
// integer => drop ., fractional => float_ops
let mut im_part = if im.fract() == 0.0 {
im.to_string()
} else {
float::to_string(im)
};
let mut im_part = component_to_string(im);
im_part.push('j');

// positive empty => return im_part, integer => drop ., fractional => float_ops
Expand All @@ -19,10 +44,8 @@ pub fn to_string(re: f64, im: f64) -> String {
} else {
"-0".to_owned()
}
} else if re.fract() == 0.0 {
re.to_string()
} else {
float::to_string(re)
component_to_string(re)
};
let mut result =
String::with_capacity(re_part.len() + im_part.len() + 2 + im.is_sign_positive() as usize);
Expand Down
32 changes: 32 additions & 0 deletions extra_tests/snippets/builtin_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,35 @@ class complex_subclass(complex):
z = complex_subclass(3 + 4j)
assert z.__complex__() == 3 + 4j
assert type(z.__complex__()) == complex


# repr must use scientific notation for |value| >= 1e16 or < 1e-4, matching
# CPython. Previously integer-valued large magnitudes (e.g. 1e16, 1e100) hit
# a `fract() == 0.0` branch in rustpython_literal::complex::to_string that
# used Rust's default Display — which emits the full decimal expansion
# (`10000...000`) instead of `1e+16`.
assert repr(1e16 + 1j) == "(1e+16+1j)"
assert repr(1e17 + 1j) == "(1e+17+1j)"
assert repr(1e100 + 1e100j) == "(1e+100+1e+100j)"
assert repr(-1e100 - 1e100j) == "(-1e+100-1e+100j)"
assert repr(1e-100 + 1e100j) == "(1e-100+1e+100j)"
assert repr(1 + 1e100j) == "(1+1e+100j)"
assert repr(1e100 + 1j) == "(1e+100+1j)"

# Threshold boundary: |x| in [1e-4, 1e16) renders in decimal form; values
# outside that range use scientific notation. These three assertions pin
# the exact transition points.
assert repr(1e15 + 1j) == "(1000000000000000+1j)" # below 1e16 -> decimal
assert repr(1e-4 + 1j) == "(0.0001+1j)" # at 1e-4 (inclusive) -> decimal
assert repr(1e-5 + 1j) == "(1e-05+1j)" # below 1e-4 -> scientific

# Integer-valued components render without trailing ".0".
assert repr(1 + 2j) == "(1+2j)"
assert repr(1.0 + 2.0j) == "(1+2j)"

# Special values still round-trip correctly.
assert repr(float("nan") + 1j) == "(nan+1j)"
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)"
Loading