Implement negative zero coercion format option (#8475)

Assisted-by: Codex:gpt-5
This commit is contained in:
Seonghun An authored and GitHub committed 2026-08-09 14:58:07 +09:00
1 parent ee94b5b723
commit 33d98d122e
3 files changed
+173 -20

No files matched your search

-1
View File
@@ -558,7 +558,6 @@ class FormatTest(unittest.TestCase):
with self.assertRaisesRegex(ValueError, str_err):
"{a:%ЫйЯЧ}".format(a='a')
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_negative_zero(self):
## default behavior
self.assertEqual(f"{-0.:.1f}", "-0.0")
+167 -19
View File
@@ -229,6 +229,7 @@ pub struct FormatSpec {
align: Option<FormatAlign>,
align_specified: bool,
sign: Option<FormatSign>,
no_neg_0: bool,
alternate_form: bool,
width: Option<usize>,
grouping_option: Option<FormatGrouping>,
@@ -285,6 +286,14 @@ fn parse_alternate_form(text: &Wtf8) -> (bool, &Wtf8) {
}
}
fn parse_no_negative_zero(text: &Wtf8) -> (bool, &Wtf8) {
let mut chars = text.code_points();
match chars.next().and_then(CodePoint::to_char) {
Some('z') => (true, chars.as_wtf8()),
_ => (false, text),
}
}
fn parse_zero(text: &Wtf8) -> (bool, &Wtf8) {
let mut chars = text.code_points();
match chars.next().and_then(CodePoint::to_char) {
@@ -349,6 +358,7 @@ impl FormatSpec {
let (mut fill, mut align, text) = parse_fill_and_align(text);
let align_specified = align.is_some();
let (sign, text) = FormatSign::parse(text);
let (no_neg_0, text) = parse_no_negative_zero(text);
let (alternate_form, text) = parse_alternate_form(text);
let (zero, text) = parse_zero(text);
let (width, text) = parse_number(text)?;
@@ -378,6 +388,7 @@ impl FormatSpec {
align,
align_specified,
sign,
no_neg_0,
alternate_form,
width,
grouping_option,
@@ -505,6 +516,25 @@ impl FormatSpec {
Ok(())
}
fn formatted_magnitude_is_zero(magnitude: &str) -> bool {
let mut saw_digit = false;
for byte in magnitude.bytes() {
if byte.is_ascii_digit() {
saw_digit = true;
if byte != b'0' {
return false;
}
}
}
saw_digit
}
fn is_negative_after_zero_coercion(&self, num: f64, magnitude: &str) -> bool {
num.is_sign_negative()
&& !num.is_nan()
&& !(self.no_neg_0 && Self::formatted_magnitude_is_zero(magnitude))
}
fn validate_complex_padding_and_alignment(&self) -> Result<(), FormatSpecError> {
match &self.fill.unwrap_or_else(|| ' '.into()).to_char() {
Some('0') => Err(FormatSpecError::ZeroPadding),
@@ -674,6 +704,9 @@ impl FormatSpec {
Some(FormatType::Number(Case::Lower)) => self.format_int_radix(magnitude, 10),
_ => return self.format_int(num),
}?;
if self.no_neg_0 {
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
}
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
@@ -723,7 +756,7 @@ impl FormatSpec {
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
let sign_str = if num.is_sign_negative() && !num.is_nan() {
let sign_str = if self.is_negative_after_zero_coercion(num, &magnitude_str) {
"-"
} else {
match format_sign {
@@ -812,11 +845,16 @@ impl FormatSpec {
self.format_float(x as f64)
}
None => {
if self.no_neg_0 {
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
}
let first_letter = (input.to_string().as_bytes()[0] as char).to_uppercase();
Ok(first_letter.collect::<String>() + &input.to_string()[1..])
}
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(*c, "int")),
_ => Err(FormatSpecError::InvalidFormatSpecifier),
Some(format_type) => {
let ch = char::from(format_type);
Err(FormatSpecError::UnknownFormatCode(ch, "bool"))
}
}
}
@@ -925,8 +963,9 @@ impl FormatSpec {
},
},
};
let raw_magnitude_str = raw_magnitude_str?;
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
let sign_str = if num.is_sign_negative() && !num.is_nan() {
let sign_str = if self.is_negative_after_zero_coercion(num, &raw_magnitude_str) {
"-"
} else {
match format_sign {
@@ -935,7 +974,7 @@ impl FormatSpec {
FormatSign::MinusOrSpace => " ",
}
};
let magnitude_str = self.add_magnitude_separators(raw_magnitude_str?, sign_str);
let magnitude_str = self.add_magnitude_separators(raw_magnitude_str, sign_str);
let magnitude_str = self.add_frac_separators(magnitude_str);
Ok(
self.format_sign_and_align(
@@ -986,15 +1025,24 @@ impl FormatSpec {
Err(FormatSpecError::UnknownFormatCode('N', "int"))
}
Some(FormatType::String) => Err(FormatSpecError::UnknownFormatCode('s', "int")),
Some(FormatType::Character) => match (self.precision, self.sign, self.alternate_form) {
(Some(_), _, _) => Err(FormatSpecError::PrecisionNotAllowed),
(_, Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")),
(_, _, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")),
(_, _, _) => match num.to_u32() {
Some(n) if n <= 0x10ffff => Ok(core::char::from_u32(n).unwrap().to_string()),
Some(_) | None => Err(FormatSpecError::CodeNotInRange),
},
},
Some(FormatType::Character) => {
if self.precision.is_some() {
Err(FormatSpecError::PrecisionNotAllowed)
} else if self.no_neg_0 {
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
} else {
match (self.sign, self.alternate_form) {
(Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")),
(_, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")),
_ => match num.to_u32() {
Some(n) if n <= 0x10ffff => {
Ok(core::char::from_u32(n).unwrap().to_string())
}
Some(_) | None => Err(FormatSpecError::CodeNotInRange),
},
}
}
}
Some(
FormatType::GeneralFormat(_)
| FormatType::FixedPoint(_)
@@ -1007,6 +1055,9 @@ impl FormatSpec {
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(c, "int")),
None => self.format_int_radix(magnitude, 10),
}?;
if self.no_neg_0 {
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
}
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
let sign_str = match num.sign() {
Sign::Minus => "-",
@@ -1032,6 +1083,9 @@ impl FormatSpec {
self.validate_format(FormatType::String)?;
match self.format_type {
Some(FormatType::String) | None => {
if self.no_neg_0 {
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string"));
}
if self.align == Some(FormatAlign::AfterSign) && self.align_specified {
return Err(FormatSpecError::StringAlignmentFlag);
}
@@ -1074,7 +1128,8 @@ impl FormatSpec {
// Format real part
let formatted_re =
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() {
let re = self.format_complex_float(num.re)?;
let sign_re = if self.is_negative_after_zero_coercion(num.re, &re) {
"-"
} else {
match self.sign.unwrap_or(FormatSign::Minus) {
@@ -1083,21 +1138,24 @@ impl FormatSpec {
FormatSign::MinusOrSpace => " ",
}
};
let re = self.format_complex_float(num.re)?;
format!("{sign_re}{re}")
} else {
String::new()
};
// Format imaginary part
let sign_im = if num.im.is_sign_negative() && !num.im.is_nan() {
let im = self.format_complex_float(num.im)?;
let sign_im = if self.is_negative_after_zero_coercion(num.im, &im) {
"-"
} else if formatted_re.is_empty() {
""
match self.sign.unwrap_or(FormatSign::Minus) {
FormatSign::Plus => "+",
FormatSign::Minus => "",
FormatSign::MinusOrSpace => " ",
}
} else {
"+"
};
let im = self.format_complex_float(num.im)?;
Ok((formatted_re, format!("{sign_im}{im}j")))
}
@@ -1274,6 +1332,7 @@ pub enum FormatSpecError {
CodeNotInRange,
ZeroPadding,
AlignmentFlag,
NegativeZeroCoercionNotAllowed(&'static str),
StringAlignmentFlag,
NotImplemented(char, &'static str),
}
@@ -1625,6 +1684,7 @@ mod tests {
align: None,
align_specified: false,
sign: None,
no_neg_0: false,
alternate_form: false,
width: Some(33),
grouping_option: None,
@@ -1643,6 +1703,7 @@ mod tests {
align: Some(FormatAlign::Right),
align_specified: true,
sign: None,
no_neg_0: false,
alternate_form: false,
width: Some(33),
grouping_option: None,
@@ -1661,6 +1722,7 @@ mod tests {
align: Some(FormatAlign::Right),
align_specified: true,
sign: Some(FormatSign::Minus),
no_neg_0: false,
alternate_form: true,
width: Some(23),
grouping_option: Some(FormatGrouping::Comma),
@@ -1779,6 +1841,92 @@ mod tests {
);
}
#[test]
fn format_negative_zero_coercion() {
let int_spec = FormatSpec::parse("z8").unwrap();
assert_eq!(
int_spec.format_int(&BigInt::from(-42)),
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
);
assert_eq!(
FormatSpec::parse("zs")
.unwrap()
.format_int(&BigInt::from(0)),
Err(FormatSpecError::UnknownFormatCode('s', "int"))
);
assert_eq!(
FormatSpec::parse("z.1d")
.unwrap()
.format_int(&BigInt::from(0)),
Err(FormatSpecError::PrecisionNotAllowed)
);
assert_eq!(
FormatSpec::parse("+zc")
.unwrap()
.format_int(&BigInt::from(0)),
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
);
let float_spec = FormatSpec::parse("z.2f").unwrap();
assert_eq!(float_spec.format_float(-0.0001), Ok("0.00".to_owned()));
let complex_spec = FormatSpec::parse("z").unwrap();
assert_eq!(
complex_spec.format_complex(&Complex64::new(-0.0, -0.0)),
Ok("(0+0j)".to_owned())
);
let pure_imaginary = Complex64::new(0.0, -0.0);
assert_eq!(
FormatSpec::parse("+z")
.unwrap()
.format_complex(&pure_imaginary),
Ok("+0j".to_owned())
);
assert_eq!(
FormatSpec::parse(" z")
.unwrap()
.format_complex(&pure_imaginary),
Ok(" 0j".to_owned())
);
let string_value = "value".to_owned();
assert_eq!(
FormatSpec::parse("z").unwrap().format_string(&string_value),
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string"))
);
assert_eq!(
FormatSpec::parse("zd")
.unwrap()
.format_string(&string_value),
Err(FormatSpecError::UnknownFormatCode('d', "str"))
);
assert_eq!(
FormatSpec::parse("zs").unwrap().format_bool(false),
Err(FormatSpecError::UnknownFormatCode('s', "bool"))
);
let locale = LocaleInfo {
thousands_sep: ",".to_owned(),
decimal_point: ".".to_owned(),
grouping: vec![3, 0],
};
let locale_spec = FormatSpec::parse("zn").unwrap();
assert_eq!(
locale_spec.format_float_locale(-0.0, &locale),
Ok("0".to_owned())
);
assert_eq!(
locale_spec.format_complex_locale(&Complex64::new(-0.0, -0.0), &locale),
Ok("0+0j".to_owned())
);
assert_eq!(
FormatSpec::parse("z.1n")
.unwrap()
.format_int_locale(&BigInt::from(0), &locale),
Err(FormatSpecError::PrecisionNotAllowed)
);
}
#[test]
fn format_int() {
assert_eq!(
+6
View File
@@ -77,6 +77,12 @@ impl IntoPyException for FormatSpecError {
Self::AlignmentFlag => {
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
}
Self::NegativeZeroCoercionNotAllowed(type_name) => {
let msg = format!(
"Negative zero coercion (z) not allowed in {type_name} format specifier"
);
vm.new_value_error(msg)
}
Self::StringAlignmentFlag => {
vm.new_value_error("'=' alignment not allowed in string format specifier")
}