"""Time elements implementation."""

import decimal

from dfdatetime import definitions
from dfdatetime import factory
from dfdatetime import interface
from dfdatetime import precisions


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"])

    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]

    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)

    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)

    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)

    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)

    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)

    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)

    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}"
        )

    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

    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

    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)


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.
    """

    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

    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

    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

    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
        )

    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,
        )

    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)


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).
    """

    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)

    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)

    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

    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,
        )

    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)


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).
    """

    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)

    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)

    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

    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,
        )

    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)


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).
    """

    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)

    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)

    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

    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,
        )

    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)
