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)" ,
@@ -553,107 +679,6 @@ def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
553679 if line_num == last_blank_line_num and line_num > 0 :
554680 self .add_message ("trailing-newlines" , line = line_num )
555681
556- @classmethod
557- def to_standard_or_engineering_base (cls , number : float ) -> tuple [str , str ]:
558- """Calculate scientific notation components (base, exponent) for a number.
559-
560- Returns a tuple (base, exponent) where:
561- - base is a number between 1 and 10 (or exact 0)
562- - exponent is the power of 10 needed to represent the original number
563- """
564- if number == 0 :
565- return "0" , "0"
566- if number == math .inf :
567- return "math.inf" , "0"
568- exponent = math .floor (math .log10 (abs (number )))
569- if exponent == 0 :
570- return str (number ), "0"
571- base_value = number / (10 ** exponent )
572- # 15 significant digits because if we add more precision then
573- # we get into rounding errors territory
574- base_str = f"{ base_value :.15g} " .rstrip ("0" ).rstrip ("." )
575- exp_str = str (exponent )
576- return base_str , exp_str
577-
578- @classmethod
579- def to_standard_scientific_notation (cls , number : float ) -> str :
580- base , exp = cls .to_standard_or_engineering_base (number )
581- if base == "math.inf" :
582- return "math.inf"
583- if exp != "0" :
584- return f"{ base } e{ int (exp )} "
585- if "." in base :
586- return base
587- return f"{ base } .0"
588-
589- @classmethod
590- def to_understandable_time (cls , number : float ) -> str :
591- if number % 3600 != 0 :
592- return "" # Not a suspected time
593- parts : list [int ] = [3600 ]
594- number //= 3600
595- for divisor in (
596- 24 ,
597- 7 ,
598- 365 ,
599- ):
600- if number % divisor == 0 :
601- parts .append (divisor )
602- number //= divisor
603- remainder = int (number )
604- if remainder != 1 :
605- parts .append (remainder )
606- return " * " .join ([str (p ) for p in parts ])
607-
608- @classmethod
609- def to_standard_engineering_notation (cls , number : float ) -> str :
610- base , exp = cls .to_standard_or_engineering_base (number )
611- if base == "math.inf" :
612- return "math.inf"
613- exp_value = int (exp )
614- remainder = exp_value % 3
615- # For negative exponents, the adjustment is different
616- if exp_value < 0 :
617- # For negative exponents, we need to round down to the next multiple of 3
618- # e.g., -5 should go to -6, so we get 3 - ((-5) % 3) = 3 - 1 = 2
619- adjustment = 3 - ((- exp_value ) % 3 )
620- if adjustment == 3 :
621- adjustment = 0
622- exp_value = exp_value - adjustment
623- base_value = float (base ) * (10 ** adjustment )
624- elif remainder != 0 :
625- # For positive exponents, keep the existing logic
626- exp_value = exp_value - remainder
627- base_value = float (base ) * (10 ** remainder )
628- else :
629- base_value = float (base )
630- base = str (base_value ).rstrip ("0" ).rstrip ("." )
631- if exp_value != 0 :
632- return f"{ base } e{ exp_value } "
633- if "." in base :
634- return base
635- return f"{ base } .0"
636-
637- @classmethod
638- def to_standard_underscore_grouping (cls , number : float ) -> str :
639- number_str = str (number )
640- if "e" in number_str or "E" in number_str :
641- # python itself want to display this as exponential there's no reason to
642- # not use exponential notation for very small number even for strict
643- # underscore grouping notation
644- return number_str
645- if "." in number_str :
646- int_part , dec_part = number_str .split ("." )
647- else :
648- int_part = number_str
649- dec_part = "0"
650- grouped_int_part = ""
651- for i , digit in enumerate (reversed (int_part )):
652- if i > 0 and i % 3 == 0 :
653- grouped_int_part = "_" + grouped_int_part
654- grouped_int_part = digit + grouped_int_part
655- return f"{ grouped_int_part } .{ dec_part } "
656-
657682 def _check_bad_float_notation ( # pylint: disable=too-many-locals
658683 self , line_num : int , start : tuple [int , int ], string : str
659684 ) -> None :
@@ -674,20 +699,12 @@ def _check_bad_float_notation( # pylint: disable=too-many-locals
674699 def raise_bad_float_notation (
675700 reason : str , time_suggestion : bool = False
676701 ) -> None :
677- suggested = set ()
678- if scientific :
679- suggested .add (self .to_standard_scientific_notation (value ))
680- if engineering :
681- suggested .add (self .to_standard_engineering_notation (value ))
682- if pep515 :
683- suggested .add (self .to_standard_underscore_grouping (value ))
684- if time_suggestion :
685- maybe_a_time = self .to_understandable_time (value )
686- if maybe_a_time :
687- suggested .add (maybe_a_time )
702+ suggestion = FloatFormatterHelper .standardize (
703+ value , scientific , engineering , pep515 , time_suggestion
704+ )
688705 return self .add_message (
689706 "bad-float-notation" ,
690- args = (string , reason , "' or '" . join ( sorted ( suggested )) ),
707+ args = (string , reason , suggestion ),
691708 line = line_num ,
692709 end_lineno = line_num ,
693710 col_offset = start [1 ],
@@ -729,10 +746,10 @@ def raise_bad_float_notation(
729746 # written complexly, then it could be badly written
730747 return None
731748 threshold = self .linter .config .float_notation_threshold
732- close_to_zero_threshold = self . to_standard_scientific_notation (
733- 1 / threshold
749+ close_to_zero_threshold = (
750+ FloatFormatterHelper . to_standard_scientific_notation ( 1 / threshold )
734751 )
735- threshold = self .to_standard_scientific_notation (threshold )
752+ threshold = FloatFormatterHelper .to_standard_scientific_notation (threshold )
736753 if under_threshold :
737754 return raise_bad_float_notation (
738755 f"is smaller than { close_to_zero_threshold } "
0 commit comments