Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility function to convert string to datetime #405

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
21 changes: 14 additions & 7 deletions herbie/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from datetime import datetime, timedelta
from io import StringIO
from shutil import which
from typing import Union, Optional, Literal
from typing import Literal, Optional, Union

import cfgrib
import pandas as pd
Expand All @@ -31,6 +31,7 @@
from herbie import Path, config
from herbie.help import _search_help
from herbie.misc import ANSI
from herbie.toolbox import str_to_datetime

Datetime = Union[datetime, pd.Timestamp, str]

Expand Down Expand Up @@ -191,6 +192,12 @@ def __init__(
self.fxx = pd.to_timedelta(fxx).round("1h").total_seconds() / 60 / 60
self.fxx = int(self.fxx)

# TODO: Do I really need this if I'm using Pandas?
# if isinstance(date, str):
# date = str_to_datetime(date)
# if isinstance(valid_date, str):
# valid_date = str_to_datetime(valid_date)

if date:
# User supplied `date`, which is the model initialization datetime.
self.date = pd.to_datetime(date)
Expand Down Expand Up @@ -1146,13 +1153,13 @@ def xarray(
use_pygrib = False
if use_pygrib:
with pygrib.open(str(local_file)) as grb:
msg = grb.message(1)
cf_params = CRS(msg.projparams).to_cf()
msg = grb.message(1)
cf_params = CRS(msg.projparams).to_cf()

#grb = pygrib.open(str(local_file))
#msg = grb.message(1)
#cf_params = CRS(msg.projparams).to_cf()
#grb.close()
# grb = pygrib.open(str(local_file))
# msg = grb.message(1)
# cf_params = CRS(msg.projparams).to_cf()
# grb.close()

# Funny stuff with polar stereographic (https://github.com/pyproj4/pyproj/issues/856)
# TODO: Is there a better way to handle this? What about south pole?
Expand Down
1 change: 1 addition & 0 deletions herbie/toolbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .cartopy_tools import EasyMap, ccrs, pc, to_180, to_360
from .wind import spddir_to_uv, uv_to_spddir, wind_degree_labels
from .datetime_tools import str_to_datetime
71 changes: 71 additions & 0 deletions herbie/toolbox/datetime_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Tools for datetime related operations."""

import re
from datetime import datetime


def str_to_datetime(date_str: str) -> datetime:
"""
Convert a string representing a date and time into a `datetime` object.

The function uses regular expressions with named groups to extract components of a
date and time (year, month, day, hour, minute, second). It supports formats like:
- ISO 8601 (e.g., "2025-01-26T14:30:45")
- Compact representations (e.g., "20250126143045")
- Common variations (e.g., "2025/01/26 14:30")

Parameters
----------
date_str : str
The input string containing the date and time.

Returns
-------
datetime
A `datetime` object representing the parsed date and time.

Raises
------
ValueError
If the input string does not match the expected format or contains invalid date-time components.

Examples
--------
>>> str_to_datetime("2025-01-26T14:30:45")
datetime.datetime(2025, 1, 26, 14, 30, 45)
>>> str_to_datetime("20250126143045")
datetime.datetime(2025, 1, 26, 14, 30, 45)
>>> str_to_datetime("2025/01/26 14:30")
datetime.datetime(2025, 1, 26, 14, 30)
>>> str_to_datetime("invalid-string")
Traceback (most recent call last):
...
ValueError: Input string 'invalid-string' does not match the expected date-time format.
"""
pattern = re.compile(
r"(?P<year>\d{4})[-/]?"
r"(?P<month>\d{2})[-/]?"
r"(?P<day>\d{2})[T ]?"
r"(?P<hour>\d{2})?:?(?P<minute>\d{2})?:?(?P<second>\d{2})?"
)
match = pattern.search(date_str)

if not match:
raise ValueError(
f"Input string '{date_str}' does not match the expected date-time format."
)

# Extract groups and convert them to integers, filling in defaults for missing values
year = int(match.group("year"))
month = int(match.group("month"))
day = int(match.group("day"))
hour = int(match.group("hour") or 0)
minute = int(match.group("minute") or 0)
second = int(match.group("second") or 0)

try:
return datetime(year, month, day, hour, minute, second)
except ValueError as e:
raise ValueError(
f"Invalid date-time components extracted from '{date_str}': {e}"
)
45 changes: 45 additions & 0 deletions tests/test_toolbox_datetime.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest
from datetime import datetime
from herbie.toolbox import str_to_datetime


def test_valid_iso_format():
"""Test parsing of ISO 8601 format."""
assert str_to_datetime("2025-01-26T14:30:45") == datetime(2025, 1, 26, 14, 30, 45)


def test_valid_compact_format():
"""Test parsing of compact date-time format."""
assert str_to_datetime("20250126143045") == datetime(2025, 1, 26, 14, 30, 45)


def test_valid_slash_separated_format():
"""Test parsing of slash-separated format."""
assert str_to_datetime("2025/01/26 14:30") == datetime(2025, 1, 26, 14, 30)


def test_missing_time():
"""Test parsing of a string with only the date."""
assert str_to_datetime("2025-01-26") == datetime(2025, 1, 26, 0, 0, 0)


def test_invalid_format():
"""Test handling of an invalid format."""
with pytest.raises(
ValueError, match=r"does not match the expected date-time format"
):
str_to_datetime("invalid-string")


def test_invalid_date():
"""Test handling of an invalid date."""
with pytest.raises(ValueError, match=r"Invalid date-time components extracted"):
str_to_datetime("2025-02-30T12:00:00")


def test_missing_components():
"""Test handling of missing components in the input."""
with pytest.raises(
ValueError, match=r"does not match the expected date-time format"
):
str_to_datetime("2025")
Loading