mirror of
https://github.com/python/cpython.git
synced 2025-08-01 15:43:13 +00:00
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.
This commit is contained in:
parent
bf4c7c8c0d
commit
45169fbc80
3 changed files with 142 additions and 17 deletions
|
@ -15,6 +15,7 @@ Rational number class.
|
||||||
|
|
||||||
.. class:: Rational(numerator=0, denominator=1)
|
.. class:: Rational(numerator=0, denominator=1)
|
||||||
Rational(other_rational)
|
Rational(other_rational)
|
||||||
|
Rational(string)
|
||||||
|
|
||||||
The first version requires that *numerator* and *denominator* are
|
The first version requires that *numerator* and *denominator* are
|
||||||
instances of :class:`numbers.Integral` and returns a new
|
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
|
*denominator* is :const:`0`, raises a :exc:`ZeroDivisionError`. The
|
||||||
second version requires that *other_rational* is an instance of
|
second version requires that *other_rational* is an instance of
|
||||||
:class:`numbers.Rational` and returns 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
|
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)
|
.. method:: Rational.from_float(flt)
|
||||||
|
@ -36,6 +39,13 @@ Rational number class.
|
||||||
10)``
|
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__()
|
.. method:: Rational.__floor__()
|
||||||
|
|
||||||
Returns the greatest :class:`int` ``<= self``. Will be accessible
|
Returns the greatest :class:`int` ``<= self``. Will be accessible
|
||||||
|
|
|
@ -7,6 +7,7 @@ from __future__ import division
|
||||||
import math
|
import math
|
||||||
import numbers
|
import numbers
|
||||||
import operator
|
import operator
|
||||||
|
import re
|
||||||
|
|
||||||
__all__ = ["Rational"]
|
__all__ = ["Rational"]
|
||||||
|
|
||||||
|
@ -76,6 +77,10 @@ def _binary_float_to_ratio(x):
|
||||||
return (top, 2 ** -e)
|
return (top, 2 ** -e)
|
||||||
|
|
||||||
|
|
||||||
|
_RATIONAL_FORMAT = re.compile(
|
||||||
|
r'^\s*(?P<sign>[-+]?)(?P<num>\d+)(?:/(?P<denom>\d+))?\s*$')
|
||||||
|
|
||||||
|
|
||||||
class Rational(RationalAbc):
|
class Rational(RationalAbc):
|
||||||
"""This class implements rational numbers.
|
"""This class implements rational numbers.
|
||||||
|
|
||||||
|
@ -84,18 +89,41 @@ class Rational(RationalAbc):
|
||||||
and the denominator defaults to 1 so that Rational(3) == 3 and
|
and the denominator defaults to 1 so that Rational(3) == 3 and
|
||||||
Rational() == 0.
|
Rational() == 0.
|
||||||
|
|
||||||
|
Rationals can also be constructed from strings of the form
|
||||||
|
'[-+]?[0-9]+(/[0-9]+)?', optionally surrounded by spaces.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_numerator', '_denominator')
|
__slots__ = ('_numerator', '_denominator')
|
||||||
|
|
||||||
def __init__(self, numerator=0, denominator=1):
|
# We're immutable, so use __new__ not __init__
|
||||||
if (not isinstance(numerator, numbers.Integral) and
|
def __new__(cls, numerator=0, denominator=1):
|
||||||
isinstance(numerator, RationalAbc) and
|
"""Constructs a Rational.
|
||||||
denominator == 1):
|
|
||||||
# Handle copies from other rationals.
|
Takes a string, another Rational, or a numerator/denominator pair.
|
||||||
other_rational = numerator
|
|
||||||
numerator = other_rational.numerator
|
"""
|
||||||
denominator = other_rational.denominator
|
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
|
if (not isinstance(numerator, numbers.Integral) or
|
||||||
not isinstance(denominator, numbers.Integral)):
|
not isinstance(denominator, numbers.Integral)):
|
||||||
|
@ -108,10 +136,15 @@ class Rational(RationalAbc):
|
||||||
g = _gcd(numerator, denominator)
|
g = _gcd(numerator, denominator)
|
||||||
self._numerator = int(numerator // g)
|
self._numerator = int(numerator // g)
|
||||||
self._denominator = int(denominator // g)
|
self._denominator = int(denominator // g)
|
||||||
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_float(cls, f):
|
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):
|
if not isinstance(f, float):
|
||||||
raise TypeError("%s.from_float() only takes floats, not %r (%s)" %
|
raise TypeError("%s.from_float() only takes floats, not %r (%s)" %
|
||||||
(cls.__name__, f, type(f).__name__))
|
(cls.__name__, f, type(f).__name__))
|
||||||
|
@ -119,6 +152,26 @@ class Rational(RationalAbc):
|
||||||
raise TypeError("Cannot convert %r to %s." % (f, cls.__name__))
|
raise TypeError("Cannot convert %r to %s." % (f, cls.__name__))
|
||||||
return cls(*_binary_float_to_ratio(f))
|
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
|
@property
|
||||||
def numerator(a):
|
def numerator(a):
|
||||||
return a._numerator
|
return a._numerator
|
||||||
|
@ -129,15 +182,14 @@ class Rational(RationalAbc):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""repr(self)"""
|
"""repr(self)"""
|
||||||
return ('rational.Rational(%r,%r)' %
|
return ('Rational(%r,%r)' % (self.numerator, self.denominator))
|
||||||
(self.numerator, self.denominator))
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""str(self)"""
|
"""str(self)"""
|
||||||
if self.denominator == 1:
|
if self.denominator == 1:
|
||||||
return str(self.numerator)
|
return str(self.numerator)
|
||||||
else:
|
else:
|
||||||
return '(%s/%s)' % (self.numerator, self.denominator)
|
return '%s/%s' % (self.numerator, self.denominator)
|
||||||
|
|
||||||
def _operator_fallbacks(monomorphic_operator, fallback_operator):
|
def _operator_fallbacks(monomorphic_operator, fallback_operator):
|
||||||
"""Generates forward and reverse operators given a purely-rational
|
"""Generates forward and reverse operators given a purely-rational
|
||||||
|
|
|
@ -45,6 +45,44 @@ class RationalTest(unittest.TestCase):
|
||||||
self.assertRaises(TypeError, R, 1.5)
|
self.assertRaises(TypeError, R, 1.5)
|
||||||
self.assertRaises(TypeError, R, 1.5 + 3j)
|
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):
|
def testFromFloat(self):
|
||||||
self.assertRaisesMessage(
|
self.assertRaisesMessage(
|
||||||
TypeError, "Rational.from_float() only takes floats, not 3 (int)",
|
TypeError, "Rational.from_float() only takes floats, not 3 (int)",
|
||||||
|
@ -72,6 +110,31 @@ class RationalTest(unittest.TestCase):
|
||||||
TypeError, "Cannot convert nan to Rational.",
|
TypeError, "Cannot convert nan to Rational.",
|
||||||
R.from_float, nan)
|
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):
|
def testConversions(self):
|
||||||
self.assertTypedEquals(-1, trunc(R(-11, 10)))
|
self.assertTypedEquals(-1, trunc(R(-11, 10)))
|
||||||
self.assertTypedEquals(-2, R(-11, 10).__floor__())
|
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))
|
self.assertTypedEquals(1.0 + 0j, (1.0 + 0j) ** R(1, 10))
|
||||||
|
|
||||||
def testMixingWithDecimal(self):
|
def testMixingWithDecimal(self):
|
||||||
"""Decimal refuses mixed comparisons."""
|
# Decimal refuses mixed comparisons.
|
||||||
self.assertRaisesMessage(
|
self.assertRaisesMessage(
|
||||||
TypeError,
|
TypeError,
|
||||||
"unsupported operand type(s) for +: 'Rational' and 'Decimal'",
|
"unsupported operand type(s) for +: 'Rational' and 'Decimal'",
|
||||||
|
@ -236,8 +299,8 @@ class RationalTest(unittest.TestCase):
|
||||||
self.assertFalse(R(5, 2) == 2)
|
self.assertFalse(R(5, 2) == 2)
|
||||||
|
|
||||||
def testStringification(self):
|
def testStringification(self):
|
||||||
self.assertEquals("rational.Rational(7,3)", repr(R(7, 3)))
|
self.assertEquals("Rational(7,3)", repr(R(7, 3)))
|
||||||
self.assertEquals("(7/3)", str(R(7, 3)))
|
self.assertEquals("7/3", str(R(7, 3)))
|
||||||
self.assertEquals("7", str(R(7, 1)))
|
self.assertEquals("7", str(R(7, 1)))
|
||||||
|
|
||||||
def testHash(self):
|
def testHash(self):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue