mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
Make rational.gcd() public and allow Rational to take decimal strings, per
Raymond's advice.
This commit is contained in:
parent
46c61b2c1d
commit
3e1a373616
2 changed files with 62 additions and 21 deletions
|
@ -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):
|
||||||
>>> _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):
|
||||||
|
|
||||||
|
|
||||||
_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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue