[Python-checkins] r60068 - in python/trunk: Doc/library/rational.rst Lib/rational.py Lib/test/test_rational.py

jeffrey.yasskin python-checkins at python.org
Sat Jan 19 10:56:06 CET 2008


Author: jeffrey.yasskin
Date: Sat Jan 19 10:56:06 2008
New Revision: 60068

Modified:
   python/trunk/Doc/library/rational.rst
   python/trunk/Lib/rational.py
   python/trunk/Lib/test/test_rational.py
Log:
Several tweaks: add construction from strings and .from_decimal(), change
__init__ to __new__ to enforce immutability, and remove "rational." from repr
and the parens from str.


Modified: python/trunk/Doc/library/rational.rst
==============================================================================
--- python/trunk/Doc/library/rational.rst	(original)
+++ python/trunk/Doc/library/rational.rst	Sat Jan 19 10:56:06 2008
@@ -15,6 +15,7 @@
 
 .. class:: Rational(numerator=0, denominator=1)
            Rational(other_rational)
+           Rational(string)
 
    The first version requires that *numerator* and *denominator* are
    instances of :class:`numbers.Integral` and returns a new
@@ -22,10 +23,12 @@
    *denominator* is :const:`0`, raises a :exc:`ZeroDivisionError`. The
    second version requires that *other_rational* is an instance of
    :class:`numbers.Rational` and returns an instance of
-   :class:`Rational` with the same value.
+   :class:`Rational` with the same value. The third version expects a
+   string of the form ``[-+]?[0-9]+(/[0-9]+)?``, optionally surrounded
+   by spaces.
 
    Implements all of the methods and operations from
-   :class:`numbers.Rational` and is hashable.
+   :class:`numbers.Rational` and is immutable and hashable.
 
 
 .. method:: Rational.from_float(flt)
@@ -36,6 +39,13 @@
    10)``
 
 
+.. method:: Rational.from_decimal(dec)
+
+   This classmethod constructs a :class:`Rational` representing the
+   exact value of *dec*, which must be a
+   :class:`decimal.Decimal`.
+
+
 .. method:: Rational.__floor__()
 
    Returns the greatest :class:`int` ``<= self``. Will be accessible

Modified: python/trunk/Lib/rational.py
==============================================================================
--- python/trunk/Lib/rational.py	(original)
+++ python/trunk/Lib/rational.py	Sat Jan 19 10:56:06 2008
@@ -7,6 +7,7 @@
 import math
 import numbers
 import operator
+import re
 
 __all__ = ["Rational"]
 
@@ -76,6 +77,10 @@
         return (top, 2 ** -e)
 
 
+_RATIONAL_FORMAT = re.compile(
+    r'^\s*(?P<sign>[-+]?)(?P<num>\d+)(?:/(?P<denom>\d+))?\s*$')
+
+
 class Rational(RationalAbc):
     """This class implements rational numbers.
 
@@ -84,18 +89,41 @@
     and the denominator defaults to 1 so that Rational(3) == 3 and
     Rational() == 0.
 
+    Rationals can also be constructed from strings of the form
+    '[-+]?[0-9]+(/[0-9]+)?', optionally surrounded by spaces.
+
     """
 
     __slots__ = ('_numerator', '_denominator')
 
-    def __init__(self, numerator=0, denominator=1):
-        if (not isinstance(numerator, numbers.Integral) and
-            isinstance(numerator, RationalAbc) and
-            denominator == 1):
-            # Handle copies from other rationals.
-            other_rational = numerator
-            numerator = other_rational.numerator
-            denominator = other_rational.denominator
+    # We're immutable, so use __new__ not __init__
+    def __new__(cls, numerator=0, denominator=1):
+        """Constructs a Rational.
+
+        Takes a string, another Rational, or a numerator/denominator pair.
+
+        """
+        self = super(Rational, cls).__new__(cls)
+
+        if denominator == 1:
+            if isinstance(numerator, basestring):
+                # Handle construction from strings.
+                input = numerator
+                m = _RATIONAL_FORMAT.match(input)
+                if m is None:
+                    raise ValueError('Invalid literal for Rational: ' + input)
+                numerator = int(m.group('num'))
+                # Default denominator to 1. That's the only optional group.
+                denominator = int(m.group('denom') or 1)
+                if m.group('sign') == '-':
+                    numerator = -numerator
+
+            elif (not isinstance(numerator, numbers.Integral) and
+                  isinstance(numerator, RationalAbc)):
+                # Handle copies from other rationals.
+                other_rational = numerator
+                numerator = other_rational.numerator
+                denominator = other_rational.denominator
 
         if (not isinstance(numerator, numbers.Integral) or
             not isinstance(denominator, numbers.Integral)):
@@ -108,10 +136,15 @@
         g = _gcd(numerator, denominator)
         self._numerator = int(numerator // g)
         self._denominator = int(denominator // g)
+        return self
 
     @classmethod
     def from_float(cls, f):
-        """Converts a float to a rational number, exactly."""
+        """Converts a finite float to a rational number, exactly.
+
+        Beware that Rational.from_float(0.3) != Rational(3, 10).
+
+        """
         if not isinstance(f, float):
             raise TypeError("%s.from_float() only takes floats, not %r (%s)" %
                             (cls.__name__, f, type(f).__name__))
@@ -119,6 +152,26 @@
             raise TypeError("Cannot convert %r to %s." % (f, cls.__name__))
         return cls(*_binary_float_to_ratio(f))
 
+    @classmethod
+    def from_decimal(cls, dec):
+        """Converts a finite Decimal instance to a rational number, exactly."""
+        from decimal import Decimal
+        if not isinstance(dec, Decimal):
+            raise TypeError(
+                "%s.from_decimal() only takes Decimals, not %r (%s)" %
+                (cls.__name__, dec, type(dec).__name__))
+        if not dec.is_finite():
+            # Catches infinities and nans.
+            raise TypeError("Cannot convert %s to %s." % (dec, cls.__name__))
+        sign, digits, exp = dec.as_tuple()
+        digits = int(''.join(map(str, digits)))
+        if sign:
+            digits = -digits
+        if exp >= 0:
+            return cls(digits * 10 ** exp)
+        else:
+            return cls(digits, 10 ** -exp)
+
     @property
     def numerator(a):
         return a._numerator
@@ -129,15 +182,14 @@
 
     def __repr__(self):
         """repr(self)"""
-        return ('rational.Rational(%r,%r)' %
-                (self.numerator, self.denominator))
+        return ('Rational(%r,%r)' % (self.numerator, self.denominator))
 
     def __str__(self):
         """str(self)"""
         if self.denominator == 1:
             return str(self.numerator)
         else:
-            return '(%s/%s)' % (self.numerator, self.denominator)
+            return '%s/%s' % (self.numerator, self.denominator)
 
     def _operator_fallbacks(monomorphic_operator, fallback_operator):
         """Generates forward and reverse operators given a purely-rational

Modified: python/trunk/Lib/test/test_rational.py
==============================================================================
--- python/trunk/Lib/test/test_rational.py	(original)
+++ python/trunk/Lib/test/test_rational.py	Sat Jan 19 10:56:06 2008
@@ -45,6 +45,44 @@
         self.assertRaises(TypeError, R, 1.5)
         self.assertRaises(TypeError, R, 1.5 + 3j)
 
+        self.assertRaises(TypeError, R, R(1, 2), 3)
+        self.assertRaises(TypeError, R, "3/2", 3)
+
+    def testFromString(self):
+        self.assertEquals((5, 1), _components(R("5")))
+        self.assertEquals((3, 2), _components(R("3/2")))
+        self.assertEquals((3, 2), _components(R(" \n  +3/2")))
+        self.assertEquals((-3, 2), _components(R("-3/2  ")))
+        self.assertEquals((3, 2), _components(R("    03/02 \n  ")))
+        self.assertEquals((3, 2), _components(R(u"    03/02 \n  ")))
+
+        self.assertRaisesMessage(
+            ZeroDivisionError, "Rational(3, 0)",
+            R, "3/0")
+        self.assertRaisesMessage(
+            ValueError, "Invalid literal for Rational: 3/",
+            R, "3/")
+        self.assertRaisesMessage(
+            ValueError, "Invalid literal for Rational: 3 /2",
+            R, "3 /2")
+        self.assertRaisesMessage(
+            # Denominators don't need a sign.
+            ValueError, "Invalid literal for Rational: 3/+2",
+            R, "3/+2")
+        self.assertRaisesMessage(
+            # Imitate float's parsing.
+            ValueError, "Invalid literal for Rational: + 3/2",
+            R, "+ 3/2")
+        self.assertRaisesMessage(
+            # Only parse fractions, not decimals.
+            ValueError, "Invalid literal for Rational: 3.2",
+            R, "3.2")
+
+    def testImmutable(self):
+        r = R(7, 3)
+        r.__init__(2, 15)
+        self.assertEquals((7, 3), _components(r))
+
     def testFromFloat(self):
         self.assertRaisesMessage(
             TypeError, "Rational.from_float() only takes floats, not 3 (int)",
@@ -72,6 +110,31 @@
             TypeError, "Cannot convert nan to Rational.",
             R.from_float, nan)
 
+    def testFromDecimal(self):
+        self.assertRaisesMessage(
+            TypeError,
+            "Rational.from_decimal() only takes Decimals, not 3 (int)",
+            R.from_decimal, 3)
+        self.assertEquals(R(0), R.from_decimal(Decimal("-0")))
+        self.assertEquals(R(5, 10), R.from_decimal(Decimal("0.5")))
+        self.assertEquals(R(5, 1000), R.from_decimal(Decimal("5e-3")))
+        self.assertEquals(R(5000), R.from_decimal(Decimal("5e3")))
+        self.assertEquals(1 - R(1, 10**30),
+                          R.from_decimal(Decimal("0." + "9" * 30)))
+
+        self.assertRaisesMessage(
+            TypeError, "Cannot convert Infinity to Rational.",
+            R.from_decimal, Decimal("inf"))
+        self.assertRaisesMessage(
+            TypeError, "Cannot convert -Infinity to Rational.",
+            R.from_decimal, Decimal("-inf"))
+        self.assertRaisesMessage(
+            TypeError, "Cannot convert NaN to Rational.",
+            R.from_decimal, Decimal("nan"))
+        self.assertRaisesMessage(
+            TypeError, "Cannot convert sNaN to Rational.",
+            R.from_decimal, Decimal("snan"))
+
     def testConversions(self):
         self.assertTypedEquals(-1, trunc(R(-11, 10)))
         self.assertTypedEquals(-2, R(-11, 10).__floor__())
@@ -173,7 +236,7 @@
         self.assertTypedEquals(1.0 + 0j, (1.0 + 0j) ** R(1, 10))
 
     def testMixingWithDecimal(self):
-        """Decimal refuses mixed comparisons."""
+        # Decimal refuses mixed comparisons.
         self.assertRaisesMessage(
             TypeError,
             "unsupported operand type(s) for +: 'Rational' and 'Decimal'",
@@ -236,8 +299,8 @@
         self.assertFalse(R(5, 2) == 2)
 
     def testStringification(self):
-        self.assertEquals("rational.Rational(7,3)", repr(R(7, 3)))
-        self.assertEquals("(7/3)", str(R(7, 3)))
+        self.assertEquals("Rational(7,3)", repr(R(7, 3)))
+        self.assertEquals("7/3", str(R(7, 3)))
         self.assertEquals("7", str(R(7, 1)))
 
     def testHash(self):


More information about the Python-checkins mailing list