diff -r e1dfecfcfa04 Lib/fractions.py --- a/Lib/fractions.py Mon Nov 07 23:15:10 2016 -0500 +++ b/Lib/fractions.py Wed Nov 16 19:00:09 2016 +0100 @@ -3,17 +3,14 @@ """Fraction, infinite-precision, real numbers.""" -from decimal import Decimal import math import numbers import operator -import re import sys __all__ = ['Fraction', 'gcd'] - def gcd(a, b): """Calculate the Greatest Common Divisor of a and b. @@ -42,20 +39,6 @@ # _PyHASH_MODULUS. _PyHASH_INF = sys.hash_info.inf -_RATIONAL_FORMAT = re.compile(r""" - \A\s* # optional whitespace at the start, then - (?P[-+]?) # an optional sign, then - (?=\d|\.\d) # lookahead for digit or .digit - (?P\d*) # numerator (possibly empty) - (?: # followed by - (?:/(?P\d+))? # an optional denominator - | # or - (?:\.(?P\d*))? # an optional fractional part - (?:E(?P[-+]?\d+))? # and optional exponent - ) - \s*\Z # and optional whitespace to finish -""", re.VERBOSE | re.IGNORECASE) - class Fraction(numbers.Rational): """This class implements rational numbers. @@ -79,6 +62,7 @@ """ __slots__ = ('_numerator', '_denominator') + _Decimal = None # We're immutable, so use __new__ not __init__ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): @@ -120,46 +104,75 @@ self._denominator = 1 return self + elif isinstance(numerator, str): + # Handle construction from strings. + fraction_literal = numerator + num, _, denom = fraction_literal.partition('/') + try: + if _ == '': + # Numerator-only form allows for optional fractional + # and exponent parts. + # Partition the fraction literal on the exponent sign, + # while ignoring leading and trailing whitespace + # and the case of the exponent sign. + num, _, exp = num.strip().replace('e', 'E').partition('E') + if _ and not exp: + # No exponent notation without value! + raise ValueError + num, _, decimal = num.partition('.') + shift = 0 + if decimal: + # Zero-pad integer-less fraction literals. + if num == '' or num == '+': + num = '0' + elif num == '-': + num += '0' + # Joining literal parts by underscores ensures + # that result strings can only be parsed as ints + # if there parts were parseable. + num = f'{num}_{decimal}' + shift -= len(decimal)-decimal.count('_') + if exp: + shift += int(exp) + numerator = int(num) + denominator = 1 + if shift > 0: + numerator *= 10 ** shift + elif shift < 0: + denominator = 10 ** -shift + + elif num and denom and num[-1].isdigit() and denom[0].isdigit(): + numerator = int(num) + denominator = int(denom) + else: + raise ValueError + except ValueError: + raise ValueError('Invalid literal for Fraction: %r' % + fraction_literal) + elif isinstance(numerator, numbers.Rational): self._numerator = numerator.numerator self._denominator = numerator.denominator return self - elif isinstance(numerator, (float, Decimal)): - # Exact conversion - self._numerator, self._denominator = numerator.as_integer_ratio() - return self - - elif isinstance(numerator, str): - # Handle construction from strings. - m = _RATIONAL_FORMAT.match(numerator) - if m is None: - raise ValueError('Invalid literal for Fraction: %r' % - numerator) - numerator = int(m.group('num') or '0') - denom = m.group('denom') - if denom: - denominator = int(denom) - else: - denominator = 1 - decimal = m.group('decimal') - if decimal: - scale = 10**len(decimal) - numerator = numerator * scale + int(decimal) - denominator *= scale - exp = m.group('exp') - if exp: - exp = int(exp) - if exp >= 0: - numerator *= 10**exp - else: - denominator *= 10**-exp - if m.group('sign') == '-': - numerator = -numerator - else: - raise TypeError("argument should be a string " - "or a Rational instance") + try: + # Maybe numerator supports its exact representation + # as an integer ratio? + # This will always work for finite float and + # decimal.Decimal instances. + # 'nan' and 'inf' result in a ValueError, which we + # let bubble up. + integer_ratio = numerator.as_integer_ratio() + try: + numerator, denominator = integer_ratio + except ValueError: + pass + except (AttributeError, TypeError): + pass + if denominator is None: + raise TypeError("argument should be a string " + "or a Rational instance") elif type(numerator) is int is type(denominator): pass # *very* normal case @@ -197,24 +210,27 @@ Beware that Fraction.from_float(0.3) != Fraction(3, 10). """ + + if isinstance(f, float): + return cls(*f.as_integer_ratio(), _normalize=False) if isinstance(f, numbers.Integral): return cls(f) - elif not isinstance(f, float): - raise TypeError("%s.from_float() only takes floats, not %r (%s)" % - (cls.__name__, f, type(f).__name__)) - return cls(*f.as_integer_ratio()) + raise TypeError("%s.from_float() only takes floats, not %r (%s)" % + (cls.__name__, f, type(f).__name__)) @classmethod def from_decimal(cls, dec): """Converts a finite Decimal instance to a rational number, exactly.""" - from decimal import Decimal + if cls._Decimal is None: + from decimal import Decimal + cls._Decimal = Decimal + if isinstance(dec, cls._Decimal): + return cls(*dec.as_integer_ratio(), _normalize=False) if isinstance(dec, numbers.Integral): - dec = Decimal(int(dec)) - elif not isinstance(dec, Decimal): - raise TypeError( - "%s.from_decimal() only takes Decimals, not %r (%s)" % - (cls.__name__, dec, type(dec).__name__)) - return cls(*dec.as_integer_ratio()) + return cls(dec) + raise TypeError( + "%s.from_decimal() only takes Decimals, not %r (%s)" % + (cls.__name__, dec, type(dec).__name__)) def limit_denominator(self, max_denominator=1000000): """Closest Fraction to self with denominator at most max_denominator.