summaryrefslogtreecommitdiffstats
path: root/pendulum/tz/zoneinfo/timezone.py
blob: 2147774abd9d261b97bae7fa5cd6bc72f7a0310a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from datetime import datetime
from typing import List
from typing import Optional

from pendulum.constants import DAYS_PER_YEAR
from pendulum.constants import SECS_PER_YEAR
from pendulum.helpers import is_leap
from pendulum.helpers import local_time
from pendulum.helpers import timestamp
from pendulum.helpers import week_day

from .posix_timezone import PosixTimezone
from .transition import Transition
from .transition_type import TransitionType


class Timezone:
    def __init__(
        self,
        transitions,  # type: List[Transition]
        posix_rule=None,  # type: Optional[PosixTimezone]
        extended=True,  # type: bool
    ):
        self._posix_rule = posix_rule
        self._transitions = transitions

        if extended:
            self._extends()

    @property
    def transitions(self):  # type: () -> List[Transition]
        return self._transitions

    @property
    def posix_rule(self):
        return self._posix_rule

    def _extends(self):
        if not self._posix_rule:
            return

        posix = self._posix_rule

        if not posix.dst_abbr:
            # std only
            # The future specification should match the last/default transition
            ttype = self._transitions[-1].ttype
            if not self._check_ttype(ttype, posix.std_offset, False, posix.std_abbr):
                raise ValueError("Posix spec does not match last transition")

            return

        if len(self._transitions) < 2:
            raise ValueError("Too few transitions for POSIX spec")

        # Extend the transitions for an additional 400 years
        # using the future specification

        # The future specification should match the last two transitions,
        # and those transitions should have different is_dst flags.
        tr0 = self._transitions[-1]
        tr1 = self._transitions[-2]
        tt0 = tr0.ttype
        tt1 = tr1.ttype
        if tt0.is_dst():
            dst = tt0
            std = tt1
        else:
            dst = tt1
            std = tt0

        self._check_ttype(dst, posix.dst_offset, True, posix.dst_abbr)
        self._check_ttype(std, posix.std_offset, False, posix.std_abbr)

        # Add the transitions to tr1 and back to tr0 for each extra year.
        last_year = local_time(tr0.local, 0, 0)[0]
        leap_year = is_leap(last_year)
        jan1 = datetime(last_year, 1, 1)
        jan1_time = timestamp(jan1)
        jan1_weekday = week_day(jan1.year, jan1.month, jan1.day) % 7

        if local_time(tr1.local, 0, 0)[0] != last_year:
            # Add a single extra transition to align to a calendar year.
            if tt0.is_dst():
                pt1 = posix.dst_end
            else:
                pt1 = posix.dst_start

            tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
            tr = Transition(jan1_time + tr1_offset - tt0.offset, tr1.ttype, tr0)
            tr0 = tr
            tr1 = tr0
            tt0 = tr0.ttype
            tt1 = tr1.ttype

        if tt0.is_dst():
            pt1 = posix.dst_end
            pt0 = posix.dst_start
        else:
            pt1 = posix.dst_start
            pt0 = posix.dst_end

        tr = tr0
        for year in range(last_year + 1, last_year + 401):
            jan1_time += SECS_PER_YEAR[leap_year]
            jan1_weekday = (jan1_weekday + DAYS_PER_YEAR[leap_year]) % 7
            leap_year = not leap_year and is_leap(year)

            tr1_offset = pt1.trans_offset(leap_year, jan1_weekday)
            tr = Transition(jan1_time + tr1_offset - tt0.offset, tt1, tr)
            self._transitions.append(tr)

            tr0_offset = pt0.trans_offset(leap_year, jan1_weekday)
            tr = Transition(jan1_time + tr0_offset - tt1.offset, tt0, tr)
            self._transitions.append(tr)

    def _check_ttype(
        self,
        ttype,  # type: TransitionType
        offset,  # type: int
        is_dst,  # type: bool
        abbr,  # type: str
    ):  # type: (...) -> bool
        return (
            ttype.offset == offset
            and ttype.is_dst() == is_dst
            and ttype.abbreviation == abbr
        )