Skip to content

Commit 79799b3

Browse files
committed
ENH: Implement np.floating.as_integer_ratio
This matches the builtin `float.as_integer_ratio` and (in recent python versions) `int.as_integer_ratio`.
1 parent d7a73f8 commit 79799b3

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

doc/release/1.17.0-notes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ New mode "empty" for ``np.pad``
129129
This mode pads an array to a desired shape without initializing the new
130130
entries.
131131

132+
Floating point scalars implement ``as_integer_ratio`` to match the builtin float
133+
--------------------------------------------------------------------------------
134+
This returns a (numerator, denominator) pair, which can be used to construct a
135+
`fractions.Fraction`.
136+
137+
132138
Improvements
133139
============
134140

numpy/core/_add_newdocs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6903,3 +6903,21 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
69036903
"""
69046904
Any Python object.
69056905
""")
6906+
6907+
# TODO: work out how to put this on the base class, np.floating
6908+
for float_name in ('half', 'single', 'double', 'longdouble'):
6909+
add_newdoc('numpy.core.numerictypes', float_name, ('as_integer_ratio',
6910+
"""
6911+
{ftype}.as_integer_ratio() -> (int, int)
6912+
6913+
Return a pair of integers, whose ratio is exactly equal to the original
6914+
floating point number, and with a positive denominator.
6915+
Raise OverflowError on infinities and a ValueError on NaNs.
6916+
6917+
>>> np.{ftype}(10.0).as_integer_ratio()
6918+
(10, 1)
6919+
>>> np.{ftype}(0.0).as_integer_ratio()
6920+
(0, 1)
6921+
>>> np.{ftype}(-.25).as_integer_ratio()
6922+
(-1, 4)
6923+
""".format(ftype=float_name)))

numpy/core/src/multiarray/scalartypes.c.src

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1993,6 +1993,92 @@ static PyObject *
19931993
}
19941994
/**end repeat**/
19951995

1996+
/**begin repeat
1997+
* #name = half, float, double, longdouble#
1998+
* #Name = Half, Float, Double, LongDouble#
1999+
* #is_half = 1,0,0,0#
2000+
* #c = f, f, , l#
2001+
* #convert = PyLong_FromDouble, PyLong_FromDouble, PyLong_FromDouble,
2002+
* npy_longdouble_to_PyLong#
2003+
* #
2004+
*/
2005+
/* Heavily copied from the builtin float.as_integer_ratio */
2006+
static PyObject *
2007+
@name@_as_integer_ratio(PyObject *self)
2008+
{
2009+
#if @is_half@
2010+
npy_double val = npy_half_to_double(PyArrayScalar_VAL(self, @Name@));
2011+
npy_double frac;
2012+
#else
2013+
npy_@name@ val = PyArrayScalar_VAL(self, @Name@);
2014+
npy_@name@ frac;
2015+
#endif
2016+
int exponent;
2017+
int i;
2018+
2019+
PyObject *py_exponent = NULL;
2020+
PyObject *numerator = NULL;
2021+
PyObject *denominator = NULL;
2022+
PyObject *result_pair = NULL;
2023+
PyNumberMethods *long_methods = PyLong_Type.tp_as_number;
2024+
2025+
if (npy_isnan(val)) {
2026+
PyErr_SetString(PyExc_ValueError,
2027+
"cannot convert NaN to integer ratio");
2028+
return NULL;
2029+
}
2030+
if (!npy_isfinite(val)) {
2031+
PyErr_SetString(PyExc_OverflowError,
2032+
"cannot convert Infinity to integer ratio");
2033+
return NULL;
2034+
}
2035+
2036+
frac = npy_frexp@c@(val, &exponent); /* val == frac * 2**exponent exactly */
2037+
2038+
/* This relies on the floating point type being base 2 to converge */
2039+
for (i = 0; frac != npy_floor@c@(frac); i++) {
2040+
frac *= 2.0;
2041+
exponent--;
2042+
}
2043+
2044+
/* self == frac * 2**exponent exactly and frac is integral. */
2045+
numerator = @convert@(frac);
2046+
if (numerator == NULL)
2047+
goto error;
2048+
denominator = PyLong_FromLong(1);
2049+
if (denominator == NULL)
2050+
goto error;
2051+
py_exponent = PyLong_FromLong(exponent < 0 ? -exponent : exponent);
2052+
if (py_exponent == NULL)
2053+
goto error;
2054+
2055+
/* fold in 2**exponent */
2056+
if (exponent > 0) {
2057+
PyObject *temp = long_methods->nb_lshift(numerator, py_exponent);
2058+
if (temp == NULL)
2059+
goto error;
2060+
Py_DECREF(numerator);
2061+
numerator = temp;
2062+
}
2063+
else {
2064+
PyObject *temp = long_methods->nb_lshift(denominator, py_exponent);
2065+
if (temp == NULL)
2066+
goto error;
2067+
Py_DECREF(denominator);
2068+
denominator = temp;
2069+
}
2070+
2071+
result_pair = PyTuple_Pack(2, numerator, denominator);
2072+
2073+
error:
2074+
Py_XDECREF(py_exponent);
2075+
Py_XDECREF(denominator);
2076+
Py_XDECREF(numerator);
2077+
return result_pair;
2078+
}
2079+
/**end repeat**/
2080+
2081+
19962082
/*
19972083
* need to fill in doc-strings for these methods on import -- copy from
19982084
* array docstrings
@@ -2256,6 +2342,17 @@ static PyMethodDef @name@type_methods[] = {
22562342
};
22572343
/**end repeat**/
22582344

2345+
/**begin repeat
2346+
* #name = half,float,double,longdouble#
2347+
*/
2348+
static PyMethodDef @name@type_methods[] = {
2349+
{"as_integer_ratio",
2350+
(PyCFunction)@name@_as_integer_ratio,
2351+
METH_NOARGS, NULL},
2352+
{NULL, NULL, 0, NULL}
2353+
};
2354+
/**end repeat**/
2355+
22592356
/************* As_mapping functions for void array scalar ************/
22602357

22612358
static Py_ssize_t
@@ -4311,6 +4408,15 @@ initialize_numeric_types(void)
43114408

43124409
/**end repeat**/
43134410

4411+
/**begin repeat
4412+
* #name = half, float, double, longdouble#
4413+
* #Name = Half, Float, Double, LongDouble#
4414+
*/
4415+
4416+
Py@Name@ArrType_Type.tp_methods = @name@type_methods;
4417+
4418+
/**end repeat**/
4419+
43144420
#if (NPY_SIZEOF_INT != NPY_SIZEOF_LONG) || defined(NPY_PY3K)
43154421
/* We won't be inheriting from Python Int type. */
43164422
PyIntArrType_Type.tp_hash = int_arrtype_hash;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Test the scalar constructors, which also do type-coercion
3+
"""
4+
from __future__ import division, absolute_import, print_function
5+
6+
import fractions
7+
import numpy as np
8+
9+
from numpy.testing import (
10+
run_module_suite,
11+
assert_equal, assert_almost_equal, assert_raises, assert_warns,
12+
dec
13+
)
14+
15+
float_types = [np.half, np.single, np.double, np.longdouble]
16+
17+
def test_float_as_integer_ratio():
18+
# derived from the cpython test "test_floatasratio"
19+
for ftype in float_types:
20+
for f, ratio in [
21+
(0.875, (7, 8)),
22+
(-0.875, (-7, 8)),
23+
(0.0, (0, 1)),
24+
(11.5, (23, 2)),
25+
]:
26+
assert_equal(ftype(f).as_integer_ratio(), ratio)
27+
28+
rstate = np.random.RandomState(0)
29+
fi = np.finfo(ftype)
30+
for i in range(1000):
31+
exp = rstate.randint(fi.minexp, fi.maxexp - 1)
32+
frac = rstate.rand()
33+
f = np.ldexp(frac, exp, dtype=ftype)
34+
35+
n, d = f.as_integer_ratio()
36+
37+
try:
38+
dn = np.longdouble(str(n))
39+
df = np.longdouble(str(d))
40+
except (OverflowError, RuntimeWarning):
41+
# the values may not fit in any float type
42+
continue
43+
44+
assert_equal(
45+
dn / df, f,
46+
"{}/{} (dtype={})".format(n, d, ftype.__name__))
47+
48+
R = fractions.Fraction
49+
assert_equal(R(0, 1),
50+
R(*ftype(0.0).as_integer_ratio()))
51+
assert_equal(R(5, 2),
52+
R(*ftype(2.5).as_integer_ratio()))
53+
assert_equal(R(1, 2),
54+
R(*ftype(0.5).as_integer_ratio()))
55+
assert_equal(R(-2100, 1),
56+
R(*ftype(-2100.0).as_integer_ratio()))
57+
58+
assert_raises(OverflowError, ftype('inf').as_integer_ratio)
59+
assert_raises(OverflowError, ftype('-inf').as_integer_ratio)
60+
assert_raises(ValueError, ftype('nan').as_integer_ratio)
61+
62+
63+
assert_equal(R(1075, 512),
64+
R(*np.half(2.1).as_integer_ratio()))
65+
assert_equal(R(-1075, 512),
66+
R(*np.half(-2.1).as_integer_ratio()))
67+
assert_equal(R(4404019, 2097152),
68+
R(*np.single(2.1).as_integer_ratio()))
69+
assert_equal(R(-4404019, 2097152),
70+
R(*np.single(-2.1).as_integer_ratio()))
71+
assert_equal(R(4728779608739021, 2251799813685248),
72+
R(*np.double(2.1).as_integer_ratio()))
73+
assert_equal(R(-4728779608739021, 2251799813685248),
74+
R(*np.double(-2.1).as_integer_ratio()))
75+
# longdouble is platform depedent
76+
77+
78+
if __name__ == "__main__":
79+
run_module_suite()

0 commit comments

Comments
 (0)