"""Time elements implementation."""
import decimal
from dfdatetime import definitions
from dfdatetime import factory
from dfdatetime import interface
from dfdatetime import precisions
[docs]
class TimeElements(interface.DateTimeValues):
"""Time elements.
Time elements contain separate values for year, month, day of month,
hours, minutes and seconds.
Attributes:
is_local_time (bool): True if the date and time value is in local time.
"""
# Maps the RFC 822, RFC 1123 and RFC 2822 definitions to their corresponding
# integer values.
_RFC_MONTH_MAPPINGS = {
"Jan": 1,
"Feb": 2,
"Mar": 3,
"Apr": 4,
"May": 5,
"Jun": 6,
"Jul": 7,
"Aug": 8,
"Sep": 9,
"Oct": 10,
"Nov": 11,
"Dec": 12,
}
_RFC_TIME_ZONE_MAPPINGS = {
"UT": 0,
"GMT": 0,
"EST": -5,
"EDT": -4,
"CST": -6,
"CDT": -5,
"MST": -7,
"MDT": -6,
"PST": -8,
"PDT": -7,
"A": -1,
"B": -2,
"C": -3,
"D": -4,
"E": -5,
"F": -6,
"G": -7,
"H": -8,
"I": -9,
"K": -10,
"L": -11,
"M": -12,
"N": 1,
"O": 2,
"P": 3,
"Q": 4,
"R": 5,
"S": 6,
"T": 7,
"U": 8,
"V": 9,
"W": 10,
"X": 11,
"Y": 12,
"Z": 0,
}
_RFC_WEEKDAYS = frozenset(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
[docs]
def __init__(
self,
is_delta=False,
precision=None,
time_elements_tuple=None,
time_zone_offset=None,
):
"""Initializes time elements.
Args:
is_delta (Optional[bool]): True if the date and time value is relative to
another date and time value.
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
time_elements_tuple (Optional[tuple[int, int, int, int, int, int]]):
time elements, contains year, month, day of month, hours, minutes and
seconds.
time_zone_offset (Optional[int]): time zone offset in number of minutes
from UTC or None if not set.
Raises:
ValueError: if the time elements tuple is invalid.
"""
super().__init__(
is_delta=is_delta,
precision=precision or definitions.PRECISION_1_SECOND,
time_zone_offset=time_zone_offset,
)
self._number_of_seconds = None
self._time_elements_tuple = time_elements_tuple
if time_elements_tuple:
number_of_elements = len(time_elements_tuple)
if number_of_elements < 6:
raise ValueError(
(
f"Invalid time elements tuple at least 6 elements required,"
f"got: {number_of_elements:d}"
)
)
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
time_elements_tuple[0],
time_elements_tuple[1],
time_elements_tuple[2],
time_elements_tuple[3],
time_elements_tuple[4],
time_elements_tuple[5],
)
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._number_of_seconds)
if self._time_zone_offset:
self._normalized_timestamp -= self._time_zone_offset * 60
return self._normalized_timestamp
def _CopyDateTimeFromStringISO8601(self, time_string):
"""Copies a date and time from an ISO 8601 date and time string.
Args:
time_string (str): time value formatted as:
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 fraction of second and
time zone offset are optional.
Returns:
dict[str, int]: date and time values, such as year, month, day of month,
hours, minutes, seconds, nanoseconds, time zone offset in minutes.
Raises:
ValueError: if the time string is invalid or not supported.
"""
if not time_string:
raise ValueError("Invalid time string.")
time_string_length = len(time_string)
year, month, day_of_month = self._CopyDateFromString(time_string)
if time_string_length <= 10:
return {"year": year, "month": month, "day_of_month": day_of_month}
# If a time of day is specified the time string it should at least
# contain 'YYYY-MM-DDThh'.
if time_string[10] != "T":
raise ValueError("Invalid time string - missing date and time separator.")
hours, minutes, seconds, nanoseconds, time_zone_offset = (
self._CopyTimeFromStringISO8601(time_string[11:])
)
date_time_values = {
"year": year,
"month": month,
"day_of_month": day_of_month,
"hours": hours,
"minutes": minutes,
"seconds": seconds,
}
if nanoseconds is not None:
date_time_values["nanoseconds"] = nanoseconds
if time_zone_offset is not None:
date_time_values["time_zone_offset"] = time_zone_offset
return date_time_values
def _CopyDateTimeFromStringRFC822(self, time_string):
"""Copies a date and time from a RFC 822 date and time string.
Args:
time_string (str): date and time value formatted as:
DAY, D MONTH YY hh:mm:ss ZONE
Where weekday (DAY) and seconds (ss) are optional and day of
month (D) can consist of 1 or 2 digits.
Returns:
dict[str, int]: date and time values, such as year, month, day of month,
hours, minutes, seconds, time zone offset in minutes.
Raises:
ValueError: if the time string is invalid or not supported.
"""
if not time_string:
raise ValueError("Invalid time string.")
string_segments = time_string.split(" ")
if len(string_segments) not in (5, 6):
raise ValueError("Unsupported number of time string segments.")
weekday_string = string_segments[0]
if weekday_string.endswith(","):
weekday_string = weekday_string[:-1]
if weekday_string not in self._RFC_WEEKDAYS:
raise ValueError(f"Invalid weekday: {weekday_string:s}.")
string_segments.pop(0)
day_of_month_string = string_segments[0]
day_of_month = 0
if len(day_of_month_string) in (1, 2):
try:
day_of_month = int(day_of_month_string, 10)
except ValueError:
pass
if day_of_month == 0:
raise ValueError(f"Invalid day of month: {day_of_month_string:s}.")
month_string = string_segments[1]
month = self._RFC_MONTH_MAPPINGS.get(month_string)
if not month:
raise ValueError(f"Invalid month: {month_string:s}.")
year_string = string_segments[2]
year = None
if len(year_string) == 2:
try:
year = int(year_string, 10)
except ValueError:
pass
if year is None:
raise ValueError(f"Invalid year: {0:s}.")
year += 1900
hours, minutes, seconds, time_zone_offset = self._CopyTimeFromStringRFC(
string_segments[3], string_segments[4]
)
date_time_values = {
"year": year,
"month": month,
"day_of_month": day_of_month,
"hours": hours,
"minutes": minutes,
"time_zone_offset": time_zone_offset,
}
if seconds is not None:
date_time_values["seconds"] = seconds
return date_time_values
def _CopyDateTimeFromStringRFC1123(self, time_string):
"""Copies a date and time from a RFC 1123 date and time string.
Args:
time_string (str): date and time value formatted as:
DAY, D MONTH YYYY hh:mm:ss ZONE
Where weekday (DAY) and seconds (ss) are optional and day of
month (D) can consist of 1 or 2 digits.
Returns:
dict[str, int]: date and time values, such as year, month, day of month,
hours, minutes, seconds, time zone offset in minutes.
Raises:
ValueError: if the time string is invalid or not supported.
"""
if not time_string:
raise ValueError("Invalid time string.")
string_segments = time_string.split(" ")
if len(string_segments) not in (5, 6):
raise ValueError("Unsupported number of time string segments.")
weekday_string = string_segments[0]
if weekday_string.endswith(","):
weekday_string = weekday_string[:-1]
if weekday_string not in self._RFC_WEEKDAYS:
raise ValueError(f"Invalid weekday: {weekday_string:s}.")
string_segments.pop(0)
day_of_month_string = string_segments[0]
day_of_month = 0
if len(day_of_month_string) in (1, 2):
try:
day_of_month = int(day_of_month_string, 10)
except ValueError:
pass
if day_of_month == 0:
raise ValueError(f"Invalid day of month: {day_of_month_string:s}.")
month_string = string_segments[1]
month = self._RFC_MONTH_MAPPINGS.get(month_string)
if not month:
raise ValueError(f"Invalid month: {month_string:s}.")
year_string = string_segments[2]
year = None
if len(year_string) == 4:
try:
year = int(year_string, 10)
except ValueError:
pass
if year is None:
raise ValueError(f"Invalid year: {year_string:s}.")
hours, minutes, seconds, time_zone_offset = self._CopyTimeFromStringRFC(
string_segments[3], string_segments[4]
)
date_time_values = {
"year": year,
"month": month,
"day_of_month": day_of_month,
"hours": hours,
"minutes": minutes,
"time_zone_offset": time_zone_offset,
}
if seconds is not None:
date_time_values["seconds"] = seconds
return date_time_values
def _CopyFromDateTimeValues(self, date_time_values):
"""Copies time elements from date and time values.
Args:
date_time_values (dict[str, int]): date and time values, such as year,
month, day of month, hours, minutes, seconds, nanoseconds, time zone
offset in minutes.
"""
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)
time_zone_offset = date_time_values.get("time_zone_offset")
self._normalized_timestamp = None
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds
)
self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
self._time_zone_offset = time_zone_offset
def _CopyTimeFromStringISO8601(self, time_string):
"""Copies a time from an ISO 8601 time string.
Args:
time_string (str): time value formatted as:
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 fraction of second and
time zone offset are optional.
Returns:
tuple[int, int, int, int, int]: hours, minutes, seconds, nanoseconds,
time zone offset in minutes.
Raises:
ValueError: if the time string is invalid or not supported.
"""
if time_string.endswith("Z"):
time_string = "".join([time_string[:-1], "+00:00"])
time_string_length = len(time_string)
# The time string should at least contain 'hh'.
if time_string_length < 2:
raise ValueError("Time string too short.")
try:
hours = int(time_string[0:2], 10)
except ValueError:
raise ValueError("Unable to parse hours.")
if hours not in range(0, 24):
raise ValueError(f"Hours value: {hours:d} out of bounds.")
minutes = None
seconds = None
nanoseconds = None
time_zone_offset = None
time_string_index = 2
# Minutes are either specified as 'hhmm', 'hh:mm' or as a fractional part
# 'hh[.,]###'.
if time_string_index + 1 < time_string_length and time_string[
time_string_index
] not in (".", ","):
if time_string[time_string_index] == ":":
time_string_index += 1
if time_string_index + 2 > time_string_length:
raise ValueError("Time string too short.")
try:
minutes = time_string[time_string_index : time_string_index + 2]
minutes = int(minutes, 10)
except ValueError:
raise ValueError("Unable to parse minutes.")
time_string_index += 2
# Seconds are either specified as 'hhmmss', 'hh:mm:ss' or as a fractional
# part 'hh:mm[.,]###' or 'hhmm[.,]###'.
if time_string_index + 1 < time_string_length and time_string[
time_string_index
] not in (".", ","):
if time_string[time_string_index] == ":":
time_string_index += 1
if time_string_index + 2 > time_string_length:
raise ValueError("Time string too short.")
try:
seconds = time_string[time_string_index : time_string_index + 2]
seconds = int(seconds, 10)
except ValueError:
raise ValueError("Unable to parse day of seconds.")
time_string_index += 2
time_zone_string_index = time_string_index
while time_zone_string_index < time_string_length:
if time_string[time_zone_string_index] in ("+", "-"):
break
time_zone_string_index += 1
# The calculations that follow rely on the time zone string index
# to point beyond the string in case no time zone offset was defined.
if time_zone_string_index == time_string_length - 1:
time_zone_string_index += 1
if time_string_length > time_string_index and time_string[
time_string_index
] in (".", ","):
time_string_index += 1
time_fraction_length = time_zone_string_index - time_string_index
try:
time_fraction = time_string[time_string_index:time_zone_string_index]
time_fraction = int(time_fraction, 10)
time_fraction = decimal.Decimal(time_fraction) / decimal.Decimal(
10**time_fraction_length
)
except ValueError:
raise ValueError("Unable to parse time fraction.")
if minutes is None:
time_fraction *= 60
minutes = int(time_fraction)
time_fraction -= minutes
if seconds is None:
time_fraction *= 60
seconds = int(time_fraction)
time_fraction -= seconds
time_fraction *= definitions.NANOSECONDS_PER_SECOND
nanoseconds = int(time_fraction)
if minutes is not None and minutes not in range(0, 60):
raise ValueError(f"Minutes value: {minutes:d} out of bounds.")
# TODO: support a leap second?
if seconds is not None and seconds not in range(0, 60):
raise ValueError(f"Seconds value: {seconds:d} out of bounds.")
if time_zone_string_index < time_string_length:
if (
time_string_length - time_zone_string_index != 6
or time_string[time_zone_string_index + 3] != ":"
):
raise ValueError("Invalid time string.")
try:
hours_from_utc = int(
time_string[time_zone_string_index + 1 : time_zone_string_index + 3]
)
except ValueError:
raise ValueError("Unable to parse time zone hours offset.")
if hours_from_utc not in range(0, 15):
raise ValueError("Time zone hours offset value out of bounds.")
try:
minutes_from_utc = int(
time_string[time_zone_string_index + 4 : time_zone_string_index + 6]
)
except ValueError:
raise ValueError("Unable to parse time zone minutes offset.")
if minutes_from_utc not in range(0, 60):
raise ValueError("Time zone minutes offset value out of bounds.")
# pylint: disable=invalid-unary-operand-type
time_zone_offset = (hours_from_utc * 60) + minutes_from_utc
if time_string[time_zone_string_index] == "-":
time_zone_offset = -time_zone_offset
return hours, minutes, seconds, nanoseconds, time_zone_offset
def _CopyTimeFromStringRFC(self, time_string, time_zone_string):
"""Copies a time from a RFC 822, RFC 1123 or RFC 2822 time string.
Args:
time_string (str): time value formatted as: hh:mm[:ss], where seconds (ss)
are optional.
time_zone_string (str): time zone value formatted as predefined time zone
indicator or [+-]HHMM
Returns:
tuple[int, int, int, int]: hours, minutes, seconds, time zone offset in
minutes.
Raises:
ValueError: if the time string is invalid or not supported.
"""
time_string_length = len(time_string)
# The time string should at least contain 'hh:mm'.
if time_string_length < 5:
raise ValueError("Time string too short.")
if time_string_length > 8:
raise ValueError("Time string too long.")
if time_string[2] != ":":
raise ValueError("Invalid hours and minutes separator.")
try:
hours = int(time_string[0:2], 10)
except ValueError:
raise ValueError("Unable to parse hours.")
if hours not in range(0, 24):
raise ValueError(f"Hours value: {hours:d} out of bounds.")
try:
minutes = int(time_string[3:5], 10)
except ValueError:
raise ValueError("Unable to parse minutes.")
if minutes not in range(0, 60):
raise ValueError(f"Minutes value: {minutes:d} out of bounds.")
seconds = None
if time_string_length > 5:
if time_string_length < 8:
raise ValueError("Time string too short.")
if time_string[5] != ":":
raise ValueError("Invalid minutes and seconds separator.")
try:
seconds = int(time_string[6:8], 10)
except ValueError:
raise ValueError("Unable to parse seconds.")
if seconds not in range(0, 60):
raise ValueError(f"Seconds value: {seconds:d} out of bounds.")
time_zone_string_length = len(time_zone_string)
if time_zone_string_length > 5:
raise ValueError("Time zone string too long.")
if time_zone_string_length < 5:
hours_from_utc = self._RFC_TIME_ZONE_MAPPINGS.get(time_zone_string)
minutes_from_utc = 0
if hours_from_utc is None:
raise ValueError(f"Invalid time zone: {time_zone_string:s}.")
else:
if time_zone_string[0] not in ("+", "-"):
raise ValueError(f"Invalid time zone: {time_zone_string:s}.")
try:
hours_from_utc = int(time_zone_string[1:3], 10)
except ValueError:
raise ValueError("Unable to parse time zone hours offset.")
if hours_from_utc not in range(0, 15):
raise ValueError("Time zone hours offset value out of bounds.")
try:
minutes_from_utc = int(time_zone_string[3:5], 10)
except ValueError:
raise ValueError("Unable to parse time zone minutes offset.")
if minutes_from_utc not in range(0, 60):
raise ValueError("Time zone minutes offset value out of bounds.")
time_zone_offset = (hours_from_utc * 60) + minutes_from_utc
if time_zone_string[0] == "-":
time_zone_offset = -time_zone_offset
return hours, minutes, seconds, time_zone_offset
@property
def day_of_month(self):
"""int: day of month or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[2]
@property
def hours(self):
"""int: number of hours or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[3]
@property
def minutes(self):
"""int: number of minutes or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[4]
@property
def month(self):
"""int: month or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[1]
@property
def seconds(self):
"""int: number of seconds or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[5]
@property
def year(self):
"""int: year or None if not set."""
if not self._time_elements_tuple:
return None
return self._time_elements_tuple[0]
[docs]
def CopyFromDatetime(self, datetime_object):
"""Copies time elements from a Python datetime object.
A naive datetime object is considered in local time.
Args:
datetime_object (datetime.datetime): Python datetime object.
"""
year, month, day_of_month, hours, minutes, seconds, _, _, _ = (
datetime_object.utctimetuple()
)
date_time_values = {
"year": year,
"month": month,
"day_of_month": day_of_month,
"hours": hours,
"minutes": minutes,
"seconds": seconds,
}
self._CopyFromDateTimeValues(date_time_values)
self.is_local_time = bool(datetime_object.tzinfo is None)
[docs]
def CopyFromDateTimeString(self, time_string):
"""Copies time elements 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.
"""
date_time_values = self._CopyDateTimeFromString(time_string)
self._CopyFromDateTimeValues(date_time_values)
[docs]
def CopyFromStringISO8601(self, time_string):
"""Copies time elements from an ISO 8601 date and time string.
Currently not supported:
* Duration notation: "P..."
* Week notation "2016-W33"
* Date with week number notation "2016-W33-3"
* Date without year notation "--08-17"
* Ordinal date notation "2016-230"
Args:
time_string (str): date and time value formatted as:
YYYY-MM-DDThh: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 time string is invalid or not supported.
"""
date_time_values = self._CopyDateTimeFromStringISO8601(time_string)
self._CopyFromDateTimeValues(date_time_values)
[docs]
def CopyFromStringRFC822(self, time_string):
"""Copies time elements from a RFC 822 date and time string.
Args:
time_string (str): date and time value formatted as:
DAY, D MONTH YY hh:mm:ss ZONE
Where weekday (DAY) and seconds (ss) are optional and day of
month (D) can consist of 1 or 2 digits.
Raises:
ValueError: if the time string is invalid or not supported.
"""
date_time_values = self._CopyDateTimeFromStringRFC822(time_string)
self._CopyFromDateTimeValues(date_time_values)
[docs]
def CopyFromStringRFC1123(self, time_string):
"""Copies time elements from a RFC 1123 date and time string.
Args:
time_string (str): date and time value formatted as:
DAY, D MONTH YYYY hh:mm:ss ZONE
Where weekday (DAY) and seconds (ss) are optional and day of
month (D) can consist of 1 or 2 digits.
Raises:
ValueError: if the time string is invalid or not supported.
"""
date_time_values = self._CopyDateTimeFromStringRFC1123(time_string)
self._CopyFromDateTimeValues(date_time_values)
[docs]
def CopyFromStringTuple(self, time_elements_tuple):
"""Copies time elements from string-based time elements tuple.
Args:
time_elements_tuple (Optional[tuple[str, str, str, str, str, str]]):
time elements, contains year, month, day of month, hours, minutes and
seconds.
Raises:
ValueError: if the time elements tuple is invalid.
"""
number_of_elements = len(time_elements_tuple)
if number_of_elements < 6:
raise ValueError(
(
f"Invalid time elements tuple at least 6 elements required,"
f"got: {number_of_elements:d}"
)
)
year_string = time_elements_tuple[0]
month_string = time_elements_tuple[1]
day_of_month_string = time_elements_tuple[2]
hours_string = time_elements_tuple[3]
minutes_string = time_elements_tuple[4]
seconds_string = time_elements_tuple[5]
try:
year = int(year_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid year value: {year_string!s}")
try:
month = int(month_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid month value: {month_string!s}")
try:
day_of_month = int(day_of_month_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid day of month value: {day_of_month_string!s}")
try:
hours = int(hours_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid hours value: {hours_string!s}")
try:
minutes = int(minutes_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid minutes value: {minutes_string!s}")
try:
seconds = int(seconds_string, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid seconds value: {seconds_string!s}")
self._normalized_timestamp = None
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds
)
self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
[docs]
def CopyToDateTimeString(self):
"""Copies the time elements to a date and time string.
Returns:
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or None
if time elements are missing.
"""
if self._number_of_seconds is None:
return None
year, month, day_of_month, hours, minutes, seconds = self._time_elements_tuple
return (
f"{year:04d}-{month:02d}-{day_of_month:02d} "
f"{hours:02d}:{minutes:02d}:{seconds:02d}"
)
[docs]
def CopyToSerializableDict(self):
"""Copies the date time value to a serializable dictionary.
Returns:
dict[str, object]: serializable dictionary.
"""
serializable_dict = self._CreateSerializableDict()
serializable_dict["time_elements_tuple"] = self._time_elements_tuple
if self._is_delta:
serializable_dict["is_delta"] = True
return serializable_dict
[docs]
def NewFromDeltaAndDate(self, year, month, day_of_month):
"""Creates a new time elements instance from a date time delta and a date.
Args:
year (int): year.
month (int): month, where 1 represents January and 0 if not set.
day_of_month (int): day of month, where 1 represents the first day and 0
if not set.
Returns:
TimeElements: time elements or None if time elements are missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
if not self._is_delta:
raise ValueError("Not a date time delta.")
if self._time_elements_tuple is None:
return None
delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
self._time_elements_tuple
)
time_elements_tuple = (
year + delta_year,
month + delta_month,
day_of_month + delta_day_of_month,
hours,
minutes,
seconds,
)
date_time = TimeElements(
precision=self._precision,
time_elements_tuple=time_elements_tuple,
time_zone_offset=self._time_zone_offset,
)
date_time.is_local_time = self.is_local_time
return date_time
[docs]
def NewFromDeltaAndYear(self, year):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
Returns:
TimeElements: time elements or None if time elements are missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
return self.NewFromDeltaAndDate(year, 0, 0)
[docs]
class TimeElementsWithFractionOfSecond(TimeElements):
"""Time elements with a fraction of second interface.
Attributes:
fraction_of_second (decimal.Decimal): fraction of second, which must be a
value between 0.0 and 1.0.
is_local_time (bool): True if the date and time value is in local time.
"""
[docs]
def __init__(
self,
fraction_of_second=None,
is_delta=False,
precision=None,
time_elements_tuple=None,
time_zone_offset=None,
):
"""Initializes time elements.
Args:
fraction_of_second (Optional[decimal.Decimal]): fraction of second, which
must be a value between 0.0 and 1.0.
is_delta (Optional[bool]): True if the date and time value is relative to
another date and time value.
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
time_elements_tuple (Optional[tuple[int, int, int, int, int, int]]):
time elements, contains year, month, day of month, hours, minutes and
seconds.
time_zone_offset (Optional[int]): time zone offset in number of minutes
from UTC or None if not set.
Raises:
ValueError: if the time elements tuple is invalid or fraction of second
value is out of bounds.
"""
if fraction_of_second is not None:
if fraction_of_second < 0.0 or fraction_of_second >= 1.0:
raise ValueError(
f"Fraction of second value: {fraction_of_second:f} out of bounds."
)
super().__init__(
is_delta=is_delta,
precision=precision or definitions.PRECISION_1_SECOND,
time_elements_tuple=time_elements_tuple,
time_zone_offset=time_zone_offset,
)
self.fraction_of_second = fraction_of_second
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
and self.fraction_of_second is not None
):
self._normalized_timestamp = (
decimal.Decimal(self._number_of_seconds) + self.fraction_of_second
)
if self._time_zone_offset:
self._normalized_timestamp -= self._time_zone_offset * 60
return self._normalized_timestamp
def _CopyFromDateTimeValues(self, date_time_values):
"""Copies time elements from date and time values.
Args:
date_time_values (dict[str, int]): date and time values, such as year,
month, day of month, hours, minutes, seconds, nanoseconds, time zone
offset in minutes.
Raises:
ValueError: if no helper can be created for the current precision.
"""
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")
precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
self._precision
)
fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond(
nanoseconds
)
self._normalized_timestamp = None
self._number_of_seconds = self._GetNumberOfSecondsFromElements(
year, month, day_of_month, hours, minutes, seconds
)
self._time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)
self._time_zone_offset = time_zone_offset
self.fraction_of_second = fraction_of_second
[docs]
def CopyFromDatetime(self, datetime_object):
"""Copies time elements from a Python datetime object.
A naive datetime object is considered in local time.
Args:
datetime_object (datetime.datetime): Python datetime object.
"""
super().CopyFromDatetime(datetime_object)
precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
self._precision
)
fraction_of_second = precision_helper.CopyNanosecondsToFractionOfSecond(
datetime_object.microsecond * definitions.NANOSECONDS_PER_MICROSECOND
)
self.fraction_of_second = fraction_of_second
[docs]
def CopyFromStringTuple(self, time_elements_tuple):
"""Copies time elements from string-based time elements tuple.
Args:
time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and fraction of seconds.
Raises:
ValueError: if the time elements tuple is invalid.
"""
number_of_elements = len(time_elements_tuple)
if number_of_elements < 7:
raise ValueError(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
super().CopyFromStringTuple(time_elements_tuple)
fraction_of_second_string = time_elements_tuple[6]
try:
fraction_of_second = decimal.Decimal(fraction_of_second_string)
except (TypeError, ValueError):
raise ValueError(
f"Invalid fraction of second value: {fraction_of_second_string!s}"
)
if fraction_of_second < 0.0 or fraction_of_second >= 1.0:
raise ValueError(
f"Fraction of second value: {fraction_of_second:f} out of bounds."
)
self.fraction_of_second = fraction_of_second
[docs]
def CopyToDateTimeString(self):
"""Copies the time elements to a date and time string.
Returns:
str: date and time value formatted as: "YYYY-MM-DD hh:mm:ss" or
"YYYY-MM-DD hh:mm:ss.######" or None if time elements are missing.
Raises:
ValueError: if the precision value is unsupported.
"""
if self._number_of_seconds is None or self.fraction_of_second is None:
return None
precision_helper = precisions.PrecisionHelperFactory.CreatePrecisionHelper(
self._precision
)
return precision_helper.CopyToDateTimeString(
self._time_elements_tuple, self.fraction_of_second
)
[docs]
def NewFromDeltaAndDate(self, year, month, day_of_month):
"""Creates a new time elements instance from a date time delta and a date.
Args:
year (int): year.
month (int): month, where 1 represents January and 0 if not set.
day_of_month (int): day of month, where 1 represents the first day and 0
if not set.
Returns:
TimeElementsWithFractionOfSecond: time elements or None if time elements
are missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
if not self._is_delta:
raise ValueError("Not a date time delta.")
if self._time_elements_tuple is None:
return None
delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
self._time_elements_tuple
)
time_elements_tuple = (
year + delta_year,
month + delta_month,
day_of_month + delta_day_of_month,
hours,
minutes,
seconds,
)
return TimeElementsWithFractionOfSecond(
fraction_of_second=self.fraction_of_second,
precision=self._precision,
time_elements_tuple=time_elements_tuple,
time_zone_offset=self._time_zone_offset,
)
[docs]
def NewFromDeltaAndYear(self, year):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
Returns:
TimeElementsWithFractionOfSecond: time elements or None if time elements
are missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
return self.NewFromDeltaAndDate(year, 0, 0)
[docs]
class TimeElementsInMilliseconds(TimeElementsWithFractionOfSecond):
"""Time elements in milliseconds.
Attributes:
fraction_of_second (decimal.Decimal): fraction of second, which must be a
value between 0.0 and 1.0.
is_local_time (bool): True if the date and time value is in local time.
precision (str): precision of the date of the date and time value, that
represents 1 millisecond (PRECISION_1_MILLISECOND).
"""
[docs]
def __init__(
self,
is_delta=False,
precision=None,
time_elements_tuple=None,
time_zone_offset=None,
):
"""Initializes time elements.
Args:
is_delta (Optional[bool]): True if the date and time value is relative to
another date and time value.
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and milliseconds.
time_zone_offset (Optional[int]): time zone offset in number of minutes
from UTC or None if not set.
Raises:
ValueError: if the time elements tuple is invalid.
"""
fraction_of_second = None
if time_elements_tuple:
number_of_elements = len(time_elements_tuple)
if number_of_elements < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
milliseconds = time_elements_tuple[6]
time_elements_tuple = time_elements_tuple[:6]
if milliseconds < 0 or milliseconds >= definitions.MILLISECONDS_PER_SECOND:
raise ValueError("Invalid number of milliseconds.")
fraction_of_second = (
decimal.Decimal(milliseconds) / definitions.MILLISECONDS_PER_SECOND
)
super().__init__(
fraction_of_second=fraction_of_second,
is_delta=is_delta,
precision=precision or definitions.PRECISION_1_MILLISECOND,
time_elements_tuple=time_elements_tuple,
time_zone_offset=time_zone_offset,
)
@property
def milliseconds(self):
"""int: number of milliseconds."""
return int(self.fraction_of_second * definitions.MILLISECONDS_PER_SECOND)
[docs]
def CopyFromStringTuple(self, time_elements_tuple):
"""Copies time elements from string-based time elements tuple.
Args:
time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and milliseconds.
Raises:
ValueError: if the time elements tuple is invalid.
"""
number_of_elements = len(time_elements_tuple)
if len(time_elements_tuple) < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
year, month, day_of_month, hours, minutes, seconds, milliseconds = (
time_elements_tuple
)
try:
milliseconds = int(milliseconds, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid millisecond value: {milliseconds!s}")
if milliseconds < 0 or milliseconds >= definitions.MILLISECONDS_PER_SECOND:
raise ValueError("Invalid number of milliseconds.")
fraction_of_second = (
decimal.Decimal(milliseconds) / definitions.MILLISECONDS_PER_SECOND
)
time_elements_tuple = (
year,
month,
day_of_month,
hours,
minutes,
seconds,
str(fraction_of_second),
)
super().CopyFromStringTuple(time_elements_tuple)
[docs]
def CopyToSerializableDict(self):
"""Copies the date time value to a serializable dictionary.
Returns:
dict[str, object]: serializable dictionary.
"""
serializable_dict = self._CreateSerializableDict()
serializable_dict["time_elements_tuple"] = (
*self._time_elements_tuple,
self.milliseconds,
)
if self._is_delta:
serializable_dict["is_delta"] = True
return serializable_dict
[docs]
def NewFromDeltaAndDate(self, year, month, day_of_month):
"""Creates a new time elements instance from a date time delta and a date.
Args:
year (int): year.
month (int): month, where 1 represents January and 0 if not set.
day_of_month (int): day of month, where 1 represents the first day and 0
if not set.
Returns:
TimeElementsInMilliseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
if not self._is_delta:
raise ValueError("Not a date time delta.")
if self._time_elements_tuple is None:
return None
delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
self._time_elements_tuple
)
time_elements_tuple = (
year + delta_year,
month + delta_month,
day_of_month + delta_day_of_month,
hours,
minutes,
seconds,
self.milliseconds,
)
return TimeElementsInMilliseconds(
precision=self._precision,
time_elements_tuple=time_elements_tuple,
time_zone_offset=self._time_zone_offset,
)
[docs]
def NewFromDeltaAndYear(self, year):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
Returns:
TimeElementsInMilliseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
return self.NewFromDeltaAndDate(year, 0, 0)
[docs]
class TimeElementsInMicroseconds(TimeElementsWithFractionOfSecond):
"""Time elements in microseconds.
Attributes:
fraction_of_second (decimal.Decimal): fraction of second, which must be a
value between 0.0 and 1.0.
is_local_time (bool): True if the date and time value is in local time.
precision (str): precision of the date of the date and time value, that
represents 1 microsecond (PRECISION_1_MICROSECOND).
"""
[docs]
def __init__(
self,
is_delta=False,
precision=None,
time_elements_tuple=None,
time_zone_offset=None,
):
"""Initializes time elements.
Args:
is_delta (Optional[bool]): True if the date and time value is relative to
another date and time value.
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and microseconds.
time_zone_offset (Optional[int]): time zone offset in number of minutes
from UTC or None if not set.
Raises:
ValueError: if the time elements tuple is invalid.
"""
fraction_of_second = None
if time_elements_tuple:
number_of_elements = len(time_elements_tuple)
if number_of_elements < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
microseconds = time_elements_tuple[6]
time_elements_tuple = time_elements_tuple[:6]
if microseconds < 0 or microseconds >= definitions.MICROSECONDS_PER_SECOND:
raise ValueError("Invalid number of microseconds.")
fraction_of_second = (
decimal.Decimal(microseconds) / definitions.MICROSECONDS_PER_SECOND
)
super().__init__(
fraction_of_second=fraction_of_second,
is_delta=is_delta,
precision=precision or definitions.PRECISION_1_MICROSECOND,
time_elements_tuple=time_elements_tuple,
time_zone_offset=time_zone_offset,
)
@property
def microseconds(self):
"""int: number of microseconds."""
return int(self.fraction_of_second * definitions.MICROSECONDS_PER_SECOND)
[docs]
def CopyFromStringTuple(self, time_elements_tuple):
"""Copies time elements from string-based time elements tuple.
Args:
time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and microseconds.
Raises:
ValueError: if the time elements tuple is invalid.
"""
number_of_elements = len(time_elements_tuple)
if len(time_elements_tuple) < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
year, month, day_of_month, hours, minutes, seconds, microseconds = (
time_elements_tuple
)
try:
microseconds = int(microseconds, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid microsecond value: {microseconds!s}")
if microseconds < 0 or microseconds >= definitions.MICROSECONDS_PER_SECOND:
raise ValueError("Invalid number of microseconds.")
fraction_of_second = (
decimal.Decimal(microseconds) / definitions.MICROSECONDS_PER_SECOND
)
time_elements_tuple = (
year,
month,
day_of_month,
hours,
minutes,
seconds,
str(fraction_of_second),
)
super().CopyFromStringTuple(time_elements_tuple)
[docs]
def CopyToSerializableDict(self):
"""Copies the date time value to a serializable dictionary.
Returns:
dict[str, object]: serializable dictionary.
"""
serializable_dict = self._CreateSerializableDict()
serializable_dict["time_elements_tuple"] = (
*self._time_elements_tuple,
self.microseconds,
)
if self._is_delta:
serializable_dict["is_delta"] = True
return serializable_dict
[docs]
def NewFromDeltaAndDate(self, year, month, day_of_month):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
month (int): month, where 1 represents January and 0 if not set.
day_of_month (int): day of month, where 1 represents the first day and 0
if not set.
Returns:
TimeElementsInMicroseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
if not self._is_delta:
raise ValueError("Not a date time delta.")
if self._time_elements_tuple is None:
return None
delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
self._time_elements_tuple
)
time_elements_tuple = (
year + delta_year,
month + delta_month,
day_of_month + delta_day_of_month,
hours,
minutes,
seconds,
self.microseconds,
)
return TimeElementsInMicroseconds(
precision=self._precision,
time_elements_tuple=time_elements_tuple,
time_zone_offset=self._time_zone_offset,
)
[docs]
def NewFromDeltaAndYear(self, year):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
Returns:
TimeElementsInMicroseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
return self.NewFromDeltaAndDate(year, 0, 0)
[docs]
class TimeElementsInNanoseconds(TimeElementsWithFractionOfSecond):
"""Time elements in nanoseconds.
Attributes:
fraction_of_second (decimal.Decimal): fraction of second, which must be a
value between 0.0 and 1.0.
is_local_time (bool): True if the date and time value is in local time.
precision (str): precision of the date of the date and time value, that
represents 1 nanosecond (PRECISION_1_NANOSECOND).
"""
[docs]
def __init__(
self,
is_delta=False,
precision=None,
time_elements_tuple=None,
time_zone_offset=None,
):
"""Initializes time elements.
Args:
is_delta (Optional[bool]): True if the date and time value is relative to
another date and time value.
precision (Optional[str]): precision of the date and time value, which
should be one of the PRECISION_VALUES in definitions.
time_elements_tuple (Optional[tuple[int, int, int, int, int, int, int]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and nanoseconds.
time_zone_offset (Optional[int]): time zone offset in number of minutes
from UTC or None if not set.
Raises:
ValueError: if the time elements tuple is invalid.
"""
fraction_of_second = None
if time_elements_tuple:
number_of_elements = len(time_elements_tuple)
if number_of_elements < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
nanoseconds = time_elements_tuple[6]
time_elements_tuple = time_elements_tuple[:6]
if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND:
raise ValueError("Invalid number of nanoseconds.")
fraction_of_second = (
decimal.Decimal(nanoseconds) / definitions.NANOSECONDS_PER_SECOND
)
super().__init__(
fraction_of_second=fraction_of_second,
is_delta=is_delta,
precision=precision or definitions.PRECISION_1_NANOSECOND,
time_elements_tuple=time_elements_tuple,
time_zone_offset=time_zone_offset,
)
@property
def nanoseconds(self):
"""int: number of nanoseconds."""
return int(self.fraction_of_second * definitions.NANOSECONDS_PER_SECOND)
[docs]
def CopyFromStringTuple(self, time_elements_tuple):
"""Copies time elements from string-based time elements tuple.
Args:
time_elements_tuple (Optional[tuple[str, str, str, str, str, str, str]]):
time elements, contains year, month, day of month, hours, minutes,
seconds and nanoseconds.
Raises:
ValueError: if the time elements tuple is invalid.
"""
number_of_elements = len(time_elements_tuple)
if len(time_elements_tuple) < 7:
raise ValueError(
(
f"Invalid time elements tuple at least 7 elements required,"
f"got: {number_of_elements:d}"
)
)
year, month, day_of_month, hours, minutes, seconds, nanoseconds = (
time_elements_tuple
)
try:
nanoseconds = int(nanoseconds, 10)
except (TypeError, ValueError):
raise ValueError(f"Invalid nanosecond value: {nanoseconds!s}")
if nanoseconds < 0 or nanoseconds >= definitions.NANOSECONDS_PER_SECOND:
raise ValueError("Invalid number of nanoseconds.")
fraction_of_second = (
decimal.Decimal(nanoseconds) / definitions.NANOSECONDS_PER_SECOND
)
time_elements_tuple = (
year,
month,
day_of_month,
hours,
minutes,
seconds,
str(fraction_of_second),
)
super().CopyFromStringTuple(time_elements_tuple)
[docs]
def CopyToSerializableDict(self):
"""Copies the date time value to a serializable dictionary.
Returns:
dict[str, object]: serializable dictionary.
"""
serializable_dict = self._CreateSerializableDict()
serializable_dict["time_elements_tuple"] = (
*self._time_elements_tuple,
self.nanoseconds,
)
if self._is_delta:
serializable_dict["is_delta"] = True
return serializable_dict
[docs]
def NewFromDeltaAndDate(self, year, month, day_of_month):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
month (int): month, where 1 represents January and 0 if not set.
day_of_month (int): day of month, where 1 represents the first day and 0
if not set.
Returns:
TimeElementsInNanoseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
if not self._is_delta:
raise ValueError("Not a date time delta.")
if self._time_elements_tuple is None:
return None
delta_year, delta_month, delta_day_of_month, hours, minutes, seconds = (
self._time_elements_tuple
)
time_elements_tuple = (
year + delta_year,
month + delta_month,
day_of_month + delta_day_of_month,
hours,
minutes,
seconds,
self.nanoseconds,
)
return TimeElementsInNanoseconds(
precision=self._precision,
time_elements_tuple=time_elements_tuple,
time_zone_offset=self._time_zone_offset,
)
[docs]
def NewFromDeltaAndYear(self, year):
"""Creates a new time elements instance from a date time delta and a year.
Args:
year (int): year.
Returns:
TimeElementsInNanoseconds: time elements or None if time elements are
missing.
Raises:
ValueError: if the instance is not a date time delta.
"""
return self.NewFromDeltaAndDate(year, 0, 0)
factory.Factory.RegisterDateTimeValues(TimeElements)
factory.Factory.RegisterDateTimeValues(TimeElementsInMilliseconds)
factory.Factory.RegisterDateTimeValues(TimeElementsInMicroseconds)
factory.Factory.RegisterDateTimeValues(TimeElementsInNanoseconds)