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
50 changes: 34 additions & 16 deletions src/earthkit/data/utils/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,27 @@
STEP_RANGE_PATTERN = re.compile(r"\d+-\d+")


def _is_null_like(value):
"""Return True if value is a null-like object (None, NaN, NaT, pd.NaT)."""
if value is None:
return True

try:
# covers nan, np.datetime64('NaT') and np.timedelta64('NaT')
if np.isnan(value):
return True
except Exception:
pass
try:
# covers pd.NaT
if np.isnan(value.second):
return True
except Exception:
pass

return False


def _handle_complex_object_datetime(dt: np.ndarray) -> np.ndarray:
"""Attempt to handle complex object datetime arrays."""
dt = dt.ravel()
Expand All @@ -45,6 +66,9 @@ def convert(x):


def to_datetime(dt):
if _is_null_like(dt):
return None

if isinstance(dt, datetime.datetime):
return dt

Expand All @@ -67,22 +91,6 @@ def to_datetime(dt):
if hasattr(dt, "isoformat"):
return datetime.datetime.fromisoformat(dt.isoformat())

# if dt is NaT-like, then return None
if dt is None:
return dt
try:
# covers nan, np.datetime64('NaT') and np.timedelta64('NaT')
if np.isnan(dt):
return None
except Exception:
pass
try:
# covers pd.NaT
if np.isnan(dt.second):
return None
except Exception:
pass

dt = from_object(dt)

return to_datetime(dt.to_datetime())
Expand Down Expand Up @@ -142,6 +150,9 @@ def to_date_list(obj):


def to_time(dt):
if _is_null_like(dt):
return None

if isinstance(dt, float):
dt = int(dt)

Expand Down Expand Up @@ -194,6 +205,9 @@ def to_time_list(times):


def to_timedelta(td):
if _is_null_like(td):
return None

if isinstance(td, int):
return datetime.timedelta(hours=td)

Expand Down Expand Up @@ -234,11 +248,15 @@ def to_timedelta_list(td):


def numpy_timedelta_to_timedelta(td):
if np.isnat(td):
return None
td = td.astype("timedelta64[s]").astype(int)
return datetime.timedelta(seconds=int(td))


def numpy_datetime_to_datetime(dt):
if np.isnat(dt):
return None
dt = dt.astype("datetime64[s]").astype(int)
return datetime.datetime.fromtimestamp(int(dt), datetime.timezone.utc).replace(tzinfo=None)

Expand Down
66 changes: 66 additions & 0 deletions tests/utils/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
#

import datetime
import math

import numpy as np
import pandas as pd
import pytest

from earthkit.data.utils.dates import (
Expand Down Expand Up @@ -363,6 +365,70 @@ def test_time_to_grib(d, expected_value, error):
time_to_grib(d)


@pytest.mark.parametrize(
"d",
[
None,
math.nan,
np.nan,
np.datetime64("NaT"),
np.timedelta64("NaT"),
pd.NaT,
],
)
def test_to_datetime_null_like(d):
assert to_datetime(d) is None


@pytest.mark.parametrize(
"d",
[
None,
math.nan,
np.nan,
np.datetime64("NaT"),
np.timedelta64("NaT"),
pd.NaT,
],
)
def test_to_time_null_like(d):
assert to_time(d) is None


@pytest.mark.parametrize(
"d",
[
None,
math.nan,
np.nan,
np.timedelta64("NaT"),
pd.NaT,
],
)
def test_to_timedelta_null_like(d):
assert to_timedelta(d) is None


@pytest.mark.parametrize(
"d",
[
np.datetime64("NaT"),
],
)
def test_numpy_datetime_to_datetime_null_like(d):
assert numpy_datetime_to_datetime(d) is None


@pytest.mark.parametrize(
"d",
[
np.timedelta64("NaT"),
],
)
def test_numpy_timedelta_to_timedelta_null_like(d):
assert numpy_timedelta_to_timedelta(d) is None


@pytest.mark.parametrize(
"step,expected_value,error",
[
Expand Down
Loading