summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Reddy <tyler.je.reddy@gmail.com>2019-04-18 18:47:52 -0700
committerGitHub <noreply@github.com>2019-04-18 18:47:52 -0700
commitddd59a939356ad9b3548159b2c1d783853ed65e2 (patch)
treed0d4bc30474ac11fd5702528d6104e39781f6aa7
parent32acbd34aa334771afdac602badca8dea66f1332 (diff)
parent4bebf05532f466ffe81b95c8b25c23dd00867f30 (diff)
downloadnumpy-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.rst6
-rw-r--r--numpy/core/_add_newdocs.py18
-rw-r--r--numpy/core/src/multiarray/scalartypes.c.src106
-rw-r--r--numpy/core/tests/test_scalar_methods.py109
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))