Skip to content
Merged
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
6 changes: 6 additions & 0 deletions doc/release/1.17.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
============

Expand Down
18 changes: 18 additions & 0 deletions numpy/core/_add_newdocs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6903,3 +6903,21 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
"""
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)))
106 changes: 106 additions & 0 deletions numpy/core/src/multiarray/scalartypes.c.src
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
109 changes: 109 additions & 0 deletions numpy/core/tests/test_scalar_methods.py
Original file line number Diff line number Diff line change
@@ -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))