summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEric Wieser <wieser.eric@gmail.com>2017-11-05 01:16:16 -0800
committerEric Wieser <wieser.eric@gmail.com>2019-04-10 23:40:54 -0700
commit79799b32a13a3dfb15eb48bdd8b4c6a433ce50df (patch)
treef9e1c25d13b052c29b288e9e29f4cf9667c6abfe
parentd7a73f8c700edcf150d59a570e0173b60f84c7a7 (diff)
downloadnumpy-79799b32a13a3dfb15eb48bdd8b4c6a433ce50df.tar.gz
ENH: Implement `np.floating.as_integer_ratio`
This matches the builtin `float.as_integer_ratio` and (in recent python versions) `int.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.py79
4 files changed, 209 insertions, 0 deletions
diff --git a/doc/release/1.17.0-notes.rst b/doc/release/1.17.0-notes.rst
index 1155449a7..d454c09f7 100644
--- a/doc/release/1.17.0-notes.rst
+++ b/doc/release/1.17.0-notes.rst
@@ -129,6 +129,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 cd2a6ce4e..d9c7cf5a4 100644
--- a/numpy/core/_add_newdocs.py
+++ b/numpy/core/_add_newdocs.py
@@ -6903,3 +6903,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..439fe9de2
--- /dev/null
+++ b/numpy/core/tests/test_scalar_methods.py
@@ -0,0 +1,79 @@
+"""
+Test the scalar constructors, which also do type-coercion
+"""
+from __future__ import division, absolute_import, print_function
+
+import fractions
+import numpy as np
+
+from numpy.testing import (
+ run_module_suite,
+ assert_equal, assert_almost_equal, assert_raises, assert_warns,
+ dec
+)
+
+float_types = [np.half, np.single, np.double, np.longdouble]
+
+def test_float_as_integer_ratio():
+ # derived from the cpython test "test_floatasratio"
+ for ftype in float_types:
+ for f, ratio in [
+ (0.875, (7, 8)),
+ (-0.875, (-7, 8)),
+ (0.0, (0, 1)),
+ (11.5, (23, 2)),
+ ]:
+ assert_equal(ftype(f).as_integer_ratio(), ratio)
+
+ rstate = np.random.RandomState(0)
+ fi = np.finfo(ftype)
+ for i in range(1000):
+ exp = rstate.randint(fi.minexp, fi.maxexp - 1)
+ frac = rstate.rand()
+ f = np.ldexp(frac, exp, dtype=ftype)
+
+ n, d = f.as_integer_ratio()
+
+ try:
+ dn = np.longdouble(str(n))
+ df = np.longdouble(str(d))
+ except (OverflowError, RuntimeWarning):
+ # the values may not fit in any float type
+ continue
+
+ assert_equal(
+ dn / df, f,
+ "{}/{} (dtype={})".format(n, d, ftype.__name__))
+
+ 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()))
+
+ assert_raises(OverflowError, ftype('inf').as_integer_ratio)
+ assert_raises(OverflowError, ftype('-inf').as_integer_ratio)
+ assert_raises(ValueError, ftype('nan').as_integer_ratio)
+
+
+ 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
+
+
+if __name__ == "__main__":
+ run_module_suite()