17db96d56Sopenharmony_ciimport bisect
27db96d56Sopenharmony_ciimport calendar
37db96d56Sopenharmony_ciimport collections
47db96d56Sopenharmony_ciimport functools
57db96d56Sopenharmony_ciimport re
67db96d56Sopenharmony_ciimport weakref
77db96d56Sopenharmony_cifrom datetime import datetime, timedelta, tzinfo
87db96d56Sopenharmony_ci
97db96d56Sopenharmony_cifrom . import _common, _tzpath
107db96d56Sopenharmony_ci
117db96d56Sopenharmony_ciEPOCH = datetime(1970, 1, 1)
127db96d56Sopenharmony_ciEPOCHORDINAL = datetime(1970, 1, 1).toordinal()
137db96d56Sopenharmony_ci
147db96d56Sopenharmony_ci# It is relatively expensive to construct new timedelta objects, and in most
157db96d56Sopenharmony_ci# cases we're looking at the same deltas, like integer numbers of hours, etc.
167db96d56Sopenharmony_ci# To improve speed and memory use, we'll keep a dictionary with references
177db96d56Sopenharmony_ci# to the ones we've already used so far.
187db96d56Sopenharmony_ci#
197db96d56Sopenharmony_ci# Loading every time zone in the 2020a version of the time zone database
207db96d56Sopenharmony_ci# requires 447 timedeltas, which requires approximately the amount of space
217db96d56Sopenharmony_ci# that ZoneInfo("America/New_York") with 236 transitions takes up, so we will
227db96d56Sopenharmony_ci# set the cache size to 512 so that in the common case we always get cache
237db96d56Sopenharmony_ci# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts
247db96d56Sopenharmony_ci# of memory.
257db96d56Sopenharmony_ci@functools.lru_cache(maxsize=512)
267db96d56Sopenharmony_cidef _load_timedelta(seconds):
277db96d56Sopenharmony_ci    return timedelta(seconds=seconds)
287db96d56Sopenharmony_ci
297db96d56Sopenharmony_ci
307db96d56Sopenharmony_ciclass ZoneInfo(tzinfo):
317db96d56Sopenharmony_ci    _strong_cache_size = 8
327db96d56Sopenharmony_ci    _strong_cache = collections.OrderedDict()
337db96d56Sopenharmony_ci    _weak_cache = weakref.WeakValueDictionary()
347db96d56Sopenharmony_ci    __module__ = "zoneinfo"
357db96d56Sopenharmony_ci
367db96d56Sopenharmony_ci    def __init_subclass__(cls):
377db96d56Sopenharmony_ci        cls._strong_cache = collections.OrderedDict()
387db96d56Sopenharmony_ci        cls._weak_cache = weakref.WeakValueDictionary()
397db96d56Sopenharmony_ci
407db96d56Sopenharmony_ci    def __new__(cls, key):
417db96d56Sopenharmony_ci        instance = cls._weak_cache.get(key, None)
427db96d56Sopenharmony_ci        if instance is None:
437db96d56Sopenharmony_ci            instance = cls._weak_cache.setdefault(key, cls._new_instance(key))
447db96d56Sopenharmony_ci            instance._from_cache = True
457db96d56Sopenharmony_ci
467db96d56Sopenharmony_ci        # Update the "strong" cache
477db96d56Sopenharmony_ci        cls._strong_cache[key] = cls._strong_cache.pop(key, instance)
487db96d56Sopenharmony_ci
497db96d56Sopenharmony_ci        if len(cls._strong_cache) > cls._strong_cache_size:
507db96d56Sopenharmony_ci            cls._strong_cache.popitem(last=False)
517db96d56Sopenharmony_ci
527db96d56Sopenharmony_ci        return instance
537db96d56Sopenharmony_ci
547db96d56Sopenharmony_ci    @classmethod
557db96d56Sopenharmony_ci    def no_cache(cls, key):
567db96d56Sopenharmony_ci        obj = cls._new_instance(key)
577db96d56Sopenharmony_ci        obj._from_cache = False
587db96d56Sopenharmony_ci
597db96d56Sopenharmony_ci        return obj
607db96d56Sopenharmony_ci
617db96d56Sopenharmony_ci    @classmethod
627db96d56Sopenharmony_ci    def _new_instance(cls, key):
637db96d56Sopenharmony_ci        obj = super().__new__(cls)
647db96d56Sopenharmony_ci        obj._key = key
657db96d56Sopenharmony_ci        obj._file_path = obj._find_tzfile(key)
667db96d56Sopenharmony_ci
677db96d56Sopenharmony_ci        if obj._file_path is not None:
687db96d56Sopenharmony_ci            file_obj = open(obj._file_path, "rb")
697db96d56Sopenharmony_ci        else:
707db96d56Sopenharmony_ci            file_obj = _common.load_tzdata(key)
717db96d56Sopenharmony_ci
727db96d56Sopenharmony_ci        with file_obj as f:
737db96d56Sopenharmony_ci            obj._load_file(f)
747db96d56Sopenharmony_ci
757db96d56Sopenharmony_ci        return obj
767db96d56Sopenharmony_ci
777db96d56Sopenharmony_ci    @classmethod
787db96d56Sopenharmony_ci    def from_file(cls, fobj, /, key=None):
797db96d56Sopenharmony_ci        obj = super().__new__(cls)
807db96d56Sopenharmony_ci        obj._key = key
817db96d56Sopenharmony_ci        obj._file_path = None
827db96d56Sopenharmony_ci        obj._load_file(fobj)
837db96d56Sopenharmony_ci        obj._file_repr = repr(fobj)
847db96d56Sopenharmony_ci
857db96d56Sopenharmony_ci        # Disable pickling for objects created from files
867db96d56Sopenharmony_ci        obj.__reduce__ = obj._file_reduce
877db96d56Sopenharmony_ci
887db96d56Sopenharmony_ci        return obj
897db96d56Sopenharmony_ci
907db96d56Sopenharmony_ci    @classmethod
917db96d56Sopenharmony_ci    def clear_cache(cls, *, only_keys=None):
927db96d56Sopenharmony_ci        if only_keys is not None:
937db96d56Sopenharmony_ci            for key in only_keys:
947db96d56Sopenharmony_ci                cls._weak_cache.pop(key, None)
957db96d56Sopenharmony_ci                cls._strong_cache.pop(key, None)
967db96d56Sopenharmony_ci
977db96d56Sopenharmony_ci        else:
987db96d56Sopenharmony_ci            cls._weak_cache.clear()
997db96d56Sopenharmony_ci            cls._strong_cache.clear()
1007db96d56Sopenharmony_ci
1017db96d56Sopenharmony_ci    @property
1027db96d56Sopenharmony_ci    def key(self):
1037db96d56Sopenharmony_ci        return self._key
1047db96d56Sopenharmony_ci
1057db96d56Sopenharmony_ci    def utcoffset(self, dt):
1067db96d56Sopenharmony_ci        return self._find_trans(dt).utcoff
1077db96d56Sopenharmony_ci
1087db96d56Sopenharmony_ci    def dst(self, dt):
1097db96d56Sopenharmony_ci        return self._find_trans(dt).dstoff
1107db96d56Sopenharmony_ci
1117db96d56Sopenharmony_ci    def tzname(self, dt):
1127db96d56Sopenharmony_ci        return self._find_trans(dt).tzname
1137db96d56Sopenharmony_ci
1147db96d56Sopenharmony_ci    def fromutc(self, dt):
1157db96d56Sopenharmony_ci        """Convert from datetime in UTC to datetime in local time"""
1167db96d56Sopenharmony_ci
1177db96d56Sopenharmony_ci        if not isinstance(dt, datetime):
1187db96d56Sopenharmony_ci            raise TypeError("fromutc() requires a datetime argument")
1197db96d56Sopenharmony_ci        if dt.tzinfo is not self:
1207db96d56Sopenharmony_ci            raise ValueError("dt.tzinfo is not self")
1217db96d56Sopenharmony_ci
1227db96d56Sopenharmony_ci        timestamp = self._get_local_timestamp(dt)
1237db96d56Sopenharmony_ci        num_trans = len(self._trans_utc)
1247db96d56Sopenharmony_ci
1257db96d56Sopenharmony_ci        if num_trans >= 1 and timestamp < self._trans_utc[0]:
1267db96d56Sopenharmony_ci            tti = self._tti_before
1277db96d56Sopenharmony_ci            fold = 0
1287db96d56Sopenharmony_ci        elif (
1297db96d56Sopenharmony_ci            num_trans == 0 or timestamp > self._trans_utc[-1]
1307db96d56Sopenharmony_ci        ) and not isinstance(self._tz_after, _ttinfo):
1317db96d56Sopenharmony_ci            tti, fold = self._tz_after.get_trans_info_fromutc(
1327db96d56Sopenharmony_ci                timestamp, dt.year
1337db96d56Sopenharmony_ci            )
1347db96d56Sopenharmony_ci        elif num_trans == 0:
1357db96d56Sopenharmony_ci            tti = self._tz_after
1367db96d56Sopenharmony_ci            fold = 0
1377db96d56Sopenharmony_ci        else:
1387db96d56Sopenharmony_ci            idx = bisect.bisect_right(self._trans_utc, timestamp)
1397db96d56Sopenharmony_ci
1407db96d56Sopenharmony_ci            if num_trans > 1 and timestamp >= self._trans_utc[1]:
1417db96d56Sopenharmony_ci                tti_prev, tti = self._ttinfos[idx - 2 : idx]
1427db96d56Sopenharmony_ci            elif timestamp > self._trans_utc[-1]:
1437db96d56Sopenharmony_ci                tti_prev = self._ttinfos[-1]
1447db96d56Sopenharmony_ci                tti = self._tz_after
1457db96d56Sopenharmony_ci            else:
1467db96d56Sopenharmony_ci                tti_prev = self._tti_before
1477db96d56Sopenharmony_ci                tti = self._ttinfos[0]
1487db96d56Sopenharmony_ci
1497db96d56Sopenharmony_ci            # Detect fold
1507db96d56Sopenharmony_ci            shift = tti_prev.utcoff - tti.utcoff
1517db96d56Sopenharmony_ci            fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1]
1527db96d56Sopenharmony_ci        dt += tti.utcoff
1537db96d56Sopenharmony_ci        if fold:
1547db96d56Sopenharmony_ci            return dt.replace(fold=1)
1557db96d56Sopenharmony_ci        else:
1567db96d56Sopenharmony_ci            return dt
1577db96d56Sopenharmony_ci
1587db96d56Sopenharmony_ci    def _find_trans(self, dt):
1597db96d56Sopenharmony_ci        if dt is None:
1607db96d56Sopenharmony_ci            if self._fixed_offset:
1617db96d56Sopenharmony_ci                return self._tz_after
1627db96d56Sopenharmony_ci            else:
1637db96d56Sopenharmony_ci                return _NO_TTINFO
1647db96d56Sopenharmony_ci
1657db96d56Sopenharmony_ci        ts = self._get_local_timestamp(dt)
1667db96d56Sopenharmony_ci
1677db96d56Sopenharmony_ci        lt = self._trans_local[dt.fold]
1687db96d56Sopenharmony_ci
1697db96d56Sopenharmony_ci        num_trans = len(lt)
1707db96d56Sopenharmony_ci
1717db96d56Sopenharmony_ci        if num_trans and ts < lt[0]:
1727db96d56Sopenharmony_ci            return self._tti_before
1737db96d56Sopenharmony_ci        elif not num_trans or ts > lt[-1]:
1747db96d56Sopenharmony_ci            if isinstance(self._tz_after, _TZStr):
1757db96d56Sopenharmony_ci                return self._tz_after.get_trans_info(ts, dt.year, dt.fold)
1767db96d56Sopenharmony_ci            else:
1777db96d56Sopenharmony_ci                return self._tz_after
1787db96d56Sopenharmony_ci        else:
1797db96d56Sopenharmony_ci            # idx is the transition that occurs after this timestamp, so we
1807db96d56Sopenharmony_ci            # subtract off 1 to get the current ttinfo
1817db96d56Sopenharmony_ci            idx = bisect.bisect_right(lt, ts) - 1
1827db96d56Sopenharmony_ci            assert idx >= 0
1837db96d56Sopenharmony_ci            return self._ttinfos[idx]
1847db96d56Sopenharmony_ci
1857db96d56Sopenharmony_ci    def _get_local_timestamp(self, dt):
1867db96d56Sopenharmony_ci        return (
1877db96d56Sopenharmony_ci            (dt.toordinal() - EPOCHORDINAL) * 86400
1887db96d56Sopenharmony_ci            + dt.hour * 3600
1897db96d56Sopenharmony_ci            + dt.minute * 60
1907db96d56Sopenharmony_ci            + dt.second
1917db96d56Sopenharmony_ci        )
1927db96d56Sopenharmony_ci
1937db96d56Sopenharmony_ci    def __str__(self):
1947db96d56Sopenharmony_ci        if self._key is not None:
1957db96d56Sopenharmony_ci            return f"{self._key}"
1967db96d56Sopenharmony_ci        else:
1977db96d56Sopenharmony_ci            return repr(self)
1987db96d56Sopenharmony_ci
1997db96d56Sopenharmony_ci    def __repr__(self):
2007db96d56Sopenharmony_ci        if self._key is not None:
2017db96d56Sopenharmony_ci            return f"{self.__class__.__name__}(key={self._key!r})"
2027db96d56Sopenharmony_ci        else:
2037db96d56Sopenharmony_ci            return f"{self.__class__.__name__}.from_file({self._file_repr})"
2047db96d56Sopenharmony_ci
2057db96d56Sopenharmony_ci    def __reduce__(self):
2067db96d56Sopenharmony_ci        return (self.__class__._unpickle, (self._key, self._from_cache))
2077db96d56Sopenharmony_ci
2087db96d56Sopenharmony_ci    def _file_reduce(self):
2097db96d56Sopenharmony_ci        import pickle
2107db96d56Sopenharmony_ci
2117db96d56Sopenharmony_ci        raise pickle.PicklingError(
2127db96d56Sopenharmony_ci            "Cannot pickle a ZoneInfo file created from a file stream."
2137db96d56Sopenharmony_ci        )
2147db96d56Sopenharmony_ci
2157db96d56Sopenharmony_ci    @classmethod
2167db96d56Sopenharmony_ci    def _unpickle(cls, key, from_cache, /):
2177db96d56Sopenharmony_ci        if from_cache:
2187db96d56Sopenharmony_ci            return cls(key)
2197db96d56Sopenharmony_ci        else:
2207db96d56Sopenharmony_ci            return cls.no_cache(key)
2217db96d56Sopenharmony_ci
2227db96d56Sopenharmony_ci    def _find_tzfile(self, key):
2237db96d56Sopenharmony_ci        return _tzpath.find_tzfile(key)
2247db96d56Sopenharmony_ci
2257db96d56Sopenharmony_ci    def _load_file(self, fobj):
2267db96d56Sopenharmony_ci        # Retrieve all the data as it exists in the zoneinfo file
2277db96d56Sopenharmony_ci        trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = _common.load_data(
2287db96d56Sopenharmony_ci            fobj
2297db96d56Sopenharmony_ci        )
2307db96d56Sopenharmony_ci
2317db96d56Sopenharmony_ci        # Infer the DST offsets (needed for .dst()) from the data
2327db96d56Sopenharmony_ci        dstoff = self._utcoff_to_dstoff(trans_idx, utcoff, isdst)
2337db96d56Sopenharmony_ci
2347db96d56Sopenharmony_ci        # Convert all the transition times (UTC) into "seconds since 1970-01-01 local time"
2357db96d56Sopenharmony_ci        trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff)
2367db96d56Sopenharmony_ci
2377db96d56Sopenharmony_ci        # Construct `_ttinfo` objects for each transition in the file
2387db96d56Sopenharmony_ci        _ttinfo_list = [
2397db96d56Sopenharmony_ci            _ttinfo(
2407db96d56Sopenharmony_ci                _load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname
2417db96d56Sopenharmony_ci            )
2427db96d56Sopenharmony_ci            for utcoffset, dstoffset, tzname in zip(utcoff, dstoff, abbr)
2437db96d56Sopenharmony_ci        ]
2447db96d56Sopenharmony_ci
2457db96d56Sopenharmony_ci        self._trans_utc = trans_utc
2467db96d56Sopenharmony_ci        self._trans_local = trans_local
2477db96d56Sopenharmony_ci        self._ttinfos = [_ttinfo_list[idx] for idx in trans_idx]
2487db96d56Sopenharmony_ci
2497db96d56Sopenharmony_ci        # Find the first non-DST transition
2507db96d56Sopenharmony_ci        for i in range(len(isdst)):
2517db96d56Sopenharmony_ci            if not isdst[i]:
2527db96d56Sopenharmony_ci                self._tti_before = _ttinfo_list[i]
2537db96d56Sopenharmony_ci                break
2547db96d56Sopenharmony_ci        else:
2557db96d56Sopenharmony_ci            if self._ttinfos:
2567db96d56Sopenharmony_ci                self._tti_before = self._ttinfos[0]
2577db96d56Sopenharmony_ci            else:
2587db96d56Sopenharmony_ci                self._tti_before = None
2597db96d56Sopenharmony_ci
2607db96d56Sopenharmony_ci        # Set the "fallback" time zone
2617db96d56Sopenharmony_ci        if tz_str is not None and tz_str != b"":
2627db96d56Sopenharmony_ci            self._tz_after = _parse_tz_str(tz_str.decode())
2637db96d56Sopenharmony_ci        else:
2647db96d56Sopenharmony_ci            if not self._ttinfos and not _ttinfo_list:
2657db96d56Sopenharmony_ci                raise ValueError("No time zone information found.")
2667db96d56Sopenharmony_ci
2677db96d56Sopenharmony_ci            if self._ttinfos:
2687db96d56Sopenharmony_ci                self._tz_after = self._ttinfos[-1]
2697db96d56Sopenharmony_ci            else:
2707db96d56Sopenharmony_ci                self._tz_after = _ttinfo_list[-1]
2717db96d56Sopenharmony_ci
2727db96d56Sopenharmony_ci        # Determine if this is a "fixed offset" zone, meaning that the output
2737db96d56Sopenharmony_ci        # of the utcoffset, dst and tzname functions does not depend on the
2747db96d56Sopenharmony_ci        # specific datetime passed.
2757db96d56Sopenharmony_ci        #
2767db96d56Sopenharmony_ci        # We make three simplifying assumptions here:
2777db96d56Sopenharmony_ci        #
2787db96d56Sopenharmony_ci        # 1. If _tz_after is not a _ttinfo, it has transitions that might
2797db96d56Sopenharmony_ci        #    actually occur (it is possible to construct TZ strings that
2807db96d56Sopenharmony_ci        #    specify STD and DST but no transitions ever occur, such as
2817db96d56Sopenharmony_ci        #    AAA0BBB,0/0,J365/25).
2827db96d56Sopenharmony_ci        # 2. If _ttinfo_list contains more than one _ttinfo object, the objects
2837db96d56Sopenharmony_ci        #    represent different offsets.
2847db96d56Sopenharmony_ci        # 3. _ttinfo_list contains no unused _ttinfos (in which case an
2857db96d56Sopenharmony_ci        #    otherwise fixed-offset zone with extra _ttinfos defined may
2867db96d56Sopenharmony_ci        #    appear to *not* be a fixed offset zone).
2877db96d56Sopenharmony_ci        #
2887db96d56Sopenharmony_ci        # Violations to these assumptions would be fairly exotic, and exotic
2897db96d56Sopenharmony_ci        # zones should almost certainly not be used with datetime.time (the
2907db96d56Sopenharmony_ci        # only thing that would be affected by this).
2917db96d56Sopenharmony_ci        if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo):
2927db96d56Sopenharmony_ci            self._fixed_offset = False
2937db96d56Sopenharmony_ci        elif not _ttinfo_list:
2947db96d56Sopenharmony_ci            self._fixed_offset = True
2957db96d56Sopenharmony_ci        else:
2967db96d56Sopenharmony_ci            self._fixed_offset = _ttinfo_list[0] == self._tz_after
2977db96d56Sopenharmony_ci
2987db96d56Sopenharmony_ci    @staticmethod
2997db96d56Sopenharmony_ci    def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts):
3007db96d56Sopenharmony_ci        # Now we must transform our ttis and abbrs into `_ttinfo` objects,
3017db96d56Sopenharmony_ci        # but there is an issue: .dst() must return a timedelta with the
3027db96d56Sopenharmony_ci        # difference between utcoffset() and the "standard" offset, but
3037db96d56Sopenharmony_ci        # the "base offset" and "DST offset" are not encoded in the file;
3047db96d56Sopenharmony_ci        # we can infer what they are from the isdst flag, but it is not
3057db96d56Sopenharmony_ci        # sufficient to just look at the last standard offset, because
3067db96d56Sopenharmony_ci        # occasionally countries will shift both DST offset and base offset.
3077db96d56Sopenharmony_ci
3087db96d56Sopenharmony_ci        typecnt = len(isdsts)
3097db96d56Sopenharmony_ci        dstoffs = [0] * typecnt  # Provisionally assign all to 0.
3107db96d56Sopenharmony_ci        dst_cnt = sum(isdsts)
3117db96d56Sopenharmony_ci        dst_found = 0
3127db96d56Sopenharmony_ci
3137db96d56Sopenharmony_ci        for i in range(1, len(trans_idx)):
3147db96d56Sopenharmony_ci            if dst_cnt == dst_found:
3157db96d56Sopenharmony_ci                break
3167db96d56Sopenharmony_ci
3177db96d56Sopenharmony_ci            idx = trans_idx[i]
3187db96d56Sopenharmony_ci
3197db96d56Sopenharmony_ci            dst = isdsts[idx]
3207db96d56Sopenharmony_ci
3217db96d56Sopenharmony_ci            # We're only going to look at daylight saving time
3227db96d56Sopenharmony_ci            if not dst:
3237db96d56Sopenharmony_ci                continue
3247db96d56Sopenharmony_ci
3257db96d56Sopenharmony_ci            # Skip any offsets that have already been assigned
3267db96d56Sopenharmony_ci            if dstoffs[idx] != 0:
3277db96d56Sopenharmony_ci                continue
3287db96d56Sopenharmony_ci
3297db96d56Sopenharmony_ci            dstoff = 0
3307db96d56Sopenharmony_ci            utcoff = utcoffsets[idx]
3317db96d56Sopenharmony_ci
3327db96d56Sopenharmony_ci            comp_idx = trans_idx[i - 1]
3337db96d56Sopenharmony_ci
3347db96d56Sopenharmony_ci            if not isdsts[comp_idx]:
3357db96d56Sopenharmony_ci                dstoff = utcoff - utcoffsets[comp_idx]
3367db96d56Sopenharmony_ci
3377db96d56Sopenharmony_ci            if not dstoff and idx < (typecnt - 1):
3387db96d56Sopenharmony_ci                comp_idx = trans_idx[i + 1]
3397db96d56Sopenharmony_ci
3407db96d56Sopenharmony_ci                # If the following transition is also DST and we couldn't
3417db96d56Sopenharmony_ci                # find the DST offset by this point, we're going to have to
3427db96d56Sopenharmony_ci                # skip it and hope this transition gets assigned later
3437db96d56Sopenharmony_ci                if isdsts[comp_idx]:
3447db96d56Sopenharmony_ci                    continue
3457db96d56Sopenharmony_ci
3467db96d56Sopenharmony_ci                dstoff = utcoff - utcoffsets[comp_idx]
3477db96d56Sopenharmony_ci
3487db96d56Sopenharmony_ci            if dstoff:
3497db96d56Sopenharmony_ci                dst_found += 1
3507db96d56Sopenharmony_ci                dstoffs[idx] = dstoff
3517db96d56Sopenharmony_ci        else:
3527db96d56Sopenharmony_ci            # If we didn't find a valid value for a given index, we'll end up
3537db96d56Sopenharmony_ci            # with dstoff = 0 for something where `isdst=1`. This is obviously
3547db96d56Sopenharmony_ci            # wrong - one hour will be a much better guess than 0
3557db96d56Sopenharmony_ci            for idx in range(typecnt):
3567db96d56Sopenharmony_ci                if not dstoffs[idx] and isdsts[idx]:
3577db96d56Sopenharmony_ci                    dstoffs[idx] = 3600
3587db96d56Sopenharmony_ci
3597db96d56Sopenharmony_ci        return dstoffs
3607db96d56Sopenharmony_ci
3617db96d56Sopenharmony_ci    @staticmethod
3627db96d56Sopenharmony_ci    def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):
3637db96d56Sopenharmony_ci        """Generate number of seconds since 1970 *in the local time*.
3647db96d56Sopenharmony_ci
3657db96d56Sopenharmony_ci        This is necessary to easily find the transition times in local time"""
3667db96d56Sopenharmony_ci        if not trans_list_utc:
3677db96d56Sopenharmony_ci            return [[], []]
3687db96d56Sopenharmony_ci
3697db96d56Sopenharmony_ci        # Start with the timestamps and modify in-place
3707db96d56Sopenharmony_ci        trans_list_wall = [list(trans_list_utc), list(trans_list_utc)]
3717db96d56Sopenharmony_ci
3727db96d56Sopenharmony_ci        if len(utcoffsets) > 1:
3737db96d56Sopenharmony_ci            offset_0 = utcoffsets[0]
3747db96d56Sopenharmony_ci            offset_1 = utcoffsets[trans_idx[0]]
3757db96d56Sopenharmony_ci            if offset_1 > offset_0:
3767db96d56Sopenharmony_ci                offset_1, offset_0 = offset_0, offset_1
3777db96d56Sopenharmony_ci        else:
3787db96d56Sopenharmony_ci            offset_0 = offset_1 = utcoffsets[0]
3797db96d56Sopenharmony_ci
3807db96d56Sopenharmony_ci        trans_list_wall[0][0] += offset_0
3817db96d56Sopenharmony_ci        trans_list_wall[1][0] += offset_1
3827db96d56Sopenharmony_ci
3837db96d56Sopenharmony_ci        for i in range(1, len(trans_idx)):
3847db96d56Sopenharmony_ci            offset_0 = utcoffsets[trans_idx[i - 1]]
3857db96d56Sopenharmony_ci            offset_1 = utcoffsets[trans_idx[i]]
3867db96d56Sopenharmony_ci
3877db96d56Sopenharmony_ci            if offset_1 > offset_0:
3887db96d56Sopenharmony_ci                offset_1, offset_0 = offset_0, offset_1
3897db96d56Sopenharmony_ci
3907db96d56Sopenharmony_ci            trans_list_wall[0][i] += offset_0
3917db96d56Sopenharmony_ci            trans_list_wall[1][i] += offset_1
3927db96d56Sopenharmony_ci
3937db96d56Sopenharmony_ci        return trans_list_wall
3947db96d56Sopenharmony_ci
3957db96d56Sopenharmony_ci
3967db96d56Sopenharmony_ciclass _ttinfo:
3977db96d56Sopenharmony_ci    __slots__ = ["utcoff", "dstoff", "tzname"]
3987db96d56Sopenharmony_ci
3997db96d56Sopenharmony_ci    def __init__(self, utcoff, dstoff, tzname):
4007db96d56Sopenharmony_ci        self.utcoff = utcoff
4017db96d56Sopenharmony_ci        self.dstoff = dstoff
4027db96d56Sopenharmony_ci        self.tzname = tzname
4037db96d56Sopenharmony_ci
4047db96d56Sopenharmony_ci    def __eq__(self, other):
4057db96d56Sopenharmony_ci        return (
4067db96d56Sopenharmony_ci            self.utcoff == other.utcoff
4077db96d56Sopenharmony_ci            and self.dstoff == other.dstoff
4087db96d56Sopenharmony_ci            and self.tzname == other.tzname
4097db96d56Sopenharmony_ci        )
4107db96d56Sopenharmony_ci
4117db96d56Sopenharmony_ci    def __repr__(self):  # pragma: nocover
4127db96d56Sopenharmony_ci        return (
4137db96d56Sopenharmony_ci            f"{self.__class__.__name__}"
4147db96d56Sopenharmony_ci            + f"({self.utcoff}, {self.dstoff}, {self.tzname})"
4157db96d56Sopenharmony_ci        )
4167db96d56Sopenharmony_ci
4177db96d56Sopenharmony_ci
4187db96d56Sopenharmony_ci_NO_TTINFO = _ttinfo(None, None, None)
4197db96d56Sopenharmony_ci
4207db96d56Sopenharmony_ci
4217db96d56Sopenharmony_ciclass _TZStr:
4227db96d56Sopenharmony_ci    __slots__ = (
4237db96d56Sopenharmony_ci        "std",
4247db96d56Sopenharmony_ci        "dst",
4257db96d56Sopenharmony_ci        "start",
4267db96d56Sopenharmony_ci        "end",
4277db96d56Sopenharmony_ci        "get_trans_info",
4287db96d56Sopenharmony_ci        "get_trans_info_fromutc",
4297db96d56Sopenharmony_ci        "dst_diff",
4307db96d56Sopenharmony_ci    )
4317db96d56Sopenharmony_ci
4327db96d56Sopenharmony_ci    def __init__(
4337db96d56Sopenharmony_ci        self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None
4347db96d56Sopenharmony_ci    ):
4357db96d56Sopenharmony_ci        self.dst_diff = dst_offset - std_offset
4367db96d56Sopenharmony_ci        std_offset = _load_timedelta(std_offset)
4377db96d56Sopenharmony_ci        self.std = _ttinfo(
4387db96d56Sopenharmony_ci            utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr
4397db96d56Sopenharmony_ci        )
4407db96d56Sopenharmony_ci
4417db96d56Sopenharmony_ci        self.start = start
4427db96d56Sopenharmony_ci        self.end = end
4437db96d56Sopenharmony_ci
4447db96d56Sopenharmony_ci        dst_offset = _load_timedelta(dst_offset)
4457db96d56Sopenharmony_ci        delta = _load_timedelta(self.dst_diff)
4467db96d56Sopenharmony_ci        self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr)
4477db96d56Sopenharmony_ci
4487db96d56Sopenharmony_ci        # These are assertions because the constructor should only be called
4497db96d56Sopenharmony_ci        # by functions that would fail before passing start or end
4507db96d56Sopenharmony_ci        assert start is not None, "No transition start specified"
4517db96d56Sopenharmony_ci        assert end is not None, "No transition end specified"
4527db96d56Sopenharmony_ci
4537db96d56Sopenharmony_ci        self.get_trans_info = self._get_trans_info
4547db96d56Sopenharmony_ci        self.get_trans_info_fromutc = self._get_trans_info_fromutc
4557db96d56Sopenharmony_ci
4567db96d56Sopenharmony_ci    def transitions(self, year):
4577db96d56Sopenharmony_ci        start = self.start.year_to_epoch(year)
4587db96d56Sopenharmony_ci        end = self.end.year_to_epoch(year)
4597db96d56Sopenharmony_ci        return start, end
4607db96d56Sopenharmony_ci
4617db96d56Sopenharmony_ci    def _get_trans_info(self, ts, year, fold):
4627db96d56Sopenharmony_ci        """Get the information about the current transition - tti"""
4637db96d56Sopenharmony_ci        start, end = self.transitions(year)
4647db96d56Sopenharmony_ci
4657db96d56Sopenharmony_ci        # With fold = 0, the period (denominated in local time) with the
4667db96d56Sopenharmony_ci        # smaller offset starts at the end of the gap and ends at the end of
4677db96d56Sopenharmony_ci        # the fold; with fold = 1, it runs from the start of the gap to the
4687db96d56Sopenharmony_ci        # beginning of the fold.
4697db96d56Sopenharmony_ci        #
4707db96d56Sopenharmony_ci        # So in order to determine the DST boundaries we need to know both
4717db96d56Sopenharmony_ci        # the fold and whether DST is positive or negative (rare), and it
4727db96d56Sopenharmony_ci        # turns out that this boils down to fold XOR is_positive.
4737db96d56Sopenharmony_ci        if fold == (self.dst_diff >= 0):
4747db96d56Sopenharmony_ci            end -= self.dst_diff
4757db96d56Sopenharmony_ci        else:
4767db96d56Sopenharmony_ci            start += self.dst_diff
4777db96d56Sopenharmony_ci
4787db96d56Sopenharmony_ci        if start < end:
4797db96d56Sopenharmony_ci            isdst = start <= ts < end
4807db96d56Sopenharmony_ci        else:
4817db96d56Sopenharmony_ci            isdst = not (end <= ts < start)
4827db96d56Sopenharmony_ci
4837db96d56Sopenharmony_ci        return self.dst if isdst else self.std
4847db96d56Sopenharmony_ci
4857db96d56Sopenharmony_ci    def _get_trans_info_fromutc(self, ts, year):
4867db96d56Sopenharmony_ci        start, end = self.transitions(year)
4877db96d56Sopenharmony_ci        start -= self.std.utcoff.total_seconds()
4887db96d56Sopenharmony_ci        end -= self.dst.utcoff.total_seconds()
4897db96d56Sopenharmony_ci
4907db96d56Sopenharmony_ci        if start < end:
4917db96d56Sopenharmony_ci            isdst = start <= ts < end
4927db96d56Sopenharmony_ci        else:
4937db96d56Sopenharmony_ci            isdst = not (end <= ts < start)
4947db96d56Sopenharmony_ci
4957db96d56Sopenharmony_ci        # For positive DST, the ambiguous period is one dst_diff after the end
4967db96d56Sopenharmony_ci        # of DST; for negative DST, the ambiguous period is one dst_diff before
4977db96d56Sopenharmony_ci        # the start of DST.
4987db96d56Sopenharmony_ci        if self.dst_diff > 0:
4997db96d56Sopenharmony_ci            ambig_start = end
5007db96d56Sopenharmony_ci            ambig_end = end + self.dst_diff
5017db96d56Sopenharmony_ci        else:
5027db96d56Sopenharmony_ci            ambig_start = start
5037db96d56Sopenharmony_ci            ambig_end = start - self.dst_diff
5047db96d56Sopenharmony_ci
5057db96d56Sopenharmony_ci        fold = ambig_start <= ts < ambig_end
5067db96d56Sopenharmony_ci
5077db96d56Sopenharmony_ci        return (self.dst if isdst else self.std, fold)
5087db96d56Sopenharmony_ci
5097db96d56Sopenharmony_ci
5107db96d56Sopenharmony_cidef _post_epoch_days_before_year(year):
5117db96d56Sopenharmony_ci    """Get the number of days between 1970-01-01 and YEAR-01-01"""
5127db96d56Sopenharmony_ci    y = year - 1
5137db96d56Sopenharmony_ci    return y * 365 + y // 4 - y // 100 + y // 400 - EPOCHORDINAL
5147db96d56Sopenharmony_ci
5157db96d56Sopenharmony_ci
5167db96d56Sopenharmony_ciclass _DayOffset:
5177db96d56Sopenharmony_ci    __slots__ = ["d", "julian", "hour", "minute", "second"]
5187db96d56Sopenharmony_ci
5197db96d56Sopenharmony_ci    def __init__(self, d, julian, hour=2, minute=0, second=0):
5207db96d56Sopenharmony_ci        if not (0 + julian) <= d <= 365:
5217db96d56Sopenharmony_ci            min_day = 0 + julian
5227db96d56Sopenharmony_ci            raise ValueError(f"d must be in [{min_day}, 365], not: {d}")
5237db96d56Sopenharmony_ci
5247db96d56Sopenharmony_ci        self.d = d
5257db96d56Sopenharmony_ci        self.julian = julian
5267db96d56Sopenharmony_ci        self.hour = hour
5277db96d56Sopenharmony_ci        self.minute = minute
5287db96d56Sopenharmony_ci        self.second = second
5297db96d56Sopenharmony_ci
5307db96d56Sopenharmony_ci    def year_to_epoch(self, year):
5317db96d56Sopenharmony_ci        days_before_year = _post_epoch_days_before_year(year)
5327db96d56Sopenharmony_ci
5337db96d56Sopenharmony_ci        d = self.d
5347db96d56Sopenharmony_ci        if self.julian and d >= 59 and calendar.isleap(year):
5357db96d56Sopenharmony_ci            d += 1
5367db96d56Sopenharmony_ci
5377db96d56Sopenharmony_ci        epoch = (days_before_year + d) * 86400
5387db96d56Sopenharmony_ci        epoch += self.hour * 3600 + self.minute * 60 + self.second
5397db96d56Sopenharmony_ci
5407db96d56Sopenharmony_ci        return epoch
5417db96d56Sopenharmony_ci
5427db96d56Sopenharmony_ci
5437db96d56Sopenharmony_ciclass _CalendarOffset:
5447db96d56Sopenharmony_ci    __slots__ = ["m", "w", "d", "hour", "minute", "second"]
5457db96d56Sopenharmony_ci
5467db96d56Sopenharmony_ci    _DAYS_BEFORE_MONTH = (
5477db96d56Sopenharmony_ci        -1,
5487db96d56Sopenharmony_ci        0,
5497db96d56Sopenharmony_ci        31,
5507db96d56Sopenharmony_ci        59,
5517db96d56Sopenharmony_ci        90,
5527db96d56Sopenharmony_ci        120,
5537db96d56Sopenharmony_ci        151,
5547db96d56Sopenharmony_ci        181,
5557db96d56Sopenharmony_ci        212,
5567db96d56Sopenharmony_ci        243,
5577db96d56Sopenharmony_ci        273,
5587db96d56Sopenharmony_ci        304,
5597db96d56Sopenharmony_ci        334,
5607db96d56Sopenharmony_ci    )
5617db96d56Sopenharmony_ci
5627db96d56Sopenharmony_ci    def __init__(self, m, w, d, hour=2, minute=0, second=0):
5637db96d56Sopenharmony_ci        if not 0 < m <= 12:
5647db96d56Sopenharmony_ci            raise ValueError("m must be in (0, 12]")
5657db96d56Sopenharmony_ci
5667db96d56Sopenharmony_ci        if not 0 < w <= 5:
5677db96d56Sopenharmony_ci            raise ValueError("w must be in (0, 5]")
5687db96d56Sopenharmony_ci
5697db96d56Sopenharmony_ci        if not 0 <= d <= 6:
5707db96d56Sopenharmony_ci            raise ValueError("d must be in [0, 6]")
5717db96d56Sopenharmony_ci
5727db96d56Sopenharmony_ci        self.m = m
5737db96d56Sopenharmony_ci        self.w = w
5747db96d56Sopenharmony_ci        self.d = d
5757db96d56Sopenharmony_ci        self.hour = hour
5767db96d56Sopenharmony_ci        self.minute = minute
5777db96d56Sopenharmony_ci        self.second = second
5787db96d56Sopenharmony_ci
5797db96d56Sopenharmony_ci    @classmethod
5807db96d56Sopenharmony_ci    def _ymd2ord(cls, year, month, day):
5817db96d56Sopenharmony_ci        return (
5827db96d56Sopenharmony_ci            _post_epoch_days_before_year(year)
5837db96d56Sopenharmony_ci            + cls._DAYS_BEFORE_MONTH[month]
5847db96d56Sopenharmony_ci            + (month > 2 and calendar.isleap(year))
5857db96d56Sopenharmony_ci            + day
5867db96d56Sopenharmony_ci        )
5877db96d56Sopenharmony_ci
5887db96d56Sopenharmony_ci    # TODO: These are not actually epoch dates as they are expressed in local time
5897db96d56Sopenharmony_ci    def year_to_epoch(self, year):
5907db96d56Sopenharmony_ci        """Calculates the datetime of the occurrence from the year"""
5917db96d56Sopenharmony_ci        # We know year and month, we need to convert w, d into day of month
5927db96d56Sopenharmony_ci        #
5937db96d56Sopenharmony_ci        # Week 1 is the first week in which day `d` (where 0 = Sunday) appears.
5947db96d56Sopenharmony_ci        # Week 5 represents the last occurrence of day `d`, so we need to know
5957db96d56Sopenharmony_ci        # the range of the month.
5967db96d56Sopenharmony_ci        first_day, days_in_month = calendar.monthrange(year, self.m)
5977db96d56Sopenharmony_ci
5987db96d56Sopenharmony_ci        # This equation seems magical, so I'll break it down:
5997db96d56Sopenharmony_ci        # 1. calendar says 0 = Monday, POSIX says 0 = Sunday
6007db96d56Sopenharmony_ci        #    so we need first_day + 1 to get 1 = Monday -> 7 = Sunday,
6017db96d56Sopenharmony_ci        #    which is still equivalent because this math is mod 7
6027db96d56Sopenharmony_ci        # 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need
6037db96d56Sopenharmony_ci        #    to do anything to adjust negative numbers.
6047db96d56Sopenharmony_ci        # 3. Add 1 because month days are a 1-based index.
6057db96d56Sopenharmony_ci        month_day = (self.d - (first_day + 1)) % 7 + 1
6067db96d56Sopenharmony_ci
6077db96d56Sopenharmony_ci        # Now use a 0-based index version of `w` to calculate the w-th
6087db96d56Sopenharmony_ci        # occurrence of `d`
6097db96d56Sopenharmony_ci        month_day += (self.w - 1) * 7
6107db96d56Sopenharmony_ci
6117db96d56Sopenharmony_ci        # month_day will only be > days_in_month if w was 5, and `w` means
6127db96d56Sopenharmony_ci        # "last occurrence of `d`", so now we just check if we over-shot the
6137db96d56Sopenharmony_ci        # end of the month and if so knock off 1 week.
6147db96d56Sopenharmony_ci        if month_day > days_in_month:
6157db96d56Sopenharmony_ci            month_day -= 7
6167db96d56Sopenharmony_ci
6177db96d56Sopenharmony_ci        ordinal = self._ymd2ord(year, self.m, month_day)
6187db96d56Sopenharmony_ci        epoch = ordinal * 86400
6197db96d56Sopenharmony_ci        epoch += self.hour * 3600 + self.minute * 60 + self.second
6207db96d56Sopenharmony_ci        return epoch
6217db96d56Sopenharmony_ci
6227db96d56Sopenharmony_ci
6237db96d56Sopenharmony_cidef _parse_tz_str(tz_str):
6247db96d56Sopenharmony_ci    # The tz string has the format:
6257db96d56Sopenharmony_ci    #
6267db96d56Sopenharmony_ci    # std[offset[dst[offset],start[/time],end[/time]]]
6277db96d56Sopenharmony_ci    #
6287db96d56Sopenharmony_ci    # std and dst must be 3 or more characters long and must not contain
6297db96d56Sopenharmony_ci    # a leading colon, embedded digits, commas, nor a plus or minus signs;
6307db96d56Sopenharmony_ci    # The spaces between "std" and "offset" are only for display and are
6317db96d56Sopenharmony_ci    # not actually present in the string.
6327db96d56Sopenharmony_ci    #
6337db96d56Sopenharmony_ci    # The format of the offset is ``[+|-]hh[:mm[:ss]]``
6347db96d56Sopenharmony_ci
6357db96d56Sopenharmony_ci    offset_str, *start_end_str = tz_str.split(",", 1)
6367db96d56Sopenharmony_ci
6377db96d56Sopenharmony_ci    # fmt: off
6387db96d56Sopenharmony_ci    parser_re = re.compile(
6397db96d56Sopenharmony_ci        r"(?P<std>[^<0-9:.+-]+|<[a-zA-Z0-9+\-]+>)" +
6407db96d56Sopenharmony_ci        r"((?P<stdoff>[+-]?\d{1,2}(:\d{2}(:\d{2})?)?)" +
6417db96d56Sopenharmony_ci            r"((?P<dst>[^0-9:.+-]+|<[a-zA-Z0-9+\-]+>)" +
6427db96d56Sopenharmony_ci                r"((?P<dstoff>[+-]?\d{1,2}(:\d{2}(:\d{2})?)?))?" +
6437db96d56Sopenharmony_ci            r")?" + # dst
6447db96d56Sopenharmony_ci        r")?$" # stdoff
6457db96d56Sopenharmony_ci    )
6467db96d56Sopenharmony_ci    # fmt: on
6477db96d56Sopenharmony_ci
6487db96d56Sopenharmony_ci    m = parser_re.match(offset_str)
6497db96d56Sopenharmony_ci
6507db96d56Sopenharmony_ci    if m is None:
6517db96d56Sopenharmony_ci        raise ValueError(f"{tz_str} is not a valid TZ string")
6527db96d56Sopenharmony_ci
6537db96d56Sopenharmony_ci    std_abbr = m.group("std")
6547db96d56Sopenharmony_ci    dst_abbr = m.group("dst")
6557db96d56Sopenharmony_ci    dst_offset = None
6567db96d56Sopenharmony_ci
6577db96d56Sopenharmony_ci    std_abbr = std_abbr.strip("<>")
6587db96d56Sopenharmony_ci
6597db96d56Sopenharmony_ci    if dst_abbr:
6607db96d56Sopenharmony_ci        dst_abbr = dst_abbr.strip("<>")
6617db96d56Sopenharmony_ci
6627db96d56Sopenharmony_ci    if std_offset := m.group("stdoff"):
6637db96d56Sopenharmony_ci        try:
6647db96d56Sopenharmony_ci            std_offset = _parse_tz_delta(std_offset)
6657db96d56Sopenharmony_ci        except ValueError as e:
6667db96d56Sopenharmony_ci            raise ValueError(f"Invalid STD offset in {tz_str}") from e
6677db96d56Sopenharmony_ci    else:
6687db96d56Sopenharmony_ci        std_offset = 0
6697db96d56Sopenharmony_ci
6707db96d56Sopenharmony_ci    if dst_abbr is not None:
6717db96d56Sopenharmony_ci        if dst_offset := m.group("dstoff"):
6727db96d56Sopenharmony_ci            try:
6737db96d56Sopenharmony_ci                dst_offset = _parse_tz_delta(dst_offset)
6747db96d56Sopenharmony_ci            except ValueError as e:
6757db96d56Sopenharmony_ci                raise ValueError(f"Invalid DST offset in {tz_str}") from e
6767db96d56Sopenharmony_ci        else:
6777db96d56Sopenharmony_ci            dst_offset = std_offset + 3600
6787db96d56Sopenharmony_ci
6797db96d56Sopenharmony_ci        if not start_end_str:
6807db96d56Sopenharmony_ci            raise ValueError(f"Missing transition rules: {tz_str}")
6817db96d56Sopenharmony_ci
6827db96d56Sopenharmony_ci        start_end_strs = start_end_str[0].split(",", 1)
6837db96d56Sopenharmony_ci        try:
6847db96d56Sopenharmony_ci            start, end = (_parse_dst_start_end(x) for x in start_end_strs)
6857db96d56Sopenharmony_ci        except ValueError as e:
6867db96d56Sopenharmony_ci            raise ValueError(f"Invalid TZ string: {tz_str}") from e
6877db96d56Sopenharmony_ci
6887db96d56Sopenharmony_ci        return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end)
6897db96d56Sopenharmony_ci    elif start_end_str:
6907db96d56Sopenharmony_ci        raise ValueError(f"Transition rule present without DST: {tz_str}")
6917db96d56Sopenharmony_ci    else:
6927db96d56Sopenharmony_ci        # This is a static ttinfo, don't return _TZStr
6937db96d56Sopenharmony_ci        return _ttinfo(
6947db96d56Sopenharmony_ci            _load_timedelta(std_offset), _load_timedelta(0), std_abbr
6957db96d56Sopenharmony_ci        )
6967db96d56Sopenharmony_ci
6977db96d56Sopenharmony_ci
6987db96d56Sopenharmony_cidef _parse_dst_start_end(dststr):
6997db96d56Sopenharmony_ci    date, *time = dststr.split("/")
7007db96d56Sopenharmony_ci    if date[0] == "M":
7017db96d56Sopenharmony_ci        n_is_julian = False
7027db96d56Sopenharmony_ci        m = re.match(r"M(\d{1,2})\.(\d).(\d)$", date)
7037db96d56Sopenharmony_ci        if m is None:
7047db96d56Sopenharmony_ci            raise ValueError(f"Invalid dst start/end date: {dststr}")
7057db96d56Sopenharmony_ci        date_offset = tuple(map(int, m.groups()))
7067db96d56Sopenharmony_ci        offset = _CalendarOffset(*date_offset)
7077db96d56Sopenharmony_ci    else:
7087db96d56Sopenharmony_ci        if date[0] == "J":
7097db96d56Sopenharmony_ci            n_is_julian = True
7107db96d56Sopenharmony_ci            date = date[1:]
7117db96d56Sopenharmony_ci        else:
7127db96d56Sopenharmony_ci            n_is_julian = False
7137db96d56Sopenharmony_ci
7147db96d56Sopenharmony_ci        doy = int(date)
7157db96d56Sopenharmony_ci        offset = _DayOffset(doy, n_is_julian)
7167db96d56Sopenharmony_ci
7177db96d56Sopenharmony_ci    if time:
7187db96d56Sopenharmony_ci        time_components = list(map(int, time[0].split(":")))
7197db96d56Sopenharmony_ci        n_components = len(time_components)
7207db96d56Sopenharmony_ci        if n_components < 3:
7217db96d56Sopenharmony_ci            time_components.extend([0] * (3 - n_components))
7227db96d56Sopenharmony_ci        offset.hour, offset.minute, offset.second = time_components
7237db96d56Sopenharmony_ci
7247db96d56Sopenharmony_ci    return offset
7257db96d56Sopenharmony_ci
7267db96d56Sopenharmony_ci
7277db96d56Sopenharmony_cidef _parse_tz_delta(tz_delta):
7287db96d56Sopenharmony_ci    match = re.match(
7297db96d56Sopenharmony_ci        r"(?P<sign>[+-])?(?P<h>\d{1,2})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
7307db96d56Sopenharmony_ci        tz_delta,
7317db96d56Sopenharmony_ci    )
7327db96d56Sopenharmony_ci    # Anything passed to this function should already have hit an equivalent
7337db96d56Sopenharmony_ci    # regular expression to find the section to parse.
7347db96d56Sopenharmony_ci    assert match is not None, tz_delta
7357db96d56Sopenharmony_ci
7367db96d56Sopenharmony_ci    h, m, s = (
7377db96d56Sopenharmony_ci        int(v) if v is not None else 0
7387db96d56Sopenharmony_ci        for v in map(match.group, ("h", "m", "s"))
7397db96d56Sopenharmony_ci    )
7407db96d56Sopenharmony_ci
7417db96d56Sopenharmony_ci    total = h * 3600 + m * 60 + s
7427db96d56Sopenharmony_ci
7437db96d56Sopenharmony_ci    if not -86400 < total < 86400:
7447db96d56Sopenharmony_ci        raise ValueError(
7457db96d56Sopenharmony_ci            f"Offset must be strictly between -24h and +24h: {tz_delta}"
7467db96d56Sopenharmony_ci        )
7477db96d56Sopenharmony_ci
7487db96d56Sopenharmony_ci    # Yes, +5 maps to an offset of -5h
7497db96d56Sopenharmony_ci    if match.group("sign") != "-":
7507db96d56Sopenharmony_ci        total *= -1
7517db96d56Sopenharmony_ci
7527db96d56Sopenharmony_ci    return total
753