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