diff options
Diffstat (limited to 'pendulum/tz/zoneinfo/posix_timezone.py')
-rw-r--r-- | pendulum/tz/zoneinfo/posix_timezone.py | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/pendulum/tz/zoneinfo/posix_timezone.py b/pendulum/tz/zoneinfo/posix_timezone.py new file mode 100644 index 0000000..74a32eb --- /dev/null +++ b/pendulum/tz/zoneinfo/posix_timezone.py @@ -0,0 +1,270 @@ +""" +Parsing of a POSIX zone spec as described in the TZ part of section 8.3 in +http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html. +""" +import re + +from typing import Optional + +from pendulum.constants import MONTHS_OFFSETS +from pendulum.constants import SECS_PER_DAY + +from .exceptions import InvalidPosixSpec + + +_spec = re.compile( + "^" + r"(?P<std_abbr><.*?>|[^-+,\d]{3,})" + r"(?P<std_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)" + r"(?P<dst_info>" + r" (?P<dst_abbr><.*?>|[^-+,\d]{3,})" + r" (?P<dst_offset>([+-])?(\d{1,2})(:\d{2}(:\d{2})?)?)?" + r")?" + r"(?:,(?P<rules>" + r" (?P<dst_start>" + r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" + r" (?:/(?P<dst_start_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" + " )" + " ," + r" (?P<dst_end>" + r" (?:J\d+|\d+|M\d{1,2}.\d.[0-6])" + r" (?:/(?P<dst_end_offset>([+-])?(\d+)(:\d{2}(:\d{2})?)?))?" + " )" + "))?" + "$", + re.VERBOSE, +) + + +def posix_spec(spec): # type: (str) -> PosixTimezone + try: + return _posix_spec(spec) + except ValueError: + raise InvalidPosixSpec(spec) + + +def _posix_spec(spec): # type: (str) -> PosixTimezone + m = _spec.match(spec) + if not m: + raise ValueError("Invalid posix spec") + + std_abbr = _parse_abbr(m.group("std_abbr")) + std_offset = _parse_offset(m.group("std_offset")) + + dst_abbr = None + dst_offset = None + if m.group("dst_info"): + dst_abbr = _parse_abbr(m.group("dst_abbr")) + if m.group("dst_offset"): + dst_offset = _parse_offset(m.group("dst_offset")) + else: + dst_offset = std_offset + 3600 + + dst_start = None + dst_end = None + if m.group("rules"): + dst_start = _parse_rule(m.group("dst_start")) + dst_end = _parse_rule(m.group("dst_end")) + + return PosixTimezone(std_abbr, std_offset, dst_abbr, dst_offset, dst_start, dst_end) + + +def _parse_abbr(text): # type: (str) -> str + return text.lstrip("<").rstrip(">") + + +def _parse_offset(text, sign=-1): # type: (str, int) -> int + if text.startswith(("+", "-")): + if text.startswith("-"): + sign *= -1 + + text = text[1:] + + minutes = 0 + seconds = 0 + + parts = text.split(":") + hours = int(parts[0]) + + if len(parts) > 1: + minutes = int(parts[1]) + + if len(parts) > 2: + seconds = int(parts[2]) + + return sign * ((((hours * 60) + minutes) * 60) + seconds) + + +def _parse_rule(rule): # type: (str) -> PosixTransition + klass = NPosixTransition + args = () + + if rule.startswith("M"): + rule = rule[1:] + parts = rule.split(".") + month = int(parts[0]) + week = int(parts[1]) + day = int(parts[2].split("/")[0]) + + args += (month, week, day) + klass = MPosixTransition + elif rule.startswith("J"): + rule = rule[1:] + args += (int(rule.split("/")[0]),) + klass = JPosixTransition + else: + args += (int(rule.split("/")[0]),) + + # Checking offset + parts = rule.split("/") + if len(parts) > 1: + offset = _parse_offset(parts[-1], sign=1) + else: + offset = 7200 + + args += (offset,) + + return klass(*args) + + +class PosixTransition(object): + def __init__(self, offset): # type: (int) -> None + self._offset = offset + + @property + def offset(self): # type: () -> int + return self._offset + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + raise NotImplementedError() + + +class JPosixTransition(PosixTransition): + def __init__(self, day, offset): # type: (int, int) -> None + self._day = day + + super(JPosixTransition, self).__init__(offset) + + @property + def day(self): # type: () -> int + """ + day of non-leap year [1:365] + """ + return self._day + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + days = self._day + if not is_leap or days < MONTHS_OFFSETS[1][3]: + days -= 1 + + return (days * SECS_PER_DAY) + self._offset + + +class NPosixTransition(PosixTransition): + def __init__(self, day, offset): # type: (int, int) -> None + self._day = day + + super(NPosixTransition, self).__init__(offset) + + @property + def day(self): # type: () -> int + """ + day of year [0:365] + """ + return self._day + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + days = self._day + + return (days * SECS_PER_DAY) + self._offset + + +class MPosixTransition(PosixTransition): + def __init__(self, month, week, weekday, offset): + # type: (int, int, int, int) -> None + self._month = month + self._week = week + self._weekday = weekday + + super(MPosixTransition, self).__init__(offset) + + @property + def month(self): # type: () -> int + """ + month of year [1:12] + """ + return self._month + + @property + def week(self): # type: () -> int + """ + week of month [1:5] (5==last) + """ + return self._week + + @property + def weekday(self): # type: () -> int + """ + 0==Sun, ..., 6=Sat + """ + return self._weekday + + def trans_offset(self, is_leap, jan1_weekday): # type: (bool, int) -> int + last_week = self._week == 5 + days = MONTHS_OFFSETS[is_leap][self._month + int(last_week)] + weekday = (jan1_weekday + days) % 7 + if last_week: + days -= (weekday + 7 - 1 - self._weekday) % 7 + 1 + else: + days += (self._weekday + 7 - weekday) % 7 + days += (self._week - 1) * 7 + + return (days * SECS_PER_DAY) + self._offset + + +class PosixTimezone: + """ + The entirety of a POSIX-string specified time-zone rule. + + The standard abbreviation and offset are always given. + """ + + def __init__( + self, + std_abbr, # type: str + std_offset, # type: int + dst_abbr, # type: Optional[str] + dst_offset, # type: Optional[int] + dst_start=None, # type: Optional[PosixTransition] + dst_end=None, # type: Optional[PosixTransition] + ): + self._std_abbr = std_abbr + self._std_offset = std_offset + self._dst_abbr = dst_abbr + self._dst_offset = dst_offset + self._dst_start = dst_start + self._dst_end = dst_end + + @property + def std_abbr(self): # type: () -> str + return self._std_abbr + + @property + def std_offset(self): # type: () -> int + return self._std_offset + + @property + def dst_abbr(self): # type: () -> Optional[str] + return self._dst_abbr + + @property + def dst_offset(self): # type: () -> Optional[int] + return self._dst_offset + + @property + def dst_start(self): # type: () -> Optional[PosixTransition] + return self._dst_start + + @property + def dst_end(self): # type: () -> Optional[PosixTransition] + return self._dst_end |