# Source code for xsdata.models.datatype

    import datetime
    import operator
    import re
    from collections import UserString
    from typing import Any, Callable, Dict, NamedTuple, Optional, Union

    from xsdata.utils.dates import (
        calculate_offset,
        calculate_timezone,
        format_date,
        format_offset,
        format_time,
        parse_date_args,
        validate_date,
        validate_time,
    )

    xml_duration_re = re.compile(
        r"^([-]?)P"
        r"(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?"
        r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(.\d+)?)S)?)?$"
    )

    DS_YEAR = 31556926.0
    DS_MONTH = 2629743
    DS_DAY = 86400
    DS_HOUR = 3600
    DS_MINUTE = 60
    DS_FRACTIONAL_SECOND = 0.000000001
    DS_OFFSET = -60

    class DateFormat:
        DATE = "%Y-%m-%d%z"
        TIME = "%H:%M:%S%z"
        DATE_TIME = "%Y-%m-%dT%H:%M:%S%z"
        G_DAY = "---%d%z"
        G_MONTH = "--%m%z"
        G_MONTH_DAY = "--%m-%d%z"
        G_YEAR = "%Y%z"
        G_YEAR_MONTH = "%Y-%m%z"

    class XmlDate(NamedTuple):
        """
        Concrete xs:date builtin type.

        Represents iso 8601 date format [-]CCYY-MM-DD[Z|(+|-)hh:mm] with
        rich comparisons and hashing.

        :param year: Any signed integer, eg (0, -535, 2020)
        :param month: Unsigned integer between 1-12
        :param day: Unsigned integer between 1-31
        :param offset: Signed integer representing timezone offset in
            minutes
        """

        year: int
        month: int
        day: int
        offset: Optional[int] = None

        def replace(
            self,
            year: Optional[int] = None,
            month: Optional[int] = None,
            day: Optional[int] = None,
            offset: Optional[int] = True,
        ) -> "XmlDate":
            """Return a new instance replacing the specified fields with new
            values."""

            if year is None:
                year = self.year
            if month is None:
                month = self.month
            if day is None:
                day = self.day
            if offset is True:
                offset = self.offset

            return type(self)(year, month, day, offset)

        @classmethod
        def from_string(cls, string: str) -> "XmlDate":
            """Initialize from string with format ``%Y-%m-%dT%z``"""
            return cls(*parse_date_args(string, DateFormat.DATE))

        @classmethod
        def from_date(cls, obj: datetime.date) -> "XmlDate":
            """
            Initialize from :class:`datetime.date` instance.

            .. warning::

                date instances don't have timezone information!
            """
            return cls(obj.year, obj.month, obj.day)

        @classmethod
        def from_datetime(cls, obj: datetime.datetime) -> "XmlDate":
            """Initialize from :class:`datetime.datetime` instance."""
            return cls(obj.year, obj.month, obj.day, calculate_offset(obj))

        @classmethod
        def today(cls) -> "XmlDate":
            """Initialize from datetime.date.today()"""
            return cls.from_date(datetime.date.today())

        def to_date(self) -> datetime.date:
            """Return a :class:`datetime.date` instance."""
            return datetime.date(self.year, self.month, self.day)

        def to_datetime(self) -> datetime.datetime:
            """Return a :class:`datetime.datetime` instance."""
            tz_info = calculate_timezone(self.offset)
            return datetime.datetime(self.year, self.month, self.day, tzinfo=tz_info)

        def __str__(self) -> str:
            """
            Return the date formatted according to ISO 8601 for xml.

            Examples:
                - 2001-10-26
                - 2001-10-26+02:00
                - 2001-10-26Z
            """
            return format_date(self.year, self.month, self.day) + format_offset(self.offset)

        def __repr__(self) -> str:
            args = [self.year, self.month, self.day, self.offset]
            if args[-1] is None:
                del args[-1]

            return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"

    class XmlDateTime(NamedTuple):
        """
        Concrete xs:dateTime builtin type.

        Represents iso 8601 date time format [-]CCYY-MM-DDThh
        :mm: ss[Z|(+|-)hh:mm] with rich comparisons and hashing.
        :param year: Any signed integer, eg (0, -535, 2020)
        :param month: Unsigned integer between 1-12
        :param day: Unsigned integer between 1-31
        :param hour: Unsigned integer between 0-24
        :param minute: Unsigned integer between 0-59
        :param second: Unsigned integer between 0-59
        :param fractional_second: Unsigned integer between 0-999999999
        :param offset: Signed integer representing timezone offset in
            minutes
        """

        year: int
        month: int
        day: int
        hour: int
        minute: int
        second: int
        fractional_second: int = 0
        offset: Optional[int] = None

        @property
        def microsecond(self) -> int:
            return self.fractional_second // 1000

        @property
        def duration(self) -> float:
            if self.year < 0:
                negative = True
                year = -self.year
            else:
                negative = False
                year = self.year

            total = (
                year * DS_YEAR
                + self.month * DS_MONTH
                + self.day * DS_DAY
                + self.hour * DS_HOUR
                + self.minute * DS_MINUTE
                + self.second
                + self.fractional_second * DS_FRACTIONAL_SECOND
                + (self.offset or 0) * DS_OFFSET
            )
            return -total if negative else total

        @classmethod
        def from_string(cls, string: str) -> "XmlDateTime":
            """Initialize from string with format ``%Y-%m-%dT%H:%M:%S%z``"""
            (
                year,
                month,
                day,
                hour,
                minute,
                second,
                fractional_second,
                offset,
            ) = parse_date_args(string, DateFormat.DATE_TIME)
            validate_date(year, month, day)
            validate_time(hour, minute, second, fractional_second)

            return cls(year, month, day, hour, minute, second, fractional_second, offset)

        @classmethod
        def from_datetime(cls, obj: datetime.datetime) -> "XmlDateTime":
            """Initialize from :class:`datetime.datetime` instance."""
            return cls(
                obj.year,
                obj.month,
                obj.day,
                obj.hour,
                obj.minute,
                obj.second,
                obj.microsecond * 1000,
                calculate_offset(obj),
            )

        @classmethod
        def now(cls, tz: Optional[datetime.timezone] = None) -> "XmlDateTime":
            """Initialize from datetime.datetime.now()"""
            return cls.from_datetime(datetime.datetime.now(tz=tz))

        @classmethod
        def utcnow(cls) -> "XmlDateTime":
            """Initialize from datetime.now(timezone.utc)"""
            return cls.from_datetime(datetime.datetime.now(datetime.timezone.utc))

        def to_datetime(self) -> datetime.datetime:
            """Return a :class:`datetime.datetime` instance."""
            return datetime.datetime(
                self.year,
                self.month,
                self.day,
                self.hour,
                self.minute,
                self.second,
                self.microsecond,
                tzinfo=calculate_timezone(self.offset),
            )

        def replace(
            self,
            year: Optional[int] = None,
            month: Optional[int] = None,
            day: Optional[int] = None,
            hour: Optional[int] = None,
            minute: Optional[int] = None,
            second: Optional[int] = None,
            fractional_second: Optional[int] = None,
            offset: Optional[int] = True,
        ) -> "XmlDateTime":
            """Return a new instance replacing the specified fields with new
            values."""

            if year is None:
                year = self.year
            if month is None:
                month = self.month
            if day is None:
                day = self.day
            if hour is None:
                hour = self.hour
            if minute is None:
                minute = self.minute
            if second is None:
                second = self.second
            if fractional_second is None:
                fractional_second = self.fractional_second
            if offset is True:
                offset = self.offset

            return type(self)(
                year, month, day, hour, minute, second, fractional_second, offset
            )

        def __str__(self) -> str:
            """
            Return the datetime formatted according to ISO 8601 for xml.

            Examples:
                - 2001-10-26T21:32:52
                - 2001-10-26T21:32:52+02:00
                - 2001-10-26T19:32:52Z
                - 2001-10-26T19:32:52.126789
                - 2001-10-26T21:32:52.126
                - -2001-10-26T21:32:52.126Z
            """
            return "{}T{}{}".format(
                format_date(self.year, self.month, self.day),
                format_time(self.hour, self.minute, self.second, self.fractional_second),
                format_offset(self.offset),
            )

        def __repr__(self) -> str:
            args = tuple(self)
            if args[-1] is None:
                args = args[:-1]

                if args[-1] == 0:
                    args = args[:-1]

            return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"

        def __eq__(self, other: Any) -> bool:
            return cmp(self, other, operator.eq)

        def __ne__(self, other: Any) -> bool:
            return cmp(self, other, operator.ne)

        def __lt__(self, other: Any) -> bool:
            return cmp(self, other, operator.lt)

        def __le__(self, other: Any) -> bool:
            return cmp(self, other, operator.le)

        def __gt__(self, other: Any) -> bool:
            return cmp(self, other, operator.gt)

        def __ge__(self, other: Any) -> bool:
            return cmp(self, other, operator.ge)

    class XmlTime(NamedTuple):
        """
        Concrete xs:time builtin type.

        Represents iso 8601 time format hh
        :mm: ss[Z|(+|-)hh:mm] with rich comparisons and hashing.
        :param hour: Unsigned integer between 0-24
        :param minute: Unsigned integer between 0-59
        :param second: Unsigned integer between 0-59
        :param fractional_second: Unsigned integer between 0-999999999
        :param offset: Signed integer representing timezone offset in
            minutes
        """

        hour: int
        minute: int
        second: int
        fractional_second: int = 0
        offset: Optional[int] = None

        @property
        def microsecond(self) -> int:
            return self.fractional_second // 1000

        @property
        def duration(self) -> float:
            return (
                self.hour * DS_HOUR
                + self.minute * DS_MINUTE
                + self.second
                + self.fractional_second * DS_FRACTIONAL_SECOND
                + (self.offset or 0) * DS_OFFSET
            )

        def replace(
            self,
            hour: Optional[int] = None,
            minute: Optional[int] = None,
            second: Optional[int] = None,
            fractional_second: Optional[int] = None,
            offset: Optional[int] = True,
        ) -> "XmlTime":
            """Return a new instance replacing the specified fields with new
            values."""

            if hour is None:
                hour = self.hour
            if minute is None:
                minute = self.minute
            if second is None:
                second = self.second
            if fractional_second is None:
                fractional_second = self.fractional_second
            if offset is True:
                offset = self.offset

            return type(self)(hour, minute, second, fractional_second, offset)

        @classmethod
        def from_string(cls, string: str) -> "XmlTime":
            """Initialize from string format ``%H:%M:%S%z``"""
            hour, minute, second, fractional_second, offset = parse_date_args(
                string, DateFormat.TIME
            )
            validate_time(hour, minute, second, fractional_second)
            return cls(hour, minute, second, fractional_second, offset)

        @classmethod
        def from_time(cls, obj: datetime.time) -> "XmlTime":
            """Initialize from :class:`datetime.time` instance."""
            return cls(
                obj.hour,
                obj.minute,
                obj.second,
                obj.microsecond * 1000,
                calculate_offset(obj),
            )

        @classmethod
        def now(cls, tz: Optional[datetime.timezone] = None) -> "XmlTime":
            """Initialize from datetime.datetime.now()"""
            return cls.from_time(datetime.datetime.now(tz=tz).time())

        @classmethod
        def utcnow(cls) -> "XmlTime":
            """Initialize from datetime.now(timezone.utc)"""
            return cls.from_time(datetime.datetime.now(datetime.timezone.utc).time())

        def to_time(self) -> datetime.time:
            """Return a :class:`datetime.time` instance."""
            return datetime.time(
                self.hour,
                self.minute,
                self.second,
                self.microsecond,
                tzinfo=calculate_timezone(self.offset),
            )

        def __str__(self) -> str:
            """
            Return the time formatted according to ISO 8601 for xml.

            Examples:
                - 21:32:52
                - 21:32:52+02:00,
                - 19:32:52Z
                - 21:32:52.126789
                - 21:32:52.126Z
            """
            return "{}{}".format(
                format_time(self.hour, self.minute, self.second, self.fractional_second),
                format_offset(self.offset),
            )

        def __repr__(self) -> str:
            args = list(self)
            if args[-1] is None:
                del args[-1]

            return f"{self.__class__.__qualname__}({', '.join(map(str, args))})"

        def __eq__(self, other: Any) -> bool:
            return cmp(self, other, operator.eq)

        def __ne__(self, other: Any) -> bool:
            return cmp(self, other, operator.ne)

        def __lt__(self, other: Any) -> bool:
            return cmp(self, other, operator.lt)

        def __le__(self, other: Any) -> bool:
            return cmp(self, other, operator.le)

        def __gt__(self, other: Any) -> bool:
            return cmp(self, other, operator.gt)

        def __ge__(self, other: Any) -> bool:
            return cmp(self, other, operator.ge)

    DurationType = Union[XmlTime, XmlDateTime]

    def cmp(a: DurationType, b: DurationType, op: Callable) -> bool:
        if isinstance(b, a.__class__):
            return op(a.duration, b.duration)

        return NotImplemented

    class TimeInterval(NamedTuple):
        negative: bool
        years: Optional[int]
        months: Optional[int]
        days: Optional[int]
        hours: Optional[int]
        minutes: Optional[int]
        seconds: Optional[float]

    class XmlDuration(UserString):
        """
        Concrete xs:duration builtin type.

        Represents iso 8601 duration format PnYnMnDTnHnMnS
        with rich comparisons and hashing.

        Format PnYnMnDTnHnMnS:
            - **P**: literal value that starts the expression
            - **nY**: the number of years followed by a literal Y
            - **nM**: the number of months followed by a literal M
            - **nD**: the number of days followed by a literal D
            - **T**: literal value that separates date and time parts
            - **nH**: the number of hours followed by a literal H
            - **nM**: the number of minutes followed by a literal M
            - **nS**: the number of seconds followed by a literal S

        :param value: String representation of a xs:duration, eg **P2Y6M5DT12H**
        """

        def __init__(self, value: str) -> None:
            super().__init__(value)
            self._interval = self._parse_interval(value)

        @property
        def years(self) -> Optional[int]:
            """Number of years in the interval."""
            return self._interval.years

        @property
        def months(self) -> Optional[int]:
            """Number of months in the interval."""
            return self._interval.months

        @property
        def days(self) -> Optional[int]:
            """Number of days in the interval."""
            return self._interval.days

        @property
        def hours(self) -> Optional[int]:
            """Number of hours in the interval."""
            return self._interval.hours

        @property
        def minutes(self) -> Optional[int]:
            """Number of minutes in the interval."""
            return self._interval.minutes

        @property
        def seconds(self) -> Optional[float]:
            """Number of seconds in the interval."""
            return self._interval.seconds

        @property
        def negative(self) -> bool:
            """Negative flag of the interval."""
            return self._interval.negative

        @classmethod
        def _parse_interval(cls, value: str) -> TimeInterval:
            if not isinstance(value, str):
                raise ValueError("Value must be string")

            if len(value) < 3 or value.endswith("T"):
                raise ValueError(f"Invalid format '{value}'")

            match = xml_duration_re.match(value)
            if not match:
                raise ValueError(f"Invalid format '{value}'")

            sign, years, months, days, hours, minutes, seconds, _ = match.groups()
            return TimeInterval(
                negative=sign == "-",
                years=int(years) if years else None,
                months=int(months) if months else None,
                days=int(days) if days else None,
                hours=int(hours) if hours else None,
                minutes=int(minutes) if minutes else None,
                seconds=float(seconds) if seconds else None,
            )

        def asdict(self) -> Dict:
            return self._interval._asdict()

        def __repr__(self) -> str:
            return f'{self.__class__.__qualname__}("{self.data}")'

    class TimePeriod(NamedTuple):
        year: Optional[int]
        month: Optional[int]
        day: Optional[int]
        offset: Optional[int]

    class XmlPeriod(UserString):
        """
        Concrete xs:gYear/Month/Day builtin type.

        Represents iso 8601 period formats with rich comparisons and hashing.

        Formats:
            - xs:gDay: **---%d%z**
            - xs:gMonth: **--%m%z**
            - xs:gYear: **%Y%z**
            - xs:gMonthDay: **--%m-%d%z**
            - xs:gYearMonth: **%Y-%m%z**

        :param value: String representation of a xs:period, eg **--11-01Z**
        """

        def __init__(self, value: str) -> None:
            value = value.strip()
            super().__init__(value)
            self._period = self._parse_period(value)

        @property
        def year(self) -> Optional[int]:
            """Period year."""
            return self._period.year

        @property
        def month(self) -> Optional[int]:
            """Period month."""
            return self._period.month

        @property
        def day(self) -> Optional[int]:
            """Period day."""
            return self._period.day

        @property
        def offset(self) -> Optional[int]:
            """Period timezone offset in minutes."""
            return self._period.offset

        @classmethod
        def _parse_period(cls, value: str) -> TimePeriod:
            year = month = day = offset = None
            if value.startswith("---"):
                day, offset = parse_date_args(value, DateFormat.G_DAY)
            elif value.startswith("--"):
                # Bogus format --MM--, --05---05:00
                if value[4:6] == "--":
                    value = value[:4] + value[6:]

                if len(value) in (4, 5, 10):  # fixed lengths with/out timezone
                    month, offset = parse_date_args(value, DateFormat.G_MONTH)
                else:
                    month, day, offset = parse_date_args(value, DateFormat.G_MONTH_DAY)
            else:
                end = len(value)
                if value.find(":") > -1:  # offset
                    end -= 6

                if value[:end].rfind("-") > 3:  # Minimum position for month sep
                    year, month, offset = parse_date_args(value, DateFormat.G_YEAR_MONTH)
                else:
                    year, offset = parse_date_args(value, DateFormat.G_YEAR)

            validate_date(0, month or 1, day or 1)

            return TimePeriod(year=year, month=month, day=day, offset=offset)

        def as_dict(self) -> Dict:
            """Return date units as dict."""
            return self._period._asdict()

        def __repr__(self) -> str:
            return f'{self.__class__.__qualname__}("{self.data}")'

        def __eq__(self, other: Any) -> bool:
            if isinstance(other, XmlPeriod):
                return self._period == other._period

            return NotImplemented

    class XmlHexBinary(bytes):
        """
        Subclass bytes to infer base16 format.

        This type can be used with xs:anyType fields that don't have a
        format property to specify the target output format.
        """

    class XmlBase64Binary(bytes):
        """
        Subclass bytes to infer base64 format.

        This type can be used with xs:anyType fields that don't have a
        format property to specify the target output format.
        """