diff options
author | Tyler Reddy <tyler.je.reddy@gmail.com> | 2019-04-18 18:47:52 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-18 18:47:52 -0700 |
commit | ddd59a939356ad9b3548159b2c1d783853ed65e2 (patch) | |
tree | d0d4bc30474ac11fd5702528d6104e39781f6aa7 | |
parent | 32acbd34aa334771afdac602badca8dea66f1332 (diff) | |
parent | 4bebf05532f466ffe81b95c8b25c23dd00867f30 (diff) | |
download | numpy-ddd59a939356ad9b3548159b2c1d783853ed65e2.tar.gz |
Merge pull request #10741 from eric-wieser/as_integer_ratio
ENH: Implement `np.floating.as_integer_ratio`
-rw-r--r-- | doc/release/1.17.0-notes.rst | 6 | ||||
-rw-r--r-- | numpy/core/_add_newdocs.py | 18 | ||||
-rw-r--r-- | numpy/core/src/multiarray/scalartypes.c.src | 106 | ||||
-rw-r--r-- | numpy/core/tests/test_scalar_methods.py | 109 |
4 files changed, 239 insertions, 0 deletions
diff --git a/doc/release/1.17.0-notes.rst b/doc/release/1.17.0-notes.rst index 7bf6bf949..fa6a132dd 100644 --- a/doc/release/1.17.0-notes.rst +++ b/doc/release/1.17.0-notes.rst @@ -134,6 +134,12 @@ New mode "empty" for ``np.pad`` This mode pads an array to a desired shape without initializing the new entries. +Floating point scalars implement ``as_integer_ratio`` to match the builtin float +-------------------------------------------------------------------------------- +This returns a (numerator, denominator) pair, which can be used to construct a +`fractions.Fraction`. + + Improvements ============ diff --git a/numpy/core/_add_newdocs.py b/numpy/core/_add_newdocs.py index c023f0526..52ab9c994 100644 --- a/numpy/core/_add_newdocs.py +++ b/numpy/core/_add_newdocs.py @@ -6799,3 +6799,21 @@ add_newdoc_for_scalar_type('object_', [], """ Any Python object. """) + +# TODO: work out how to put this on the base class, np.floating +for float_name in ('half', 'single', 'double', 'longdouble'): + add_newdoc('numpy.core.numerictypes', float_name, ('as_integer_ratio', + """ + {ftype}.as_integer_ratio() -> (int, int) + + Return a pair of integers, whose ratio is exactly equal to the original + floating point number, and with a positive denominator. + Raise OverflowError on infinities and a ValueError on NaNs. + + >>> np.{ftype}(10.0).as_integer_ratio() + (10, 1) + >>> np.{ftype}(0.0).as_integer_ratio() + (0, 1) + >>> np.{ftype}(-.25).as_integer_ratio() + (-1, 4) + """.format(ftype=float_name))) diff --git a/numpy/core/src/multiarray/scalartypes.c.src b/numpy/core/src/multiarray/scalartypes.c.src index 52de31289..715decbde 100644 --- a/numpy/core/src/multiarray/scalartypes.c.src +++ b/numpy/core/src/multiarray/scalartypes.c.src @@ -1993,6 +1993,92 @@ static PyObject * } /**end repeat**/ +/**begin repeat + * #name = half, float, double, longdouble# + * #Name = Half, Float, Double, LongDouble# + * #is_half = 1,0,0,0# + * #c = f, f, , l# + * #convert = PyLong_FromDouble, PyLong_FromDouble, PyLong_FromDouble, + * npy_longdouble_to_PyLong# + * # + */ +/* Heavily copied from the builtin float.as_integer_ratio */ +static PyObject * +@name@_as_integer_ratio(PyObject *self) +{ +#if @is_half@ + npy_double val = npy_half_to_double(PyArrayScalar_VAL(self, @Name@)); + npy_double frac; +#else + npy_@name@ val = PyArrayScalar_VAL(self, @Name@); + npy_@name@ frac; +#endif + int exponent; + int i; + + PyObject *py_exponent = NULL; + PyObject *numerator = NULL; + PyObject *denominator = NULL; + PyObject *result_pair = NULL; + PyNumberMethods *long_methods = PyLong_Type.tp_as_number; + + if (npy_isnan(val)) { + PyErr_SetString(PyExc_ValueError, + "cannot convert NaN to integer ratio"); + return NULL; + } + if (!npy_isfinite(val)) { + PyErr_SetString(PyExc_OverflowError, + "cannot convert Infinity to integer ratio"); + return NULL; + } + + frac = npy_frexp@c@(val, &exponent); /* val == frac * 2**exponent exactly */ + + /* This relies on the floating point type being base 2 to converge */ + for (i = 0; frac != npy_floor@c@(frac); i++) { + frac *= 2.0; + exponent--; + } + + /* self == frac * 2**exponent exactly and frac is integral. */ + numerator = @convert@(frac); + if (numerator == NULL) + goto error; + denominator = PyLong_FromLong(1); + if (denominator == NULL) + goto error; + py_exponent = PyLong_FromLong(exponent < 0 ? -exponent : exponent); + if (py_exponent == NULL) + goto error; + + /* fold in 2**exponent */ + if (exponent > 0) { + PyObject *temp = long_methods->nb_lshift(numerator, py_exponent); + if (temp == NULL) + goto error; + Py_DECREF(numerator); + numerator = temp; + } + else { + PyObject *temp = long_methods->nb_lshift(denominator, py_exponent); + if (temp == NULL) + goto error; + Py_DECREF(denominator); + denominator = temp; + } + + result_pair = PyTuple_Pack(2, numerator, denominator); + +error: + Py_XDECREF(py_exponent); + Py_XDECREF(denominator); + Py_XDECREF(numerator); + return result_pair; +} +/**end repeat**/ + + /* * need to fill in doc-strings for these methods on import -- copy from * array docstrings @@ -2256,6 +2342,17 @@ static PyMethodDef @name@type_methods[] = { }; /**end repeat**/ +/**begin repeat + * #name = half,float,double,longdouble# + */ +static PyMethodDef @name@type_methods[] = { + {"as_integer_ratio", + (PyCFunction)@name@_as_integer_ratio, + METH_NOARGS, NULL}, + {NULL, NULL, 0, NULL} +}; +/**end repeat**/ + /************* As_mapping functions for void array scalar ************/ static Py_ssize_t @@ -4311,6 +4408,15 @@ initialize_numeric_types(void) /**end repeat**/ + /**begin repeat + * #name = half, float, double, longdouble# + * #Name = Half, Float, Double, LongDouble# + */ + + Py@Name@ArrType_Type.tp_methods = @name@type_methods; + + /**end repeat**/ + #if (NPY_SIZEOF_INT != NPY_SIZEOF_LONG) || defined(NPY_PY3K) /* We won't be inheriting from Python Int type. */ PyIntArrType_Type.tp_hash = int_arrtype_hash; diff --git a/numpy/core/tests/test_scalar_methods.py b/numpy/core/tests/test_scalar_methods.py new file mode 100644 index 000000000..0e4ac5f39 --- /dev/null +++ b/numpy/core/tests/test_scalar_methods.py @@ -0,0 +1,109 @@ +""" +Test the scalar constructors, which also do type-coercion +""" +from __future__ import division, absolute_import, print_function + +import os +import fractions +import platform + +import pytest +import numpy as np + +from numpy.testing import ( + run_module_suite, + assert_equal, assert_almost_equal, assert_raises, assert_warns, + dec +) + +class TestAsIntegerRatio(object): + # derived in part from the cpython test "test_floatasratio" + + @pytest.mark.parametrize("ftype", [ + np.half, np.single, np.double, np.longdouble]) + @pytest.mark.parametrize("f, ratio", [ + (0.875, (7, 8)), + (-0.875, (-7, 8)), + (0.0, (0, 1)), + (11.5, (23, 2)), + ]) + def test_small(self, ftype, f, ratio): + assert_equal(ftype(f).as_integer_ratio(), ratio) + + @pytest.mark.parametrize("ftype", [ + np.half, np.single, np.double, np.longdouble]) + def test_simple_fractions(self, ftype): + R = fractions.Fraction + assert_equal(R(0, 1), + R(*ftype(0.0).as_integer_ratio())) + assert_equal(R(5, 2), + R(*ftype(2.5).as_integer_ratio())) + assert_equal(R(1, 2), + R(*ftype(0.5).as_integer_ratio())) + assert_equal(R(-2100, 1), + R(*ftype(-2100.0).as_integer_ratio())) + + @pytest.mark.parametrize("ftype", [ + np.half, np.single, np.double, np.longdouble]) + def test_errors(self, ftype): + assert_raises(OverflowError, ftype('inf').as_integer_ratio) + assert_raises(OverflowError, ftype('-inf').as_integer_ratio) + assert_raises(ValueError, ftype('nan').as_integer_ratio) + + def test_against_known_values(self): + R = fractions.Fraction + assert_equal(R(1075, 512), + R(*np.half(2.1).as_integer_ratio())) + assert_equal(R(-1075, 512), + R(*np.half(-2.1).as_integer_ratio())) + assert_equal(R(4404019, 2097152), + R(*np.single(2.1).as_integer_ratio())) + assert_equal(R(-4404019, 2097152), + R(*np.single(-2.1).as_integer_ratio())) + assert_equal(R(4728779608739021, 2251799813685248), + R(*np.double(2.1).as_integer_ratio())) + assert_equal(R(-4728779608739021, 2251799813685248), + R(*np.double(-2.1).as_integer_ratio())) + # longdouble is platform depedent + + @pytest.mark.parametrize("ftype, frac_vals, exp_vals", [ + # dtype test cases generated using hypothesis + # first five generated cases per dtype + (np.half, [0.0, 0.01154830649280303, 0.31082276347447274, + 0.527350517124794, 0.8308562335072596], + [0, 1, 0, -8, 12]), + (np.single, [0.0, 0.09248576989263226, 0.8160498218131407, + 0.17389442853722373, 0.7956044195067877], + [0, 12, 10, 17, -26]), + (np.double, [0.0, 0.031066908499895136, 0.5214135908877832, + 0.45780736035689296, 0.5906586745934036], + [0, -801, 51, 194, -653]), + pytest.param( + np.longdouble, + [0.0, 0.20492557202724854, 0.4277180662199366, 0.9888085019891495, + 0.9620175814461964], + [0, -7400, 14266, -7822, -8721], + marks=[ + pytest.mark.skipif( + np.finfo(np.double) == np.finfo(np.longdouble), + reason="long double is same as double"), + pytest.mark.skipif( + platform.machine().startswith("ppc"), + reason="IBM double double"), + ] + ) + ]) + def test_roundtrip(self, ftype, frac_vals, exp_vals): + for frac, exp in zip(frac_vals, exp_vals): + f = np.ldexp(frac, exp, dtype=ftype) + n, d = f.as_integer_ratio() + + try: + # workaround for gh-9968 + nf = np.longdouble(str(n)) + df = np.longdouble(str(d)) + except (OverflowError, RuntimeWarning): + # the values may not fit in any float type + pytest.skip("longdouble too small on this platform") + + assert_equal(nf / df, f, "{}/{}".format(n, d)) |