]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-121797: Add class method Fraction.from_number() (GH-121800)
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 14 Oct 2024 07:54:59 +0000 (10:54 +0300)
committerGitHub <noreply@github.com>
Mon, 14 Oct 2024 07:54:59 +0000 (07:54 +0000)
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.

Doc/library/fractions.rst
Doc/whatsnew/3.14.rst
Lib/fractions.py
Lib/test/test_fractions.py
Misc/NEWS.d/next/Library/2024-07-15-19-34-56.gh-issue-121797.qDqj59.rst [new file with mode: 0644]

index 2ee154952549acae81d90b633967a86e409ae554..fc7f9a6301a9153f945b227021c775ba7932133f 100644 (file)
@@ -166,6 +166,16 @@ another rational number, or from a string.
          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)
 
       Finds and returns the closest :class:`Fraction` to ``self`` that has
index c62a3ca5872eefde0ffb6bb3a053feb22b5c5343..b22d1bd1e99d4e5df88f946f88ac3cc9396a6abe 100644 (file)
@@ -263,6 +263,10 @@ fractions
   :meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
   (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
 ---------
index 34fd0803d1b1ab6c3c9c044b735143504e501c61..f0cbc8c2e6c012aa4a21257e575d780bf9492e94 100644 (file)
@@ -279,7 +279,8 @@ class Fraction(numbers.Rational):
                     numerator = -numerator
 
             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):
             pass # *very* normal case
@@ -305,6 +306,28 @@ class Fraction(numbers.Rational):
         self._denominator = denominator
         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
     def from_float(cls, f):
         """Converts a finite float to a rational number, exactly.
index 4907f4093f52c9e3356b77cada2bae1a00b34b26..98dccbec9566acced6231a3e0a84b7d51e75cf68 100644 (file)
@@ -283,6 +283,13 @@ numbers.Complex.register(Rect)
 class RectComplex(Rect, complex):
     pass
 
+class Ratio:
+    def __init__(self, ratio):
+        self._ratio = ratio
+    def as_integer_ratio(self):
+        return self._ratio
+
+
 class FractionTest(unittest.TestCase):
 
     def assertTypedEquals(self, expected, actual):
@@ -355,14 +362,9 @@ class FractionTest(unittest.TestCase):
         self.assertRaises(OverflowError, F, Decimal('-inf'))
 
     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)))))
-        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.
         self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
         # bad ratio
@@ -388,6 +390,8 @@ class FractionTest(unittest.TestCase):
             pass
         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):
         self.assertEqual((5, 1), _components(F("5")))
@@ -594,6 +598,37 @@ class FractionTest(unittest.TestCase):
             ValueError, "cannot convert NaN to integer ratio",
             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):
         self.assertTrue(F(1, 1).is_integer())
         self.assertTrue(F(-1, 1).is_integer())
diff --git a/Misc/NEWS.d/next/Library/2024-07-15-19-34-56.gh-issue-121797.qDqj59.rst b/Misc/NEWS.d/next/Library/2024-07-15-19-34-56.gh-issue-121797.qDqj59.rst
new file mode 100644 (file)
index 0000000..9525379
--- /dev/null
@@ -0,0 +1,2 @@
+Add alternative :class:`~fractions.Fraction` constructor
+:meth:`Fraction.from_number() <fractions.Fraction.from_number>`.