1f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer/*
2f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer * Copyright (C) 2016 The Android Open Source Project
3f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer *
4012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * This code is free software; you can redistribute it and/or modify it
5012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * under the terms of the GNU General Public License version 2 only, as
6012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * published by the Free Software Foundation.  The Android Open Source
7012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * Project designates this particular file as subject to the "Classpath"
8012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * exception as provided by The Android Open Source Project in the LICENSE
9012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * file that accompanied this code.
10f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer *
11012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * This code is distributed in the hope that it will be useful, but WITHOUT
12012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * version 2 for more details (a copy is included in the LICENSE file that
15012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * accompanied this code).
16f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer *
17012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * You should have received a copy of the GNU General Public License version
18012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * 2 along with this work; if not, write to the Free Software Foundation,
19012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer */
21012dec09a4b15456c9979eda4990913d710172c3Tobias Thierer
22f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerpackage java.time.zone;
23f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
24f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.impl.OlsonTimeZone;
256cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauerimport android.icu.impl.ZoneMeta;
26f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.AnnualTimeZoneRule;
27f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.DateTimeRule;
28f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.InitialTimeZoneRule;
29f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.TimeZone;
30f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.TimeZoneRule;
31f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport android.icu.util.TimeZoneTransition;
32f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.time.DayOfWeek;
33f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.time.LocalTime;
34f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.time.Month;
35f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.time.ZoneOffset;
36f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.ArrayList;
37f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.Collections;
38f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.HashSet;
39f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.List;
40f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.NavigableMap;
41f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.Set;
42f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.TreeMap;
43f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport java.util.concurrent.TimeUnit;
44f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerimport libcore.util.BasicLruCache;
45f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
46f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer/**
47f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer * A ZoneRulesProvider that generates rules from ICU4J TimeZones.
48f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer * This provider ensures that classes in {@link java.time} use the same time zone information
49f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer * as ICU4J.
50f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer */
51f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauerpublic class IcuZoneRulesProvider extends ZoneRulesProvider {
52f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
53f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    // Arbitrary upper limit to number of transitions including the final rules.
54f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static final int MAX_TRANSITIONS = 10000;
55f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
56f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static final int SECONDS_IN_DAY = 24 * 60 * 60;
57f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
58f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private final BasicLruCache<String, ZoneRules> cache = new ZoneRulesCache(8);
59f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
60f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    @Override
61f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    protected Set<String> provideZoneIds() {
626cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        Set<String> zoneIds = ZoneMeta.getAvailableIDs(TimeZone.SystemTimeZoneType.ANY, null, null);
636cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        zoneIds = new HashSet<>(zoneIds);
646cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        // java.time assumes ZoneId that start with "GMT" fit the pattern "GMT+HH:mm:ss" which these
656cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        // do not. Since they are equivalent to GMT, just remove these aliases.
666cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        zoneIds.remove("GMT+0");
676cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        zoneIds.remove("GMT-0");
686cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer        return zoneIds;
69f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
70f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
71f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    @Override
72f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    protected ZoneRules provideRules(String zoneId, boolean forCaching) {
73f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Ignore forCaching, as this is a static provider.
74f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        return cache.get(zoneId);
75f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
76f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
77f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    @Override
78f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
79f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        return new TreeMap<>(
80f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                Collections.singletonMap(TimeZone.getTZDataVersion(),
81f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        provideRules(zoneId, /* forCaching */ false)));
82f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
83f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
84f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    /*
85f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * This implementation is only tested with OlsonTimeZone objects and depends on
86f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * implementation details of that class:
87f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *
88f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 0. TimeZone.getFrozenTimeZone() always returns an OlsonTimeZone object.
89f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 1. The first rule is always an InitialTimeZoneRule (guaranteed by spec).
90f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 2. AnnualTimeZoneRules are only used as "final rules".
91f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 3. The final rules are either 0 or 2 AnnualTimeZoneRules
92f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 4. The final rules have endYear set to MAX_YEAR.
93f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 5. Each transition generated by the rules changes either the raw offset, the total offset
94f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *    or both.
95f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * 6. There is a non-immense number of transitions for any rule before the final rules apply
96f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *    (enforced via the arbitrary limit defined in MAX_TRANSITIONS).
97f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *
98f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * Assumptions #5 and #6 are not strictly required for this code to work, but hold for the
99f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * the data and code at the time of implementation. If they were broken they would indicate
100f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * an incomplete understanding of how ICU TimeZoneRules are used which would probably mean that
101f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * this code needs to be updated.
102f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *
103f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * These assumptions are verified using the verify() method where appropriate.
104f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     */
1056cfa38d67bfe090c8aeb809cc01f772b257e0a7aJoachim Sauer    static ZoneRules generateZoneRules(String zoneId) {
106f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        TimeZone timeZone = TimeZone.getFrozenTimeZone(zoneId);
107f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Assumption #0
108f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        verify(timeZone instanceof OlsonTimeZone, zoneId,
109f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                "Unexpected time zone class " + timeZone.getClass());
110f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        OlsonTimeZone tz = (OlsonTimeZone) timeZone;
111f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        TimeZoneRule[] rules = tz.getTimeZoneRules();
112f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Assumption #1
113f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        InitialTimeZoneRule initial = (InitialTimeZoneRule) rules[0];
114f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
115f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffset baseStandardOffset = millisToOffset(initial.getRawOffset());
116f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffset baseWallOffset =
117f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                millisToOffset((initial.getRawOffset() + initial.getDSTSavings()));
118f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
119f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>();
120f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        List<ZoneOffsetTransition> transitionList = new ArrayList<>();
121f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>();
122f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
123f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        int preLastDstSavings = 0;
124f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        AnnualTimeZoneRule last1 = null;
125f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        AnnualTimeZoneRule last2 = null;
126f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
127f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        TimeZoneTransition transition = tz.getNextTransition(Long.MIN_VALUE, false);
128f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        int transitionCount = 1;
129f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // This loop has two possible exit conditions (in normal operation):
130f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // 1. for zones that end with a static value and have no ongoing DST changes, it will exit
131f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        //    via the normal condition (transition != null)
132f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // 2. for zones with ongoing DST changes (represented by a "final zone" in ICU4J, and by
133f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        //    "last rules" in java.time) the "break transitionLoop" will be used to exit the loop.
134f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        transitionLoop:
135f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        while (transition != null) {
136f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            TimeZoneRule from = transition.getFrom();
137f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            TimeZoneRule to = transition.getTo();
138f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            boolean hadEffect = false;
139f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            if (from.getRawOffset() != to.getRawOffset()) {
140f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                standardOffsetTransitionList.add(new ZoneOffsetTransition(
141f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        TimeUnit.MILLISECONDS.toSeconds(transition.getTime()),
142f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        millisToOffset(from.getRawOffset()),
143f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        millisToOffset(to.getRawOffset())));
144f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                hadEffect = true;
145f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            }
146f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            int fromTotalOffset = from.getRawOffset() + from.getDSTSavings();
147f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            int toTotalOffset = to.getRawOffset() + to.getDSTSavings();
148f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            if (fromTotalOffset != toTotalOffset) {
149f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                transitionList.add(new ZoneOffsetTransition(
150f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        TimeUnit.MILLISECONDS.toSeconds(transition.getTime()),
151f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        millisToOffset(fromTotalOffset),
152f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        millisToOffset(toTotalOffset)));
153f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                hadEffect = true;
154f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            }
155f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            // Assumption #5
156f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            verify(hadEffect, zoneId, "Transition changed neither total nor raw offset.");
157f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            if (to instanceof AnnualTimeZoneRule) {
158f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // The presence of an AnnualTimeZoneRule is taken as an indication of a final rule.
159f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                if (last1 == null) {
160f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    preLastDstSavings = from.getDSTSavings();
161f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    last1 = (AnnualTimeZoneRule) to;
162f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    // Assumption #4
163f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    verify(last1.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId,
164f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                            "AnnualTimeZoneRule is not permanent.");
165f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                } else {
166f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    last2 = (AnnualTimeZoneRule) to;
167f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    // Assumption #4
168f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    verify(last2.getEndYear() == AnnualTimeZoneRule.MAX_YEAR, zoneId,
169f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                            "AnnualTimeZoneRule is not permanent.");
170f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
171f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    // Assumption #3
172f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    transition = tz.getNextTransition(transition.getTime(), false);
173f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    verify(transition.getTo() == last1, zoneId,
174f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                            "Unexpected rule after 2 AnnualTimeZoneRules.");
175f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    break transitionLoop;
176f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                }
177f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            } else {
178f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // Assumption #2
179f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                verify(last1 == null, zoneId, "Unexpected rule after AnnualTimeZoneRule.");
180f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            }
181f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            verify(transitionCount <= MAX_TRANSITIONS, zoneId,
182f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    "More than " + MAX_TRANSITIONS + " transitions.");
183f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            transition = tz.getNextTransition(transition.getTime(), false);
184f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            transitionCount++;
185f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
186f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        if (last1 != null) {
187f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            // Assumption #3
188f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            verify(last2 != null, zoneId, "Only one AnnualTimeZoneRule.");
189f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            lastRules.add(toZoneOffsetTransitionRule(last1, preLastDstSavings));
190f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            lastRules.add(toZoneOffsetTransitionRule(last2, last1.getDSTSavings()));
191f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
192f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
193f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        return ZoneRules.of(baseStandardOffset, baseWallOffset, standardOffsetTransitionList,
194f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                transitionList, lastRules);
195f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
196f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
197f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    /**
198f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * Verify an assumption about the zone rules.
199f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *
200f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @param check
201f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         {@code true} if the assumption holds, {@code false} otherwise.
202f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @param zoneId
203f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         Zone ID for which to check.
204f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @param message
205f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         Error description of a failed check.
206f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @throws ZoneRulesException
207f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         If and only if {@code check} is {@code false}.
208f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     */
209f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static void verify(boolean check, String zoneId, String message) {
210f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        if (!check) {
211f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            throw new ZoneRulesException(
212f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                    String.format("Failed verification of zone %s: %s", zoneId, message));
213f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
214f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
215f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
216f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    /**
217f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * Transform an {@link AnnualTimeZoneRule} into an equivalent {@link ZoneOffsetTransitionRule}.
218f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * This is only used for the "final rules".
219f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *
220f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @param rule
221f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         The rule to transform.
222f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     * @param dstSavingMillisBefore
223f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     *         The DST offset before the first transition in milliseconds.
224f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer     */
225f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static ZoneOffsetTransitionRule toZoneOffsetTransitionRule(
226f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            AnnualTimeZoneRule rule, int dstSavingMillisBefore) {
227f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        DateTimeRule dateTimeRule = rule.getRule();
228f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Calendar.JANUARY is 0, transform it into a proper Month.
229f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        Month month = Month.JANUARY.plus(dateTimeRule.getRuleMonth());
230f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        int dayOfMonthIndicator;
231f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Calendar.SUNDAY is 1, transform it into a proper DayOfWeek.
232f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        DayOfWeek dayOfWeek = DayOfWeek.SATURDAY.plus(dateTimeRule.getRuleDayOfWeek());
233f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        switch (dateTimeRule.getDateRuleType()) {
234f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.DOM:
235f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // Transition always on a specific day of the month.
236f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth();
237f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                dayOfWeek = null;
238f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
239f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.DOW_GEQ_DOM:
240f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // ICU representation matches java.time representation.
241f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth();
242f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
243f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.DOW_LEQ_DOM:
244f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // java.time uses a negative dayOfMonthIndicator to represent "Sun<=X" or "lastSun"
245f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // rules. ICU uses this constant and the normal day. So "lastSun" in January would
246f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // ruleDayOfMonth = 31 in ICU and dayOfMonthIndicator = -1 in java.time.
247f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                dayOfMonthIndicator = -month.maxLength() + dateTimeRule.getRuleDayOfMonth() - 1;
248f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
249f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.DOW:
250f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // DOW is unspecified in the documentation and seems to never be used.
251f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                throw new ZoneRulesException("Date rule type DOW is unsupported");
252f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            default:
253f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                throw new ZoneRulesException(
254f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        "Unexpected date rule type: " + dateTimeRule.getDateRuleType());
255f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
256f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Cast to int is save, as input is int.
257f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        int secondOfDay = (int) TimeUnit.MILLISECONDS.toSeconds(dateTimeRule.getRuleMillisInDay());
258f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        LocalTime time;
259f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        boolean timeEndOfDay;
260f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        if (secondOfDay == SECONDS_IN_DAY) {
261f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            time = LocalTime.MIDNIGHT;
262f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            timeEndOfDay = true;
263f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        } else {
264f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            time = LocalTime.ofSecondOfDay(secondOfDay);
265f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            timeEndOfDay = false;
266f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
267f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffsetTransitionRule.TimeDefinition timeDefinition;
268f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        switch (dateTimeRule.getTimeRuleType()) {
269f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.WALL_TIME:
270f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.WALL;
271f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
272f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.STANDARD_TIME:
273f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.STANDARD;
274f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
275f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            case DateTimeRule.UTC_TIME:
276f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                timeDefinition = ZoneOffsetTransitionRule.TimeDefinition.UTC;
277f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                break;
278f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            default:
279f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                throw new ZoneRulesException(
280f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                        "Unexpected time rule type " + dateTimeRule.getTimeRuleType());
281f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
282f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffset standardOffset = millisToOffset(rule.getRawOffset());
283f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffset offsetBefore = millisToOffset(rule.getRawOffset() + dstSavingMillisBefore);
284f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneOffset offsetAfter = millisToOffset(
285f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                rule.getRawOffset() + rule.getDSTSavings());
286f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        return ZoneOffsetTransitionRule.of(
287f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition,
288f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                standardOffset, offsetBefore, offsetAfter);
289f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
290f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
291f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static ZoneOffset millisToOffset(int offset) {
292f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        // Cast to int is save, as input is int.
293f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        return ZoneOffset.ofTotalSeconds((int) TimeUnit.MILLISECONDS.toSeconds(offset));
294f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
295f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
296f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    private static class ZoneRulesCache extends BasicLruCache<String, ZoneRules> {
297f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
298f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        ZoneRulesCache(int maxSize) {
299f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            super(maxSize);
300f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
301f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer
302f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        @Override
303f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        protected ZoneRules create(String zoneId) {
304f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            String canonicalId = TimeZone.getCanonicalID(zoneId);
305f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            if (!canonicalId.equals(zoneId)) {
306f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // Return the same object as the canonical one, to avoid wasting space, but cache
307f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                // it under the non-cannonical name as well, to avoid future getCanonicalID calls.
308f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer                return get(canonicalId);
309f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            }
310f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer            return generateZoneRules(zoneId);
311f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer        }
312f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer    }
313f9b6ef9f20cb3b0410c0efbacc77533f33687e5fJoachim Sauer}
314