5353_JUNK_TOKENS = {tokenize .COMMENT , tokenize .NL }
5454
5555
56+ class FloatFormatterHelper :
57+
58+ @classmethod
59+ def standardize (
60+ cls ,
61+ number : float ,
62+ scientific : bool = True ,
63+ engineering : bool = True ,
64+ pep515 : bool = True ,
65+ time_suggestion : bool = False ,
66+ ) -> str :
67+ suggested = set ()
68+ if scientific :
69+ suggested .add (cls .to_standard_scientific_notation (number ))
70+ if engineering :
71+ suggested .add (cls .to_standard_engineering_notation (number ))
72+ if pep515 :
73+ suggested .add (cls .to_standard_underscore_grouping (number ))
74+ if time_suggestion :
75+ maybe_a_time = cls .to_understandable_time (number )
76+ if maybe_a_time :
77+ suggested .add (maybe_a_time )
78+ return "' or '" .join (sorted (suggested ))
79+
80+ @classmethod
81+ def to_standard_or_engineering_base (cls , number : float ) -> tuple [str , str ]:
82+ """Calculate scientific notation components (base, exponent) for a number.
83+
84+ Returns a tuple (base, exponent) where:
85+ - base is a number between 1 and 10 (or exact 0)
86+ - exponent is the power of 10 needed to represent the original number
87+ """
88+ if number == 0 :
89+ return "0" , "0"
90+ if number == math .inf :
91+ return "math.inf" , "0"
92+ exponent = math .floor (math .log10 (abs (number )))
93+ if exponent == 0 :
94+ return str (number ), "0"
95+ base_value = number / (10 ** exponent )
96+ # 15 significant digits because if we add more precision then
97+ # we get into rounding errors territory
98+ base_str = f"{ base_value :.15g} " .rstrip ("0" ).rstrip ("." )
99+ exp_str = str (exponent )
100+ return base_str , exp_str
101+
102+ @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" :
106+ 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"
112+
113+ @classmethod
114+ def to_understandable_time (cls , number : float ) -> str :
115+ if number == 0.0 or number % 3600 != 0 :
116+ return "" # Not a suspected time
117+ parts : list [int ] = [3600 ]
118+ number //= 3600
119+ for divisor in (
120+ 24 ,
121+ 7 ,
122+ 365 ,
123+ ):
124+ if number % divisor == 0 :
125+ parts .append (divisor )
126+ number //= divisor
127+ remainder = int (number )
128+ if remainder != 1 :
129+ parts .append (remainder )
130+ return " * " .join ([str (p ) for p in parts ])
131+
132+ @classmethod
133+ def to_standard_engineering_notation (cls , number : float ) -> str :
134+ base , exp = cls .to_standard_or_engineering_base (number )
135+ if base == "math.inf" :
136+ return "math.inf"
137+ exp_value = int (exp )
138+ remainder = exp_value % 3
139+ # For negative exponents, the adjustment is different
140+ if exp_value < 0 :
141+ # For negative exponents, we need to round down to the next multiple of 3
142+ # e.g., -5 should go to -6, so we get 3 - ((-5) % 3) = 3 - 1 = 2
143+ adjustment = 3 - ((- exp_value ) % 3 )
144+ if adjustment == 3 :
145+ adjustment = 0
146+ exp_value = exp_value - adjustment
147+ base_value = float (base ) * (10 ** adjustment )
148+ elif remainder != 0 :
149+ # For positive exponents, keep the existing logic
150+ exp_value = exp_value - remainder
151+ base_value = float (base ) * (10 ** remainder )
152+ else :
153+ base_value = float (base )
154+ base = str (base_value ).rstrip ("0" ).rstrip ("." )
155+ if exp_value != 0 :
156+ return f"{ base } e{ exp_value } "
157+ if "." in base :
158+ return base
159+ return f"{ base } .0"
160+
161+ @classmethod
162+ def to_standard_underscore_grouping (cls , number : float ) -> str :
163+ number_str = str (number )
164+ if "e" in number_str or "E" in number_str :
165+ # python itself want to display this as exponential there's no reason to
166+ # not use exponential notation for very small number even for strict
167+ # underscore grouping notation
168+ return number_str
169+ if "." in number_str :
170+ int_part , dec_part = number_str .split ("." )
171+ else :
172+ int_part = number_str
173+ dec_part = "0"
174+ grouped_int_part = ""
175+ for i , digit in enumerate (reversed (int_part )):
176+ if i > 0 and i % 3 == 0 :
177+ grouped_int_part = "_" + grouped_int_part
178+ grouped_int_part = digit + grouped_int_part
179+ return f"{ grouped_int_part } .{ dec_part } "
180+
181+
56182MSGS : dict [str , MessageDefinitionTuple ] = {
57183 "C0301" : (
58184 "Line too long (%s/%s)" ,
@@ -562,107 +688,6 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
562688 if line_num == last_blank_line_num and line_num > 0 :
563689 self .add_message ("trailing-newlines" , line = line_num )
564690
565- @classmethod
566- def to_standard_or_engineering_base (cls , number : float ) -> tuple [str , str ]:
567- """Calculate scientific notation components (base, exponent) for a number.
568-
569- Returns a tuple (base, exponent) where:
570- - base is a number between 1 and 10 (or exact 0)
571- - exponent is the power of 10 needed to represent the original number
572- """
573- if number == 0 :
574- return "0" , "0"
575- if number == math .inf :
576- return "math.inf" , "0"
577- exponent = math .floor (math .log10 (abs (number )))
578- if exponent == 0 :
579- return str (number ), "0"
580- base_value = number / (10 ** exponent )
581- # 15 significant digits because if we add more precision then
582- # we get into rounding errors territory
583- base_str = f"{ base_value :.15g} " .rstrip ("0" ).rstrip ("." )
584- exp_str = str (exponent )
585- return base_str , exp_str
586-
587- @classmethod
588- def to_standard_scientific_notation (cls , number : float ) -> str :
589- base , exp = cls .to_standard_or_engineering_base (number )
590- if base == "math.inf" :
591- return "math.inf"
592- if exp != "0" :
593- return f"{ base } e{ int (exp )} "
594- if "." in base :
595- return base
596- return f"{ base } .0"
597-
598- @classmethod
599- def to_understandable_time (cls , number : float ) -> str :
600- if number % 3600 != 0 :
601- return "" # Not a suspected time
602- parts : list [int ] = [3600 ]
603- number //= 3600
604- for divisor in (
605- 24 ,
606- 7 ,
607- 365 ,
608- ):
609- if number % divisor == 0 :
610- parts .append (divisor )
611- number //= divisor
612- remainder = int (number )
613- if remainder != 1 :
614- parts .append (remainder )
615- return " * " .join ([str (p ) for p in parts ])
616-
617- @classmethod
618- def to_standard_engineering_notation (cls , number : float ) -> str :
619- base , exp = cls .to_standard_or_engineering_base (number )
620- if base == "math.inf" :
621- return "math.inf"
622- exp_value = int (exp )
623- remainder = exp_value % 3
624- # For negative exponents, the adjustment is different
625- if exp_value < 0 :
626- # For negative exponents, we need to round down to the next multiple of 3
627- # e.g., -5 should go to -6, so we get 3 - ((-5) % 3) = 3 - 1 = 2
628- adjustment = 3 - ((- exp_value ) % 3 )
629- if adjustment == 3 :
630- adjustment = 0
631- exp_value = exp_value - adjustment
632- base_value = float (base ) * (10 ** adjustment )
633- elif remainder != 0 :
634- # For positive exponents, keep the existing logic
635- exp_value = exp_value - remainder
636- base_value = float (base ) * (10 ** remainder )
637- else :
638- base_value = float (base )
639- base = str (base_value ).rstrip ("0" ).rstrip ("." )
640- if exp_value != 0 :
641- return f"{ base } e{ exp_value } "
642- if "." in base :
643- return base
644- return f"{ base } .0"
645-
646- @classmethod
647- def to_standard_underscore_grouping (cls , number : float ) -> str :
648- number_str = str (number )
649- if "e" in number_str or "E" in number_str :
650- # python itself want to display this as exponential there's no reason to
651- # not use exponential notation for very small number even for strict
652- # underscore grouping notation
653- return number_str
654- if "." in number_str :
655- int_part , dec_part = number_str .split ("." )
656- else :
657- int_part = number_str
658- dec_part = "0"
659- grouped_int_part = ""
660- for i , digit in enumerate (reversed (int_part )):
661- if i > 0 and i % 3 == 0 :
662- grouped_int_part = "_" + grouped_int_part
663- grouped_int_part = digit + grouped_int_part
664- return f"{ grouped_int_part } .{ dec_part } "
665-
666691 def _check_bad_float_notation ( # pylint: disable=too-many-locals
667692 self , line_num : int , start : tuple [int , int ], string : str
668693 ) -> None :
@@ -683,20 +708,12 @@ def _check_bad_float_notation( # pylint: disable=too-many-locals
683708 def raise_bad_float_notation (
684709 reason : str , time_suggestion : bool = False
685710 ) -> None :
686- suggested = set ()
687- if scientific :
688- suggested .add (self .to_standard_scientific_notation (value ))
689- if engineering :
690- suggested .add (self .to_standard_engineering_notation (value ))
691- if pep515 :
692- suggested .add (self .to_standard_underscore_grouping (value ))
693- if time_suggestion :
694- maybe_a_time = self .to_understandable_time (value )
695- if maybe_a_time :
696- suggested .add (maybe_a_time )
711+ suggestion = FloatFormatterHelper .standardize (
712+ value , scientific , engineering , pep515 , time_suggestion
713+ )
697714 return self .add_message (
698715 "bad-float-notation" ,
699- args = (string , reason , "' or '" . join ( sorted ( suggested )) ),
716+ args = (string , reason , suggestion ),
700717 line = line_num ,
701718 end_lineno = line_num ,
702719 col_offset = start [1 ],
@@ -738,10 +755,10 @@ def raise_bad_float_notation(
738755 # written complexly, then it could be badly written
739756 return None
740757 threshold = self .linter .config .float_notation_threshold
741- close_to_zero_threshold = self . to_standard_scientific_notation (
742- 1 / threshold
758+ close_to_zero_threshold = (
759+ FloatFormatterHelper . to_standard_scientific_notation ( 1 / threshold )
743760 )
744- threshold = self .to_standard_scientific_notation (threshold )
761+ threshold = FloatFormatterHelper .to_standard_scientific_notation (threshold )
745762 if under_threshold :
746763 return raise_bad_float_notation (
747764 f"is smaller than { close_to_zero_threshold } "
0 commit comments