Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 27 additions & 21 deletions dascore/utils/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ def _float_to_datetime(num: float | int) -> np.datetime64:
def _array_to_datetime64(array: np.ndarray) -> np.datetime64 | np.ndarray:
"""Convert an array of floating point timestamps to an array of np.datatime64."""
array = np.asarray(array)
# 0-D arrays cannot be indexed or iterated, which the branches below do;
# use the length-one array it stands for and unpack the scalar at the end.
degenerate = array.ndim == 0
if degenerate:
array = array.reshape(1)
nans = pd.isnull(array)
# dealing with objects
if np.issubdtype(array.dtype, np.dtype(object)):
Expand All @@ -136,8 +141,6 @@ def _array_to_datetime64(array: np.ndarray) -> np.datetime64 | np.ndarray:
# dealing with an array of datetime64 or empty array
if np.issubdtype(array.dtype, np.datetime64) or len(array) == 0:
out = array.astype("datetime64[ns]")
if not array.shape: # unpack degenerate (0-D) array to a scalar
out = out[()]
# dealing with numerical data
elif np.issubdtype(array.dtype, np.timedelta64) or np.isreal(array[0]):
with np.errstate(divide="ignore", invalid="ignore"):
Expand All @@ -147,7 +150,7 @@ def _array_to_datetime64(array: np.ndarray) -> np.datetime64 | np.ndarray:
out = _float_array_to_ns(array).astype("datetime64[ns]")
# fill NaN Back in
out[nans] = _NAT_DATETIME64
return out
return out[0] if degenerate else out


@to_datetime64.register(pd.Series)
Expand Down Expand Up @@ -245,30 +248,33 @@ def _pass_time_delta(time_delta):
def _array_to_timedelta64(array: np.ndarray) -> np.timedelta64 | np.ndarray:
"""Convert an array of floating point durations to np.timedelta64."""
array = np.asarray(array)
# See the note in _array_to_datetime64.
degenerate = array.ndim == 0
if degenerate:
array = array.reshape(1)
# convert pure object arrays into float so sign casting works.
if np.issubdtype(array.dtype, np.dtype(object)):
array = array.astype(np.float64)
if np.issubdtype(array.dtype, np.timedelta64) or len(array) == 0:
out = array.astype("timedelta64[ns]")
# unpack degenerate (0-D) array to a scalar
return out[()] if not array.shape else out
# Need to just get the ns form datetime64
# A datetime becomes its offset from the epoch. The unit has to be
# normalized first, or viewing e.g. datetime64[s] as int64 would label
# its second count as nanoseconds.
elif np.issubdtype(array.dtype, np.datetime64):
int_array = array.view(np.int64)
return np.array(int_array).astype("timedelta64[ns]")

assert np.isreal(array[0])
invalid = pd.isnull(array) | ~np.isfinite(array)
# Need to make copy to 1) not change original array and 2) handle
# immutable arrays. See #575.
if np.any(invalid):
array = np.array(array)
array[invalid] = 0
# inf/NaN complain, salience these types of warnings for this block.
with np.errstate(divide="ignore", invalid="ignore"):
out = _float_array_to_ns(array).astype("timedelta64[ns]")
out[invalid] = _NAT_TIMEDELTA64
return out
out = array.astype("datetime64[ns]").view("timedelta64[ns]")
else:
assert np.isreal(array[0])
invalid = pd.isnull(array) | ~np.isfinite(array)
# Need to make copy to 1) not change original array and 2) handle
# immutable arrays. See #575.
if np.any(invalid):
array = np.array(array)
array[invalid] = 0
# inf/NaN complain, salience these types of warnings for this block.
with np.errstate(divide="ignore", invalid="ignore"):
out = _float_array_to_ns(array).astype("timedelta64[ns]")
out[invalid] = _NAT_TIMEDELTA64
return out[0] if degenerate else out


@to_timedelta64.register(pd.Series)
Expand Down
34 changes: 34 additions & 0 deletions tests/test_utils/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,40 @@ def test_immutable_inputs(self):
time = to_timedelta64(array_no_nan)
assert np.issubdtype(time.dtype, "m8")

@pytest.mark.parametrize("unit", ("D", "s", "ms", "ns"))
def test_datetime_array_is_epoch_offset(self, unit):
"""A datetime becomes its offset from the epoch, whatever its unit."""
value = np.datetime64("2020-01-01", unit)
expected = np.timedelta64(1577836800, "s")
assert to_timedelta64(np.array([value])) == expected
assert to_timedelta64(np.array(value)) == expected


class TestDegenerateArrays:
"""Tests for 0-D array inputs to the array converters."""

values = (
np.datetime64("2020-01-01", "s"),
np.timedelta64(5, "s"),
5.0,
5,
)

@pytest.mark.parametrize("func", (to_datetime64, to_timedelta64))
@pytest.mark.parametrize("value", values)
def test_matches_length_one_array(self, func, value):
"""A 0-D array converts like the length-one array it stands for."""
out = func(np.array(value))
assert np.shape(out) == ()
assert out == func(np.array([value]))[0]

@pytest.mark.parametrize("func", (to_datetime64, to_timedelta64))
def test_nan_becomes_nat(self, func):
"""A 0-D NaN converts to NaT rather than an epoch value."""
out = func(np.array(np.nan))
assert np.shape(out) == ()
assert pd.isnull(out)


class TestToInt:
"""Tests for converting time-like types to ints, or passing through reals."""
Expand Down
Loading