"""RFC2579 date-time implementation."""
import decimal
from dfdatetime import definitions
from dfdatetime import factory
from dfdatetime import interface
[docs]
class RFC2579DateTime(interface.DateTimeValues):
"""RFC2579 date-time.
The RFC2579 date-time structure is 11 bytes of size and contains:
struct {
uin16_t year,
uint8_t month,
uint8_t day_of_month,
uint8_t hours,
uint8_t minutes,
uint8_t seconds,
uint8_t deciseconds,
char direction_from_utc,
uint8_t hours_from_utc,
uint8_t minutes_from_utc
}
Also see:
https://datatracker.ietf.org/doc/html/rfc2579
Attributes:
year (int): year, 0 through 65536.
month (int): month of year, 1 through 12.
day_of_month (int): day of month, 1 through 31.
hours (int): hours, 0 through 23.
minutes (int): minutes, 0 through 59.
seconds (int): seconds, 0 through 59, where 60 is used to represent
a leap-second.
deciseconds (int): deciseconds, 0 through 9.
"""
# TODO: make attributes read-only.
# pylint: disable=missing-type-doc
[docs]
def __init__(self, precision=None, rfc2579_date_time_tuple=None):
"""Initializes a RFC2579 date-time.
Args:
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
rfc2579_date_time_tuple:
(Optional[tuple[int, int, int, int, int, int, int, str, int, int]]):
RFC2579 date-time time, contains year, month, day of month, hours,
minutes, seconds and deciseconds, and time zone offset in hours and
minutes from UTC.
Raises:
ValueError: if the system time is invalid.
"""
super().__init__(precision=precision or definitions.PRECISION_100_MILLISECONDS)
self._day_of_month = None
self._deciseconds = None
self._hours = None
self._minutes = None
self._month = None
self._number_of_seconds = None
self._seconds = None
self._year = None
if rfc2579_date_time_tuple:
if len(rfc2579_date_time_tuple) < 10:
raise ValueError(
"Invalid RFC2579 date-time tuple 10 elements required."
)
if rfc2579_date_time_tuple[0] < 0 or rfc2579_date_time_tuple[0] > 65536:
raise ValueError("Year value out of bounds.")
if rfc2579_date_time_tuple[1] not in range(1, 13):
raise ValueError("Month value out of bounds.")
days_per_month = self._GetDaysPerMonth(
rfc2579_date_time_tuple[0], rfc2579_date_time_tuple[1]
)
if (
rfc2579_date_time_tuple[2] < 1
or rfc2579_date_time_tuple[2] > days_per_month
):
raise ValueError("Day of month value out of bounds.")
if rfc2579_date_time_tuple[3] not in range(0, 24):
raise ValueError("Hours value out of bounds.")
if rfc2579_date_time_tuple[4] not in range(0, 60):
raise ValueError("Minutes value out of bounds.")
# TODO: support a leap second?
if rfc2579_date_time_tuple[5] not in range(0, 60):
raise ValueError("Seconds value out of bounds.")
if rfc2579_date_time_tuple[6] < 0 or rfc2579_date_time_tuple[6] > 9:
raise ValueError("Deciseconds value out of bounds.")
if rfc2579_date_time_tuple[7] not in ("+", "-"):
raise ValueError("Direction from UTC value out of bounds.")
if rfc2579_date_time_tuple[8] not in range(0, 14):
raise ValueError("Hours from UTC value out of bounds.")
if rfc2579_date_time_tuple[9] not in range(0, 60):
raise ValueError("Minutes from UTC value out of bounds.")
time_zone_offset = (
rfc2579_date_time_tuple[8] * 60
) + rfc2579_date_time_tuple[9]
if rfc2579_date_time_tuple[7] == "-":
time_zone_offset = -time_zone_offset
self._time_zone_offset = time_zone_offset
self._year = rfc2579_date_time_tuple[0]
self._month = rfc2579_date_time_tuple[1]
self._day_of_month = rfc2579_date_time_tuple[2]
self._hours = rfc2579_date_time_tuple[3]
self._minutes = rfc2579_date_time_tuple[4]
self._seconds = rfc2579_date_time_tuple[5]
self._deciseconds = rfc2579_date_time_tuple[6]
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
self._year,
self._month,
self._day_of_month,
self._hours,
self._minutes,
self._seconds,
)
def _GetNormalizedTimestamp(self):
"""Retrieves the normalized timestamp.
Returns:
decimal.Decimal: normalized timestamp, which contains the number of
seconds since January 1, 1970 00:00:00 and a fraction of second used
for increased precision, or None if the normalized timestamp cannot be
determined.
"""
if self._normalized_timestamp is None:
if self._number_of_seconds is not None:
self._normalized_timestamp = (
decimal.Decimal(self._deciseconds)
/ definitions.DECISECONDS_PER_SECOND
)
self._normalized_timestamp += decimal.Decimal(self._number_of_seconds)
if self._time_zone_offset:
self._normalized_timestamp -= self._time_zone_offset * 60
return self._normalized_timestamp
@property
def deciseconds(self):
"""int: number of deciseconds or None if not set."""
return self._deciseconds
@property
def day_of_month(self):
"""int: day of month or None if not set."""
return self._day_of_month
@property
def hours(self):
"""int: number of hours or None if not set."""
return self._hours
@property
def minutes(self):
"""int: number of minutes or None if not set."""
return self._minutes
@property
def month(self):
"""int: month or None if not set."""
return self._month
@property
def seconds(self):
"""int: number of seconds or None if not set."""
return self._seconds
@property
def year(self):
"""int: year or None if not set."""
return self._year
[docs]
def CopyFromDateTimeString(self, time_string):
"""Copies a RFC2579 date-time from a date and time string.
Args:
time_string (str): date and time value formatted as:
YYYY-MM-DD hh:mm:ss.######[+-]##:##
Where # are numeric digits ranging from 0 to 9 and the seconds
fraction can be either 3, 6 or 9 digits. The time of day, seconds
fraction and time zone offset are optional. The default time zone
is UTC.
Raises:
ValueError: if the date string is invalid or not supported.
"""
date_time_values = self._CopyDateTimeFromString(time_string)
year = date_time_values.get("year", 0)
month = date_time_values.get("month", 0)
day_of_month = date_time_values.get("day_of_month", 0)
hours = date_time_values.get("hours", 0)
minutes = date_time_values.get("minutes", 0)
seconds = date_time_values.get("seconds", 0)
nanoseconds = date_time_values.get("nanoseconds", 0)
time_zone_offset = date_time_values.get("time_zone_offset")
deciseconds, _ = divmod(nanoseconds, definitions.NANOSECONDS_PER_DECISECOND)
if year < 0 or year > 65536:
raise ValueError(f"Unsupported year value: {year:d}.")
self._normalized_timestamp = None
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds
)
self._time_zone_offset = time_zone_offset
self._year = year
self._month = month
self._day_of_month = day_of_month
self._hours = hours
self._minutes = minutes
self._seconds = seconds
self._deciseconds = deciseconds
[docs]
def CopyToDateTimeString(self):
"""Copies the RFC2579 date-time to a date and time string.
Returns:
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss.#" or
None if the number of seconds is missing.
"""
if self._number_of_seconds is None:
return None
return (
f"{self._year:04d}-{self._month:02d}-{self._day_of_month:02d} "
f"{self._hours:02d}:{self._minutes:02d}:{self._seconds:02d}"
f".{self._deciseconds:01d}"
)
[docs]
def CopyToSerializableDict(self):
"""Copies the date time value to a serializable dictionary.
Returns:
dict[str, object]: serializable dictionary.
"""
time_zone_hours, time_zone_minutes = divmod(self._time_zone_offset, 60)
if self._time_zone_offset < 0:
time_zone_sign = "-"
time_zone_hours *= -1
else:
time_zone_sign = "+"
return {
"__class_name__": type(self).__name__,
"__type__": "DateTimeValues",
"rfc2579_date_time_tuple": (
self._year,
self._month,
self._day_of_month,
self._hours,
self._minutes,
self._seconds,
self._deciseconds,
time_zone_sign,
time_zone_hours,
time_zone_minutes,
),
}
factory.Factory.RegisterDateTimeValues(RFC2579DateTime)