Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
9487985
Add sketch of basic TimeDelta array
jfriedri-ni Jul 24, 2025
b263c5f
Do not incorrectly swap the struct packing order
jfriedri-ni Jul 24, 2025
3772dc4
Specialize the numpy storage as much as supported
jfriedri-ni Jul 25, 2025
d73ae19
Stop using Union
jfriedri-ni Jul 25, 2025
9a15209
Use len() for a 1D numpy array
jfriedri-ni Jul 25, 2025
5dda9f0
Test with negative and fractional values as well
jfriedri-ni Jul 25, 2025
25dd201
Add CVI representation helpers to TimeValueTuple
jfriedri-ni Jul 25, 2025
6626785
Fix docstring typo
jfriedri-ni Jul 25, 2025
3645d6a
Use the CVI helper when getting an item
jfriedri-ni Jul 25, 2025
d25b88f
Try using a generator when constructing the TimeDeltaArray
jfriedri-ni Jul 25, 2025
0bdac3d
Address formatter to get to the type checker
jfriedri-ni Jul 25, 2025
7af2a9a
Iterate over the input sequence when constructing the np array
jfriedri-ni Jul 25, 2025
f35b7ff
Add a test for indexing by integer
jfriedri-ni Jul 25, 2025
268351f
Use np.fromiter() to initialize the backing ndarray
jfriedri-ni Jul 25, 2025
6a6861e
Add test for len()
jfriedri-ni Jul 25, 2025
363740a
Add TimeDeltaArray constructor benchmark tests
jfriedri-ni Jul 25, 2025
56f0d7c
Promote to MutableSequence
jfriedri-ni Jul 28, 2025
27a0c91
Add signatures for remaining MutableSequence methods
jfriedri-ni Jul 28, 2025
37470da
Add impl and test for slicing
jfriedri-ni Jul 28, 2025
30dda3a
Add test for indexing with invalid indices
jfriedri-ni Jul 28, 2025
27faef7
Add test cases for negative index and out of bounds index
jfriedri-ni Jul 29, 2025
c1a2265
Implement and test __setitem__()
jfriedri-ni Jul 29, 2025
f116a78
Implement and test __delitem__()
jfriedri-ni Jul 29, 2025
13fa2a1
Implement and test insert()
jfriedri-ni Jul 29, 2025
1600306
Validate and test constructor argument
jfriedri-ni Jul 30, 2025
4f421e6
Test constructing from another TimeDeltaArray
jfriedri-ni Jul 30, 2025
67db372
Test MutableSequence mixin methods
jfriedri-ni Jul 30, 2025
dceb88c
Implement and test builtin functions
jfriedri-ni Jul 30, 2025
cc7fd8a
Address formatter
jfriedri-ni Jul 30, 2025
afa6e19
Optimize when slicing
jfriedri-ni Jul 31, 2025
57d39af
Clarify error messages
jfriedri-ni Jul 31, 2025
0228bf0
Implement and test __imul__()
jfriedri-ni Jul 31, 2025
f1d20e2
Remove antipattern from benchmark test
jfriedri-ni Jul 31, 2025
0a86f24
Update README and .gitignore to support comparing benchmarks
jfriedri-ni Jul 31, 2025
356d82e
Group the constructor benchmarks into a single test with parametrize
jfriedri-ni Jul 31, 2025
69ae183
Add docstrings from Claude Sonnet 4
jfriedri-ni Aug 1, 2025
d613342
Remove implementation details and example usage from method docstrings
jfriedri-ni Aug 1, 2025
4865b2c
Add TimeDeltaArray to module docstring
jfriedri-ni Aug 1, 2025
192ea1c
Address the analyzers
jfriedri-ni Aug 1, 2025
9f2aac4
Fix docstring tests
jfriedri-ni Aug 1, 2025
b45507b
Switch to google-style docstrings
jfriedri-ni Aug 1, 2025
55c524e
Remove unnecessary detail from descriptions, rely on default docgen b…
jfriedri-ni Aug 1, 2025
2d011fd
Remove the how-to-use narrative section
jfriedri-ni Aug 1, 2025
f280f97
Guess and check -- was it this one?
jfriedri-ni Aug 1, 2025
6935936
Allow indexing with bools
jfriedri-ni Aug 5, 2025
547d051
Use object for __eq__'s type hint on other
jfriedri-ni Aug 5, 2025
4b6e25a
Use flattened strings for test parameter declarations
jfriedri-ni Aug 5, 2025
c5b6fde
Make test section comments match other files' style
jfriedri-ni Aug 5, 2025
b3169c2
Remove __imul__ and tests
jfriedri-ni Aug 5, 2025
e504a6f
Use error factories for user input validation
jfriedri-ni Aug 5, 2025
21f3b09
Fix test name typo
jfriedri-ni Aug 6, 2025
7a9f9a2
Validate that an unpickled TimeDeltaArray has its own backing array
jfriedri-ni Aug 6, 2025
01a2aa4
Remove test for sorted
jfriedri-ni Aug 6, 2025
03ffd4e
Add test for deepcopy()
jfriedri-ni Aug 6, 2025
17c090f
Use pytest parameters for validating equals
jfriedri-ni Aug 6, 2025
7b91f69
TDD: use full truth-table coverage for setting with a slice
jfriedri-ni Aug 6, 2025
4891ab4
Make TimeDeltaArray behave like list when setting by slice
jfriedri-ni Aug 6, 2025
fdb40bb
Unpack the slice into named variables
jfriedri-ni Aug 7, 2025
9e3c67a
Prefer builtin functions over magic methods
jfriedri-ni Aug 7, 2025
8543def
Group the slice assignment tests together
jfriedri-ni Aug 7, 2025
3b198e0
Add test cases for settings mismatched-length values on contiguous sl…
jfriedri-ni Aug 7, 2025
89b857a
Raise when trying to shrink the array using a stride
jfriedri-ni Aug 7, 2025
896abc8
Do not hokey-pokey when setting a single value with a slice
jfriedri-ni Aug 7, 2025
8c87415
Group selected==incoming test cases
jfriedri-ni Aug 7, 2025
83e6bcf
Remove 'expected to raise' test cases that should pass for plain lists
jfriedri-ni Aug 7, 2025
f9ff477
Group and add mismatched slice vs incoming test cases
jfriedri-ni Aug 7, 2025
c891490
Address the analyzers
jfriedri-ni Aug 7, 2025
26a18ba
Contiguous slices accept any number of incoming entries
jfriedri-ni Aug 7, 2025
176aa33
Implement plain old list slice assignment
jfriedri-ni Aug 7, 2025
157d3e4
Use np.insert to grow the array once
jfriedri-ni Aug 8, 2025
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
70 changes: 32 additions & 38 deletions src/nitypes/bintime/_timedelta_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import numpy as np
import numpy.typing as npt

from nitypes._exceptions import invalid_arg_value, invalid_arg_type

if TYPE_CHECKING:
# Import from the public package so the docs don't reference private submodules.
from nitypes.bintime import CVITimeIntervalDType, TimeDelta, TimeValueTuple
Expand Down Expand Up @@ -40,7 +42,7 @@ def __init__(
if value is None:
value = []
if not all(isinstance(item, TimeDelta) for item in value):
raise TypeError("Cannot assign values that are not of type TimeDelta")
raise invalid_arg_type("value", "iterable of TimeDelta", value)
self._array = np.fromiter(
(entry.to_tuple().to_cvi() for entry in value),
dtype=CVITimeIntervalDType,
Expand All @@ -64,8 +66,6 @@ def __getitem__(self, index: int | slice) -> TimeDelta | TimeDeltaArray:
TypeError: If index is an invalid type.
IndexError: If index is out of range.
"""
if isinstance(index, bool):
raise TypeError("Cannot index with bool")
if isinstance(index, int):
entry = self._array[index].item()
as_tuple = TimeValueTuple.from_cvi(*entry)
Expand All @@ -76,7 +76,7 @@ def __getitem__(self, index: int | slice) -> TimeDelta | TimeDeltaArray:
new_array._array = sliced_entries
return new_array
else:
raise TypeError("Index must be an int or slice")
raise invalid_arg_type("index", "int or slice", index)

def __len__(self) -> int:
"""Return len(self)."""
Expand All @@ -100,26 +100,40 @@ def __setitem__(self, index: int | slice, value: TimeDelta | Iterable[TimeDelta]
ValueError: If slice assignment length doesn't match the selected range.
IndexError: If index is out of range.
"""
if isinstance(index, bool):
raise TypeError("Cannot index with bool")
if isinstance(index, int):
if not isinstance(value, TimeDelta):
raise TypeError("Cannot assign value that is not of type TimeDelta")
raise invalid_arg_type("value", "TimeDelta", value)
self._array[index] = value.to_tuple().to_cvi()
elif isinstance(index, slice):
selection = index.indices(len(self))
selected_count = len(range(*selection))
if selected_count == 0:
return

if not isinstance(value, Iterable):
raise TypeError("Cannot assign a slice with a non-iterable")
raise invalid_arg_type("value", "iterable of TimeDelta", value)
if not all(isinstance(item, TimeDelta) for item in value):
raise TypeError("Cannot assign values that are not of type TimeDelta")
selected_count = len(range(*index.indices(len(self))))
raise invalid_arg_type("value", "iterable of TimeDelta", value)

values = list(value)
new_entry_count = len(values)
if new_entry_count != selected_count:
message = f"Cannot assign slice with unmatched length. Expected {selected_count} but received {new_entry_count}"
raise ValueError(message)
step_size = selection[-1]
if step_size > 1:
if new_entry_count <= 1:
raise invalid_arg_value("index", "slice with step size 1", index)
elif new_entry_count != selected_count:
raise invalid_arg_value(
"value", "iterable with the same length as the slice", value
)

if new_entry_count == 0:
return self.__delitem__(index)
if new_entry_count == 1:
self.__delitem__(index)
return self.insert(selection[0], values[0])
self._array[index] = [item.to_tuple().to_cvi() for item in values]
else:
raise TypeError("Index must be an int or slice")
raise invalid_arg_type("index", "int or slice", index)

@overload
def __delitem__(self, index: int) -> None: ... # noqa: D105 - missing docstring in magic method
Expand All @@ -136,48 +150,28 @@ def __delitem__(self, index: int | slice) -> None:
TypeError: If index is an invalid type.
IndexError: If index is out of range.
"""
if isinstance(index, bool):
raise TypeError("Cannot index with bool")
if isinstance(index, (int, slice)):
self._array = np.delete(self._array, index)
else:
raise TypeError("Index must be an int or slice")
raise invalid_arg_type("index", "int or slice", index)

def insert(self, index: int, value: TimeDelta) -> None:
"""Insert the TimeDelta value before the specified index.

Raises:
TypeError: If index is not int or value is not TimeDelta.
"""
if isinstance(index, bool):
raise TypeError("Cannot insert with bool")
if not isinstance(index, int):
raise TypeError("Index must be an int")
raise invalid_arg_type("index", "int", index)
if not isinstance(value, TimeDelta):
raise TypeError("Cannot assign value that is not of type TimeDelta")
raise invalid_arg_type("value", "TimeDelta", value)
lower = -len(self._array)
upper = len(self._array)
index = min(max(index, lower), upper)
as_cvi = value.to_tuple().to_cvi()
self._array = np.insert(self._array, index, as_cvi)

def __imul__(self, multiplier: int) -> TimeDeltaArray:
"""Return self*=multiplier.

Raises:
TypeError: If multiplier is not an integer.
"""
if isinstance(multiplier, bool):
raise TypeError("Cannot multiply with bool")
if not isinstance(multiplier, int):
raise TypeError("Multiplier must be an int")
if multiplier <= 0:
self._array = np.array([], dtype=CVITimeIntervalDType)
else:
self._array = np.tile(self._array, multiplier)
return self

def __eq__(self, other: Any) -> bool:
def __eq__(self, other: object) -> bool:
"""Return self == other."""
if not isinstance(other, TimeDeltaArray):
return NotImplemented
Expand Down
28 changes: 14 additions & 14 deletions tests/unit/bintime/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
from nitypes.bintime._timedelta import _BITS_PER_SECOND, _FRACTIONAL_SECONDS_MASK


#############
###############################################################################
# Constructor
#############
###############################################################################
def test___no_args___construct___returns_epoch() -> None:
value = DateTime()

Expand Down Expand Up @@ -124,9 +124,9 @@ def test___local_unit_args___construct___raises_value_error() -> None:
assert exc.value.args[0].startswith("The tzinfo must be datetime.timezone.utc.")


############
###############################################################################
# from_ticks
############
###############################################################################
def test___int_ticks___from_ticks___returns_time_value() -> None:
value = DateTime.from_ticks(0x12345678_90ABCDEF_FEDCBA09_87654321)

Expand All @@ -135,9 +135,9 @@ def test___int_ticks___from_ticks___returns_time_value() -> None:
assert value._offset._ticks == 0x12345678_90ABCDEF_FEDCBA09_87654321


#############
###############################################################################
# from_offset
#############
###############################################################################
def test___time_value___from_offset___returns_time_value() -> None:
value = DateTime.from_offset(TimeDelta.from_ticks(0x12345678_90ABCDEF_FEDCBA09_87654321))

Expand All @@ -146,9 +146,9 @@ def test___time_value___from_offset___returns_time_value() -> None:
assert value._offset._ticks == 0x12345678_90ABCDEF_FEDCBA09_87654321


##############################################
###############################################################################
# year, month, day, hour, minute, second, etc.
##############################################
###############################################################################
@pytest.mark.parametrize(
"other, expected",
[
Expand Down Expand Up @@ -210,9 +210,9 @@ def test___various_values___unit_properties___return_unit_values(
) == expected


###################
###############################################################################
# Binary arithmetic
###################
###############################################################################
@pytest.mark.parametrize(
"left, right, expected",
[
Expand Down Expand Up @@ -304,9 +304,9 @@ def test___datetime___sub___returns_time_value(
assert left - right == expected


############
###############################################################################
# Comparison
############
###############################################################################
@pytest.mark.parametrize(
"left, right",
[
Expand Down Expand Up @@ -395,9 +395,9 @@ def test___lesser_value___comparison___lesser(
assert not (left >= right)


###############
###############################################################################
# Miscellaneous
###############
###############################################################################
_VARIOUS_VALUES = [
DateTime(dt.MINYEAR, 1, 1, 0, 0, 0, 0, 0, 0, dt.timezone.utc),
DateTime(1850, 12, 25, 8, 15, 30, 123_456, 234_567_789, 345_567_890, dt.timezone.utc),
Expand Down
36 changes: 18 additions & 18 deletions tests/unit/bintime/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
_DT_EPSILON = ht.timedelta(microseconds=1)


#############
###############################################################################
# Constructor
#############
###############################################################################
def test___no_args___construct___returns_zero_timedelta() -> None:
value = TimeDelta()

Expand Down Expand Up @@ -127,9 +127,9 @@ def test___invalid_seconds_type___construct___raises_type_error() -> None:
assert exc.value.args[0].startswith("The seconds must be a number or timedelta.")


############
###############################################################################
# from_ticks
############
###############################################################################
def test___int_ticks___from_ticks___returns_timedelta() -> None:
value = TimeDelta.from_ticks(0x12345678_90ABCDEF_FEDCBA09_87654321)

Expand All @@ -153,9 +153,9 @@ def test___unsupported_type___from_ticks___raises_type_error() -> None:
assert exc.value.args[0].startswith("The ticks must be an integer.")


#########################################################
###############################################################################
# days, seconds, microseconds, femtoseconds, yoctoseconds
#########################################################
###############################################################################
@pytest.mark.parametrize(
"value, expected",
[
Expand Down Expand Up @@ -202,9 +202,9 @@ def test___various_values___unit_properties___return_unit_values(
) == expected


###############
###############################################################################
# total_seconds
###############
###############################################################################
@pytest.mark.parametrize(
"seconds",
[0.0, 1.0, 3.14159, -3.14159, 1.23456789e18, -1.23456789e18, 1.23456789e-18, -1.23456789e-18],
Expand All @@ -217,9 +217,9 @@ def test___float_seconds___total_seconds___approximate_match(seconds: float) ->
assert total_seconds == pytest.approx(seconds)


#########################
###############################################################################
# precision_total_seconds
#########################
###############################################################################
@pytest.mark.parametrize(
"seconds",
[
Expand Down Expand Up @@ -311,9 +311,9 @@ def _random_state() -> Generator[tuple[Any, ...]]:
random.setstate(state)


##################
###############################################################################
# Unary arithmetic
##################
###############################################################################
@pytest.mark.parametrize(
"value, expected",
[
Expand Down Expand Up @@ -370,9 +370,9 @@ def test___timedeltas___abs___returns_absolute_value(value: TimeDelta, expected:
assert abs(value) == expected


###################
###############################################################################
# Binary arithmetic
###################
###############################################################################
@pytest.mark.parametrize(
"left, right, expected",
[
Expand Down Expand Up @@ -1054,9 +1054,9 @@ def test___ht_timedelta___divmod___returns_int_and_timedelta(
assert divmod(left, right) == expected


############
###############################################################################
# Comparison
############
###############################################################################
@pytest.mark.parametrize(
"left, right",
[
Expand Down Expand Up @@ -1123,9 +1123,9 @@ def test___lesser_value___comparison___lesser(
assert not (left >= right)


###############
###############################################################################
# Miscellaneous
###############
###############################################################################
@pytest.mark.parametrize(
"value, expected",
[
Expand Down
Loading
Loading