From 98127c37167a6735dba576819514c334c89e5b9c Mon Sep 17 00:00:00 2001 From: Mark Dickinson Date: Sat, 3 Apr 2010 11:18:52 +0000 Subject: [PATCH] Merged revisions 79629 via svnmerge from svn+ssh://pythondev@svn.python.org/python/trunk ........ r79629 | mark.dickinson | 2010-04-02 23:27:36 +0100 (Fri, 02 Apr 2010) | 2 lines Issue #8294: Allow float and Decimal arguments in Fraction constructor. ........ --- Doc/library/fractions.rst | 49 +++++++++++++++++++++++------- Lib/fractions.py | 62 +++++++++++++++++++++++++++++++++----- Lib/test/test_fractions.py | 27 ++++++++++++++++- Misc/NEWS | 3 ++ 4 files changed, 121 insertions(+), 20 deletions(-) diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index 79600269c95..f08cfafaee4 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -15,17 +15,24 @@ another rational number, or from a string. .. class:: Fraction(numerator=0, denominator=1) Fraction(other_fraction) + Fraction(float) + Fraction(decimal) Fraction(string) - The first version requires that *numerator* and *denominator* are - instances of :class:`numbers.Rational` and returns a new - :class:`Fraction` instance with value ``numerator/denominator``. If - *denominator* is :const:`0`, it raises a - :exc:`ZeroDivisionError`. The second version requires that - *other_fraction* is an instance of :class:`numbers.Rational` and - returns an :class:`Fraction` instance with the same value. The - last version of the constructor expects a string instance. The - usual form for this string is:: + The first version requires that *numerator* and *denominator* are instances + of :class:`numbers.Rational` and returns a new :class:`Fraction` instance + with value ``numerator/denominator``. If *denominator* is :const:`0`, it + raises a :exc:`ZeroDivisionError`. The second version requires that + *other_fraction* is an instance of :class:`numbers.Rational` and returns a + :class:`Fraction` instance with the same value. The next two versions accept + either a :class:`float` or a :class:`decimal.Decimal` instance, and return a + :class:`Fraction` instance with exactly the same value. Note that due to the + usual issues with binary floating-point (see :ref:`tut-fp-issues`), the + argument to ``Fraction(1.1)`` is not exactly equal to 11/10, and so + ``Fraction(1.1)`` does *not* return ``Fraction(11, 10)`` as one might expect. + (But see the documentation for the :meth:`limit_denominator` method below.) + The last version of the constructor expects a string or unicode instance. + The usual form for this instance is:: [sign] numerator ['/' denominator] @@ -55,6 +62,13 @@ another rational number, or from a string. Fraction(-1, 8) >>> Fraction('7e-6') Fraction(7, 1000000) + >>> Fraction(2.25) + Fraction(9, 4) + >>> Fraction(1.1) + Fraction(2476979795053773, 2251799813685248) + >>> from decimal import Decimal + >>> Fraction(Decimal('1.1')) + Fraction(11, 10) The :class:`Fraction` class inherits from the abstract base class @@ -63,6 +77,10 @@ another rational number, or from a string. and should be treated as immutable. In addition, :class:`Fraction` has the following methods: + .. versionchanged:: 3.2 + The :class:`Fraction` constructor now accepts :class:`float` and + :class:`decimal.Decimal` instances. + .. method:: from_float(flt) @@ -70,12 +88,19 @@ another rational number, or from a string. value of *flt*, which must be a :class:`float`. Beware that ``Fraction.from_float(0.3)`` is not the same value as ``Fraction(3, 10)`` + .. note:: From Python 3.2 onwards, you can also construct a + :class:`Fraction` instance directly from a :class:`float`. + .. method:: from_decimal(dec) This class method constructs a :class:`Fraction` representing the exact value of *dec*, which must be a :class:`decimal.Decimal` instance. + .. note:: From Python 3.2 onwards, you can also construct a + :class:`Fraction` instance directly from a :class:`decimal.Decimal` + instance. + .. method:: limit_denominator(max_denominator=1000000) @@ -90,10 +115,12 @@ another rational number, or from a string. or for recovering a rational number that's represented as a float: >>> from math import pi, cos - >>> Fraction.from_float(cos(pi/3)) + >>> Fraction(cos(pi/3)) Fraction(4503599627370497, 9007199254740992) - >>> Fraction.from_float(cos(pi/3)).limit_denominator() + >>> Fraction(cos(pi/3)).limit_denominator() Fraction(1, 2) + >>> Fraction(1.1).limit_denominator() + Fraction(11, 10) .. method:: __floor__() diff --git a/Lib/fractions.py b/Lib/fractions.py index 9624c901438..fc8a12c0144 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -3,6 +3,7 @@ """Fraction, infinite-precision, real numbers.""" +from decimal import Decimal import math import numbers import operator @@ -41,13 +42,21 @@ _RATIONAL_FORMAT = re.compile(r""" class Fraction(numbers.Rational): """This class implements rational numbers. - Fraction(8, 6) will produce a rational number equivalent to - 4/3. Both arguments must be Integral. The numerator defaults to 0 - and the denominator defaults to 1 so that Fraction(3) == 3 and - Fraction() == 0. + In the two-argument form of the constructor, Fraction(8, 6) will + produce a rational number equivalent to 4/3. Both arguments must + be Rational. The numerator defaults to 0 and the denominator + defaults to 1 so that Fraction(3) == 3 and Fraction() == 0. - Fraction can also be constructed from strings of the form - '[-+]?[0-9]+((/|.)[0-9]+)?', optionally surrounded by spaces. + Fractions can also be constructed from: + + - numeric strings similar to those accepted by the + float constructor (for example, '-2.3' or '1e10') + + - strings of the form '123/456' + + - float and Decimal instances + + - other Rational instances (including integers) """ @@ -57,8 +66,32 @@ class Fraction(numbers.Rational): def __new__(cls, numerator=0, denominator=None): """Constructs a Rational. - Takes a string like '3/2' or '1.5', another Rational, or a - numerator/denominator pair. + Takes a string like '3/2' or '1.5', another Rational instance, a + numerator/denominator pair, or a float. + + Examples + -------- + + >>> Fraction(10, -8) + Fraction(-5, 4) + >>> Fraction(Fraction(1, 7), 5) + Fraction(1, 35) + >>> Fraction(Fraction(1, 7), Fraction(2, 3)) + Fraction(3, 14) + >>> Fraction('314') + Fraction(314, 1) + >>> Fraction('-35/4') + Fraction(-35, 4) + >>> Fraction('3.1415') # conversion from numeric string + Fraction(6283, 2000) + >>> Fraction('-47e-2') # string may include a decimal exponent + Fraction(-47, 100) + >>> Fraction(1.47) # direct construction from float (exact conversion) + Fraction(6620291452234629, 4503599627370496) + >>> Fraction(2.25) + Fraction(9, 4) + >>> Fraction(Decimal('1.47')) + Fraction(147, 100) """ self = super(Fraction, cls).__new__(cls) @@ -69,6 +102,19 @@ class Fraction(numbers.Rational): self._denominator = numerator.denominator return self + elif isinstance(numerator, float): + # Exact conversion from float + value = Fraction.from_float(numerator) + self._numerator = value._numerator + self._denominator = value._denominator + return self + + elif isinstance(numerator, Decimal): + value = Fraction.from_decimal(numerator) + self._numerator = value._numerator + self._denominator = value._denominator + return self + elif isinstance(numerator, str): # Handle construction from strings. m = _RATIONAL_FORMAT.match(numerator) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 5ad0742c5ea..dd51f9b9e6b 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -12,6 +12,11 @@ from pickle import dumps, loads F = fractions.Fraction gcd = fractions.gcd +# decorator for skipping tests on non-IEEE 754 platforms +requires_IEEE_754 = unittest.skipUnless( + float.__getformat__("double").startswith("IEEE"), + "test requires IEEE 754 doubles") + class DummyFloat(object): """Dummy float class for testing comparisons with Fractions""" @@ -130,13 +135,33 @@ class FractionTest(unittest.TestCase): self.assertRaisesMessage(ZeroDivisionError, "Fraction(12, 0)", F, 12, 0) - self.assertRaises(TypeError, F, 1.5) self.assertRaises(TypeError, F, 1.5 + 3j) self.assertRaises(TypeError, F, "3/2", 3) self.assertRaises(TypeError, F, 3, 0j) self.assertRaises(TypeError, F, 3, 1j) + @requires_IEEE_754 + def testInitFromFloat(self): + self.assertEquals((5, 2), _components(F(2.5))) + self.assertEquals((0, 1), _components(F(-0.0))) + self.assertEquals((3602879701896397, 36028797018963968), + _components(F(0.1))) + self.assertRaises(TypeError, F, float('nan')) + self.assertRaises(TypeError, F, float('inf')) + self.assertRaises(TypeError, F, float('-inf')) + + def testInitFromDecimal(self): + self.assertEquals((11, 10), + _components(F(Decimal('1.1')))) + self.assertEquals((7, 200), + _components(F(Decimal('3.5e-2')))) + self.assertEquals((0, 1), + _components(F(Decimal('.000e20')))) + self.assertRaises(TypeError, F, Decimal('nan')) + self.assertRaises(TypeError, F, Decimal('snan')) + self.assertRaises(TypeError, F, Decimal('inf')) + self.assertRaises(TypeError, F, Decimal('-inf')) def testFromString(self): self.assertEquals((5, 1), _components(F("5"))) diff --git a/Misc/NEWS b/Misc/NEWS index 988f71d2a34..d006b6f1905 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -301,6 +301,9 @@ C-API Library ------- +- Issue #8294: The Fraction constructor now accepts Decimal and float + instances directly. + - Issue #7279: Comparisons involving a Decimal signaling NaN now signal InvalidOperation instead of returning False. (Comparisons involving a quiet NaN are unchanged.) Also, Decimal quiet NaNs