Skip to content

Commit

Permalink
gh-82017: Support as_integer_ratio() in the Fraction constructor (GH-…
Browse files Browse the repository at this point in the history
…120271)

Any objects that have the as_integer_ratio() method (e.g. numpy.float128)
can now be converted to a fraction.
  • Loading branch information
serhiy-storchaka committed Jul 19, 2024
1 parent eaf094c commit c8d2630
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 13 deletions.
27 changes: 18 additions & 9 deletions Doc/library/fractions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@ The :mod:`fractions` module provides support for rational number arithmetic.
A Fraction instance can be constructed from a pair of integers, from
another rational number, or from a string.

.. index:: single: as_integer_ratio()

.. class:: Fraction(numerator=0, denominator=1)
Fraction(other_fraction)
Fraction(float)
Fraction(decimal)
Fraction(number)
Fraction(string)

The first version requires that *numerator* and *denominator* are instances
of :class:`numbers.Rational` and returns a new :class:`Fraction` instance
with value ``numerator/denominator``. If *denominator* is ``0``, it
raises a :exc:`ZeroDivisionError`. The second version requires that
*other_fraction* is an instance of :class:`numbers.Rational` and returns a
:class:`Fraction` instance with the same value. The next two versions accept
either a :class:`float` or a :class:`decimal.Decimal` instance, and return a
:class:`Fraction` instance with exactly the same value. Note that due to the
raises a :exc:`ZeroDivisionError`.

The second version requires that *number* is an instance of
:class:`numbers.Rational` or has the :meth:`!as_integer_ratio` method
(this includes :class:`float` and :class:`decimal.Decimal`).
It returns a :class:`Fraction` instance with exactly the same value.
Assumed, that the :meth:`!as_integer_ratio` method returns a pair
of coprime integers and last one is positive.
Note that due to the
usual issues with binary floating-point (see :ref:`tut-fp-issues`), the
argument to ``Fraction(1.1)`` is not exactly equal to 11/10, and so
``Fraction(1.1)`` does *not* return ``Fraction(11, 10)`` as one might expect.
(But see the documentation for the :meth:`limit_denominator` method below.)
The last version of the constructor expects a string or unicode instance.

The last version of the constructor expects a string.
The usual form for this instance is::

[sign] numerator ['/' denominator]
Expand Down Expand Up @@ -110,6 +115,10 @@ another rational number, or from a string.
Formatting of :class:`Fraction` instances without a presentation type
now supports fill, alignment, sign handling, minimum width and grouping.

.. versionchanged:: 3.14
The :class:`Fraction` constructor now accepts any objects with the
:meth:`!as_integer_ratio` method.

.. attribute:: numerator

Numerator of the Fraction in lowest term.
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ ast

(Contributed by Bénédikt Tran in :gh:`121141`.)

fractions
---------

Added support for converting any objects that have the
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
(Contributed by Serhiy Storchaka in :gh:`82017`.)

os
--

Expand Down
8 changes: 4 additions & 4 deletions Lib/fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

"""Fraction, infinite-precision, rational numbers."""

from decimal import Decimal
import functools
import math
import numbers
Expand Down Expand Up @@ -244,7 +243,9 @@ def __new__(cls, numerator=0, denominator=None):
self._denominator = numerator.denominator
return self

elif isinstance(numerator, (float, Decimal)):
elif (isinstance(numerator, float) or
(not isinstance(numerator, type) and
hasattr(numerator, 'as_integer_ratio'))):
# Exact conversion
self._numerator, self._denominator = numerator.as_integer_ratio()
return self
Expand Down Expand Up @@ -278,8 +279,7 @@ def __new__(cls, numerator=0, denominator=None):
numerator = -numerator

else:
raise TypeError("argument should be a string "
"or a Rational instance")
raise TypeError("argument should be a string or a number")

elif type(numerator) is int is type(denominator):
pass # *very* normal case
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,41 @@ def testInitFromDecimal(self):
self.assertRaises(OverflowError, F, Decimal('inf'))
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"
# the type also has an "as_integer_ratio" attribute.
self.assertRaisesRegex(TypeError, errmsg, F, Ratio)
# bad ratio
self.assertRaises(TypeError, F, Ratio(7))
self.assertRaises(ValueError, F, Ratio((7,)))
self.assertRaises(ValueError, F, Ratio((7, 3, 1)))
# only single-argument form
self.assertRaises(TypeError, F, Ratio((3, 7)), 11)
self.assertRaises(TypeError, F, 2, Ratio((-10, 9)))

# as_integer_ratio not defined in a class
class A:
pass
a = A()
a.as_integer_ratio = lambda: (9, 5)
self.assertEqual((9, 5), _components(F(a)))

# as_integer_ratio defined in a metaclass
class M(type):
def as_integer_ratio(self):
return (11, 9)
class B(metaclass=M):
pass
self.assertRaisesRegex(TypeError, errmsg, F, B)
self.assertRaisesRegex(TypeError, errmsg, F, B())

def testFromString(self):
self.assertEqual((5, 1), _components(F("5")))
self.assertEqual((3, 2), _components(F("3/2")))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for converting any objects that have the
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.

0 comments on commit c8d2630

Please sign in to comment.