Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into chore/multiple-fix…
Browse files Browse the repository at this point in the history
…es-ci-pre-commit
  • Loading branch information
JesperDramsch committed Sep 18, 2024
2 parents 012bfe0 + d208d0f commit abbc04a
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 11 deletions.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ classifiers = [

dynamic = [ "version" ]
dependencies = [
"aniso8601",
"pyyaml",
"tomli", # Only needed before 3.11
"tomli", # Only needed before 3.11
"tqdm",
]

Expand Down
195 changes: 185 additions & 10 deletions src/anemoi/utils/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@

import calendar
import datetime
import re

from .hindcasts import HindcastDatesTimes
import aniso8601


def normalise_frequency(frequency) -> int:
def normalise_frequency(frequency):
if isinstance(frequency, int):
return frequency
assert isinstance(frequency, str), (type(frequency), frequency)
Expand All @@ -22,7 +23,7 @@ def normalise_frequency(frequency) -> int:
return {"h": v, "d": v * 24}[unit]


def no_time_zone(date) -> datetime.datetime:
def _no_time_zone(date) -> datetime.datetime:
"""Remove time zone information from a date.
Parameters
Expand All @@ -40,32 +41,200 @@ def no_time_zone(date) -> datetime.datetime:


# this function is use in anemoi-datasets
def as_datetime(date) -> datetime.datetime:
def as_datetime(date, keep_time_zone=False) -> datetime.datetime:
"""Convert a date to a datetime object, removing any time zone information.
Parameters
----------
date : datetime.date or datetime.datetime or str
The date to convert.
keep_time_zone : bool, optional
If True, the time zone information is kept, by default False.
Returns
-------
datetime.datetime
The datetime object.
"""

tidy = _no_time_zone if not keep_time_zone else lambda x: x

if isinstance(date, datetime.datetime):
return no_time_zone(date)
return tidy(date)

if isinstance(date, datetime.date):
return no_time_zone(datetime.datetime(date.year, date.month, date.day))
return tidy(datetime.datetime(date.year, date.month, date.day))

if isinstance(date, str):
return no_time_zone(datetime.datetime.fromisoformat(date))
return tidy(datetime.datetime.fromisoformat(date))

raise ValueError(f"Invalid date type: {type(date)}")


def _as_datetime_list(date, default_increment):
if isinstance(date, (list, tuple)):
for d in date:
yield from _as_datetime_list(d, default_increment)

if isinstance(date, str):
# Check for ISO format
try:
start, end = aniso8601.parse_interval(date)
while start <= end:
yield as_datetime(start)
start += default_increment

return

except aniso8601.exceptions.ISOFormatError:
pass

try:
intervals = aniso8601.parse_repeating_interval(date)
for date in intervals:
yield as_datetime(date)
return
except aniso8601.exceptions.ISOFormatError:
pass

yield as_datetime(date)


def as_datetime_list(date, default_increment=1):
default_increment = frequency_to_timedelta(default_increment)
return list(_as_datetime_list(date, default_increment))


def frequency_to_timedelta(frequency) -> datetime.timedelta:
"""Convert a frequency to a timedelta object.
Parameters
----------
frequency : int or str or datetime.timedelta
The frequency to convert. If an integer, it is assumed to be in hours. If a string, it can be in the format:
- "1h" for 1 hour
- "1d" for 1 day
- "1m" for 1 minute
- "1s" for 1 second
- "1:30" for 1 hour and 30 minutes
- "1:30:10" for 1 hour, 30 minutes and 10 seconds
- "PT10M" for 10 minutes (ISO8601)
If a timedelta object is provided, it is returned as is.
Returns
-------
datetime.timedelta
The timedelta object.
Raises
------
ValueError
Exception raised if the frequency cannot be converted to a timedelta.
"""

if isinstance(frequency, datetime.timedelta):
return frequency

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

assert isinstance(frequency, str), (type(frequency), frequency)

try:
return frequency_to_timedelta(int(frequency))
except ValueError:
pass

if re.match(r"^\d+[hdms]$", frequency, re.IGNORECASE):
unit = frequency[-1].lower()
v = int(frequency[:-1])
unit = {"h": "hours", "d": "days", "s": "seconds", "m": "minutes"}[unit]
return datetime.timedelta(**{unit: v})

m = frequency.split(":")
if len(m) == 2:
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]))

if len(m) == 3:
return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]), seconds=int(m[2]))

# ISO8601
try:
return aniso8601.parse_duration(frequency)
except aniso8601.exceptions.ISOFormatError:
pass

raise ValueError(f"Cannot convert frequency {frequency} to timedelta")


def frequency_to_string(frequency) -> str:
"""Convert a frequency (i.e. a datetime.timedelta) to a string.
Parameters
----------
frequency : datetime.timedelta
The frequency to convert.
Returns
-------
str
A string representation of the frequency.
"""

frequency = frequency_to_timedelta(frequency)

total_seconds = frequency.total_seconds()
assert int(total_seconds) == total_seconds, total_seconds
total_seconds = int(total_seconds)

seconds = total_seconds

days = seconds // (24 * 3600)
seconds %= 24 * 3600
hours = seconds // 3600
seconds %= 3600
minutes = seconds // 60
seconds %= 60

if days > 0 and hours == 0 and minutes == 0 and seconds == 0:
return f"{days}d"

if days == 0 and hours > 0 and minutes == 0 and seconds == 0:
return f"{hours}h"

if days == 0 and hours == 0 and minutes > 0 and seconds == 0:
return f"{minutes}m"

if days == 0 and hours == 0 and minutes == 0 and seconds > 0:
return f"{seconds}s"

if days > 0:
return f"{total_seconds}s"

return str(frequency)


def frequency_to_seconds(frequency) -> int:
"""Convert a frequency to seconds.
Parameters
----------
frequency : _type_
_description_
Returns
-------
int
Number of seconds.
"""

result = frequency_to_timedelta(frequency).total_seconds()
assert int(result) == result, result
return int(result)


DOW = {
"monday": 0,
"tuesday": 1,
Expand Down Expand Up @@ -142,7 +311,7 @@ def __init__(self, start, end, increment=24, *, day_of_month=None, day_of_week=N
"""
self.start = as_datetime(start)
self.end = as_datetime(end)
self.increment = datetime.timedelta(hours=increment)
self.increment = frequency_to_timedelta(increment)
self.day_of_month = _make_day(day_of_month)
self.day_of_week = _make_week(day_of_week)
self.calendar_months = _make_months(calendar_months)
Expand Down Expand Up @@ -270,15 +439,16 @@ def datetimes_factory(*args, **kwargs):
name = kwargs.get("name")

if name == "hindcast":
from .hindcasts import HindcastDatesTimes

reference_dates = kwargs["reference_dates"]
reference_dates = datetimes_factory(reference_dates)
years = kwargs["years"]
return HindcastDatesTimes(reference_dates=reference_dates, years=years)

kwargs = kwargs.copy()
if "frequency" in kwargs:
freq = kwargs.pop("frequency")
kwargs["increment"] = normalise_frequency(freq)
kwargs["increment"] = kwargs.pop("frequency")
return DateTimes(**kwargs)

if not any((isinstance(x, dict) or isinstance(x, list)) for x in args):
Expand All @@ -294,3 +464,8 @@ def datetimes_factory(*args, **kwargs):
return datetimes_factory(*a)

return ConcatDateTimes(*[datetimes_factory(a) for a in args])


if __name__ == "__main__":
print(as_datetime_list("R10/2023-01-01T00:00:00Z/P1D"))
print(as_datetime_list("2007-03-01T13:00:00/2008-05-11T15:30:00", "200h"))
59 changes: 59 additions & 0 deletions src/anemoi/utils/humanize.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import warnings
from collections import defaultdict

from anemoi.utils.dates import as_datetime


def bytes_to_human(n: float) -> str:
"""Convert a number of bytes to a human readable string
Expand Down Expand Up @@ -629,3 +631,60 @@ def shorten_list(lst, max_length=5) -> list:
if isinstance(lst, tuple):
return tuple(result)
return result


def _compress_dates(dates):
dates = sorted(dates)
if len(dates) < 3:
yield dates
return

prev = first = dates.pop(0)
curr = dates.pop(0)
delta = curr - prev
while curr - prev == delta:
prev = curr
if not dates:
break
curr = dates.pop(0)

yield (first, prev, delta)
if dates:
yield from _compress_dates([curr] + dates)


def compress_dates(dates) -> str:
"""Compress a list of dates into a human-readable format.
Parameters
----------
dates : list
A list of dates, as datetime objects or strings.
Returns
-------
str
A human-readable string representing the compressed dates.
"""

dates = [as_datetime(_) for _ in dates]
result = []

for n in _compress_dates(dates):
if isinstance(n, list):
result.extend([str(_) for _ in n])
else:
result.append(" ".join([str(n[0]), "to", str(n[1]), "by", str(n[2])]))

return result


def print_dates(dates) -> None:
"""Print a list of dates in a human-readable format.
Parameters
----------
dates : list
A list of dates, as datetime objects or strings.
"""
print(compress_dates(dates))
37 changes: 37 additions & 0 deletions tests/test_frequency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# (C) Copyright 2023 European Centre for Medium-Range Weather Forecasts.
# This software is licensed under the terms of the Apache Licence Version 2.0
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
# In applying this licence, ECMWF does not waive the privileges and immunities
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

import datetime

from anemoi.utils.dates import frequency_to_string
from anemoi.utils.dates import frequency_to_timedelta


def test_frequency_to_string():
assert frequency_to_string(datetime.timedelta(hours=1)) == "1h"
assert frequency_to_string(datetime.timedelta(hours=1, minutes=30)) == "1:30:00"
assert frequency_to_string(datetime.timedelta(days=10)) == "10d"
assert frequency_to_string(datetime.timedelta(minutes=10)) == "10m"
assert frequency_to_string(datetime.timedelta(minutes=90)) == "1:30:00"


def test_frequency_to_timedelta():
assert frequency_to_timedelta("1s") == datetime.timedelta(seconds=1)
assert frequency_to_timedelta("3m") == datetime.timedelta(minutes=3)
assert frequency_to_timedelta("1h") == datetime.timedelta(hours=1)
assert frequency_to_timedelta("3d") == datetime.timedelta(days=3)
assert frequency_to_timedelta("90m") == datetime.timedelta(hours=1, minutes=30)
assert frequency_to_timedelta("0:30") == datetime.timedelta(minutes=30)
assert frequency_to_timedelta("0:30:10") == datetime.timedelta(minutes=30, seconds=10)
assert frequency_to_timedelta("1:30:10") == datetime.timedelta(hours=1, minutes=30, seconds=10)

assert frequency_to_timedelta("PT10M") == datetime.timedelta(minutes=10)


if __name__ == "__main__":
test_frequency_to_string()
test_frequency_to_timedelta()

0 comments on commit abbc04a

Please sign in to comment.