Skip to content

Commit 33d98d1

Browse files
authored
Implement negative zero coercion format option (#8475)
Assisted-by: Codex:gpt-5
1 parent ee94b5b commit 33d98d1

3 files changed

Lines changed: 173 additions & 20 deletions

File tree

‎Lib/test/test_format.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,6 @@ def test_unicode_in_error_message(self):
558558
with self.assertRaisesRegex(ValueError, str_err):
559559
"{a:%ЫйЯЧ}".format(a='a')
560560

561-
@unittest.expectedFailure # TODO: RUSTPYTHON
562561
def test_negative_zero(self):
563562
## default behavior
564563
self.assertEqual(f"{-0.:.1f}", "-0.0")

‎crates/common/src/format.rs‎

Lines changed: 167 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ pub struct FormatSpec {
229229
align: Option<FormatAlign>,
230230
align_specified: bool,
231231
sign: Option<FormatSign>,
232+
no_neg_0: bool,
232233
alternate_form: bool,
233234
width: Option<usize>,
234235
grouping_option: Option<FormatGrouping>,
@@ -285,6 +286,14 @@ fn parse_alternate_form(text: &Wtf8) -> (bool, &Wtf8) {
285286
}
286287
}
287288

289+
fn parse_no_negative_zero(text: &Wtf8) -> (bool, &Wtf8) {
290+
let mut chars = text.code_points();
291+
match chars.next().and_then(CodePoint::to_char) {
292+
Some('z') => (true, chars.as_wtf8()),
293+
_ => (false, text),
294+
}
295+
}
296+
288297
fn parse_zero(text: &Wtf8) -> (bool, &Wtf8) {
289298
let mut chars = text.code_points();
290299
match chars.next().and_then(CodePoint::to_char) {
@@ -349,6 +358,7 @@ impl FormatSpec {
349358
let (mut fill, mut align, text) = parse_fill_and_align(text);
350359
let align_specified = align.is_some();
351360
let (sign, text) = FormatSign::parse(text);
361+
let (no_neg_0, text) = parse_no_negative_zero(text);
352362
let (alternate_form, text) = parse_alternate_form(text);
353363
let (zero, text) = parse_zero(text);
354364
let (width, text) = parse_number(text)?;
@@ -378,6 +388,7 @@ impl FormatSpec {
378388
align,
379389
align_specified,
380390
sign,
391+
no_neg_0,
381392
alternate_form,
382393
width,
383394
grouping_option,
@@ -505,6 +516,25 @@ impl FormatSpec {
505516
Ok(())
506517
}
507518

519+
fn formatted_magnitude_is_zero(magnitude: &str) -> bool {
520+
let mut saw_digit = false;
521+
for byte in magnitude.bytes() {
522+
if byte.is_ascii_digit() {
523+
saw_digit = true;
524+
if byte != b'0' {
525+
return false;
526+
}
527+
}
528+
}
529+
saw_digit
530+
}
531+
532+
fn is_negative_after_zero_coercion(&self, num: f64, magnitude: &str) -> bool {
533+
num.is_sign_negative()
534+
&& !num.is_nan()
535+
&& !(self.no_neg_0 && Self::formatted_magnitude_is_zero(magnitude))
536+
}
537+
508538
fn validate_complex_padding_and_alignment(&self) -> Result<(), FormatSpecError> {
509539
match &self.fill.unwrap_or_else(|| ' '.into()).to_char() {
510540
Some('0') => Err(FormatSpecError::ZeroPadding),
@@ -674,6 +704,9 @@ impl FormatSpec {
674704
Some(FormatType::Number(Case::Lower)) => self.format_int_radix(magnitude, 10),
675705
_ => return self.format_int(num),
676706
}?;
707+
if self.no_neg_0 {
708+
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
709+
}
677710

678711
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
679712

@@ -723,7 +756,7 @@ impl FormatSpec {
723756
let magnitude_str = Self::apply_locale_formatting(raw_magnitude_str, locale);
724757

725758
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
726-
let sign_str = if num.is_sign_negative() && !num.is_nan() {
759+
let sign_str = if self.is_negative_after_zero_coercion(num, &magnitude_str) {
727760
"-"
728761
} else {
729762
match format_sign {
@@ -812,11 +845,16 @@ impl FormatSpec {
812845
self.format_float(x as f64)
813846
}
814847
None => {
848+
if self.no_neg_0 {
849+
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
850+
}
815851
let first_letter = (input.to_string().as_bytes()[0] as char).to_uppercase();
816852
Ok(first_letter.collect::<String>() + &input.to_string()[1..])
817853
}
818-
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(*c, "int")),
819-
_ => Err(FormatSpecError::InvalidFormatSpecifier),
854+
Some(format_type) => {
855+
let ch = char::from(format_type);
856+
Err(FormatSpecError::UnknownFormatCode(ch, "bool"))
857+
}
820858
}
821859
}
822860

@@ -925,8 +963,9 @@ impl FormatSpec {
925963
},
926964
},
927965
};
966+
let raw_magnitude_str = raw_magnitude_str?;
928967
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
929-
let sign_str = if num.is_sign_negative() && !num.is_nan() {
968+
let sign_str = if self.is_negative_after_zero_coercion(num, &raw_magnitude_str) {
930969
"-"
931970
} else {
932971
match format_sign {
@@ -935,7 +974,7 @@ impl FormatSpec {
935974
FormatSign::MinusOrSpace => " ",
936975
}
937976
};
938-
let magnitude_str = self.add_magnitude_separators(raw_magnitude_str?, sign_str);
977+
let magnitude_str = self.add_magnitude_separators(raw_magnitude_str, sign_str);
939978
let magnitude_str = self.add_frac_separators(magnitude_str);
940979
Ok(
941980
self.format_sign_and_align(
@@ -986,15 +1025,24 @@ impl FormatSpec {
9861025
Err(FormatSpecError::UnknownFormatCode('N', "int"))
9871026
}
9881027
Some(FormatType::String) => Err(FormatSpecError::UnknownFormatCode('s', "int")),
989-
Some(FormatType::Character) => match (self.precision, self.sign, self.alternate_form) {
990-
(Some(_), _, _) => Err(FormatSpecError::PrecisionNotAllowed),
991-
(_, Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")),
992-
(_, _, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")),
993-
(_, _, _) => match num.to_u32() {
994-
Some(n) if n <= 0x10ffff => Ok(core::char::from_u32(n).unwrap().to_string()),
995-
Some(_) | None => Err(FormatSpecError::CodeNotInRange),
996-
},
997-
},
1028+
Some(FormatType::Character) => {
1029+
if self.precision.is_some() {
1030+
Err(FormatSpecError::PrecisionNotAllowed)
1031+
} else if self.no_neg_0 {
1032+
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
1033+
} else {
1034+
match (self.sign, self.alternate_form) {
1035+
(Some(_), _) => Err(FormatSpecError::NotAllowed("Sign")),
1036+
(_, true) => Err(FormatSpecError::NotAllowed("Alternate form (#)")),
1037+
_ => match num.to_u32() {
1038+
Some(n) if n <= 0x10ffff => {
1039+
Ok(core::char::from_u32(n).unwrap().to_string())
1040+
}
1041+
Some(_) | None => Err(FormatSpecError::CodeNotInRange),
1042+
},
1043+
}
1044+
}
1045+
}
9981046
Some(
9991047
FormatType::GeneralFormat(_)
10001048
| FormatType::FixedPoint(_)
@@ -1007,6 +1055,9 @@ impl FormatSpec {
10071055
Some(FormatType::Unknown(c)) => Err(FormatSpecError::UnknownFormatCode(c, "int")),
10081056
None => self.format_int_radix(magnitude, 10),
10091057
}?;
1058+
if self.no_neg_0 {
1059+
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"));
1060+
}
10101061
let format_sign = self.sign.unwrap_or(FormatSign::Minus);
10111062
let sign_str = match num.sign() {
10121063
Sign::Minus => "-",
@@ -1032,6 +1083,9 @@ impl FormatSpec {
10321083
self.validate_format(FormatType::String)?;
10331084
match self.format_type {
10341085
Some(FormatType::String) | None => {
1086+
if self.no_neg_0 {
1087+
return Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string"));
1088+
}
10351089
if self.align == Some(FormatAlign::AfterSign) && self.align_specified {
10361090
return Err(FormatSpecError::StringAlignmentFlag);
10371091
}
@@ -1074,7 +1128,8 @@ impl FormatSpec {
10741128
// Format real part
10751129
let formatted_re =
10761130
if num.re != 0.0 || num.re.is_negative_zero() || self.format_type.is_some() {
1077-
let sign_re = if num.re.is_sign_negative() && !num.is_nan() {
1131+
let re = self.format_complex_float(num.re)?;
1132+
let sign_re = if self.is_negative_after_zero_coercion(num.re, &re) {
10781133
"-"
10791134
} else {
10801135
match self.sign.unwrap_or(FormatSign::Minus) {
@@ -1083,21 +1138,24 @@ impl FormatSpec {
10831138
FormatSign::MinusOrSpace => " ",
10841139
}
10851140
};
1086-
let re = self.format_complex_float(num.re)?;
10871141
format!("{sign_re}{re}")
10881142
} else {
10891143
String::new()
10901144
};
10911145

10921146
// Format imaginary part
1093-
let sign_im = if num.im.is_sign_negative() && !num.im.is_nan() {
1147+
let im = self.format_complex_float(num.im)?;
1148+
let sign_im = if self.is_negative_after_zero_coercion(num.im, &im) {
10941149
"-"
10951150
} else if formatted_re.is_empty() {
1096-
""
1151+
match self.sign.unwrap_or(FormatSign::Minus) {
1152+
FormatSign::Plus => "+",
1153+
FormatSign::Minus => "",
1154+
FormatSign::MinusOrSpace => " ",
1155+
}
10971156
} else {
10981157
"+"
10991158
};
1100-
let im = self.format_complex_float(num.im)?;
11011159
Ok((formatted_re, format!("{sign_im}{im}j")))
11021160
}
11031161

@@ -1274,6 +1332,7 @@ pub enum FormatSpecError {
12741332
CodeNotInRange,
12751333
ZeroPadding,
12761334
AlignmentFlag,
1335+
NegativeZeroCoercionNotAllowed(&'static str),
12771336
StringAlignmentFlag,
12781337
NotImplemented(char, &'static str),
12791338
}
@@ -1625,6 +1684,7 @@ mod tests {
16251684
align: None,
16261685
align_specified: false,
16271686
sign: None,
1687+
no_neg_0: false,
16281688
alternate_form: false,
16291689
width: Some(33),
16301690
grouping_option: None,
@@ -1643,6 +1703,7 @@ mod tests {
16431703
align: Some(FormatAlign::Right),
16441704
align_specified: true,
16451705
sign: None,
1706+
no_neg_0: false,
16461707
alternate_form: false,
16471708
width: Some(33),
16481709
grouping_option: None,
@@ -1661,6 +1722,7 @@ mod tests {
16611722
align: Some(FormatAlign::Right),
16621723
align_specified: true,
16631724
sign: Some(FormatSign::Minus),
1725+
no_neg_0: false,
16641726
alternate_form: true,
16651727
width: Some(23),
16661728
grouping_option: Some(FormatGrouping::Comma),
@@ -1779,6 +1841,92 @@ mod tests {
17791841
);
17801842
}
17811843

1844+
#[test]
1845+
fn format_negative_zero_coercion() {
1846+
let int_spec = FormatSpec::parse("z8").unwrap();
1847+
assert_eq!(
1848+
int_spec.format_int(&BigInt::from(-42)),
1849+
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
1850+
);
1851+
assert_eq!(
1852+
FormatSpec::parse("zs")
1853+
.unwrap()
1854+
.format_int(&BigInt::from(0)),
1855+
Err(FormatSpecError::UnknownFormatCode('s', "int"))
1856+
);
1857+
assert_eq!(
1858+
FormatSpec::parse("z.1d")
1859+
.unwrap()
1860+
.format_int(&BigInt::from(0)),
1861+
Err(FormatSpecError::PrecisionNotAllowed)
1862+
);
1863+
assert_eq!(
1864+
FormatSpec::parse("+zc")
1865+
.unwrap()
1866+
.format_int(&BigInt::from(0)),
1867+
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("integer"))
1868+
);
1869+
1870+
let float_spec = FormatSpec::parse("z.2f").unwrap();
1871+
assert_eq!(float_spec.format_float(-0.0001), Ok("0.00".to_owned()));
1872+
1873+
let complex_spec = FormatSpec::parse("z").unwrap();
1874+
assert_eq!(
1875+
complex_spec.format_complex(&Complex64::new(-0.0, -0.0)),
1876+
Ok("(0+0j)".to_owned())
1877+
);
1878+
let pure_imaginary = Complex64::new(0.0, -0.0);
1879+
assert_eq!(
1880+
FormatSpec::parse("+z")
1881+
.unwrap()
1882+
.format_complex(&pure_imaginary),
1883+
Ok("+0j".to_owned())
1884+
);
1885+
assert_eq!(
1886+
FormatSpec::parse(" z")
1887+
.unwrap()
1888+
.format_complex(&pure_imaginary),
1889+
Ok(" 0j".to_owned())
1890+
);
1891+
1892+
let string_value = "value".to_owned();
1893+
assert_eq!(
1894+
FormatSpec::parse("z").unwrap().format_string(&string_value),
1895+
Err(FormatSpecError::NegativeZeroCoercionNotAllowed("string"))
1896+
);
1897+
assert_eq!(
1898+
FormatSpec::parse("zd")
1899+
.unwrap()
1900+
.format_string(&string_value),
1901+
Err(FormatSpecError::UnknownFormatCode('d', "str"))
1902+
);
1903+
assert_eq!(
1904+
FormatSpec::parse("zs").unwrap().format_bool(false),
1905+
Err(FormatSpecError::UnknownFormatCode('s', "bool"))
1906+
);
1907+
1908+
let locale = LocaleInfo {
1909+
thousands_sep: ",".to_owned(),
1910+
decimal_point: ".".to_owned(),
1911+
grouping: vec![3, 0],
1912+
};
1913+
let locale_spec = FormatSpec::parse("zn").unwrap();
1914+
assert_eq!(
1915+
locale_spec.format_float_locale(-0.0, &locale),
1916+
Ok("0".to_owned())
1917+
);
1918+
assert_eq!(
1919+
locale_spec.format_complex_locale(&Complex64::new(-0.0, -0.0), &locale),
1920+
Ok("0+0j".to_owned())
1921+
);
1922+
assert_eq!(
1923+
FormatSpec::parse("z.1n")
1924+
.unwrap()
1925+
.format_int_locale(&BigInt::from(0), &locale),
1926+
Err(FormatSpecError::PrecisionNotAllowed)
1927+
);
1928+
}
1929+
17821930
#[test]
17831931
fn format_int() {
17841932
assert_eq!(

‎crates/vm/src/format.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ impl IntoPyException for FormatSpecError {
7777
Self::AlignmentFlag => {
7878
vm.new_value_error("'=' alignment flag is not allowed in complex format specifier")
7979
}
80+
Self::NegativeZeroCoercionNotAllowed(type_name) => {
81+
let msg = format!(
82+
"Negative zero coercion (z) not allowed in {type_name} format specifier"
83+
);
84+
vm.new_value_error(msg)
85+
}
8086
Self::StringAlignmentFlag => {
8187
vm.new_value_error("'=' alignment not allowed in string format specifier")
8288
}

0 commit comments

Comments
 (0)