Skip to content

Commit a25c1bb

Browse files
authored
Merge pull request #118 from static-frame/116/ascending-slice
2 parents 1d69ef9 + 192827c commit a25c1bb

File tree

6 files changed

+154
-6
lines changed

6 files changed

+154
-6
lines changed

performance/__main__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from performance.reference.util import get_new_indexers_and_screen_ref
2828
from performance.reference.util import split_after_count as split_after_count_ref
2929
from performance.reference.util import count_iteration as count_iteration_ref
30+
from performance.reference.util import slice_to_ascending_slice as slice_to_ascending_slice_ref
3031

3132
from performance.reference.array_go import ArrayGO as ArrayGOREF
3233

@@ -45,6 +46,7 @@
4546
from arraykit import isna_element as isna_element_ak
4647
from arraykit import split_after_count as split_after_count_ak
4748
from arraykit import count_iteration as count_iteration_ak
49+
from arraykit import slice_to_ascending_slice as slice_to_ascending_slice_ak
4850

4951
from arraykit import ArrayGO as ArrayGOAK
5052

@@ -735,6 +737,22 @@ class CountIterationsREF(CountIterations):
735737
entry = staticmethod(count_iteration_ref)
736738

737739

740+
#-------------------------------------------------------------------------------
741+
class SliceToAscending(Perf):
742+
NUMBER = 1_000_000
743+
744+
def __init__(self):
745+
self.slc = slice(100, 1, -3)
746+
747+
def main(self):
748+
_ = self.entry(self.slc, 101)
749+
750+
class SliceToAscendingAK(SliceToAscending):
751+
entry = staticmethod(slice_to_ascending_slice_ak)
752+
753+
class SliceToAscendingREF(SliceToAscending):
754+
entry = staticmethod(slice_to_ascending_slice_ref)
755+
738756
#-------------------------------------------------------------------------------
739757

740758
def get_arg_parser():

performance/reference/util.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
DTYPES_BOOL = (DTYPE_BOOL,)
2626
DTYPES_INEXACT = (DTYPE_FLOAT_DEFAULT, DTYPE_COMPLEX_DEFAULT)
2727

28+
EMPTY_SLICE = slice(0, 0) # gathers nothing
29+
2830

2931
def mloc(array: np.ndarray) -> int:
3032
'''Return the memory location of an array.
@@ -261,5 +263,44 @@ def count_iteration(iterable: tp.Iterable):
261263
return count
262264

263265

266+
def slice_to_ascending_slice(
267+
key: slice,
268+
size: int
269+
) -> slice:
270+
'''
271+
Given a slice, return a slice that, with ascending integers, covers the same values.
272+
273+
Args:
274+
size: the length of the container on this axis
275+
'''
276+
key_step = key.step
277+
key_start = key.start
278+
key_stop = key.stop
279+
280+
if key_step is None or key_step > 0:
281+
return key
282+
283+
# will get rid of all negative values greater than the size; but will replace None with an appropriate number for usage in range
284+
norm_key_start, norm_key_stop, norm_key_step = key.indices(size)
285+
286+
# everything else should be descending, but we might have non-descending start, stop
287+
if key_start is not None and key_stop is not None:
288+
if norm_key_start <= norm_key_stop: # an ascending range
289+
return EMPTY_SLICE
290+
291+
norm_range = range(norm_key_start, norm_key_stop, norm_key_step)
292+
293+
# derive stop
294+
if key_start is None:
295+
stop = None
296+
else:
297+
stop = norm_range[0] + 1
298+
299+
if key_step == -1:
300+
# gets last realized value, not last range value
301+
return slice(None if key_stop is None else norm_range[-1], stop, 1)
302+
303+
return slice(norm_range[-1], stop, key_step * -1)
304+
264305

265306

src/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@
2626
from ._arraykit import count_iteration as count_iteration
2727
from ._arraykit import first_true_1d as first_true_1d
2828
from ._arraykit import first_true_2d as first_true_2d
29+
from ._arraykit import slice_to_ascending_slice as slice_to_ascending_slice

src/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ def get_new_indexers_and_screen(indexers: np.ndarray, positions: np.ndarray) ->
102102

103103
def first_true_1d(__array: np.ndarray, *, forward: bool) -> int: ...
104104
def first_true_2d(__array: np.ndarray, *, forward: bool, axis: int) -> np.ndarray: ...
105+
def slice_to_ascending_slice(__slice: slice, __size: int) -> slice: ...

src/_arraykit.c

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@
1616

1717
//------------------------------------------------------------------------------
1818
// Given a PyObject, raise if not an array.
19-
# define AK_CHECK_NUMPY_ARRAY(O) \
20-
if (!PyArray_Check(O)) { \
21-
return PyErr_Format(PyExc_TypeError, "Expected NumPy array (got %s)", \
22-
Py_TYPE(O)->tp_name); \
19+
# define AK_CHECK_NUMPY_ARRAY(O) \
20+
if (!PyArray_Check(O)) { \
21+
return PyErr_Format(PyExc_TypeError, \
22+
"Expected NumPy array, not %s.", \
23+
Py_TYPE(O)->tp_name); \
2324
}
2425

2526
// Given a PyObject, raise if not an array or is not one or two dimensional.
@@ -29,7 +30,7 @@
2930
int ndim = PyArray_NDIM((PyArrayObject *)O); \
3031
if (ndim != 1 && ndim != 2) { \
3132
return PyErr_Format(PyExc_NotImplementedError,\
32-
"expected 1D or 2D array (got %i)", \
33+
"Expected 1D or 2D array, not %i.", \
3334
ndim); \
3435
} \
3536
} while (0)
@@ -3322,6 +3323,53 @@ row_1d_filter(PyObject *Py_UNUSED(m), PyObject *a)
33223323
return a;
33233324
}
33243325

3326+
3327+
// Convert any slice to an ascending slice that covers the same values.
3328+
static PyObject *
3329+
slice_to_ascending_slice(PyObject *Py_UNUSED(m), PyObject *args) {
3330+
3331+
PyObject* slice;
3332+
PyObject* size;
3333+
if (!PyArg_ParseTuple(args,
3334+
"O!O!:slice_to_ascending_slice",
3335+
&PySlice_Type, &slice,
3336+
&PyLong_Type, &size)) {
3337+
return NULL;
3338+
}
3339+
3340+
Py_ssize_t step_count = -1;
3341+
Py_ssize_t start = 0;
3342+
Py_ssize_t stop = 0;
3343+
Py_ssize_t step = 0;
3344+
3345+
if (PySlice_Unpack(slice, &start, &stop, &step)) {
3346+
return NULL;
3347+
}
3348+
if (step > 0) {
3349+
Py_INCREF(slice);
3350+
return slice;
3351+
}
3352+
step_count = PySlice_AdjustIndices(
3353+
PyLong_AsSsize_t(size),
3354+
&start,
3355+
&stop,
3356+
step);
3357+
3358+
PyObject* asc_stop = PyLong_FromSsize_t(start + 1);
3359+
// step will be negative; shift original start value down to find new start
3360+
PyObject* asc_start = PyLong_FromSsize_t(start + (step * (step_count - 1)));
3361+
PyObject* asc_step = PyLong_FromSsize_t(-step);
3362+
3363+
// might be NULL, let return
3364+
PyObject* asc = PySlice_New(asc_start, asc_stop, asc_step);
3365+
3366+
Py_DECREF(asc_start);
3367+
Py_DECREF(asc_stop);
3368+
Py_DECREF(asc_step);
3369+
3370+
return asc;
3371+
}
3372+
33253373
//------------------------------------------------------------------------------
33263374
// array utility
33273375

@@ -5348,6 +5396,7 @@ static PyMethodDef arraykit_methods[] = {
53485396
{"column_2d_filter", column_2d_filter, METH_O, NULL},
53495397
{"column_1d_filter", column_1d_filter, METH_O, NULL},
53505398
{"row_1d_filter", row_1d_filter, METH_O, NULL},
5399+
{"slice_to_ascending_slice", slice_to_ascending_slice, METH_VARARGS, NULL},
53515400
{"array_deepcopy",
53525401
(PyCFunction)array_deepcopy,
53535402
METH_VARARGS | METH_KEYWORDS,

test/test_util.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@
1818
from arraykit import array_deepcopy
1919
from arraykit import isna_element
2020
from arraykit import dtype_from_element
21-
from arraykit import split_after_count
2221
from arraykit import count_iteration
2322
from arraykit import first_true_1d
2423
from arraykit import first_true_2d
24+
from arraykit import slice_to_ascending_slice
2525

2626
from performance.reference.util import get_new_indexers_and_screen_ak as get_new_indexers_and_screen_full
2727
from arraykit import get_new_indexers_and_screen
2828

2929
from performance.reference.util import mloc as mloc_ref
30+
from performance.reference.util import slice_to_ascending_slice as slice_to_ascending_slice_ref
3031

3132

3233
class TestUnit(unittest.TestCase):
@@ -703,8 +704,45 @@ def test_first_true_2d_h(self) -> None:
703704
[1, 0, 2, 1])
704705

705706

707+
#---------------------------------------------------------------------------
708+
def test_slice_to_ascending_slice_a(self) -> None:
709+
self.assertEqual(slice_to_ascending_slice(
710+
slice(5, 2, -1), 6),
711+
slice(3, 6, 1),
712+
)
713+
714+
def test_slice_to_ascending_slice_b(self) -> None:
715+
self.assertEqual(slice_to_ascending_slice(
716+
slice(2, 5, 1), 6),
717+
slice(2, 5, 1),
718+
)
719+
720+
def test_slice_to_ascending_slice_c(self) -> None:
721+
with self.assertRaises(TypeError):
722+
_ = slice_to_ascending_slice('a', 6)
706723

724+
with self.assertRaises(TypeError):
725+
_ = slice_to_ascending_slice(slice(1, 4), 'x')
707726

727+
def test_slice_to_ascending_slice_d(self) -> None:
728+
self.assertEqual(slice_to_ascending_slice(
729+
slice(10, 2, -2), 12),
730+
slice(4, 11, 2),
731+
)
732+
733+
def test_slice_to_ascending_slice_e(self) -> None:
734+
for slc, size in (
735+
(slice(10, 2, -2), 12),
736+
(slice(12, 2, -3), 12),
737+
(slice(12, None, -4), 12),
738+
(slice(76, 12, -8), 100),
739+
(slice(81, 33, -12), 100),
740+
(slice(97, 6, -7), 101),
741+
):
742+
self.assertEqual(
743+
slice_to_ascending_slice(slc, size),
744+
slice_to_ascending_slice_ref(slc, size),
745+
)
708746

709747

710748
if __name__ == '__main__':

0 commit comments

Comments
 (0)