Skip to content

Commit 399f6bd

Browse files
[refactor] Create a float formatter helper class
1 parent 2cbc302 commit 399f6bd

File tree

2 files changed

+141
-120
lines changed

2 files changed

+141
-120
lines changed

pylint/checkers/format.py

Lines changed: 133 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,132 @@
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+
56182
MSGS: 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}"

tests/checkers/unittest_format.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from pylint import lint, reporters
1515
from pylint.checkers.base.basic_checker import BasicChecker
16-
from pylint.checkers.format import FormatChecker
16+
from pylint.checkers.format import FloatFormatterHelper, FormatChecker
1717
from pylint.testutils import CheckerTestCase, MessageTest, _tokenize_str
1818

1919

@@ -229,15 +229,19 @@ def test_to_another_standard_notation(
229229
) -> None:
230230
"""Test the conversion of numbers to all possible notations."""
231231
float_value = float(value)
232-
scientific = FormatChecker.to_standard_scientific_notation(float_value)
232+
scientific = FloatFormatterHelper.to_standard_scientific_notation(float_value)
233233
assert (
234234
scientific == expected_scientific
235235
), f"Scientific notation mismatch expected {expected_scientific}, got {scientific}"
236-
engineering = FormatChecker.to_standard_engineering_notation(float_value)
236+
engineering = FloatFormatterHelper.to_standard_engineering_notation(float_value)
237237
assert (
238238
engineering == expected_engineering
239239
), f"Engineering notation mismatch expected {expected_engineering}, got {engineering}"
240-
underscore = FormatChecker.to_standard_underscore_grouping(float_value)
240+
underscore = FloatFormatterHelper.to_standard_underscore_grouping(float_value)
241241
assert (
242242
underscore == expected_underscore
243243
), f"Underscore grouping mismatch expected {expected_underscore}, got {underscore}"
244+
time = FloatFormatterHelper.to_understandable_time(float_value)
245+
assert (
246+
time == ""
247+
), f"Time notation mismatch expected {expected_underscore}, got {time}"

0 commit comments

Comments
 (0)