Skip to content

Commit abc71cc

Browse files
[refactor] Create a float formatter helper class
1 parent 42fc191 commit abc71cc

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)",
@@ -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}"

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)