Kaydet (Commit) 3e1a3736 authored tarafından Jeffrey Yasskin's avatar Jeffrey Yasskin

Make rational.gcd() public and allow Rational to take decimal strings, per

Raymond's advice.
üst 46c61b2c
...@@ -14,8 +14,8 @@ __all__ = ["Rational"] ...@@ -14,8 +14,8 @@ __all__ = ["Rational"]
RationalAbc = numbers.Rational RationalAbc = numbers.Rational
def _gcd(a, b): # XXX This is a useful function. Consider making it public. def gcd(a, b):
"""Calculate the Greatest Common Divisor. """Calculate the Greatest Common Divisor of a and b.
Unless b==0, the result will have the same sign as b (so that when Unless b==0, the result will have the same sign as b (so that when
b is divided by it, the result comes out positive). b is divided by it, the result comes out positive).
...@@ -40,8 +40,8 @@ def _binary_float_to_ratio(x): ...@@ -40,8 +40,8 @@ def _binary_float_to_ratio(x):
>>> _binary_float_to_ratio(-.25) >>> _binary_float_to_ratio(-.25)
(-1, 4) (-1, 4)
""" """
# XXX Consider moving this to to floatobject.c # XXX Move this to floatobject.c with a name like
# with a name like float.as_intger_ratio() # float.as_integer_ratio()
if x == 0: if x == 0:
return 0, 1 return 0, 1
...@@ -80,12 +80,9 @@ def _binary_float_to_ratio(x): ...@@ -80,12 +80,9 @@ def _binary_float_to_ratio(x):
_RATIONAL_FORMAT = re.compile( _RATIONAL_FORMAT = re.compile(
r'^\s*(?P<sign>[-+]?)(?P<num>\d+)(?:/(?P<denom>\d+))?\s*$') r'^\s*(?P<sign>[-+]?)(?P<num>\d+)'
r'(?:/(?P<denom>\d+)|\.(?P<decimal>\d+))?\s*$')
# XXX Consider accepting decimal strings as input since they are exact.
# Rational("2.01") --> s="2.01" ; Rational.from_decimal(Decimal(s)) --> Rational(201, 100)"
# If you want to avoid going through the decimal module, just parse the string directly:
# s.partition('.') --> ('2', '.', '01') --> Rational(int('2'+'01'), 10**len('01')) --> Rational(201, 100)
class Rational(RationalAbc): class Rational(RationalAbc):
"""This class implements rational numbers. """This class implements rational numbers.
...@@ -96,7 +93,7 @@ class Rational(RationalAbc): ...@@ -96,7 +93,7 @@ class Rational(RationalAbc):
Rational() == 0. Rational() == 0.
Rationals can also be constructed from strings of the form Rationals can also be constructed from strings of the form
'[-+]?[0-9]+(/[0-9]+)?', optionally surrounded by spaces. '[-+]?[0-9]+((/|.)[0-9]+)?', optionally surrounded by spaces.
""" """
...@@ -106,7 +103,8 @@ class Rational(RationalAbc): ...@@ -106,7 +103,8 @@ class Rational(RationalAbc):
def __new__(cls, numerator=0, denominator=1): def __new__(cls, numerator=0, denominator=1):
"""Constructs a Rational. """Constructs a Rational.
Takes a string, another Rational, or a numerator/denominator pair. Takes a string like '3/2' or '3.2', another Rational, or a
numerator/denominator pair.
""" """
self = super(Rational, cls).__new__(cls) self = super(Rational, cls).__new__(cls)
...@@ -118,9 +116,18 @@ class Rational(RationalAbc): ...@@ -118,9 +116,18 @@ class Rational(RationalAbc):
m = _RATIONAL_FORMAT.match(input) m = _RATIONAL_FORMAT.match(input)
if m is None: if m is None:
raise ValueError('Invalid literal for Rational: ' + input) raise ValueError('Invalid literal for Rational: ' + input)
numerator = int(m.group('num')) numerator = m.group('num')
# Default denominator to 1. That's the only optional group. decimal = m.group('decimal')
denominator = int(m.group('denom') or 1) if decimal:
# The literal is a decimal number.
numerator = int(numerator + decimal)
denominator = 10**len(decimal)
else:
# The literal is an integer or fraction.
numerator = int(numerator)
# Default denominator to 1.
denominator = int(m.group('denom') or 1)
if m.group('sign') == '-': if m.group('sign') == '-':
numerator = -numerator numerator = -numerator
...@@ -139,7 +146,7 @@ class Rational(RationalAbc): ...@@ -139,7 +146,7 @@ class Rational(RationalAbc):
if denominator == 0: if denominator == 0:
raise ZeroDivisionError('Rational(%s, 0)' % numerator) raise ZeroDivisionError('Rational(%s, 0)' % numerator)
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 return self
......
...@@ -9,10 +9,28 @@ import unittest ...@@ -9,10 +9,28 @@ import unittest
from copy import copy, deepcopy from copy import copy, deepcopy
from cPickle import dumps, loads from cPickle import dumps, loads
R = rational.Rational R = rational.Rational
gcd = rational.gcd
class GcdTest(unittest.TestCase):
def testMisc(self):
self.assertEquals(0, gcd(0, 0))
self.assertEquals(1, gcd(1, 0))
self.assertEquals(-1, gcd(-1, 0))
self.assertEquals(1, gcd(0, 1))
self.assertEquals(-1, gcd(0, -1))
self.assertEquals(1, gcd(7, 1))
self.assertEquals(-1, gcd(7, -1))
self.assertEquals(1, gcd(-23, 15))
self.assertEquals(12, gcd(120, 84))
self.assertEquals(-12, gcd(84, -120))
def _components(r): def _components(r):
return (r.numerator, r.denominator) return (r.numerator, r.denominator)
class RationalTest(unittest.TestCase): class RationalTest(unittest.TestCase):
def assertTypedEquals(self, expected, actual): def assertTypedEquals(self, expected, actual):
...@@ -55,8 +73,12 @@ class RationalTest(unittest.TestCase): ...@@ -55,8 +73,12 @@ class RationalTest(unittest.TestCase):
self.assertEquals((3, 2), _components(R("3/2"))) self.assertEquals((3, 2), _components(R("3/2")))
self.assertEquals((3, 2), _components(R(" \n +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("-3/2 ")))
self.assertEquals((3, 2), _components(R(" 03/02 \n "))) self.assertEquals((13, 2), _components(R(" 013/02 \n ")))
self.assertEquals((3, 2), _components(R(u" 03/02 \n "))) self.assertEquals((13, 2), _components(R(u" 013/02 \n ")))
self.assertEquals((16, 5), _components(R(" 3.2 ")))
self.assertEquals((-16, 5), _components(R(u" -3.2 ")))
self.assertRaisesMessage( self.assertRaisesMessage(
ZeroDivisionError, "Rational(3, 0)", ZeroDivisionError, "Rational(3, 0)",
...@@ -76,9 +98,21 @@ class RationalTest(unittest.TestCase): ...@@ -76,9 +98,21 @@ class RationalTest(unittest.TestCase):
ValueError, "Invalid literal for Rational: + 3/2", ValueError, "Invalid literal for Rational: + 3/2",
R, "+ 3/2") R, "+ 3/2")
self.assertRaisesMessage( self.assertRaisesMessage(
# Only parse fractions, not decimals. # Avoid treating '.' as a regex special character.
ValueError, "Invalid literal for Rational: 3.2", ValueError, "Invalid literal for Rational: 3a2",
R, "3.2") R, "3a2")
self.assertRaisesMessage(
# Only parse ordinary decimals, not scientific form.
ValueError, "Invalid literal for Rational: 3.2e4",
R, "3.2e4")
self.assertRaisesMessage(
# Don't accept combinations of decimals and rationals.
ValueError, "Invalid literal for Rational: 3/7.2",
R, "3/7.2")
self.assertRaisesMessage(
# Don't accept combinations of decimals and rationals.
ValueError, "Invalid literal for Rational: 3.2/7",
R, "3.2/7")
def testImmutable(self): def testImmutable(self):
r = R(7, 3) r = R(7, 3)
...@@ -368,7 +402,7 @@ class RationalTest(unittest.TestCase): ...@@ -368,7 +402,7 @@ class RationalTest(unittest.TestCase):
self.assertEqual(id(r), id(deepcopy(r))) self.assertEqual(id(r), id(deepcopy(r)))
def test_main(): def test_main():
run_unittest(RationalTest) run_unittest(RationalTest, GcdTest)
if __name__ == '__main__': if __name__ == '__main__':
test_main() test_main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment