Skip to content

Commit

Permalink
Merge pull request #19803 from BvB93/is_integer
Browse files Browse the repository at this point in the history
 ENH: Add `is_integer` to `np.floating` & `np.integer`
  • Loading branch information
charris committed Sep 2, 2021
2 parents 95d2540 + bb3e407 commit 6305014
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 9 deletions.
14 changes: 14 additions & 0 deletions doc/release/upcoming_changes/19803.new_feature.rst
@@ -0,0 +1,14 @@
``is_integer`` is now available to `numpy.floating` and `numpy.integer`
-----------------------------------------------------------------------
Based on its counterpart in `float` and `int`, the numpy floating point and
integer types now support `~float.is_integer`. Returns ``True`` if the
number is finite with integral value, and ``False`` otherwise.

.. code-block:: python
>>> np.float32(-2.0).is_integer()
True
>>> np.float64(3.2).is_integer()
False
>>> np.int32(-2).is_integer()
True
3 changes: 2 additions & 1 deletion numpy/__init__.pyi
Expand Up @@ -3203,6 +3203,7 @@ class integer(number[_NBit1]): # type: ignore
self, args: L[0] | Tuple[()] | Tuple[L[0]] = ..., /,
) -> int: ...
def tolist(self) -> int: ...
def is_integer(self) -> L[True]: ...
def __index__(self) -> int: ...
__truediv__: _IntTrueDiv[_NBit1]
__rtruediv__: _IntTrueDiv[_NBit1]
Expand Down Expand Up @@ -3356,7 +3357,7 @@ class floating(inexact[_NBit1]):
/,
) -> float: ...
def tolist(self) -> float: ...
def is_integer(self: float64) -> bool: ...
def is_integer(self) -> bool: ...
def hex(self: float64) -> str: ...
@classmethod
def fromhex(cls: Type[float64], string: str, /) -> float64: ...
Expand Down
45 changes: 39 additions & 6 deletions numpy/core/_add_newdocs_scalars.py
Expand Up @@ -205,12 +205,12 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
add_newdoc_for_scalar_type('void', [],
r"""
Either an opaque sequence of bytes, or a structure.
>>> np.void(b'abcd')
void(b'\x61\x62\x63\x64')
Structured `void` scalars can only be constructed via extraction from :ref:`structured_arrays`:
>>> arr = np.array((1, 2), dtype=[('x', np.int8), ('y', np.int8)])
>>> arr[()]
(1, 2) # looks like a tuple, but is `np.void`
Expand All @@ -226,20 +226,36 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
>>> np.datetime64(10, 'Y')
numpy.datetime64('1980')
>>> np.datetime64('1980', 'Y')
numpy.datetime64('1980')
numpy.datetime64('1980')
>>> np.datetime64(10, 'D')
numpy.datetime64('1970-01-11')
See :ref:`arrays.datetime` for more information.
""")

add_newdoc_for_scalar_type('timedelta64', [],
"""
A timedelta stored as a 64-bit integer.
See :ref:`arrays.datetime` for more information.
""")

add_newdoc('numpy.core.numerictypes', "integer", ('is_integer',
"""
integer.is_integer() -> bool
Return ``True`` if the number is finite with integral value.
.. versionadded:: 1.22
Examples
--------
>>> np.int64(-2).is_integer()
True
>>> np.uint32(5).is_integer()
True
"""))

# 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',
Expand All @@ -257,3 +273,20 @@ def add_newdoc_for_scalar_type(obj, fixed_aliases, doc):
>>> np.{ftype}(-.25).as_integer_ratio()
(-1, 4)
""".format(ftype=float_name)))

add_newdoc('numpy.core.numerictypes', float_name, ('is_integer',
f"""
{float_name}.is_integer() -> bool
Return ``True`` if the floating point number is finite with integral
value, and ``False`` otherwise.
.. versionadded:: 1.22
Examples
--------
>>> np.{float_name}(-2.0).is_integer()
True
>>> np.{float_name}(3.2).is_integer()
False
"""))
49 changes: 48 additions & 1 deletion numpy/core/src/multiarray/scalartypes.c.src
Expand Up @@ -1908,6 +1908,39 @@ error:
}
/**end repeat**/

/**begin repeat
* #name = half, float, double, longdouble#
* #Name = Half, Float, Double, LongDouble#
* #is_half = 1,0,0,0#
* #c = f, f, , l#
*/
static PyObject *
@name@_is_integer(PyObject *self)
{
#if @is_half@
npy_double val = npy_half_to_double(PyArrayScalar_VAL(self, @Name@));
#else
npy_@name@ val = PyArrayScalar_VAL(self, @Name@);
#endif
PyObject *ret;

if (npy_isnan(val)) {
Py_RETURN_FALSE;
}
if (!npy_isfinite(val)) {
Py_RETURN_FALSE;
}

ret = (npy_floor@c@(val) == val) ? Py_True : Py_False;
Py_INCREF(ret);
return ret;
}
/**end repeat**/

static PyObject *
integer_is_integer(PyObject *self) {
Py_RETURN_TRUE;
}

/*
* need to fill in doc-strings for these methods on import -- copy from
Expand Down Expand Up @@ -2167,7 +2200,7 @@ static PyMethodDef @name@type_methods[] = {
/**end repeat**/

/**begin repeat
* #name = integer,floating, complexfloating#
* #name = floating, complexfloating#
*/
static PyMethodDef @name@type_methods[] = {
/* Hook for the round() builtin */
Expand All @@ -2178,13 +2211,27 @@ static PyMethodDef @name@type_methods[] = {
};
/**end repeat**/

static PyMethodDef integertype_methods[] = {
/* Hook for the round() builtin */
{"__round__",
(PyCFunction)integertype_dunder_round,
METH_VARARGS | METH_KEYWORDS, NULL},
{"is_integer",
(PyCFunction)integer_is_integer,
METH_NOARGS, NULL},
{NULL, NULL, 0, NULL} /* sentinel */
};

/**begin repeat
* #name = half,float,double,longdouble#
*/
static PyMethodDef @name@type_methods[] = {
{"as_integer_ratio",
(PyCFunction)@name@_as_integer_ratio,
METH_NOARGS, NULL},
{"is_integer",
(PyCFunction)@name@_is_integer,
METH_NOARGS, NULL},
{NULL, NULL, 0, NULL}
};
/**end repeat**/
Expand Down
26 changes: 26 additions & 0 deletions numpy/core/tests/test_scalar_methods.py
Expand Up @@ -102,3 +102,29 @@ def test_roundtrip(self, ftype, frac_vals, exp_vals):
pytest.skip("longdouble too small on this platform")

assert_equal(nf / df, f, "{}/{}".format(n, d))


class TestIsInteger:
@pytest.mark.parametrize("str_value", ["inf", "nan"])
@pytest.mark.parametrize("code", np.typecodes["Float"])
def test_special(self, code: str, str_value: str) -> None:
cls = np.dtype(code).type
value = cls(str_value)
assert not value.is_integer()

@pytest.mark.parametrize(
"code", np.typecodes["Float"] + np.typecodes["AllInteger"]
)
def test_true(self, code: str) -> None:
float_array = np.arange(-5, 5).astype(code)
for value in float_array:
assert value.is_integer()

@pytest.mark.parametrize("code", np.typecodes["Float"])
def test_false(self, code: str) -> None:
float_array = np.arange(-5, 5).astype(code)
float_array *= 1.1
for value in float_array:
if value == 0:
continue
assert not value.is_integer()
1 change: 0 additions & 1 deletion numpy/typing/tests/data/fail/scalars.py
Expand Up @@ -87,7 +87,6 @@ def func(a: np.float32) -> None: ...

c8.__getnewargs__() # E: Invalid self argument
f2.__getnewargs__() # E: Invalid self argument
f2.is_integer() # E: Invalid self argument
f2.hex() # E: Invalid self argument
np.float16.fromhex("0x0.0p+0") # E: Invalid self argument
f2.__trunc__() # E: Invalid self argument
Expand Down
2 changes: 2 additions & 0 deletions numpy/typing/tests/data/reveal/scalars.py
Expand Up @@ -156,3 +156,5 @@
if sys.version_info >= (3, 9):
reveal_type(f8.__ceil__()) # E: int
reveal_type(f8.__floor__()) # E: int

reveal_type(i8.is_integer()) # E: Literal[True]

0 comments on commit 6305014

Please sign in to comment.