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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions Lib/test/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,6 @@ def test_repr_roundtrip(self):
self.assertFloatsAreIdentical(0.0 + z.imag,
0.0 + roundtrip.imag)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_format(self):
# empty format string is same as str()
self.assertEqual(format(1+3j, ''), str(1+3j))
Expand Down
4 changes: 0 additions & 4 deletions Lib/test/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,6 @@ def test_nul(self):
testformat("a%sb", ('c\0d',), 'ac\0db')
testcommon(b"a%sb", (b'c\0d',), b'ac\0db')

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_non_ascii(self):
testformat("\u20ac=%f", (1.0,), "\u20ac=1.000000")

Expand Down Expand Up @@ -468,8 +466,6 @@ def test_optimisations(self):
self.assertIs(text % (), text)
self.assertIs(text.format(), text)

# TODO: RustPython missing complex.__format__ implementation
@unittest.expectedFailure
def test_precision(self):
f = 1.2
self.assertEqual(format(f, ".0f"), "1")
Expand Down
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ radium = { workspace = true }

lock_api = "0.4"
siphasher = "1"
num-complex.workspace = true

[target.'cfg(windows)'.dependencies]
widestring = { workspace = true }
Expand Down
124 changes: 124 additions & 0 deletions common/src/format.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// spell-checker:ignore ddfe
use itertools::{Itertools, PeekingNext};
use malachite_base::num::basic::floats::PrimitiveFloat;
use malachite_bigint::{BigInt, Sign};
use num_complex::Complex64;
use num_traits::FromPrimitive;
use num_traits::{Signed, cast::ToPrimitive};
use rustpython_literal::float;
Expand Down Expand Up @@ -612,6 +614,126 @@ impl FormatSpec {
}
}

pub fn format_complex(&self, num: &Complex64) -> Result<String, FormatSpecError> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

According to Python's documentation, specifying precision is not allowed for complex numbers when no presentation type is given. CPython raises a TypeError in this case.

For example, f"{1+2j:.3}" results in TypeError: precision not allowed in complex format specifier.

The current implementation incorrectly processes the precision instead of raising an error. You should add a check at the beginning of format_complex to disallow this.

        if self.format_type.is_none() && self.precision.is_some() {
            return Err(FormatSpecError::AlignmentFlagNotAllowed);
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In my local Python 3.13.0, f"{1+2j:.3}" results in '(1+2j)'.
Precision is ignored but TypeError isn't raised.

let (formatted_re, formatted_im) = self.format_complex_re_im(num)?;
// Enclose in parentheses if there is no format type and formatted_re is not empty
let magnitude_str = if self.format_type.is_none() && !formatted_re.is_empty() {
format!("({formatted_re}{formatted_im})")
} else {
format!("{formatted_re}{formatted_im}")
};
if let Some(FormatAlign::AfterSign) = &self.align {
return Err(FormatSpecError::AlignmentFlag);
}
match &self.fill.unwrap_or(' '.into()).to_char() {
Some('0') => Err(FormatSpecError::ZeroPadding),
_ => self.format_sign_and_align(&AsciiStr::new(&magnitude_str), "", FormatAlign::Right),
}
}

fn format_complex_re_im(&self, num: &Complex64) -> Result<(String, String), FormatSpecError> {
// Format real part
let mut formatted_re = String::new();
if num.re != 0.0 || num.re.is_negative_zero() || self.format_type.is_some() {
let sign_re = if num.re.is_sign_negative() && !num.is_nan() {
"-"
} else {
match self.sign.unwrap_or(FormatSign::Minus) {
FormatSign::Plus => "+",
FormatSign::Minus => "",
FormatSign::MinusOrSpace => " ",
}
};
let re = self.format_complex_float(num.re)?;
formatted_re = format!("{sign_re}{re}");
}
// Format imaginary part
let sign_im = if num.im.is_sign_negative() && !num.im.is_nan() {
"-"
} else if formatted_re.is_empty() {
""
} else {
"+"
};
let im = self.format_complex_float(num.im)?;
Ok((formatted_re, format!("{sign_im}{im}j")))
}

fn format_complex_float(&self, num: f64) -> Result<String, FormatSpecError> {
self.validate_format(FormatType::FixedPoint(Case::Lower))?;
let precision = self.precision.unwrap_or(6);
let magnitude = num.abs();
let magnitude_str = match &self.format_type {
Some(FormatType::Decimal)
| Some(FormatType::Binary)
| Some(FormatType::Octal)
| Some(FormatType::Hex(_))
| Some(FormatType::String)
| Some(FormatType::Character)
| Some(FormatType::Number(Case::Upper))
| Some(FormatType::Percentage) => {
let ch = char::from(self.format_type.as_ref().unwrap());
Err(FormatSpecError::UnknownFormatCode(ch, "complex"))
}
Some(FormatType::FixedPoint(case)) => Ok(float::format_fixed(
precision,
magnitude,
*case,
self.alternate_form,
)),
Some(FormatType::GeneralFormat(case)) | Some(FormatType::Number(case)) => {
let precision = if precision == 0 { 1 } else { precision };
Ok(float::format_general(
precision,
magnitude,
*case,
self.alternate_form,
false,
))
}
Some(FormatType::Exponent(case)) => Ok(float::format_exponent(
precision,
magnitude,
*case,
self.alternate_form,
)),
None => match magnitude {
magnitude if magnitude.is_nan() => Ok("nan".to_owned()),
magnitude if magnitude.is_infinite() => Ok("inf".to_owned()),
_ => match self.precision {
Some(precision) => Ok(float::format_general(
precision,
magnitude,
Case::Lower,
self.alternate_form,
true,
)),
None => {
if magnitude.fract() == 0.0 {
Ok(magnitude.trunc().to_string())
} else {
Ok(magnitude.to_string())
}
}
},
},
}?;
match &self.grouping_option {
Some(fg) => {
let sep = match fg {
FormatGrouping::Comma => ',',
FormatGrouping::Underscore => '_',
};
let inter = self.get_separator_interval().try_into().unwrap();
let len = magnitude_str.len() as i32;
let separated_magnitude =
FormatSpec::add_magnitude_separators_for_char(magnitude_str, inter, sep, len);
Ok(separated_magnitude)
}
None => Ok(magnitude_str),
}
}

fn format_sign_and_align<T>(
&self,
magnitude_str: &T,
Expand Down Expand Up @@ -701,6 +823,8 @@ pub enum FormatSpecError {
NotAllowed(&'static str),
UnableToConvert,
CodeNotInRange,
ZeroPadding,
AlignmentFlag,
NotImplemented(char, &'static str),
}

Expand Down
17 changes: 17 additions & 0 deletions extra_tests/snippets/builtin_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,23 @@ def test_zero_padding():
assert f"{3.1415:#.2}" == "3.1"
assert f"{3.1415:#.3}" == "3.14"
assert f"{3.1415:#.4}" == "3.142"
assert f"{12.34 + 5.6j}" == "(12.34+5.6j)"
assert f"{12.34 - 5.6j: }" == "( 12.34-5.6j)"
assert f"{12.34 + 5.6j:20}" == " (12.34+5.6j)"
assert f"{12.34 + 5.6j:<20}" == "(12.34+5.6j) "
assert f"{-12.34 + 5.6j:^20}" == " (-12.34+5.6j) "
assert f"{12.34 + 5.6j:^+20}" == " (+12.34+5.6j) "
assert f"{12.34 + 5.6j:_^+20}" == "___(+12.34+5.6j)____"
assert f"{-12.34 + 5.6j:f}" == "-12.340000+5.600000j"
assert f"{12.34 + 5.6j:.3f}" == "12.340+5.600j"
assert f"{12.34 + 5.6j:<30.8f}" == "12.34000000+5.60000000j "
assert f"{12.34 + 5.6j:g}" == "12.34+5.6j"
assert f"{12.34 + 5.6j:e}" == "1.234000e+01+5.600000e+00j"
assert f"{12.34 + 5.6j:E}" == "1.234000E+01+5.600000E+00j"
assert f"{12.34 + 5.6j:^30E}" == " 1.234000E+01+5.600000E+00j "
assert f"{12345.6 + 7890.1j:,}" == "(12,345.6+7,890.1j)"
assert f"{12345.6 + 7890.1j:_.3f}" == "12_345.600+7_890.100j"
assert f"{12345.6 + 7890.1j:>+30,f}" == " +12,345.600000+7,890.100000j"

# test issue 4558
x = 123456789012345678901234567890
Expand Down
11 changes: 10 additions & 1 deletion vm/src/builtins/complex.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use super::{PyStr, PyType, PyTypeRef, float};
use crate::{
AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
builtins::PyStrRef,
class::PyClassImpl,
convert::{ToPyObject, ToPyResult},
common::format::FormatSpec,
convert::{IntoPyException, ToPyObject, ToPyResult},
function::{
OptionalArg, OptionalOption,
PyArithmeticValue::{self, *},
Expand Down Expand Up @@ -388,6 +390,13 @@ impl PyComplex {
let Complex64 { re, im } = self.value;
(re, im)
}

#[pymethod]
fn __format__(&self, spec: PyStrRef, vm: &VirtualMachine) -> PyResult<String> {
FormatSpec::parse(spec.as_str())
.and_then(|format_spec| format_spec.format_complex(&self.value))
.map_err(|err| err.into_pyexception(vm))
}
}

#[pyclass]
Expand Down
6 changes: 6 additions & 0 deletions vm/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ impl IntoPyException for FormatSpecError {
}
Self::UnableToConvert => vm.new_value_error("Unable to convert int to float"),
Self::CodeNotInRange => vm.new_overflow_error("%c arg not in range(0x110000)"),
Self::ZeroPadding => {
vm.new_value_error("Zero padding is not allowed in complex format specifier")
}
Self::AlignmentFlag => {
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
}
Self::NotImplemented(c, s) => {
let msg = format!("Format code '{c}' for object of type '{s}' not implemented yet");
vm.new_value_error(msg)
Expand Down