mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
* Fix complex repr to use scientific notation for large integer-valued components
repr of a complex number whose real or imaginary part is an integer-valued
float with |x| >= 1e16 emitted the full decimal expansion instead of
scientific notation, diverging from CPython:
Before (RustPython):
repr(1e100 + 1e100j)
(10000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000+1000000000000000
000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000j)
After / CPython:
(1e+100+1e+100j)
Root cause in crates/literal/src/complex.rs::to_string — it bifurcated
each component by .fract() == 0.0:
if im.fract() == 0.0 { im.to_string() } // Rust's default Display
else { float::to_string(im) } // scientific for large/small
Rust's Display never uses scientific notation, so any integer-valued f64
(including 1e16, 1e17, 1e100 which are exactly representable as integers)
routed through the wrong branch and produced the full decimal expansion.
Non-integer magnitudes reached float::to_string and rendered correctly.
The fix is to use one helper per component that implements CPython's
actual PyOS_double_to_string(format='r') rule: scientific notation when
|x| < 1e-4 or |x| >= 1e16, otherwise Rust's default Display (which drops
the trailing '.0' for integer-valued floats — matching CPython's
(1+2j) convention rather than (1.0+2.0j)). The threshold matches
float::to_string; the only behavioral difference is that complex
components render 1.0 as "1" rather than "1.0".
Verified:
* 29 CPython reference cases (normal / boundary / extremes / special /
signed-zero) — all byte-identical after fix.
* 18 additional edge cases (subnormal 5e-324, f64::MAX, MIN_POSITIVE,
DBL_EPSILON, threshold-straddling values) — all byte-identical.
* Lib/test/test_complex.py::test_repr_str /
test_negative_zero_repr_str / test_repr_roundtrip — all pass.
* cargo run -- -m test test_complex — 37 passed.
* cargo run -- -m test test_float test_long — 101 passed.
* ast.unparse() round-trip of source containing complex literals
(e.g. 1e100 + 1e-100j, 1e17 + 1j) produces CPython-identical output.
* extra_tests/snippets/builtin_complex.py — 20+ new regression cases.
* Address CodeRabbit review: clarify threshold boundary test comment
The comment claimed all three assertions stay in non-scientific form,
but the 1e-5 case explicitly verifies scientific notation (since
|1e-5| < 1e-4 falls outside the decimal-form range). Reworded the
header to describe the axis being tested (threshold boundary) and
added per-case inline notes indicating each assertion's expected
form.
271 lines
7.6 KiB
Python
271 lines
7.6 KiB
Python
import testutils
|
|
from testutils import assert_raises
|
|
|
|
# __abs__
|
|
|
|
assert abs(complex(3, 4)) == 5
|
|
assert abs(complex(3, -4)) == 5
|
|
assert abs(complex(1.5, 2.5)) == 2.9154759474226504
|
|
|
|
# __eq__
|
|
|
|
assert 3 + 02j == 3 + 2j
|
|
assert complex(1, -1) == complex(1, -1)
|
|
assert complex(1, 0) == 1
|
|
assert 1 == complex(1, 0)
|
|
assert complex(1, 1) != 1
|
|
assert 1 != complex(1, 1)
|
|
assert complex(1, 0) == 1.0
|
|
assert 1.0 == complex(1, 0)
|
|
assert complex(1, 1) != 1.0
|
|
assert 1.0 != complex(1, 1)
|
|
assert complex(1, 0) != 1.5
|
|
assert not 1.0 != complex(1, 0)
|
|
assert bool(complex(1, 0))
|
|
assert complex(1, 2) != complex(1, 1)
|
|
assert complex(1, 2) != "foo"
|
|
assert complex(1, 2).__eq__("foo") == NotImplemented
|
|
assert 1j != 10**1000
|
|
|
|
# __mul__, __rmul__
|
|
|
|
assert complex(2, -3) * complex(-5, 7) == complex(11, 29)
|
|
assert complex(2, -3) * 5 == complex(10, -15)
|
|
assert 5 * complex(2, -3) == complex(2, -3) * 5
|
|
|
|
# __truediv__, __rtruediv__
|
|
|
|
assert complex(2, -3) / 2 == complex(1, -1.5)
|
|
assert 5 / complex(3, -4) == complex(0.6, 0.8)
|
|
|
|
# __mod__, __rmod__
|
|
# "can't mod complex numbers.
|
|
assert_raises(TypeError, lambda: complex(2, -3) % 2)
|
|
assert_raises(TypeError, lambda: 2 % complex(2, -3))
|
|
|
|
# __floordiv__, __rfloordiv__
|
|
# can't take floor of complex number.
|
|
assert_raises(TypeError, lambda: complex(2, -3) // 2)
|
|
assert_raises(TypeError, lambda: 2 // complex(2, -3))
|
|
|
|
# __divmod__, __rdivmod__
|
|
# "can't take floor or mod of complex number."
|
|
assert_raises(TypeError, lambda: divmod(complex(2, -3), 2))
|
|
assert_raises(TypeError, lambda: divmod(2, complex(2, -3)))
|
|
|
|
# __pow__, __rpow__
|
|
|
|
# assert 1j ** 2 == -1
|
|
assert complex(1) ** 2 == 1
|
|
assert 2 ** complex(2) == 4
|
|
|
|
# __pos__
|
|
|
|
assert +complex(0, 1) == complex(0, 1)
|
|
assert +complex(1, 0) == complex(1, 0)
|
|
assert +complex(1, -1) == complex(1, -1)
|
|
assert +complex(0, 0) == complex(0, 0)
|
|
|
|
# __neg__
|
|
|
|
assert -complex(1, -1) == complex(-1, 1)
|
|
assert -complex(0, 0) == complex(0, 0)
|
|
|
|
# __bool__
|
|
|
|
assert bool(complex(0, 0)) is False
|
|
assert bool(complex(0, 1)) is True
|
|
assert bool(complex(1, 0)) is True
|
|
|
|
# __hash__
|
|
|
|
assert hash(complex(1)) == hash(float(1)) == hash(int(1))
|
|
assert hash(complex(-1)) == hash(float(-1)) == hash(int(-1))
|
|
assert hash(complex(3.14)) == hash(float(3.14))
|
|
assert hash(complex(-float("inf"))) == hash(-float("inf"))
|
|
assert hash(1j) != hash(1)
|
|
|
|
# TODO: Find a way to test platform dependent values
|
|
assert hash(3.1 - 4.2j) == hash(3.1 - 4.2j)
|
|
assert hash(3.1 + 4.2j) == hash(3.1 + 4.2j)
|
|
|
|
# numbers.Complex
|
|
|
|
a = complex(3, 4)
|
|
b = 4j
|
|
assert a.real == 3
|
|
assert b.real == 0
|
|
|
|
assert a.imag == 4
|
|
assert b.imag == 4
|
|
|
|
assert a.conjugate() == 3 - 4j
|
|
assert b.conjugate() == -4j
|
|
|
|
# int and complex addition
|
|
assert 1 + 1j == complex(1, 1)
|
|
assert 1j + 1 == complex(1, 1)
|
|
assert (1j + 1) + 3 == complex(4, 1)
|
|
assert 3 + (1j + 1) == complex(4, 1)
|
|
|
|
# float and complex addition
|
|
assert 1.1 + 1.2j == complex(1.1, 1.2)
|
|
assert 1.3j + 1.4 == complex(1.4, 1.3)
|
|
assert (1.5j + 1.6) + 3 == complex(4.6, 1.5)
|
|
assert 3.5 + (1.1j + 1.2) == complex(4.7, 1.1)
|
|
|
|
# subtraction
|
|
assert 1 - 1j == complex(1, -1)
|
|
assert 1j - 1 == complex(-1, 1)
|
|
assert 2j - 1j == complex(0, 1)
|
|
|
|
# type error addition
|
|
assert_raises(TypeError, lambda: 1j + "str")
|
|
assert_raises(TypeError, lambda: 1j - "str")
|
|
assert_raises(TypeError, lambda: "str" + 1j)
|
|
assert_raises(TypeError, lambda: "str" - 1j)
|
|
|
|
# overflow
|
|
assert_raises(OverflowError, lambda: complex(10**1000, 0))
|
|
assert_raises(OverflowError, lambda: complex(0, 10**1000))
|
|
assert_raises(OverflowError, lambda: 0j + 10**1000)
|
|
|
|
# str/repr
|
|
assert "(1+1j)" == str(1 + 1j)
|
|
assert "(1-1j)" == str(1 - 1j)
|
|
assert "(1+1j)" == repr(1 + 1j)
|
|
assert "(1-1j)" == repr(1 - 1j)
|
|
|
|
# __getnewargs__
|
|
assert (3 + 5j).__getnewargs__() == (3.0, 5.0)
|
|
assert (5j).__getnewargs__() == (0.0, 5.0)
|
|
|
|
|
|
class Complex:
|
|
def __init__(self, real, imag):
|
|
self.real = real
|
|
self.imag = imag
|
|
|
|
def __repr__(self):
|
|
return "Com" + str((self.real, self.imag))
|
|
|
|
def __sub__(self, other):
|
|
return Complex(self.real - other, self.imag)
|
|
|
|
def __rsub__(self, other):
|
|
return Complex(other - self.real, -self.imag)
|
|
|
|
def __eq__(self, other):
|
|
return self.real == other.real and self.imag == other.imag
|
|
|
|
|
|
assert Complex(4, 5) - 3 == Complex(1, 5)
|
|
assert 7 - Complex(4, 5) == Complex(3, -5)
|
|
|
|
assert complex("5+2j") == 5 + 2j
|
|
assert complex("5-2j") == 5 - 2j
|
|
assert complex("-2j") == -2j
|
|
assert_raises(TypeError, lambda: complex("5+2j", 1))
|
|
assert_raises(ValueError, lambda: complex("abc"))
|
|
|
|
assert complex("1+10j") == 1 + 10j
|
|
assert complex(10) == 10 + 0j
|
|
assert complex(10.0) == 10 + 0j
|
|
assert complex(10) == 10 + 0j
|
|
assert complex(10 + 0j) == 10 + 0j
|
|
assert complex(1, 10) == 1 + 10j
|
|
assert complex(1, 10) == 1 + 10j
|
|
assert complex(1, 10.0) == 1 + 10j
|
|
assert complex(1, 10) == 1 + 10j
|
|
assert complex(1, 10) == 1 + 10j
|
|
assert complex(1, 10.0) == 1 + 10j
|
|
assert complex(1.0, 10) == 1 + 10j
|
|
assert complex(1.0, 10) == 1 + 10j
|
|
assert complex(1.0, 10.0) == 1 + 10j
|
|
assert complex(3.14 + 0j) == 3.14 + 0j
|
|
assert complex(3.14) == 3.14 + 0j
|
|
assert complex(314) == 314.0 + 0j
|
|
assert complex(314) == 314.0 + 0j
|
|
assert complex(3.14 + 0j, 0j) == 3.14 + 0j
|
|
assert complex(3.14, 0.0) == 3.14 + 0j
|
|
assert complex(314, 0) == 314.0 + 0j
|
|
assert complex(314, 0) == 314.0 + 0j
|
|
assert complex(0j, 3.14j) == -3.14 + 0j
|
|
assert complex(0.0, 3.14j) == -3.14 + 0j
|
|
assert complex(0j, 3.14) == 3.14j
|
|
assert complex(0.0, 3.14) == 3.14j
|
|
assert complex("1") == 1 + 0j
|
|
assert complex("1j") == 1j
|
|
assert complex() == 0
|
|
assert complex("-1") == -1
|
|
assert complex("+1") == +1
|
|
assert complex("(1+2j)") == 1 + 2j
|
|
assert complex("(1.3+2.2j)") == 1.3 + 2.2j
|
|
assert complex("3.14+1J") == 3.14 + 1j
|
|
assert complex(" ( +3.14-6J )") == 3.14 - 6j
|
|
assert complex(" ( +3.14-J )") == 3.14 - 1j
|
|
assert complex(" ( +3.14+j )") == 3.14 + 1j
|
|
assert complex("J") == 1j
|
|
assert complex("( j )") == 1j
|
|
assert complex("+J") == 1j
|
|
assert complex("( -j)") == -1j
|
|
assert complex("1e-500") == 0.0 + 0.0j
|
|
assert complex("-1e-500j") == 0.0 - 0.0j
|
|
assert complex("-1e-500+1e-500j") == -0.0 + 0.0j
|
|
|
|
|
|
# Invalid syntax:
|
|
src = """
|
|
b = 03 + 2j
|
|
"""
|
|
|
|
with assert_raises(SyntaxError):
|
|
exec(src)
|
|
|
|
|
|
# __complex__
|
|
z = 3 + 4j
|
|
assert z.__complex__() == z
|
|
assert type(z.__complex__()) == complex
|
|
|
|
|
|
class complex_subclass(complex):
|
|
pass
|
|
|
|
|
|
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)"
|