Skip to content

Commit 0e54d59

Browse files
authored
Merge pull request #90 from static-frame/89/isna-element
2 parents 81f2665 + f96aae4 commit 0e54d59

File tree

3 files changed

+78
-30
lines changed

3 files changed

+78
-30
lines changed

src/_arraykit.c

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3385,63 +3385,86 @@ dtype_from_element(PyObject *Py_UNUSED(m), PyObject *arg)
33853385
return (PyObject*)PyArray_DescrFromType(NPY_OBJECT);
33863386
}
33873387

3388+
static char *isna_element_kwarg_names[] = {
3389+
"element",
3390+
"include_none",
3391+
NULL
3392+
};
3393+
33883394
static PyObject *
3389-
isna_element(PyObject *Py_UNUSED(m), PyObject *arg)
3395+
isna_element(PyObject *m, PyObject *args, PyObject *kwargs)
33903396
{
3397+
PyObject *element;
3398+
int include_none = 1;
3399+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
3400+
"O|p:isna_element", isna_element_kwarg_names,
3401+
&element,
3402+
&include_none)) {
3403+
return NULL;
3404+
}
3405+
33913406
// None
3392-
if (arg == Py_None) {
3407+
if (include_none && element == Py_None) {
33933408
Py_RETURN_TRUE;
33943409
}
33953410

33963411
// NaN
3397-
if (PyFloat_Check(arg)) {
3398-
return PyBool_FromLong(isnan(PyFloat_AS_DOUBLE(arg)));
3412+
if (PyFloat_Check(element)) {
3413+
return PyBool_FromLong(isnan(PyFloat_AS_DOUBLE(element)));
33993414
}
3400-
if (PyArray_IsScalar(arg, Half)) {
3401-
return PyBool_FromLong(npy_half_isnan(PyArrayScalar_VAL(arg, Half)));
3415+
if (PyArray_IsScalar(element, Half)) {
3416+
return PyBool_FromLong(npy_half_isnan(PyArrayScalar_VAL(element, Half)));
34023417
}
3403-
if (PyArray_IsScalar(arg, Float32)) {
3404-
return PyBool_FromLong(isnan(PyArrayScalar_VAL(arg, Float32)));
3418+
if (PyArray_IsScalar(element, Float32)) {
3419+
return PyBool_FromLong(isnan(PyArrayScalar_VAL(element, Float32)));
34053420
}
3406-
if (PyArray_IsScalar(arg, Float64)) {
3407-
return PyBool_FromLong(isnan(PyArrayScalar_VAL(arg, Float64)));
3421+
if (PyArray_IsScalar(element, Float64)) {
3422+
return PyBool_FromLong(isnan(PyArrayScalar_VAL(element, Float64)));
34083423
}
34093424
# ifdef PyFloat128ArrType_Type
3410-
if (PyArray_IsScalar(arg, Float128)) {
3411-
return PyBool_FromLong(isnan(PyArrayScalar_VAL(arg, Float128)));
3425+
if (PyArray_IsScalar(element, Float128)) {
3426+
return PyBool_FromLong(isnan(PyArrayScalar_VAL(element, Float128)));
34123427
}
34133428
# endif
34143429

34153430
// Complex NaN
3416-
if (PyComplex_Check(arg)) {
3417-
Py_complex val = ((PyComplexObject*)arg)->cval;
3431+
if (PyComplex_Check(element)) {
3432+
Py_complex val = ((PyComplexObject*)element)->cval;
34183433
return PyBool_FromLong(isnan(val.real) || isnan(val.imag));
34193434
}
3420-
if (PyArray_IsScalar(arg, Complex64)) {
3421-
npy_cfloat val = PyArrayScalar_VAL(arg, Complex64);
3435+
if (PyArray_IsScalar(element, Complex64)) {
3436+
npy_cfloat val = PyArrayScalar_VAL(element, Complex64);
34223437
return PyBool_FromLong(isnan(val.real) || isnan(val.imag));
34233438
}
3424-
if (PyArray_IsScalar(arg, Complex128)) {
3425-
npy_cdouble val = PyArrayScalar_VAL(arg, Complex128);
3439+
if (PyArray_IsScalar(element, Complex128)) {
3440+
npy_cdouble val = PyArrayScalar_VAL(element, Complex128);
34263441
return PyBool_FromLong(isnan(val.real) || isnan(val.imag));
34273442
}
34283443
# ifdef PyComplex256ArrType_Type
3429-
if (PyArray_IsScalar(arg, Complex256)) {
3430-
npy_clongdouble val = PyArrayScalar_VAL(arg, Complex256);
3444+
if (PyArray_IsScalar(element, Complex256)) {
3445+
npy_clongdouble val = PyArrayScalar_VAL(element, Complex256);
34313446
return PyBool_FromLong(isnan(val.real) || isnan(val.imag));
34323447
}
34333448
# endif
34343449

34353450
// NaT - Datetime
3436-
if (PyArray_IsScalar(arg, Datetime)) {
3437-
return PyBool_FromLong(PyArrayScalar_VAL(arg, Datetime) == NPY_DATETIME_NAT);
3451+
if (PyArray_IsScalar(element, Datetime)) {
3452+
return PyBool_FromLong(PyArrayScalar_VAL(element, Datetime) == NPY_DATETIME_NAT);
34383453
}
3439-
34403454
// NaT - Timedelta
3441-
if (PyArray_IsScalar(arg, Timedelta)) {
3442-
return PyBool_FromLong(PyArrayScalar_VAL(arg, Timedelta) == NPY_DATETIME_NAT);
3455+
if (PyArray_IsScalar(element, Timedelta)) {
3456+
return PyBool_FromLong(PyArrayScalar_VAL(element, Timedelta) == NPY_DATETIME_NAT);
3457+
}
3458+
// Try to identify Pandas Timestamp NATs
3459+
if (PyObject_HasAttrString(element, "to_numpy")) {
3460+
PyObject *to_numpy = PyObject_GetAttrString(element, "to_numpy");
3461+
if (!PyCallable_Check(to_numpy)) {
3462+
Py_RETURN_FALSE;
3463+
}
3464+
PyObject* post = PyObject_CallFunction(to_numpy, NULL);
3465+
if (post == NULL) return NULL;
3466+
return PyBool_FromLong(PyArrayScalar_VAL(post, Datetime) == NPY_DATETIME_NAT);
34433467
}
3444-
34453468
Py_RETURN_FALSE;
34463469
}
34473470

@@ -4042,7 +4065,10 @@ static PyMethodDef arraykit_methods[] = {
40424065
METH_VARARGS | METH_KEYWORDS,
40434066
NULL},
40444067
{"count_iteration", count_iteration, METH_O, NULL},
4045-
{"isna_element", isna_element, METH_O, NULL},
4068+
{"isna_element",
4069+
(PyCFunction)isna_element,
4070+
METH_VARARGS | METH_KEYWORDS,
4071+
NULL},
40464072
{"dtype_from_element", dtype_from_element, METH_O, NULL},
40474073
{"get_new_indexers_and_screen",
40484074
(PyCFunction)get_new_indexers_and_screen,

tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def clean(context):
2626

2727
@invoke.task(clean)
2828
def build(context):
29-
context.run('pip install -r requirements-test.txt', echo=True, pty=True)
29+
# context.run('pip install -r requirements-test.txt', echo=True, pty=True)
3030
# keep verbose to see warnings
3131
context.run(f'{sys.executable} -m pip -v install .', echo=True, pty=True)
3232

test/test_util.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import warnings
66
from io import StringIO
77
import numpy as np # type: ignore
8+
import pandas as pd
89

910
from arraykit import resolve_dtype
1011
from arraykit import resolve_dtype_iter
@@ -273,7 +274,7 @@ def test_array_deepcopy_f(self) -> None:
273274
a2 = array_deepcopy(a1)
274275
self.assertNotEqual(id(a1), id(a2))
275276

276-
def test_isna_element_true(self) -> None:
277+
def test_isna_element_a(self) -> None:
277278
class FloatSubclass(float): pass
278279
class ComplexSubclass(complex): pass
279280

@@ -308,7 +309,7 @@ class ComplexSubclass(complex): pass
308309
self.assertTrue(isna_element(-float('NaN')))
309310
self.assertTrue(isna_element(None))
310311

311-
def test_isna_element_false(self) -> None:
312+
def test_isna_element_b(self) -> None:
312313
# Test a wide range of float values, with different precision, across types
313314
for val in (
314315
1e-1000, 1e-309, 1e-39, 1e-16, 1e-5, 0.1, 0., 1.0, 1e5, 1e16, 1e39, 1e309, 1e1000,
@@ -326,6 +327,27 @@ def test_isna_element_false(self) -> None:
326327
self.assertFalse(isna_element(datetime.date(2020, 12, 31)))
327328
self.assertFalse(isna_element(False))
328329

330+
331+
def test_isna_element_c(self) -> None:
332+
self.assertFalse(isna_element(None, include_none=False))
333+
self.assertTrue(isna_element(None, include_none=True))
334+
self.assertFalse(isna_element(None, False))
335+
self.assertTrue(isna_element(None, True))
336+
337+
def test_isna_element_d(self) -> None:
338+
ts = pd.Timestamp('nat')
339+
self.assertTrue(isna_element(ts))
340+
341+
342+
def test_isna_element_d(self) -> None:
343+
from types import SimpleNamespace
344+
sn = SimpleNamespace()
345+
sn.to_numpy = None
346+
self.assertFalse(isna_element(sn))
347+
348+
349+
350+
329351
#---------------------------------------------------------------------------
330352

331353
def test_dtype_from_element_core_dtypes(self) -> None:

0 commit comments

Comments
 (0)