Skip to content

bpo-37822: Add math.as_integer_ratio(). #15210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Doc/library/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ noted otherwise, all return values are floats.
Number-theoretic and representation functions
---------------------------------------------

.. function:: as_integer_ratio(number)

Return integer ratio.

This function returns ``number.as_integer_ratio()`` if the argument has
the ``as_integer_ratio()`` method, otherwise it returns a pair
``(number.numerator, number.denominator)``.

.. versionadded:: 3.9


.. function:: ceil(x)

Return the ceiling of *x*, the smallest integer greater than or equal to *x*.
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ New Modules
Improved Modules
================


math
----

Added new function :func:`math.as_integer_ratio`.
(Contributed by Serhiy Storchaka in :issue:``.)


threading
---------

Expand Down
26 changes: 7 additions & 19 deletions Lib/fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,22 +115,7 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True):
self = super(Fraction, cls).__new__(cls)

if denominator is None:
if type(numerator) is int:
self._numerator = numerator
self._denominator = 1
return self

elif isinstance(numerator, numbers.Rational):
self._numerator = numerator.numerator
self._denominator = numerator.denominator
return self

elif isinstance(numerator, (float, Decimal)):
# Exact conversion
self._numerator, self._denominator = numerator.as_integer_ratio()
return self

elif isinstance(numerator, str):
if isinstance(numerator, str):
# Handle construction from strings.
m = _RATIONAL_FORMAT.match(numerator)
if m is None:
Expand All @@ -156,10 +141,13 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True):
denominator *= 10**-exp
if m.group('sign') == '-':
numerator = -numerator

else:
raise TypeError("argument should be a string "
"or a Rational instance")
try:
self._numerator, self._denominator = math.as_integer_ratio(numerator)
return self
except TypeError:
raise TypeError("argument should be a string or a number, "
"not %s" % type(numerator).__name__) from None

elif type(numerator) is int is type(denominator):
pass # *very* normal case
Expand Down
18 changes: 1 addition & 17 deletions Lib/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,27 +225,11 @@ def _exact_ratio(x):
x is expected to be an int, Fraction, Decimal or float.
"""
try:
# Optimise the common case of floats. We expect that the most often
# used numeric type will be builtin floats, so try to make this as
# fast as possible.
if type(x) is float or type(x) is Decimal:
return x.as_integer_ratio()
try:
# x may be an int, Fraction, or Integral ABC.
return (x.numerator, x.denominator)
except AttributeError:
try:
# x may be a float or Decimal subclass.
return x.as_integer_ratio()
except AttributeError:
# Just give up?
pass
return math.as_integer_ratio(x)
except (OverflowError, ValueError):
# float NAN or INF.
assert not _isfinite(x)
return (x, None)
msg = "can't convert type '{}' to numerator/denominator"
raise TypeError(msg.format(type(x).__name__))


def _convert(value, T):
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import random
import struct
import sys
from decimal import Decimal
from fractions import Fraction


eps = 1E-05
Expand Down Expand Up @@ -293,6 +295,33 @@ def testAcosh(self):
self.assertRaises(ValueError, math.acosh, NINF)
self.assertTrue(math.isnan(math.acosh(NAN)))

def testAsIntegerRatio(self):
as_integer_ratio = math.as_integer_ratio
self.assertEqual(as_integer_ratio(0), (0, 1))
self.assertEqual(as_integer_ratio(3), (3, 1))
self.assertEqual(as_integer_ratio(-3), (-3, 1))
self.assertEqual(as_integer_ratio(False), (0, 1))
self.assertEqual(as_integer_ratio(True), (1, 1))
self.assertEqual(as_integer_ratio(0.0), (0, 1))
self.assertEqual(as_integer_ratio(-0.0), (0, 1))
self.assertEqual(as_integer_ratio(0.875), (7, 8))
self.assertEqual(as_integer_ratio(-0.875), (-7, 8))
self.assertEqual(as_integer_ratio(Decimal('0')), (0, 1))
self.assertEqual(as_integer_ratio(Decimal('0.875')), (7, 8))
self.assertEqual(as_integer_ratio(Decimal('-0.875')), (-7, 8))
self.assertEqual(as_integer_ratio(Fraction(0)), (0, 1))
self.assertEqual(as_integer_ratio(Fraction(7, 8)), (7, 8))
self.assertEqual(as_integer_ratio(Fraction(-7, 8)), (-7, 8))

self.assertRaises(OverflowError, as_integer_ratio, float('inf'))
self.assertRaises(OverflowError, as_integer_ratio, float('-inf'))
self.assertRaises(ValueError, as_integer_ratio, float('nan'))
self.assertRaises(OverflowError, as_integer_ratio, Decimal('inf'))
self.assertRaises(OverflowError, as_integer_ratio, Decimal('-inf'))
self.assertRaises(ValueError, as_integer_ratio, Decimal('nan'))

self.assertRaises(TypeError, as_integer_ratio, '0')

def testAsin(self):
self.assertRaises(TypeError, math.asin)
self.ftest('asin(-1)', math.asin(-1), -math.pi/2)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :func:`math.as_integer_ratio`.
11 changes: 10 additions & 1 deletion Modules/clinic/mathmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -3306,9 +3306,83 @@ math_comb_impl(PyObject *module, PyObject *n, PyObject *k)
}


/*[clinic input]
math.as_integer_ratio
x: object
/
greatest common divisor of x and y
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a copy-paste error. Maybe:

Suggested change
greatest common divisor of x and y
Return the rational representation of x as the pair (numerator, denominator).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. But this proposition was rejected.

[clinic start generated code]*/

static PyObject *
math_as_integer_ratio(PyObject *module, PyObject *x)
/*[clinic end generated code: output=1844868fd4efb2f1 input=d7f2e8ffd51c6599]*/
{
_Py_IDENTIFIER(as_integer_ratio);
_Py_IDENTIFIER(numerator);
_Py_IDENTIFIER(denominator);
PyObject *ratio, *as_integer_ratio, *numerator, *denominator;

if (PyLong_CheckExact(x)) {
return PyTuple_Pack(2, x, _PyLong_One);
}

if (_PyObject_LookupAttrId(x, &PyId_as_integer_ratio, &as_integer_ratio) < 0) {
return NULL;
}
if (as_integer_ratio) {
ratio = _PyObject_CallNoArg(as_integer_ratio);
Py_DECREF(as_integer_ratio);
if (ratio == NULL) {
return NULL;
}
if (!PyTuple_Check(ratio)) {
PyErr_Format(PyExc_TypeError,
"unexpected return type from as_integer_ratio(): "
"expected tuple, got '%.200s'",
Py_TYPE(ratio)->tp_name);
Py_DECREF(ratio);
return NULL;
}
if (PyTuple_GET_SIZE(ratio) != 2) {
PyErr_SetString(PyExc_ValueError,
"as_integer_ratio() must return a 2-tuple");
Py_DECREF(ratio);
return NULL;
}
}
else {
if (_PyObject_LookupAttrId(x, &PyId_numerator, &numerator) < 0) {
return NULL;
}
if (numerator == NULL) {
PyErr_Format(PyExc_TypeError,
"required a number, not '%.200s'",
Py_TYPE(x)->tp_name);
return NULL;
}
if (_PyObject_LookupAttrId(x, &PyId_denominator, &denominator) < 0) {
Py_DECREF(numerator);
return NULL;
}
if (denominator == NULL) {
Py_DECREF(numerator);
PyErr_Format(PyExc_TypeError,
"required a number, not '%.200s'",
Py_TYPE(x)->tp_name);
return NULL;
}
ratio = PyTuple_Pack(2, numerator, denominator);
Py_DECREF(numerator);
Py_DECREF(denominator);
}
return ratio;
}


static PyMethodDef math_methods[] = {
{"acos", math_acos, METH_O, math_acos_doc},
{"acosh", math_acosh, METH_O, math_acosh_doc},
MATH_AS_INTEGER_RATIO_METHODDEF
{"asin", math_asin, METH_O, math_asin_doc},
{"asinh", math_asinh, METH_O, math_asinh_doc},
{"atan", math_atan, METH_O, math_atan_doc},
Expand Down