1616import math
1717import re
1818import tokenize
19+ from decimal import Decimal , getcontext
1920from functools import reduce
2021from re import Match
2122from typing import TYPE_CHECKING , Literal
3233if TYPE_CHECKING :
3334 from pylint .lint import PyLinter
3435
36+ getcontext ().prec = 50
37+
3538
3639_KEYWORD_TOKENS = {
3740 "assert" ,
@@ -59,16 +62,20 @@ class FloatFormatterHelper:
5962 def standardize (
6063 cls ,
6164 number : float ,
65+ original_string : str , # ← ADD THIS PARAMETER
6266 scientific : bool = True ,
6367 engineering : bool = True ,
6468 pep515 : bool = True ,
6569 time_suggestion : bool = False ,
6670 ) -> str :
71+ dec_number = Decimal (original_string )
72+ sig_figs = len (dec_number .as_tuple ().digits )
73+
6774 suggested = set ()
6875 if scientific :
69- suggested .add (cls .to_standard_scientific_notation (number ))
76+ suggested .add (cls .to_standard_scientific_notation (dec_number , sig_figs ))
7077 if engineering :
71- suggested .add (cls .to_standard_engineering_notation (number ))
78+ suggested .add (cls .to_standard_engineering_notation (dec_number , sig_figs ))
7279 if pep515 :
7380 suggested .add (cls .to_standard_underscore_grouping (number ))
7481 if time_suggestion :
@@ -100,15 +107,33 @@ def to_standard_or_engineering_base(cls, number: float) -> tuple[str, str]:
100107 return base_str , exp_str
101108
102109 @classmethod
103- def to_standard_scientific_notation (cls , number : float ) -> str :
104- base , exp = cls .to_standard_or_engineering_base (number )
105- if base == "math.inf" :
110+ def to_standard_scientific_notation (
111+ cls , dec_number : Decimal , sig_figs : int # ← CHANGE TYPE # ← ADD PARAMETER
112+ ) -> str :
113+ if dec_number == 0 :
114+ return "0.0"
115+ if dec_number == Decimal ("Infinity" ): # ← CHANGE
106116 return "math.inf"
107- if exp != "0" :
108- return f"{ base } e{ int (exp )} "
109- if "." in base :
110- return base
111- return f"{ base } .0"
117+
118+ # ← USE Decimal.adjusted() instead of log10
119+ exponent = dec_number .adjusted ()
120+
121+ if exponent == 0 :
122+ base_str = f"{ float (dec_number ):.{sig_figs }g} "
123+ if "." not in base_str :
124+ base_str += ".0"
125+ return base_str
126+
127+ # ← EXACT DIVISION with Decimal
128+ base_value = dec_number / (Decimal (10 ) ** exponent )
129+
130+ # ← FORMAT with exact sig figs
131+ base_str = f"{ float (base_value ):.{sig_figs }g} "
132+
133+ if "." not in base_str and "e" not in base_str .lower ():
134+ base_str += ".0"
135+
136+ return f"{ base_str } e{ exponent } "
112137
113138 @classmethod
114139 def to_understandable_time (cls , number : float ) -> str :
@@ -164,33 +189,44 @@ def to_understandable_time(cls, number: float) -> str:
164189 return result
165190
166191 @classmethod
167- def to_standard_engineering_notation (cls , number : float ) -> str :
168- base , exp = cls .to_standard_or_engineering_base (number )
169- if base == "math.inf" :
192+ def to_standard_engineering_notation (
193+ cls , dec_number : Decimal , sig_figs : int # ← CHANGE TYPE # ← ADD PARAMETER
194+ ) -> str :
195+ if dec_number == 0 :
196+ return "0.0"
197+ if dec_number == Decimal ("Infinity" ): # ← CHANGE
170198 return "math.inf"
171- exp_value = int (exp )
172- remainder = exp_value % 3
173- # For negative exponents, the adjustment is different
174- if exp_value < 0 :
175- # For negative exponents, we need to round down to the next multiple of 3
176- # e.g., -5 should go to -6, so we get 3 - ((-5) % 3) = 3 - 1 = 2
177- adjustment = 3 - ((- exp_value ) % 3 )
199+
200+ # ← USE Decimal.adjusted()
201+ exponent = dec_number .adjusted ()
202+
203+ remainder = exponent % 3
204+
205+ if exponent < 0 :
206+ adjustment = 3 - ((- exponent ) % 3 )
178207 if adjustment == 3 :
179208 adjustment = 0
180- exp_value = exp_value - adjustment
181- base_value = float (base ) * (10 ** adjustment )
209+ exp_value = exponent - adjustment
210+ # ← EXACT DIVISION with Decimal
211+ base_value = dec_number / (Decimal (10 ) ** exp_value )
182212 elif remainder != 0 :
183- # For positive exponents, keep the existing logic
184- exp_value = exp_value - remainder
185- base_value = float ( base ) * ( 10 ** remainder )
213+ exp_value = exponent - remainder
214+ # ← EXACT DIVISION with Decimal
215+ base_value = dec_number / ( Decimal ( 10 ) ** exp_value )
186216 else :
187- base_value = float (base )
188- base = str (base_value ).rstrip ("0" ).rstrip ("." )
217+ exp_value = exponent
218+ # ← EXACT DIVISION with Decimal
219+ base_value = dec_number / (Decimal (10 ) ** exponent )
220+
221+ # ← FORMAT with exact sig figs (no .rstrip needed!)
222+ base_str = f"{ float (base_value ):.{sig_figs }g} "
223+
224+ if "." not in base_str and "e" not in base_str .lower ():
225+ base_str += ".0"
226+
189227 if exp_value != 0 :
190- return f"{ base } e{ exp_value } "
191- if "." in base :
192- return base
193- return f"{ base } .0"
228+ return f"{ base_str } e{ exp_value } "
229+ return base_str
194230
195231 @classmethod
196232 def to_standard_underscore_grouping (cls , number : float ) -> str :
@@ -725,13 +761,14 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
725761 def _check_bad_float_notation ( # pylint: disable=too-many-locals,too-many-return-statements
726762 self , line_num : int , start : tuple [int , int ], string : str
727763 ) -> None :
764+
728765 has_dot = "." in string
729766 has_exponent = "e" in string or "E" in string
730767 if not (has_dot or has_exponent ):
731768 # it's an int, need special treatment later on
732769 return None
733770
734- value = float (string )
771+ suggestion = FloatFormatterHelper . standardize (string )
735772 engineering = (
736773 self .all_float_notation_allowed
737774 or self .linter .config .strict_engineering_notation
@@ -749,17 +786,14 @@ def raise_bad_float_notation(
749786 reason : str , time_suggestion : bool = False
750787 ) -> None :
751788 suggestion = FloatFormatterHelper .standardize (
752- value , scientific , engineering , pep515 , time_suggestion
753- )
754- return self .add_message (
755- "bad-float-notation" ,
756- args = (string , reason , suggestion ),
757- line = line_num ,
758- end_lineno = line_num ,
759- col_offset = start [1 ],
760- end_col_offset = start [1 ] + len (string ),
761- confidence = HIGH ,
789+ value ,
790+ string , # ← ADD THIS: Pass original string
791+ scientific ,
792+ engineering ,
793+ pep515 ,
794+ time_suggestion ,
762795 )
796+ return self .add_message (...)
763797
764798 if string in {"0" , "0.0" , "0." }:
765799 # 0 is a special case because it is used very often, and float approximation
0 commit comments