Skip to content

Commit 6bfbc6c

Browse files
committed
Tidying
1 parent f8dfcb9 commit 6bfbc6c

File tree

2 files changed

+127
-82
lines changed

2 files changed

+127
-82
lines changed

Lib/fractions.py

Lines changed: 107 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,76 @@ def _hash_algorithm(numerator, denominator):
6868
\s*\Z # and optional whitespace to finish
6969
""", re.VERBOSE | re.IGNORECASE)
7070

71-
# Pattern for matching format specification; only supports 'e', 'E', 'f', 'F'
72-
# and '%' presentation types.
71+
72+
# Helpers for formatting
73+
74+
def _round_to_exponent(n, d, exponent, no_neg_zero=False):
75+
"""Round a rational number to an integer multiple of a power of 10.
76+
77+
Rounds the rational number n/d to the nearest integer multiple of
78+
10**exponent using the round-ties-to-even rule, and returns a
79+
pair (sign, significand) representing the rounded value
80+
(-1)**sign * significand.
81+
82+
d must be positive, but n and d need not be relatively prime.
83+
84+
If no_neg_zero is true, then the returned sign will always be False
85+
for a zero result. Otherwise, the sign is based on the sign of the input.
86+
"""
87+
if exponent >= 0:
88+
d *= 10**exponent
89+
else:
90+
n *= 10**-exponent
91+
92+
# The divmod quotient rounds ties towards positive infinity; we then adjust
93+
# as needed for round-ties-to-even behaviour.
94+
q, r = divmod(n + (d >> 1), d)
95+
if r == 0 and d & 1 == 0: # Tie
96+
q &= -2
97+
98+
sign = q < 0 if no_neg_zero else n < 0
99+
return sign, abs(q)
100+
101+
102+
def _round_to_figures(n, d, figures):
103+
"""Round a rational number to a given number of significant figures.
104+
105+
Rounds the rational number n/d to the given number of significant figures
106+
using the round-ties-to-even rule, and returns a triple (sign, significand,
107+
exponent) representing the rounded value (-1)**sign * significand *
108+
10**exponent.
109+
110+
d must be positive, but n and d need not be relatively prime.
111+
figures must be positive.
112+
113+
In the special case where n = 0, returns an exponent of 1 - figures, for
114+
compatibility with formatting; the significand will be zero. Otherwise,
115+
the significand satisfies 10**(figures - 1) <= significand < 10**figures.
116+
"""
117+
# Find integer m satisfying 10**(m - 1) <= abs(self) <= 10**m if self
118+
# is nonzero, with m = 1 if self = 0. (The latter choice is a little
119+
# arbitrary, but gives the "right" results when formatting zero.)
120+
if n == 0:
121+
m = 1
122+
else:
123+
str_n, str_d = str(abs(n)), str(d)
124+
m = len(str_n) - len(str_d) + (str_d <= str_n)
125+
126+
# Round to a multiple of 10**(m - figures). The result will satisfy either
127+
# significand == 0 or 10**(figures - 1) <= significand <= 10**figures.
128+
exponent = m - figures
129+
sign, significand = _round_to_exponent(n, d, exponent)
130+
131+
# Adjust in the case where significand == 10**figures.
132+
if len(str(significand)) == figures + 1:
133+
significand //= 10
134+
exponent += 1
135+
136+
return sign, significand, exponent
137+
138+
139+
# Pattern for matching format specification; supports 'e', 'E', 'f', 'F',
140+
# 'g', 'G' and '%' presentation types.
73141
_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
74142
(?:
75143
(?P<fill>.)?
@@ -78,8 +146,8 @@ def _hash_algorithm(numerator, denominator):
78146
(?P<sign>[-+ ]?)
79147
(?P<no_neg_zero>z)?
80148
(?P<alt>\#)?
81-
(?P<zeropad>0(?=\d))?
82-
(?P<minimumwidth>\d+)?
149+
(?P<zeropad>0(?=\d))? # use lookahead so that an isolated '0' is treated
150+
(?P<minimumwidth>\d+)? # as minimum width rather than the zeropad flag
83151
(?P<thousands_sep>[,_])?
84152
(?:\.(?P<precision>\d+))?
85153
(?P<presentation_type>[efg%])
@@ -327,35 +395,6 @@ def __str__(self):
327395
else:
328396
return '%s/%s' % (self._numerator, self._denominator)
329397

330-
def _round_to_sig_figs(self, figures):
331-
"""Round a positive fraction to a given number of significant figures.
332-
333-
Returns a pair (significand, exponent) of integers such that
334-
significand * 10**exponent gives a rounded approximation to self, and
335-
significand lies in the range 10**(figures - 1) <= significand <
336-
10**figures.
337-
"""
338-
if not (self > 0 and figures > 0):
339-
raise ValueError("Expected self and figures to be positive")
340-
341-
# Find integer m satisfying 10**(m - 1) <= self <= 10**m.
342-
str_n, str_d = str(self.numerator), str(self.denominator)
343-
m = len(str_n) - len(str_d) + (str_d <= str_n)
344-
345-
# Find best approximation significand * 10**exponent to self, with
346-
# 10**(figures - 1) <= significand <= 10**figures.
347-
exponent = m - figures
348-
significand = round(
349-
self / 10**exponent if exponent >= 0 else self * 10**-exponent
350-
)
351-
352-
# Adjust in the case where significand == 10**figures.
353-
if len(str(significand)) == figures + 1:
354-
significand //= 10
355-
exponent += 1
356-
357-
return significand, exponent
358-
359398
def __format__(self, format_spec, /):
360399
"""Format this fraction according to the given format specification."""
361400

@@ -377,66 +416,61 @@ def __format__(self, format_spec, /):
377416
f"for object of type {type(self).__name__!r}; "
378417
"can't use explicit alignment when zero-padding"
379418
)
380-
381419
fill = match["fill"] or " "
382420
align = match["align"] or ">"
383421
pos_sign = "" if match["sign"] == "-" else match["sign"]
384-
neg_zero_ok = not match["no_neg_zero"]
422+
no_neg_zero = bool(match["no_neg_zero"])
385423
alternate_form = bool(match["alt"])
386424
zeropad = bool(match["zeropad"])
387425
minimumwidth = int(match["minimumwidth"] or "0")
388426
thousands_sep = match["thousands_sep"]
389427
precision = int(match["precision"] or "6")
390428
presentation_type = match["presentation_type"]
391429
trim_zeros = presentation_type in "gG" and not alternate_form
392-
trim_dot = not alternate_form
430+
trim_point = not alternate_form
393431
exponent_indicator = "E" if presentation_type in "EFG" else "e"
394432

395-
# Record sign, then work with absolute value.
396-
negative = self < 0
397-
self = abs(self)
398-
399-
# Round to get the digits we need; also compute the suffix.
400-
if presentation_type == "f" or presentation_type == "F":
401-
significand = round(self * 10**precision)
402-
point_pos = precision
403-
suffix = ""
404-
elif presentation_type == "%":
405-
significand = round(self * 10**(precision + 2))
433+
# Round to get the digits we need, figure out where to place the point,
434+
# and decide whether to use scientific notation.
435+
n, d = self._numerator, self._denominator
436+
if presentation_type in "fF%":
437+
exponent = -precision - (2 if presentation_type == "%" else 0)
438+
negative, significand = _round_to_exponent(
439+
n, d, exponent, no_neg_zero)
440+
scientific = False
406441
point_pos = precision
442+
else: # presentation_type in "eEgG"
443+
figures = (
444+
max(precision, 1)
445+
if presentation_type in "gG"
446+
else precision + 1
447+
)
448+
negative, significand, exponent = _round_to_figures(n, d, figures)
449+
scientific = (
450+
presentation_type in "eE"
451+
or exponent > 0 or exponent + figures <= -4
452+
)
453+
point_pos = figures - 1 if scientific else -exponent
454+
455+
# Get the suffix - the part following the digits.
456+
if presentation_type == "%":
407457
suffix = "%"
408-
elif presentation_type in "eEgG":
409-
if presentation_type in "gG":
410-
figures = max(precision, 1)
411-
else:
412-
figures = precision + 1
413-
if self:
414-
significand, exponent = self._round_to_sig_figs(figures)
415-
else:
416-
significand, exponent = 0, 1 - figures
417-
if presentation_type in "gG" and -4 - figures < exponent <= 0:
418-
point_pos = -exponent
419-
suffix = ""
420-
else:
421-
point_pos = figures - 1
422-
suffix = f"{exponent_indicator}{exponent + point_pos:+03d}"
458+
elif scientific:
459+
suffix = f"{exponent_indicator}{exponent + point_pos:+03d}"
423460
else:
424-
# It shouldn't be possible to get here.
425-
raise ValueError(
426-
f"unknown presentation type {presentation_type!r}"
427-
)
461+
suffix = ""
428462

429463
# Assemble the output: before padding, it has the form
430464
# f"{sign}{leading}{trailing}", where `leading` includes thousands
431465
# separators if necessary, and `trailing` includes the decimal
432466
# separator where appropriate.
433467
digits = f"{significand:0{point_pos + 1}d}"
434-
sign = "-" if negative and (significand or neg_zero_ok) else pos_sign
435-
leading = digits[:len(digits) - point_pos]
436-
frac_part = digits[len(digits) - point_pos:]
468+
sign = "-" if negative else pos_sign
469+
leading = digits[: len(digits) - point_pos]
470+
frac_part = digits[len(digits) - point_pos :]
437471
if trim_zeros:
438472
frac_part = frac_part.rstrip("0")
439-
separator = "" if trim_dot and not frac_part else "."
473+
separator = "" if trim_point and not frac_part else "."
440474
trailing = separator + frac_part + suffix
441475

442476
# Do zero padding if required.
@@ -452,19 +486,19 @@ def __format__(self, format_spec, /):
452486
if thousands_sep:
453487
first_pos = 1 + (len(leading) - 1) % 3
454488
leading = leading[:first_pos] + "".join(
455-
thousands_sep + leading[pos:pos+3]
489+
thousands_sep + leading[pos : pos + 3]
456490
for pos in range(first_pos, len(leading), 3)
457491
)
458492

459-
# Pad if necessary and return.
493+
# Pad with fill character if necessary and return.
460494
body = leading + trailing
461495
padding = fill * (minimumwidth - len(sign) - len(body))
462496
if align == ">":
463497
return padding + sign + body
464498
elif align == "<":
465499
return sign + body + padding
466500
elif align == "^":
467-
half = len(padding)//2
501+
half = len(padding) // 2
468502
return padding[:half] + sign + body + padding[half:]
469503
else: # align == "="
470504
return sign + padding + body

Lib/test/test_fractions.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -947,12 +947,23 @@ def test_format_f_presentation_type(self):
947947
(F(-2, 3), ' .2f', '-0.67'),
948948
# Formatting to zero places
949949
(F(1, 2), '.0f', '0'),
950+
(F(-1, 2), '.0f', '-0'),
950951
(F(22, 7), '.0f', '3'),
951952
(F(-22, 7), '.0f', '-3'),
952953
# Formatting to zero places, alternate form
953954
(F(1, 2), '#.0f', '0.'),
955+
(F(-1, 2), '#.0f', '-0.'),
954956
(F(22, 7), '#.0f', '3.'),
955957
(F(-22, 7), '#.0f', '-3.'),
958+
# z flag for suppressing negative zeros
959+
(F('-0.001'), 'z.2f', '0.00'),
960+
(F('-0.001'), '-z.2f', '0.00'),
961+
(F('-0.001'), '+z.2f', '+0.00'),
962+
(F('-0.001'), ' z.2f', ' 0.00'),
963+
(F('0.001'), 'z.2f', '0.00'),
964+
(F('0.001'), '-z.2f', '0.00'),
965+
(F('0.001'), '+z.2f', '+0.00'),
966+
(F('0.001'), ' z.2f', ' 0.00'),
956967
# Corner-case: leading zeros are allowed in the precision
957968
(F(2, 3), '.02f', '0.67'),
958969
(F(22, 7), '.000f', '3'),
@@ -1040,15 +1051,6 @@ def test_format_f_presentation_type(self):
10401051
# is being inserted programmatically: spec = f'{width}.2f'.
10411052
(F('12.34'), '0.2f', '12.34'),
10421053
(F('12.34'), 'X>0.2f', '12.34'),
1043-
# z flag for suppressing negative zeros
1044-
(F('-0.001'), 'z.2f', '0.00'),
1045-
(F('-0.001'), '-z.2f', '0.00'),
1046-
(F('-0.001'), '+z.2f', '+0.00'),
1047-
(F('-0.001'), ' z.2f', ' 0.00'),
1048-
(F('0.001'), 'z.2f', '0.00'),
1049-
(F('0.001'), '-z.2f', '0.00'),
1050-
(F('0.001'), '+z.2f', '+0.00'),
1051-
(F('0.001'), ' z.2f', ' 0.00'),
10521054
# "F" should work identically to "f"
10531055
(F(22, 7), '.5F', '3.14286'),
10541056
# %-specifier
@@ -1088,6 +1090,15 @@ def test_format_g_presentation_type(self):
10881090
(F('9.99999e+2'), '.4g', '1000'),
10891091
(F('9.99999e-8'), '.4g', '1e-07'),
10901092
(F('9.99999e+8'), '.4g', '1e+09'),
1093+
# Check round-ties-to-even behaviour
1094+
(F('-0.115'), '.2g', '-0.12'),
1095+
(F('-0.125'), '.2g', '-0.12'),
1096+
(F('-0.135'), '.2g', '-0.14'),
1097+
(F('-0.145'), '.2g', '-0.14'),
1098+
(F('0.115'), '.2g', '0.12'),
1099+
(F('0.125'), '.2g', '0.12'),
1100+
(F('0.135'), '.2g', '0.14'),
1101+
(F('0.145'), '.2g', '0.14'),
10911102
# Trailing zeros and decimal point suppressed by default ...
10921103
(F(0), '.6g', '0'),
10931104
(F('123.400'), '.6g', '123.4'),

0 commit comments

Comments
 (0)