mirror of https://github.com/python/cpython
gh-121797: Add class method Fraction.from_number() (GH-121800)
It is an alternative constructor which only accepts a single numeric argument. Unlike to Fraction.from_float() and Fraction.from_decimal() it accepts any real numbers supported by the standard constructor (int, float, Decimal, Rational numbers, objects with as_integer_ratio()). Unlike to the standard constructor, it does not accept strings.
This commit is contained in:
parent
66b3922b97
commit
b52c7306ea
|
@ -166,6 +166,16 @@ another rational number, or from a string.
|
||||||
instance.
|
instance.
|
||||||
|
|
||||||
|
|
||||||
|
.. classmethod:: from_number(number)
|
||||||
|
|
||||||
|
Alternative constructor which only accepts instances of
|
||||||
|
:class:`numbers.Integral`, :class:`numbers.Rational`,
|
||||||
|
:class:`float` or :class:`decimal.Decimal`, and objects with
|
||||||
|
the :meth:`!as_integer_ratio` method, but not strings.
|
||||||
|
|
||||||
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
||||||
.. method:: limit_denominator(max_denominator=1000000)
|
.. method:: limit_denominator(max_denominator=1000000)
|
||||||
|
|
||||||
Finds and returns the closest :class:`Fraction` to ``self`` that has
|
Finds and returns the closest :class:`Fraction` to ``self`` that has
|
||||||
|
|
|
@ -263,6 +263,10 @@ fractions
|
||||||
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
|
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
|
||||||
(Contributed by Serhiy Storchaka in :gh:`82017`.)
|
(Contributed by Serhiy Storchaka in :gh:`82017`.)
|
||||||
|
|
||||||
|
* Add alternative :class:`~fractions.Fraction` constructor
|
||||||
|
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.
|
||||||
|
(Contributed by Serhiy Storchaka in :gh:`121797`.)
|
||||||
|
|
||||||
|
|
||||||
functools
|
functools
|
||||||
---------
|
---------
|
||||||
|
|
|
@ -279,7 +279,8 @@ class Fraction(numbers.Rational):
|
||||||
numerator = -numerator
|
numerator = -numerator
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise TypeError("argument should be a string or a number")
|
raise TypeError("argument should be a string or a Rational "
|
||||||
|
"instance or have the as_integer_ratio() method")
|
||||||
|
|
||||||
elif type(numerator) is int is type(denominator):
|
elif type(numerator) is int is type(denominator):
|
||||||
pass # *very* normal case
|
pass # *very* normal case
|
||||||
|
@ -305,6 +306,28 @@ class Fraction(numbers.Rational):
|
||||||
self._denominator = denominator
|
self._denominator = denominator
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_number(cls, number):
|
||||||
|
"""Converts a finite real number to a rational number, exactly.
|
||||||
|
|
||||||
|
Beware that Fraction.from_number(0.3) != Fraction(3, 10).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if type(number) is int:
|
||||||
|
return cls._from_coprime_ints(number, 1)
|
||||||
|
|
||||||
|
elif isinstance(number, numbers.Rational):
|
||||||
|
return cls._from_coprime_ints(number.numerator, number.denominator)
|
||||||
|
|
||||||
|
elif (isinstance(number, float) or
|
||||||
|
(not isinstance(number, type) and
|
||||||
|
hasattr(number, 'as_integer_ratio'))):
|
||||||
|
return cls._from_coprime_ints(*number.as_integer_ratio())
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError("argument should be a Rational instance or "
|
||||||
|
"have the as_integer_ratio() method")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_float(cls, f):
|
def from_float(cls, f):
|
||||||
"""Converts a finite float to a rational number, exactly.
|
"""Converts a finite float to a rational number, exactly.
|
||||||
|
|
|
@ -283,6 +283,13 @@ numbers.Complex.register(Rect)
|
||||||
class RectComplex(Rect, complex):
|
class RectComplex(Rect, complex):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class Ratio:
|
||||||
|
def __init__(self, ratio):
|
||||||
|
self._ratio = ratio
|
||||||
|
def as_integer_ratio(self):
|
||||||
|
return self._ratio
|
||||||
|
|
||||||
|
|
||||||
class FractionTest(unittest.TestCase):
|
class FractionTest(unittest.TestCase):
|
||||||
|
|
||||||
def assertTypedEquals(self, expected, actual):
|
def assertTypedEquals(self, expected, actual):
|
||||||
|
@ -355,14 +362,9 @@ class FractionTest(unittest.TestCase):
|
||||||
self.assertRaises(OverflowError, F, Decimal('-inf'))
|
self.assertRaises(OverflowError, F, Decimal('-inf'))
|
||||||
|
|
||||||
def testInitFromIntegerRatio(self):
|
def testInitFromIntegerRatio(self):
|
||||||
class Ratio:
|
|
||||||
def __init__(self, ratio):
|
|
||||||
self._ratio = ratio
|
|
||||||
def as_integer_ratio(self):
|
|
||||||
return self._ratio
|
|
||||||
|
|
||||||
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
|
self.assertEqual((7, 3), _components(F(Ratio((7, 3)))))
|
||||||
errmsg = "argument should be a string or a number"
|
errmsg = (r"argument should be a string or a Rational instance or "
|
||||||
|
r"have the as_integer_ratio\(\) method")
|
||||||
# the type also has an "as_integer_ratio" attribute.
|
# the type also has an "as_integer_ratio" attribute.
|
||||||
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
|
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
|
||||||
# bad ratio
|
# bad ratio
|
||||||
|
@ -388,6 +390,8 @@ class FractionTest(unittest.TestCase):
|
||||||
pass
|
pass
|
||||||
self.assertRaisesRegex(TypeError, errmsg, F, B)
|
self.assertRaisesRegex(TypeError, errmsg, F, B)
|
||||||
self.assertRaisesRegex(TypeError, errmsg, F, B())
|
self.assertRaisesRegex(TypeError, errmsg, F, B())
|
||||||
|
self.assertRaises(TypeError, F.from_number, B)
|
||||||
|
self.assertRaises(TypeError, F.from_number, B())
|
||||||
|
|
||||||
def testFromString(self):
|
def testFromString(self):
|
||||||
self.assertEqual((5, 1), _components(F("5")))
|
self.assertEqual((5, 1), _components(F("5")))
|
||||||
|
@ -594,6 +598,37 @@ class FractionTest(unittest.TestCase):
|
||||||
ValueError, "cannot convert NaN to integer ratio",
|
ValueError, "cannot convert NaN to integer ratio",
|
||||||
F.from_decimal, Decimal("snan"))
|
F.from_decimal, Decimal("snan"))
|
||||||
|
|
||||||
|
def testFromNumber(self, cls=F):
|
||||||
|
def check(arg, numerator, denominator):
|
||||||
|
f = cls.from_number(arg)
|
||||||
|
self.assertIs(type(f), cls)
|
||||||
|
self.assertEqual(f.numerator, numerator)
|
||||||
|
self.assertEqual(f.denominator, denominator)
|
||||||
|
|
||||||
|
check(10, 10, 1)
|
||||||
|
check(2.5, 5, 2)
|
||||||
|
check(Decimal('2.5'), 5, 2)
|
||||||
|
check(F(22, 7), 22, 7)
|
||||||
|
check(DummyFraction(22, 7), 22, 7)
|
||||||
|
check(Rat(22, 7), 22, 7)
|
||||||
|
check(Ratio((22, 7)), 22, 7)
|
||||||
|
self.assertRaises(TypeError, cls.from_number, 3+4j)
|
||||||
|
self.assertRaises(TypeError, cls.from_number, '5/2')
|
||||||
|
self.assertRaises(TypeError, cls.from_number, [])
|
||||||
|
self.assertRaises(OverflowError, cls.from_number, float('inf'))
|
||||||
|
self.assertRaises(OverflowError, cls.from_number, Decimal('inf'))
|
||||||
|
|
||||||
|
# as_integer_ratio not defined in a class
|
||||||
|
class A:
|
||||||
|
pass
|
||||||
|
a = A()
|
||||||
|
a.as_integer_ratio = lambda: (9, 5)
|
||||||
|
check(a, 9, 5)
|
||||||
|
|
||||||
|
def testFromNumber_subclass(self):
|
||||||
|
self.testFromNumber(DummyFraction)
|
||||||
|
|
||||||
|
|
||||||
def test_is_integer(self):
|
def test_is_integer(self):
|
||||||
self.assertTrue(F(1, 1).is_integer())
|
self.assertTrue(F(1, 1).is_integer())
|
||||||
self.assertTrue(F(-1, 1).is_integer())
|
self.assertTrue(F(-1, 1).is_integer())
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Add alternative :class:`~fractions.Fraction` constructor
|
||||||
|
:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.
|
Loading…
Reference in New Issue