Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions Lib/fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ def _hash_algorithm(numerator, denominator):
\s*\Z # and optional whitespace to finish
""", re.VERBOSE | re.IGNORECASE)

# Pattern for matching format specification; only supports 'e', 'E', 'f', 'F'
# and '%' presentation types.
Comment thread
mdickinson marked this conversation as resolved.
Outdated
_FORMAT_SPECIFICATION_MATCHER = re.compile(r"""
(?:
(?P<fill>.)?
(?P<align>[<>=^])
)?
(?P<sign>[-+ ]?)
(?P<no_neg_zero>z)?
(?P<alt>\#)?
(?P<zeropad>0(?=\d))?
(?P<minimumwidth>\d+)?
(?P<thousands_sep>[,_])?
(?:\.(?P<precision>\d+))?
(?P<presentation_type>[efg%])
""", re.DOTALL | re.IGNORECASE | re.VERBOSE).fullmatch


class Fraction(numbers.Rational):
"""This class implements rational numbers.
Expand Down Expand Up @@ -310,6 +327,148 @@ def __str__(self):
else:
return '%s/%s' % (self._numerator, self._denominator)

def _round_to_sig_figs(self, figures):
"""Round a positive fraction to a given number of significant figures.

Returns a pair (significand, exponent) of integers such that
significand * 10**exponent gives a rounded approximation to self, and
significand lies in the range 10**(figures - 1) <= significand <
10**figures.
"""
if not (self > 0 and figures > 0):
raise ValueError("Expected self and figures to be positive")

# Find integer m satisfying 10**(m - 1) <= self <= 10**m.
str_n, str_d = str(self.numerator), str(self.denominator)
m = len(str_n) - len(str_d) + (str_d <= str_n)

# Find best approximation significand * 10**exponent to self, with
# 10**(figures - 1) <= significand <= 10**figures.
exponent = m - figures
significand = round(
self / 10**exponent if exponent >= 0 else self * 10**-exponent
)

# Adjust in the case where significand == 10**figures.
if len(str(significand)) == figures + 1:
significand //= 10
exponent += 1

return significand, exponent

def __format__(self, format_spec, /):
"""Format this fraction according to the given format specification."""

# Backwards compatiblility with existing formatting.
if not format_spec:
return str(self)

# Validate and parse the format specifier.
match = _FORMAT_SPECIFICATION_MATCHER(format_spec)
if match is None:
raise ValueError(
f"Invalid format specifier {format_spec!r} "
f"for object of type {type(self).__name__!r}"
)
elif match["align"] is not None and match["zeropad"] is not None:
# Avoid the temptation to guess.
raise ValueError(
f"Invalid format specifier {format_spec!r} "
f"for object of type {type(self).__name__!r}; "
"can't use explicit alignment when zero-padding"
)

fill = match["fill"] or " "
align = match["align"] or ">"
pos_sign = "" if match["sign"] == "-" else match["sign"]
neg_zero_ok = not match["no_neg_zero"]
alternate_form = bool(match["alt"])
zeropad = bool(match["zeropad"])
minimumwidth = int(match["minimumwidth"] or "0")
thousands_sep = match["thousands_sep"]
precision = int(match["precision"] or "6")
presentation_type = match["presentation_type"]
trim_zeros = presentation_type in "gG" and not alternate_form
trim_dot = not alternate_form
exponent_indicator = "E" if presentation_type in "EFG" else "e"

# Record sign, then work with absolute value.
negative = self < 0
self = abs(self)

# Round to get the digits we need; also compute the suffix.
if presentation_type == "f" or presentation_type == "F":
significand = round(self * 10**precision)
point_pos = precision
suffix = ""
elif presentation_type == "%":
significand = round(self * 10**(precision + 2))
point_pos = precision
suffix = "%"
elif presentation_type in "eEgG":
if presentation_type in "gG":
figures = max(precision, 1)
else:
figures = precision + 1
if self:
significand, exponent = self._round_to_sig_figs(figures)
else:
significand, exponent = 0, 1 - figures
if presentation_type in "gG" and -4 - figures < exponent <= 0:
point_pos = -exponent
suffix = ""
else:
point_pos = figures - 1
suffix = f"{exponent_indicator}{exponent + point_pos:+03d}"
else:
# It shouldn't be possible to get here.
raise ValueError(
f"unknown presentation type {presentation_type!r}"
)

# Assemble the output: before padding, it has the form
# f"{sign}{leading}{trailing}", where `leading` includes thousands
# separators if necessary, and `trailing` includes the decimal
# separator where appropriate.
digits = f"{significand:0{point_pos + 1}d}"
sign = "-" if negative and (significand or neg_zero_ok) else pos_sign
leading = digits[:len(digits) - point_pos]
frac_part = digits[len(digits) - point_pos:]
if trim_zeros:
frac_part = frac_part.rstrip("0")
separator = "" if trim_dot and not frac_part else "."
trailing = separator + frac_part + suffix

# Do zero padding if required.
if zeropad:
min_leading = minimumwidth - len(sign) - len(trailing)
# When adding thousands separators, they'll be added to the
# zero-padded portion too, so we need to compensate.
leading = leading.zfill(
3 * min_leading // 4 + 1 if thousands_sep else min_leading
)

# Insert thousands separators if required.
if thousands_sep:
first_pos = 1 + (len(leading) - 1) % 3
leading = leading[:first_pos] + "".join(
thousands_sep + leading[pos:pos+3]
for pos in range(first_pos, len(leading), 3)
)

# Pad if necessary and return.
body = leading + trailing
padding = fill * (minimumwidth - len(sign) - len(body))
if align == ">":
return padding + sign + body
elif align == "<":
return sign + body + padding
elif align == "^":
half = len(padding)//2
return padding[:half] + sign + body + padding[half:]
else: # align == "="
return sign + padding + body

def _operator_fallbacks(monomorphic_operator, fallback_operator):
"""Generates forward and reverse operators given a purely-rational
operator and a function from the operator module.
Expand Down
Loading