@@ -380,15 +380,31 @@ impl FormatSpec {
380380 sep : char ,
381381 disp_digit_cnt : i32 ,
382382 ) -> String {
383- // Don't add separators to the floating decimal point of numbers
384- let mut parts = magnitude_str. splitn ( 2 , '.' ) ;
385- let magnitude_int_str = parts. next ( ) . unwrap ( ) . to_string ( ) ;
383+ // Group only the leading integer digits; the trailing remainder must
384+ // never receive separators. For decimal and float output (interval 3)
385+ // that remainder is the decimal point and fraction, an exponent
386+ // (`e+NN`), or a trailing percent sign. Hex/octal/binary output
387+ // (interval 4) has no such tail and its `a`-`f`/`e`/`E` are digits, so
388+ // the whole magnitude is groupable.
389+ let int_len = if inter == 4 {
390+ magnitude_str. len ( )
391+ } else {
392+ magnitude_str
393+ . bytes ( )
394+ . position ( |b| !b. is_ascii_digit ( ) )
395+ . unwrap_or ( magnitude_str. len ( ) )
396+ } ;
397+ // No leading integer digits (e.g. "inf"/"nan") means nothing to group;
398+ // leave any width padding to the fill/align step.
399+ if int_len == 0 {
400+ return magnitude_str;
401+ }
402+ let magnitude_int_str = magnitude_str[ ..int_len] . to_string ( ) ;
403+ let remainder = & magnitude_str[ int_len..] ;
386404 let dec_digit_cnt = magnitude_str. len ( ) as i32 - magnitude_int_str. len ( ) as i32 ;
387405 let int_digit_cnt = disp_digit_cnt - dec_digit_cnt;
388406 let mut result = Self :: separate_integer ( magnitude_int_str, inter, sep, int_digit_cnt) ;
389- if let Some ( part) = parts. next ( ) {
390- result. push_str ( & format ! ( ".{part}" ) )
391- }
407+ result. push_str ( remainder) ;
392408 result
393409 }
394410
@@ -786,14 +802,40 @@ impl FormatSpec {
786802 magnitude if magnitude. is_nan ( ) => Ok ( "nan" . to_owned ( ) ) ,
787803 magnitude if magnitude. is_infinite ( ) => Ok ( "inf" . to_owned ( ) ) ,
788804 _ => match self . precision {
789- Some ( precision) => Ok ( float:: format_general (
790- precision,
791- magnitude,
792- Case :: Lower ,
793- self . alternate_form ,
794- true ,
795- ) ) ,
796- None => Ok ( float:: to_string ( magnitude) ) ,
805+ Some ( precision) => {
806+ // Empty presentation type with a precision behaves like
807+ // `g` but repr-like: precision is clamped to at least 1,
808+ // and an integer-looking result keeps a trailing `.0`
809+ // (Py_DTSF_ADD_DOT_0).
810+ let precision = if precision == 0 { 1 } else { precision } ;
811+ let s = float:: format_general (
812+ precision,
813+ magnitude,
814+ Case :: Lower ,
815+ self . alternate_form ,
816+ true ,
817+ ) ;
818+ Ok ( if s. bytes ( ) . any ( |b| matches ! ( b, b'.' | b'e' | b'E' ) ) {
819+ s
820+ } else {
821+ format ! ( "{s}.0" )
822+ } )
823+ }
824+ None => {
825+ let s = float:: to_string ( magnitude) ;
826+ // Alternate form forces a decimal point into the
827+ // repr-like output. Only exponent-form values lack one
828+ // (`1e+16` -> `1.e+16`); fixed-form repr already carries
829+ // a `.`.
830+ Ok ( if self . alternate_form && !s. contains ( '.' ) {
831+ match s. find ( [ 'e' , 'E' ] ) {
832+ Some ( pos) => format ! ( "{}.{}" , & s[ ..pos] , & s[ pos..] ) ,
833+ None => format ! ( "{s}." ) ,
834+ }
835+ } else {
836+ s
837+ } )
838+ }
797839 } ,
798840 } ,
799841 } ;
@@ -857,10 +899,11 @@ impl FormatSpec {
857899 Err ( FormatSpecError :: UnknownFormatCode ( 'N' , "int" ) )
858900 }
859901 Some ( FormatType :: String ) => Err ( FormatSpecError :: UnknownFormatCode ( 's' , "int" ) ) ,
860- Some ( FormatType :: Character ) => match ( self . sign , self . alternate_form ) {
861- ( Some ( _) , _) => Err ( FormatSpecError :: NotAllowed ( "Sign" ) ) ,
862- ( _, true ) => Err ( FormatSpecError :: NotAllowed ( "Alternate form (#)" ) ) ,
863- ( _, _) => match num. to_u32 ( ) {
902+ Some ( FormatType :: Character ) => match ( self . precision , self . sign , self . alternate_form ) {
903+ ( Some ( _) , _, _) => Err ( FormatSpecError :: PrecisionNotAllowed ) ,
904+ ( _, Some ( _) , _) => Err ( FormatSpecError :: NotAllowed ( "Sign" ) ) ,
905+ ( _, _, true ) => Err ( FormatSpecError :: NotAllowed ( "Alternate form (#)" ) ) ,
906+ ( _, _, _) => match num. to_u32 ( ) {
864907 Some ( n) if n <= 0x10ffff => Ok ( core:: char:: from_u32 ( n) . unwrap ( ) . to_string ( ) ) ,
865908 Some ( _) | None => Err ( FormatSpecError :: CodeNotInRange ) ,
866909 } ,
@@ -1664,6 +1707,96 @@ mod tests {
16641707 assert_eq ! ( result, "000001,234" ) ;
16651708 }
16661709
1710+ fn fmt_float ( spec : & str , value : f64 ) -> String {
1711+ FormatSpec :: parse ( spec) . unwrap ( ) . format_float ( value) . unwrap ( )
1712+ }
1713+
1714+ #[ test]
1715+ fn format_float_grouping_never_touches_exponent ( ) {
1716+ // Grouping must group only the mantissa's integer digits, never the
1717+ // exponent digits (was "1e,+20") or a trailing percent sign.
1718+ assert_eq ! ( fmt_float( ",g" , 1e20 ) , "1e+20" ) ;
1719+ assert_eq ! ( fmt_float( "_g" , 1e-10 ) , "1e-10" ) ;
1720+ assert_eq ! ( fmt_float( ",e" , 1e20 ) , "1.000000e+20" ) ;
1721+ assert_eq ! ( fmt_float( "," , 1e16 ) , "1e+16" ) ;
1722+ assert_eq ! ( fmt_float( ",.0%" , 1.0 ) , "100%" ) ;
1723+ assert_eq ! ( fmt_float( ",.2%" , 12345.0 ) , "1,234,500.00%" ) ;
1724+ // Fixed-form grouping still groups the integer part.
1725+ assert_eq ! ( fmt_float( "," , 1234567.0 ) , "1,234,567.0" ) ;
1726+ }
1727+
1728+ #[ test]
1729+ fn format_float_grouping_inf_nan ( ) {
1730+ // No integer digits to group; width padding is left to fill/align, so
1731+ // separators never land inside "inf"/"nan".
1732+ assert_eq ! ( fmt_float( "," , f64 :: INFINITY ) , "inf" ) ;
1733+ assert_eq ! ( fmt_float( "06," , f64 :: INFINITY ) , "000inf" ) ;
1734+ assert_eq ! ( fmt_float( "06," , f64 :: NAN ) , "000nan" ) ;
1735+ assert_eq ! ( fmt_float( "06,%" , f64 :: INFINITY ) , "00inf%" ) ;
1736+ }
1737+
1738+ #[ test]
1739+ fn format_float_empty_type_with_precision ( ) {
1740+ // Empty presentation type with a precision is repr-like: precision is
1741+ // clamped to at least 1 and integer-looking output keeps a `.0`.
1742+ assert_eq ! ( fmt_float( ".2" , 1.0 ) , "1.0" ) ;
1743+ assert_eq ! ( fmt_float( ".6" , 100.0 ) , "100.0" ) ;
1744+ assert_eq ! ( fmt_float( ".17" , 1234567.0 ) , "1234567.0" ) ;
1745+ assert_eq ! ( fmt_float( ".0" , 0.5 ) , "0.5" ) ;
1746+ assert_eq ! ( fmt_float( ".0" , 0.0001 ) , "0.0001" ) ;
1747+ assert_eq ! ( fmt_float( ".2" , 0.0 ) , "0.0" ) ;
1748+ assert_eq ! ( fmt_float( ".0" , 0.0 ) , "0e+00" ) ;
1749+ assert_eq ! ( fmt_float( ".2" , 100.0 ) , "1e+02" ) ;
1750+ }
1751+
1752+ #[ test]
1753+ fn format_float_alternate_form_forces_point ( ) {
1754+ // Alternate form injects a decimal point into exponent-form repr.
1755+ assert_eq ! ( fmt_float( "#" , 1e16 ) , "1.e+16" ) ;
1756+ assert_eq ! ( fmt_float( "#" , 1e-5 ) , "1.e-05" ) ;
1757+ // Fixed-form repr already has a point, so it is unchanged.
1758+ assert_eq ! ( fmt_float( "#" , 100.0 ) , "100.0" ) ;
1759+ assert_eq ! ( fmt_float( "#" , 1.5 ) , "1.5" ) ;
1760+ }
1761+
1762+ #[ test]
1763+ fn format_int_hex_grouping_preserved ( ) {
1764+ // Underscore grouping of hex/octal groups every 4 digits, including the
1765+ // `a`-`f` letters.
1766+ assert_eq ! (
1767+ FormatSpec :: parse( "_x" )
1768+ . unwrap( )
1769+ . format_int( & BigInt :: from( 1000000 ) )
1770+ . unwrap( ) ,
1771+ "f_4240"
1772+ ) ;
1773+ assert_eq ! (
1774+ FormatSpec :: parse( "_X" )
1775+ . unwrap( )
1776+ . format_int( & BigInt :: from( 0xABCDEFu32 ) )
1777+ . unwrap( ) ,
1778+ "AB_CDEF"
1779+ ) ;
1780+ }
1781+
1782+ #[ test]
1783+ fn format_int_character_rejects_precision ( ) {
1784+ // 'c' rejects precision, and precision is checked before sign/alt form.
1785+ assert_eq ! (
1786+ FormatSpec :: parse( ".2c" ) . unwrap( ) . format_int( & BigInt :: from( 65 ) ) ,
1787+ Err ( FormatSpecError :: PrecisionNotAllowed )
1788+ ) ;
1789+ assert_eq ! (
1790+ FormatSpec :: parse( "+.2c" ) . unwrap( ) . format_int( & BigInt :: from( 65 ) ) ,
1791+ Err ( FormatSpecError :: PrecisionNotAllowed )
1792+ ) ;
1793+ // Without precision, 'c' still renders the code point.
1794+ assert_eq ! (
1795+ FormatSpec :: parse( "c" ) . unwrap( ) . format_int( & BigInt :: from( 65 ) ) ,
1796+ Ok ( "A" . to_owned( ) )
1797+ ) ;
1798+ }
1799+
16671800 #[ test]
16681801 fn format_parse ( ) {
16691802 let expected = Ok ( FormatString {
0 commit comments