Skip to content

Commit c2fe7de

Browse files
authored
Add DateTimeArray to bintime (#171)
- Add implementation for extend() to improve performance Signed-off-by: Joe Friedrichsen <[email protected]>
1 parent ed55478 commit c2fe7de

File tree

7 files changed

+1628
-10
lines changed

7 files changed

+1628
-10
lines changed

src/nitypes/bintime/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
========================
2424
2525
* :class:`DateTime`: represents an NI-BTF absolute time as a Python object.
26+
* :class:`DateTimeArray`: an array of :class:`DateTime` values.
2627
* :class:`TimeDelta`: represents a NI-BTF time interval as a Python object.
2728
* :class:`TimeDeltaArray`: an array of :class:`TimeDelta` values.
2829
@@ -67,6 +68,7 @@
6768
from __future__ import annotations
6869

6970
from nitypes.bintime._datetime import DateTime
71+
from nitypes.bintime._datetime_array import DateTimeArray
7072
from nitypes.bintime._dtypes import (
7173
CVIAbsoluteTimeBase,
7274
CVIAbsoluteTimeDType,
@@ -79,6 +81,7 @@
7981

8082
__all__ = [
8183
"DateTime",
84+
"DateTimeArray",
8285
"CVIAbsoluteTimeBase",
8386
"CVIAbsoluteTimeDType",
8487
"CVITimeIntervalBase",
@@ -90,6 +93,7 @@
9093

9194
# Hide that it was defined in a helper file
9295
DateTime.__module__ = __name__
96+
DateTimeArray.__module__ = __name__
9397
TimeDelta.__module__ = __name__
9498
TimeDeltaArray.__module__ = __name__
9599
TimeValueTuple.__module__ = __name__
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable, MutableSequence
4+
from typing import (
5+
TYPE_CHECKING,
6+
Any,
7+
final,
8+
overload,
9+
)
10+
11+
import numpy as np
12+
import numpy.typing as npt
13+
14+
from nitypes._exceptions import invalid_arg_value, invalid_arg_type
15+
16+
if TYPE_CHECKING:
17+
# Import from the public package so the docs don't reference private submodules.
18+
from nitypes.bintime import CVIAbsoluteTimeDType, DateTime, TimeValueTuple
19+
else:
20+
from nitypes.bintime._dtypes import CVIAbsoluteTimeDType
21+
from nitypes.bintime._datetime import DateTime
22+
from nitypes.bintime._time_value_tuple import TimeValueTuple
23+
24+
25+
@final
26+
class DateTimeArray(MutableSequence[DateTime]):
27+
"""A mutable array of :class:`DateTime` values in NI Binary Time Format (NI-BTF).
28+
29+
Raises:
30+
TypeError: If any item in value is not a DateTime instance.
31+
"""
32+
33+
__slots__ = ["_array"]
34+
35+
_array: npt.NDArray[np.void]
36+
37+
def __init__(
38+
self,
39+
value: Iterable[DateTime] | None = None,
40+
) -> None:
41+
"""Initialize a new DateTimeArray."""
42+
value = [] if value is None else list(value)
43+
if not all(isinstance(item, DateTime) for item in value):
44+
raise invalid_arg_type("value", "iterable of DateTime", value)
45+
self._array = np.fromiter(
46+
(entry.to_tuple().to_cvi() for entry in value),
47+
dtype=CVIAbsoluteTimeDType,
48+
count=len(value),
49+
)
50+
51+
@overload
52+
def __getitem__( # noqa: D105 - missing docstring in magic method
53+
self, index: int
54+
) -> DateTime: ...
55+
56+
@overload
57+
def __getitem__( # noqa: D105 - missing docstring in magic method
58+
self, index: slice
59+
) -> DateTimeArray: ...
60+
61+
def __getitem__(self, index: int | slice) -> DateTime | DateTimeArray:
62+
"""Return self[index].
63+
64+
Raises:
65+
TypeError: If index is an invalid type.
66+
IndexError: If index is out of range.
67+
"""
68+
if isinstance(index, int):
69+
entry = self._array[index].item()
70+
as_tuple = TimeValueTuple.from_cvi(*entry)
71+
return DateTime.from_tuple(as_tuple)
72+
elif isinstance(index, slice):
73+
sliced_entries = self._array[index]
74+
new_array = DateTimeArray()
75+
new_array._array = sliced_entries
76+
return new_array
77+
else:
78+
raise invalid_arg_type("index", "int or slice", index)
79+
80+
def __len__(self) -> int:
81+
"""Return len(self)."""
82+
return len(self._array)
83+
84+
@overload
85+
def __setitem__( # noqa: D105 - missing docstring in magic method
86+
self, index: int, value: DateTime
87+
) -> None: ...
88+
89+
@overload
90+
def __setitem__( # noqa: D105 - missing docstring in magic method
91+
self, index: slice, value: Iterable[DateTime]
92+
) -> None: ...
93+
94+
def __setitem__(self, index: int | slice, value: DateTime | Iterable[DateTime]) -> None:
95+
"""Set a new value for DateTime at the specified location or slice.
96+
97+
Raises:
98+
TypeError: If index is an invalid type, or slice value is not iterable.
99+
ValueError: If slice assignment length doesn't match the selected range.
100+
IndexError: If index is out of range.
101+
"""
102+
if isinstance(index, int):
103+
if not isinstance(value, DateTime):
104+
raise invalid_arg_type("value", "DateTime", value)
105+
self._array[index] = value.to_tuple().to_cvi()
106+
elif isinstance(index, slice):
107+
if not isinstance(value, Iterable):
108+
raise invalid_arg_type("value", "iterable of DateTime", value)
109+
values = list(value)
110+
if not all(isinstance(item, DateTime) for item in values):
111+
raise invalid_arg_type("value", "iterable of DateTime", value)
112+
113+
start, stop, step = index.indices(len(self))
114+
selected_count = len(range(start, stop, step))
115+
new_entry_count = len(values)
116+
if step > 1 and new_entry_count != selected_count:
117+
raise invalid_arg_value(
118+
"value", "iterable with the same length as the slice", value
119+
)
120+
121+
if new_entry_count < selected_count:
122+
# Shrink
123+
replaced = slice(start, start + new_entry_count)
124+
removed = slice(start + new_entry_count, stop)
125+
self._array[replaced] = [item.to_tuple().to_cvi() for item in values]
126+
del self[removed]
127+
elif new_entry_count > selected_count:
128+
# Grow
129+
replaced = slice(start, stop)
130+
self._array[replaced] = [
131+
item.to_tuple().to_cvi() for item in values[:selected_count]
132+
]
133+
self._array = np.insert(
134+
self._array,
135+
stop,
136+
[item.to_tuple().to_cvi() for item in values[selected_count:]],
137+
)
138+
else:
139+
# Replace, accounting for strides
140+
self._array[index] = [item.to_tuple().to_cvi() for item in values]
141+
else:
142+
raise invalid_arg_type("index", "int or slice", index)
143+
144+
@overload
145+
def __delitem__(self, index: int) -> None: ... # noqa: D105 - missing docstring in magic method
146+
147+
@overload
148+
def __delitem__( # noqa: D105 - missing docstring in magic method
149+
self, index: slice
150+
) -> None: ...
151+
152+
def __delitem__(self, index: int | slice) -> None:
153+
"""Delete the value at the specified location or slice.
154+
155+
Raises:
156+
TypeError: If index is an invalid type.
157+
IndexError: If index is out of range.
158+
"""
159+
if isinstance(index, (int, slice)):
160+
self._array = np.delete(self._array, index)
161+
else:
162+
raise invalid_arg_type("index", "int or slice", index)
163+
164+
def insert(self, index: int, value: DateTime) -> None:
165+
"""Insert the DateTime value before the specified index.
166+
167+
Raises:
168+
TypeError: If index is not int or value is not DateTime.
169+
"""
170+
if not isinstance(index, int):
171+
raise invalid_arg_type("index", "int", index)
172+
if not isinstance(value, DateTime):
173+
raise invalid_arg_type("value", "DateTime", value)
174+
lower = -len(self._array)
175+
upper = len(self._array)
176+
index = min(max(index, lower), upper)
177+
as_cvi = value.to_tuple().to_cvi()
178+
self._array = np.insert(self._array, index, as_cvi)
179+
180+
def extend(self, values: Iterable[DateTime]) -> None:
181+
"""Extend the array by appending the elements from values."""
182+
if values is None:
183+
raise invalid_arg_type("values", "iterable of DateTime", values)
184+
new_array = DateTimeArray(values)
185+
self._array = np.append(self._array, new_array._array)
186+
187+
def __eq__(self, other: object) -> bool:
188+
"""Return self == other."""
189+
if not isinstance(other, DateTimeArray):
190+
return NotImplemented
191+
return np.array_equal(self._array, other._array)
192+
193+
def __reduce__(self) -> tuple[Any, ...]:
194+
"""Return object state for pickling."""
195+
return (self.__class__, (list(iter(self)),))
196+
197+
def __repr__(self) -> str:
198+
"""Return repr(self)."""
199+
ctor_args = list(iter(self))
200+
return f"{self.__class__.__module__}.{self.__class__.__name__}({ctor_args})"
201+
202+
def __str__(self) -> str:
203+
"""Return str(self)."""
204+
values = list(iter(self))
205+
return f"[{'; '.join(str(v) for v in values)}]"

src/nitypes/bintime/_timedelta_array.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Collection, Iterable, MutableSequence
3+
from collections.abc import Iterable, MutableSequence
44
from typing import (
55
TYPE_CHECKING,
66
Any,
@@ -36,11 +36,10 @@ class TimeDeltaArray(MutableSequence[TimeDelta]):
3636

3737
def __init__(
3838
self,
39-
value: Collection[TimeDelta] | None = None,
39+
value: Iterable[TimeDelta] | None = None,
4040
) -> None:
4141
"""Initialize a new TimeDeltaArray."""
42-
if value is None:
43-
value = []
42+
value = [] if value is None else list(value)
4443
if not all(isinstance(item, TimeDelta) for item in value):
4544
raise invalid_arg_type("value", "iterable of TimeDelta", value)
4645
self._array = np.fromiter(
@@ -107,12 +106,12 @@ def __setitem__(self, index: int | slice, value: TimeDelta | Iterable[TimeDelta]
107106
elif isinstance(index, slice):
108107
if not isinstance(value, Iterable):
109108
raise invalid_arg_type("value", "iterable of TimeDelta", value)
110-
if not all(isinstance(item, TimeDelta) for item in value):
109+
values = list(value)
110+
if not all(isinstance(item, TimeDelta) for item in values):
111111
raise invalid_arg_type("value", "iterable of TimeDelta", value)
112112

113113
start, stop, step = index.indices(len(self))
114114
selected_count = len(range(start, stop, step))
115-
values = list(value)
116115
new_entry_count = len(values)
117116
if step > 1 and new_entry_count != selected_count:
118117
raise invalid_arg_value(
@@ -178,6 +177,13 @@ def insert(self, index: int, value: TimeDelta) -> None:
178177
as_cvi = value.to_tuple().to_cvi()
179178
self._array = np.insert(self._array, index, as_cvi)
180179

180+
def extend(self, values: Iterable[TimeDelta]) -> None:
181+
"""Extend the array by appending the elements from values."""
182+
if values is None:
183+
raise invalid_arg_type("values", "iterable of DateTime", values)
184+
new_array = TimeDeltaArray(values)
185+
self._array = np.append(self._array, new_array._array)
186+
181187
def __eq__(self, other: object) -> bool:
182188
"""Return self == other."""
183189
if not isinstance(other, TimeDeltaArray):
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import numpy as np
4+
import pytest
5+
from pytest_benchmark.fixture import BenchmarkFixture
6+
7+
import nitypes.bintime as bt
8+
9+
10+
LIST_10: list[bt.DateTime] = [
11+
bt.DateTime.from_offset(bt.TimeDelta(float(offset))) for offset in np.arange(0, 10, 0.3)
12+
]
13+
LIST_100: list[bt.DateTime] = [
14+
bt.DateTime.from_offset(bt.TimeDelta(float(offset))) for offset in np.arange(0, 100, 0.3)
15+
]
16+
LIST_1000: list[bt.DateTime] = [
17+
bt.DateTime.from_offset(bt.TimeDelta(float(offset))) for offset in np.arange(0, 1000, 0.3)
18+
]
19+
LIST_10000: list[bt.DateTime] = [
20+
bt.DateTime.from_offset(bt.TimeDelta(float(offset))) for offset in np.arange(0, 10000, 0.3)
21+
]
22+
23+
24+
@pytest.mark.benchmark(group="datetime_array_construct", min_rounds=100)
25+
@pytest.mark.parametrize("constructor_list", (LIST_10, LIST_100, LIST_1000, LIST_10000))
26+
def test___bt_datetime_array___construct(
27+
benchmark: BenchmarkFixture,
28+
constructor_list: list[bt.DateTime],
29+
) -> None:
30+
benchmark(bt.DateTimeArray, constructor_list)
31+
32+
33+
@pytest.mark.benchmark(group="datetime_array_extend", min_rounds=100)
34+
@pytest.mark.parametrize("extend_list", (LIST_10, LIST_100, LIST_1000, LIST_10000))
35+
def test___bt_datetime_array___extend(
36+
benchmark: BenchmarkFixture,
37+
extend_list: list[bt.DateTime],
38+
) -> None:
39+
empty_array = bt.DateTimeArray()
40+
benchmark(empty_array.extend, extend_list)

tests/benchmark/bintime/test_timedelta_array.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,13 @@ def test___bt_timedelta_array___construct(
2424
constructor_list: list[bt.TimeDelta],
2525
) -> None:
2626
benchmark(bt.TimeDeltaArray, constructor_list)
27+
28+
29+
@pytest.mark.benchmark(group="timedelta_array_extend", min_rounds=100)
30+
@pytest.mark.parametrize("extend_list", (LIST_10, LIST_100, LIST_1000, LIST_10000))
31+
def test___bt_timedelta_array___extend(
32+
benchmark: BenchmarkFixture,
33+
extend_list: list[bt.TimeDelta],
34+
) -> None:
35+
empty_array = bt.TimeDeltaArray()
36+
benchmark(empty_array.extend, extend_list)

0 commit comments

Comments
 (0)