Module ocean_science_utilities.tools.time
Copyright (C) 2022 Sofar Ocean Technologies
Authors: Pieter Bart Smit
Expand source code
"""
Copyright (C) 2022
Sofar Ocean Technologies
Authors: Pieter Bart Smit
"""
import numpy as np
import pandas as pd
import xarray
from datetime import datetime, timezone, timedelta
from numbers import Number
from typing import Optional, Union, Sequence
scalar_input_types = Union[str, float, int, datetime, np.datetime64]
input_types = Union[scalar_input_types, Sequence[scalar_input_types]]
def to_datetime_utc(time: input_types) -> Union[datetime, Sequence[datetime], None]:
"""
Output datetimes are garantueed to be in the UTC timezone. For timezone naive input
the timezone is assumed to be UTC. None as input is translated to None as output
to allow for cases where time is optional. Note that the implementation works
with heterogeneous sequences.
:param time: Time, is either a valid scalar time type or a sequence of time types.
:return: If the input is a sequence, the output is a sequence of datetimes,
otherwise it is a scalar datetime.
"""
if time is None:
return None
if isinstance(time, (xarray.DataArray, list, tuple, np.ndarray, pd.Series)):
# if this is a sequence type, recursively call to datetime on the sequence
if isinstance(time, (xarray.DataArray, pd.Series)):
time = time.values
return [to_datetime_utc(x) for x in time] # type: ignore
else:
# if this is a scalar type, do the appropriate conversion.
if isinstance(time, datetime):
if time.tzinfo is None:
time = time.replace(tzinfo=timezone.utc)
return time.astimezone(timezone.utc)
elif isinstance(time, str):
if time[-1] == "Z":
# From isoformat does not parse "Z" as a valid timezone designator.
# This should be fixed in Python 3.11.
time = time[:-1] + "+00:00"
try:
dt = datetime.fromisoformat(time)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except ValueError:
return datetime.strptime(time, "%Y-%m-%dT%H:%M:%S.%f%z").astimezone(
timezone.utc
)
elif isinstance(time, np.datetime64):
# We first cast datetime64 explicitly to seconds and then as a float to
# allow for fractional seconds.
return datetime.fromtimestamp(
np.datetime64(time, "s").astype("float64"), tz=timezone.utc
).replace(tzinfo=timezone.utc)
elif isinstance(time, Number):
return datetime.fromtimestamp(time, tz=timezone.utc) # type: ignore
else:
raise ValueError(f"Unknown time type: {type(time)} with value {time}")
def datetime_to_iso_time_string(time: Optional[input_types]) -> Optional[str]:
if time is None:
return None
time = to_datetime_utc(time)
return time.strftime("%Y-%m-%dT%H:%M:%S.%fZ") # type: ignore
def datetime64_to_timestamp(time: Sequence[np.datetime64]) -> Sequence[np.datetime64]:
return (time - np.datetime64(0, "s")) / np.timedelta64(1, "s") # type: ignore
def to_datetime64(time) -> Union[None, np.datetime64, np.typing.NDArray[np.datetime64]]:
"""
Convert time input to numpy np.ndarrays.
:param time:
:return:
"""
if time is None:
return None
# Convert to a (sequence of) UTC datetime(s)
time = to_datetime_utc(time)
if isinstance(time, datetime):
# If a datetime- do conversion
return np.datetime64(int(time.timestamp()), "s").astype("<M8[ns]")
else:
# if a sequence, do list comprehension and return an array
return np.array(
[np.datetime64(int(x.timestamp()), "s").astype("<M8[ns]") for x in time]
)
def time_from_timeint(t: int) -> timedelta:
"""
unpack a timedelta from a time given as an integer in the form "hhmmss"
e.g. 201813 for 20:18:13
"""
if t >= 10000:
hours = t // 10000
minutes = (t - hours * 10000) // 100
seconds = t - hours * 10000 - minutes * 100
elif t >= 100:
hours = t // 100
minutes = t - hours * 100
seconds = 0
else:
hours = t
minutes = 0
seconds = 0
return timedelta(seconds=(hours * 3600 + minutes * 60 + seconds))
def date_from_dateint(t: int) -> datetime:
"""
unpack a datetime from a date given as an integer in the form "yyyymmdd" or "yymmdd"
e.g. 20221109 for 2022-11-09 or 221109 for 2022-11-09
"""
if t > 1000000:
years = t // 10000
months = (t - years * 10000) // 100
days = t - years * 10000 - months * 100
else:
years = t // 10000
months = (t - years * 10000) // 100
days = t - years * 10000 - months * 100
years = years + 2000
return datetime(years, months, days, tzinfo=timezone.utc)
def datetime_from_time_and_date_integers(
date_int: int, time_int: int, as_datetime64=False
) -> Union[datetime, None, np.datetime64, np.typing.NDArray[np.datetime64]]:
"""
Convert a date and time given as integed encoded in the form "yyyymmdd" and "hhmm"
_or_ "hhmmss" to a datetime
:param date_int: integer of the form yyyymmdd
:param time_int: time of the form "hhmm" or "hhmmss"
:return:
"""
dt = date_from_dateint(date_int) + time_from_timeint(time_int)
if as_datetime64:
return to_datetime64(dt)
else:
return dt
Functions
def date_from_dateint(t: int) ‑> datetime.datetime
-
unpack a datetime from a date given as an integer in the form "yyyymmdd" or "yymmdd" e.g. 20221109 for 2022-11-09 or 221109 for 2022-11-09
Expand source code
def date_from_dateint(t: int) -> datetime: """ unpack a datetime from a date given as an integer in the form "yyyymmdd" or "yymmdd" e.g. 20221109 for 2022-11-09 or 221109 for 2022-11-09 """ if t > 1000000: years = t // 10000 months = (t - years * 10000) // 100 days = t - years * 10000 - months * 100 else: years = t // 10000 months = (t - years * 10000) // 100 days = t - years * 10000 - months * 100 years = years + 2000 return datetime(years, months, days, tzinfo=timezone.utc)
def datetime64_to_timestamp(time: Sequence[numpy.datetime64]) ‑> Sequence[numpy.datetime64]
-
Expand source code
def datetime64_to_timestamp(time: Sequence[np.datetime64]) -> Sequence[np.datetime64]: return (time - np.datetime64(0, "s")) / np.timedelta64(1, "s") # type: ignore
def datetime_from_time_and_date_integers(date_int: int, time_int: int, as_datetime64=False) ‑> Union[datetime.datetime, ForwardRef(None), numpy.datetime64, numpy.ndarray[Any, numpy.dtype[numpy.datetime64]]]
-
Convert a date and time given as integed encoded in the form "yyyymmdd" and "hhmm" or "hhmmss" to a datetime :param date_int: integer of the form yyyymmdd :param time_int: time of the form "hhmm" or "hhmmss" :return:
Expand source code
def datetime_from_time_and_date_integers( date_int: int, time_int: int, as_datetime64=False ) -> Union[datetime, None, np.datetime64, np.typing.NDArray[np.datetime64]]: """ Convert a date and time given as integed encoded in the form "yyyymmdd" and "hhmm" _or_ "hhmmss" to a datetime :param date_int: integer of the form yyyymmdd :param time_int: time of the form "hhmm" or "hhmmss" :return: """ dt = date_from_dateint(date_int) + time_from_timeint(time_int) if as_datetime64: return to_datetime64(dt) else: return dt
def datetime_to_iso_time_string(time: Union[str, float, int, datetime.datetime, numpy.datetime64, Sequence[Union[str, float, int, datetime.datetime, numpy.datetime64]], ForwardRef(None)]) ‑> Optional[str]
-
Expand source code
def datetime_to_iso_time_string(time: Optional[input_types]) -> Optional[str]: if time is None: return None time = to_datetime_utc(time) return time.strftime("%Y-%m-%dT%H:%M:%S.%fZ") # type: ignore
def time_from_timeint(t: int) ‑> datetime.timedelta
-
unpack a timedelta from a time given as an integer in the form "hhmmss" e.g. 201813 for 20:18:13
Expand source code
def time_from_timeint(t: int) -> timedelta: """ unpack a timedelta from a time given as an integer in the form "hhmmss" e.g. 201813 for 20:18:13 """ if t >= 10000: hours = t // 10000 minutes = (t - hours * 10000) // 100 seconds = t - hours * 10000 - minutes * 100 elif t >= 100: hours = t // 100 minutes = t - hours * 100 seconds = 0 else: hours = t minutes = 0 seconds = 0 return timedelta(seconds=(hours * 3600 + minutes * 60 + seconds))
def to_datetime64(time) ‑> Union[ForwardRef(None), numpy.datetime64, numpy.ndarray[Any, numpy.dtype[numpy.datetime64]]]
-
Convert time input to numpy np.ndarrays. :param time: :return:
Expand source code
def to_datetime64(time) -> Union[None, np.datetime64, np.typing.NDArray[np.datetime64]]: """ Convert time input to numpy np.ndarrays. :param time: :return: """ if time is None: return None # Convert to a (sequence of) UTC datetime(s) time = to_datetime_utc(time) if isinstance(time, datetime): # If a datetime- do conversion return np.datetime64(int(time.timestamp()), "s").astype("<M8[ns]") else: # if a sequence, do list comprehension and return an array return np.array( [np.datetime64(int(x.timestamp()), "s").astype("<M8[ns]") for x in time] )
def to_datetime_utc(time: Union[str, float, int, datetime.datetime, numpy.datetime64, Sequence[Union[str, float, int, datetime.datetime, numpy.datetime64]]]) ‑> Union[datetime.datetime, Sequence[datetime.datetime], ForwardRef(None)]
-
Output datetimes are garantueed to be in the UTC timezone. For timezone naive input the timezone is assumed to be UTC. None as input is translated to None as output to allow for cases where time is optional. Note that the implementation works with heterogeneous sequences.
:param time: Time, is either a valid scalar time type or a sequence of time types. :return: If the input is a sequence, the output is a sequence of datetimes, otherwise it is a scalar datetime.
Expand source code
def to_datetime_utc(time: input_types) -> Union[datetime, Sequence[datetime], None]: """ Output datetimes are garantueed to be in the UTC timezone. For timezone naive input the timezone is assumed to be UTC. None as input is translated to None as output to allow for cases where time is optional. Note that the implementation works with heterogeneous sequences. :param time: Time, is either a valid scalar time type or a sequence of time types. :return: If the input is a sequence, the output is a sequence of datetimes, otherwise it is a scalar datetime. """ if time is None: return None if isinstance(time, (xarray.DataArray, list, tuple, np.ndarray, pd.Series)): # if this is a sequence type, recursively call to datetime on the sequence if isinstance(time, (xarray.DataArray, pd.Series)): time = time.values return [to_datetime_utc(x) for x in time] # type: ignore else: # if this is a scalar type, do the appropriate conversion. if isinstance(time, datetime): if time.tzinfo is None: time = time.replace(tzinfo=timezone.utc) return time.astimezone(timezone.utc) elif isinstance(time, str): if time[-1] == "Z": # From isoformat does not parse "Z" as a valid timezone designator. # This should be fixed in Python 3.11. time = time[:-1] + "+00:00" try: dt = datetime.fromisoformat(time) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc) except ValueError: return datetime.strptime(time, "%Y-%m-%dT%H:%M:%S.%f%z").astimezone( timezone.utc ) elif isinstance(time, np.datetime64): # We first cast datetime64 explicitly to seconds and then as a float to # allow for fractional seconds. return datetime.fromtimestamp( np.datetime64(time, "s").astype("float64"), tz=timezone.utc ).replace(tzinfo=timezone.utc) elif isinstance(time, Number): return datetime.fromtimestamp(time, tz=timezone.utc) # type: ignore else: raise ValueError(f"Unknown time type: {type(time)} with value {time}")