summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJeffrey Yasskin <jyasskin@gmail.com>2008-01-19 09:56:06 (GMT)
committerJeffrey Yasskin <jyasskin@gmail.com>2008-01-19 09:56:06 (GMT)
commit45169fbc800d573ddd01d3f440f264caaa93c622 (patch)
treeb565b74e43fd651e0ee163faf7c406690e5fbbf0
parentbf4c7c8c0d851b239425b53b676c71a75c7d40c8 (diff)
downloadcpython-45169fbc800d573ddd01d3f440f264caaa93c622.zip
cpython-45169fbc800d573ddd01d3f440f264caaa93c622.tar.gz
cpython-45169fbc800d573ddd01d3f440f264caaa93c622.tar.bz2
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.
-rw-r--r--Doc/library/rational.rst14
-rwxr-xr-xLib/rational.py76
-rw-r--r--Lib/test/test_rational.py69
3 files changed, 142 insertions, 17 deletions
diff --git a/Doc/library/rational.rst b/Doc/library/rational.rst
index dd18305..8ed702f 100644
--- a/Doc/library/rational.rst
+++ b/Doc/library/rational.rst
@@ -15,6 +15,7 @@ Rational number class.
.. 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 @@ Rational number class.
*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 @@ Rational number class.
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
diff --git a/Lib/rational.py b/Lib/rational.py
index d455dc6..19e7f14 100755
--- a/Lib/rational.py
+++ b/Lib/rational.py
@@ -7,6 +7,7 @@ from __future__ import division
import math
import numbers
import operator
+import re
__all__ = ["Rational"]
@@ -76,6 +77,10 @@ def _binary_float_to_ratio(x):
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 @@ class Rational(RationalAbc):
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 @@ class Rational(RationalAbc):
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 @@ class Rational(RationalAbc):
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 @@ class Rational(RationalAbc):
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
diff --git a/Lib/test/test_rational.py b/Lib/test/test_rational.py
index 654c46d..5ee7b7d 100644
--- a/Lib/test/test_rational.py
+++ b/Lib/test/test_rational.py
@@ -45,6 +45,44 @@ class RationalTest(unittest.TestCase):
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 @@ class RationalTest(unittest.TestCase):
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 @@ class RationalTest(unittest.TestCase):
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 @@ class RationalTest(unittest.TestCase):
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):