• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.icu.util;
18 
19 import android.icu.impl.Grego;
20 import android.icu.util.AnnualTimeZoneRule;
21 import android.icu.util.BasicTimeZone;
22 import android.icu.util.DateTimeRule;
23 import android.icu.util.InitialTimeZoneRule;
24 import android.icu.util.TimeArrayTimeZoneRule;
25 import android.icu.util.TimeZone;
26 import android.icu.util.TimeZoneRule;
27 import android.icu.util.TimeZoneTransition;
28 
29 import libcore.api.IntraCoreApi;
30 
31 import java.time.DayOfWeek;
32 import java.time.Instant;
33 import java.time.LocalDateTime;
34 import java.time.LocalTime;
35 import java.time.Month;
36 import java.time.ZoneOffset;
37 import java.time.zone.ZoneOffsetTransition;
38 import java.time.zone.ZoneOffsetTransitionRule;
39 import java.time.zone.ZoneRules;
40 import java.time.zone.ZoneRulesException;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.NavigableMap;
46 import java.util.TreeMap;
47 
48 
49 /**
50  * Provide extra functionalities on top of {@link TimeZone} public APIs.
51  *
52  * @hide
53  */
54 @IntraCoreApi
55 public class ExtendedTimeZone {
56 
57     private final TimeZone timezone;
58 
ExtendedTimeZone(String id)59     private ExtendedTimeZone(String id) {
60         timezone = TimeZone.getTimeZone(id);
61     }
62 
63     // The API which calls an implementation in android.icu does not use nullability annotation
64     // because the upstream can't guarantee the stability. See http://b/140196694.
65     /**
66      * Returns an instance from the time zone ID. Note that the returned instance could be shared.
67      *
68      * @see TimeZone#getTimeZone(String) for the more information.
69      * @hide
70      */
71     @IntraCoreApi
getInstance(String id)72     public static ExtendedTimeZone getInstance(String id) {
73         return new ExtendedTimeZone(id);
74     }
75 
76     /**
77      * Clears the default time zone in ICU4J. When next {@link TimeZone#getDefault()} is called,
78      * ICU4J will re-initialize the default time zone from the value obtained from the libcore's
79      * {@link java.util.TimeZone#getDefault()}.
80      *
81      * This API is useful for libcore's {@link java.util.TimeZone#setDefault(java.util.TimeZone)} to
82      * break the cycle of synchronizing the default time zone between libcore and ICU4J.
83      *
84      * @hide
85      */
86     @IntraCoreApi
clearDefaultTimeZone()87     public static void clearDefaultTimeZone() {
88         TimeZone.setICUDefault(null);
89     }
90 
91     /**
92      * Returns the underlying {@link TimeZone} instance.
93      *
94      * @hide
95      */
96     @IntraCoreApi
getTimeZone()97     public TimeZone getTimeZone() {
98         return timezone;
99     }
100 
101     /**
102      * Returns a {@link ZoneRules} instance for this time zone.
103      *
104      * @throws ZoneRulesException if the internal rules can't be parsed correctly, or it's not
105      * implemented for the subtype of {@link TimeZone}.
106      *
107      * @implNote This implementations relies on {@link BasicTimeZone#getTimeZoneRules()} in the
108      * following way:
109      * Returned array starts with {@code InitialTimeZoneRule}, followed by {@code
110      * TimeArrayTimeZoneRule}, and, if available, ends with {@code AnnualTimeZoneRule}.
111      * @hide
112      */
113     @IntraCoreApi
createZoneRules()114     public ZoneRules createZoneRules() {
115         if (!(timezone instanceof BasicTimeZone)) {
116             throw zoneRulesException("timezone is "
117                     + timezone.getClass().getCanonicalName()
118                     + " which is not instance of BasicTimeZone");
119         }
120 
121         BasicTimeZone basicTimeZone = (BasicTimeZone) timezone;
122 
123         TimeZoneRule[] timeZoneRules = basicTimeZone.getTimeZoneRules();
124 
125         if (timeZoneRules.length == 0) {
126             throw zoneRulesException("Got 0 time zone rules");
127         }
128 
129         ZoneOffset baseStandardOffset = null;
130         ZoneOffset baseWallOffset = null;
131 
132         NavigableMap<Long, TimeArrayTimeZoneRule> rulesByStartTime = new TreeMap<>();
133         boolean hasRecurringRules = false;
134 
135         for (TimeZoneRule timeZoneRule : timeZoneRules) {
136             if (timeZoneRule instanceof InitialTimeZoneRule) {
137                 InitialTimeZoneRule initialTimeZoneRule = (InitialTimeZoneRule) timeZoneRule;
138                 baseStandardOffset = standardOffset(initialTimeZoneRule);
139                 baseWallOffset = fullOffset(initialTimeZoneRule);
140             } else if (timeZoneRule instanceof TimeArrayTimeZoneRule) {
141                 TimeArrayTimeZoneRule timeArrayTimeZoneRule = (TimeArrayTimeZoneRule) timeZoneRule;
142 
143                 for (long startTime : timeArrayTimeZoneRule.getStartTimes()) {
144                     rulesByStartTime.put(
145                             utcStartTime(startTime, timeArrayTimeZoneRule), timeArrayTimeZoneRule);
146                 }
147             } else if (timeZoneRule instanceof AnnualTimeZoneRule) {
148                 // Order of AnnualTimeZoneRule-s in BasicTimeZone#getTimeZoneRules is not
149                 // specified, they will be fetched using different API.
150                 hasRecurringRules = true;
151             } else {
152                 throw zoneRulesException(
153                         "Unrecognized time zone rule " + timeZoneRule.getClass() + ".");
154             }
155         }
156 
157         // Keep in mind that transitionList is not superset of standardOffsetTransitionList.
158         // transitionList keeps track of wall clock changes, but it might remain the same after
159         // standard offset change if DST was changed too.
160         List<ZoneOffsetTransition> standardOffsetTransitionList = new ArrayList<>();
161         List<ZoneOffsetTransition> transitionList = new ArrayList<>();
162 
163         ZoneOffset lastStandardOffset = baseStandardOffset;
164         ZoneOffset lastWallOffset = baseWallOffset;
165 
166         for (Map.Entry<Long, TimeArrayTimeZoneRule> entry : rulesByStartTime.entrySet()) {
167             long startTime = entry.getKey();
168             TimeArrayTimeZoneRule timeZoneRule = entry.getValue();
169 
170             ZoneOffset ruleStandardOffset = standardOffset(timeZoneRule);
171 
172             if (!ruleStandardOffset.equals(lastStandardOffset)) {
173                 // ZoneRules needs changes in standard offsets only as an argument.
174                 // ZoneOffsetTransition requires before and after offsets to be different, so wall
175                 // clock offset can't be used as beforeOffset(it can be equal to afterOffset). Using
176                 // previous standard offset seems to be the only reasonable choice left.
177                 // As of 2021 transition and beforeOffset arguments are used to calculate UTC offset
178                 // of the switch date and previous standard offset will do the trick.
179                 ZoneOffsetTransition zoneOffsetTransition =
180                         ZoneOffsetTransition.of(
181                                 localDateTime(startTime, lastStandardOffset),
182                                 lastStandardOffset,
183                                 ruleStandardOffset);
184 
185                 standardOffsetTransitionList.add(zoneOffsetTransition);
186                 lastStandardOffset = ruleStandardOffset;
187             }
188 
189             ZoneOffset ruleWallOffset = fullOffset(timeZoneRule);
190 
191             // ZoneOffsetTransition tracks only changes in full offset - if raw and DST offsets
192             // sum is not changed after a transition, such transition is not tracked by ZoneRules.
193             // ICU does not squash such transitions.
194             if (!lastWallOffset.equals(ruleWallOffset)) {
195                 ZoneOffsetTransition zoneOffsetTransition =
196                         ZoneOffsetTransition.of(
197                                 localDateTime(startTime, lastWallOffset),
198                                 lastWallOffset,
199                                 ruleWallOffset);
200 
201                 transitionList.add(zoneOffsetTransition);
202                 lastWallOffset = ruleWallOffset;
203             }
204         }
205 
206         List<ZoneOffsetTransitionRule> lastRules = new ArrayList<>();
207 
208         if (hasRecurringRules) {
209             List<AnnualTimeZoneRule> annualTimeZoneRules = new ArrayList<>();
210 
211             // ZoneOffsetTransitionRule requires beforeOffset. As total offset in
212             // TimeArrayTimeZoneRule may differ from offset of the last recurring rule
213             // we apply all available recurring rule once. It is possible to build lastRules
214             // in loop below, but doing it in separate loop simplifies code significantly.
215             TimeZoneTransition firstTransitionToAnnualRule = basicTimeZone
216                     .getNextTransition(rulesByStartTime.lastKey(), false /* inclusive */);
217             AnnualTimeZoneRule firstAnnualRule =
218                     (AnnualTimeZoneRule) firstTransitionToAnnualRule.getTo();
219             AnnualTimeZoneRule currentTimeZoneRule = firstAnnualRule;
220             long currentUnixEpochTime = firstTransitionToAnnualRule.getTime();
221 
222             do {
223                 annualTimeZoneRules.add(currentTimeZoneRule);
224 
225                 if (annualTimeZoneRules.size() > 16) {
226                     throw zoneRulesException("More than 16 annual transitions found.");
227                 }
228 
229                 ZoneOffset ruleStandardOffset = standardOffset(currentTimeZoneRule);
230 
231                 if (!lastStandardOffset.equals(ruleStandardOffset)) {
232                     standardOffsetTransitionList.add(
233                             ZoneOffsetTransition.of(
234                                     localDateTime(currentUnixEpochTime, lastStandardOffset),
235                                     lastStandardOffset,
236                                     ruleStandardOffset
237                             ));
238                     lastStandardOffset = ruleStandardOffset;
239                 }
240 
241                 int currentYear =
242                         Instant.ofEpochMilli(currentUnixEpochTime).atOffset(lastWallOffset).getYear();
243                 ZoneOffsetTransition recurringRuleTransition =
244                         createZoneOffsetTransitionRule(
245                                 currentTimeZoneRule,
246                                 lastStandardOffset,
247                                 lastWallOffset)
248                                 .createTransition(currentYear);
249 
250                 // After introduction of first annual rule wall offset may not change.
251                 if (!lastWallOffset.equals(recurringRuleTransition.getOffsetAfter())) {
252                     transitionList.add(ZoneOffsetTransition.of(
253                             localDateTime(currentUnixEpochTime, lastWallOffset),
254                             lastWallOffset,
255                             recurringRuleTransition.getOffsetAfter()));
256                     lastWallOffset = recurringRuleTransition.getOffsetAfter();
257                 }
258 
259                 TimeZoneTransition nextTransition =
260                         basicTimeZone.getNextTransition(currentUnixEpochTime, false /* inclusive */);
261                 currentUnixEpochTime = nextTransition.getTime();
262                 currentTimeZoneRule = (AnnualTimeZoneRule) nextTransition.getTo();
263 
264                 if (currentTimeZoneRule == null) {
265                     throw zoneRulesException("No transitions after "
266                             + currentUnixEpochTime + " for a timezone with recurring rules");
267                 }
268             } while (!currentTimeZoneRule.isEquivalentTo(firstAnnualRule));
269 
270             // All annual rules use the same standard offset and wall offset is always updated on
271             // transition.
272             // The initial value of lastWallOffset is the wall offset of the last recurring
273             // AnnualTimeZoneRule.
274             for (AnnualTimeZoneRule annualTimeZoneRule : annualTimeZoneRules) {
275                 ZoneOffsetTransitionRule zoneOffsetTransitionRule =
276                         createZoneOffsetTransitionRule(
277                                 annualTimeZoneRule, lastStandardOffset, lastWallOffset);
278 
279                 lastWallOffset = zoneOffsetTransitionRule.getOffsetAfter();
280 
281                 lastRules.add(zoneOffsetTransitionRule);
282             }
283 
284             // ZoneRules does not specify it, but internally it expects lastRules to be sorted
285             // (see ZoneRules#getOffset) in the order they will happen within a year. For example,
286             // if rule A starts in October 2021, and rule B starts in March 2022, expected order
287             // is [B, A].
288             // We assume that for any year that order is fixed, even though it is possible
289             // to build set of rules where order depends on a given year.
290             // annualTimeZoneRules stores rules in the order they happened, so we just need to find
291             // a break in startYear.
292             int firstRuleIndex = 0;
293             while (firstRuleIndex < annualTimeZoneRules.size()
294                     && firstAnnualRule.getStartYear()
295                             == annualTimeZoneRules.get(firstRuleIndex).getStartYear()) {
296                 ++firstRuleIndex;
297             }
298 
299             Collections.rotate(lastRules, -firstRuleIndex);
300         }
301 
302         return ZoneRules.of(
303                 baseStandardOffset,
304                 baseWallOffset,
305                 standardOffsetTransitionList,
306                 transitionList,
307                 lastRules);
308     }
309 
310 
311     /**
312      * Converts {@link AnnualTimeZoneRule} to {@link ZoneOffsetTransitionRule}. Switch date may be
313      * represented relative to UTC, wall clock, or standard offset. For the latter 2 cases
314      * {@code lastWallOffset} and {@code lastStandardOffset} are used.
315      *
316      * @param annualTimeZoneRule  rule to be converted
317      * @param lastStandardOffset  standard offset of a rule which preceded {@code
318      *     annualTimeZoneRule}
319      * @param lastWallOffset  wall offset of a rule which preceded {@code annualTimeZoneRule}
320      */
createZoneOffsetTransitionRule( AnnualTimeZoneRule annualTimeZoneRule, ZoneOffset lastStandardOffset, ZoneOffset lastWallOffset)321     private ZoneOffsetTransitionRule createZoneOffsetTransitionRule(
322             AnnualTimeZoneRule annualTimeZoneRule,
323             ZoneOffset lastStandardOffset,
324             ZoneOffset lastWallOffset) {
325         DateTimeRule dateTimeRule = annualTimeZoneRule.getRule();
326         Month month = Month.of(dateTimeRule.getRuleMonth() + 1);
327         final DayOfWeek dayOfWeek;
328         final int dayOfMonthIndicator;
329         switch (dateTimeRule.getDateRuleType()) {
330             case DateTimeRule.DOM:
331                 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth();
332                 dayOfWeek = null;
333                 break;
334             case DateTimeRule.DOW:
335                 int weekInMonth = dateTimeRule.getRuleWeekInMonth();
336                 if (weekInMonth > 0) {
337                     dayOfMonthIndicator = (weekInMonth - 1) * 7  + 1;
338                 } else if (weekInMonth < 0) {
339                     dayOfMonthIndicator = (weekInMonth + 1) * 7 - 1;
340                 } else {
341                     throw zoneRulesException("Invalid DateTimeRule in "
342                             + annualTimeZoneRule +
343                             ". Non-zero weekInMonth expected in " + dateTimeRule);
344                 }
345                 dayOfWeek = dayOfWeek(dateTimeRule);
346                 break;
347             case DateTimeRule.DOW_GEQ_DOM:
348                 dayOfMonthIndicator = dateTimeRule.getRuleDayOfMonth();
349                 dayOfWeek = dayOfWeek(dateTimeRule);
350                 break;
351             case DateTimeRule.DOW_LEQ_DOM:
352                 // java.time.ZoneRules uses negative numbers to indicate that switch date should
353                 // come before certain date. Using leap year so that lastSun like rule will
354                 // always work correctly.
355                 dayOfMonthIndicator =
356                         dateTimeRule.getRuleDayOfMonth() - month.maxLength() - 1;
357                 dayOfWeek = dayOfWeek(dateTimeRule);
358                 break;
359             default:
360                 throw zoneRulesException("Unexpected dateTimeRule.dateRuleType="
361                         + dateTimeRule.getTimeRuleType() + " in " + annualTimeZoneRule);
362         }
363 
364         final boolean timeEndOfDay;
365         final LocalTime switchDateTime;
366         if (dateTimeRule.getRuleMillisInDay() == Grego.MILLIS_PER_DAY) {
367             timeEndOfDay = true;
368             switchDateTime = LocalTime.MIDNIGHT;
369         } else {
370             timeEndOfDay = false;
371             switchDateTime = LocalTime.ofNanoOfDay(dateTimeRule.getRuleMillisInDay() * 1_000_000L);
372         }
373 
374         ZoneOffsetTransitionRule.TimeDefinition timeDefinition = timeDefinition(annualTimeZoneRule);
375 
376         // JavaDoc for standardOffset tells that it should be "offset in force at the cutover".
377         // It's not clear what offset is in effect at the cutover moment, but zic format assumes
378         // that only DST is changed in annual rules and standard offset is handled differently.
379         return ZoneOffsetTransitionRule.of(month,
380                 dayOfMonthIndicator,
381                 dayOfWeek,
382                 switchDateTime,
383                 timeEndOfDay,
384                 timeDefinition,
385                 lastStandardOffset,
386                 lastWallOffset,
387                 fullOffset(annualTimeZoneRule));
388     }
389 
timeDefinition( AnnualTimeZoneRule annualTimeZoneRule)390     private ZoneOffsetTransitionRule.TimeDefinition timeDefinition(
391             AnnualTimeZoneRule annualTimeZoneRule) {
392         DateTimeRule dateTimeRule = annualTimeZoneRule.getRule();
393         switch (dateTimeRule.getTimeRuleType()) {
394             case DateTimeRule.STANDARD_TIME:
395                 return ZoneOffsetTransitionRule.TimeDefinition.STANDARD;
396             case DateTimeRule.UTC_TIME:
397                 return ZoneOffsetTransitionRule.TimeDefinition.UTC;
398             case DateTimeRule.WALL_TIME:
399                 return ZoneOffsetTransitionRule.TimeDefinition.WALL;
400             default:
401                 throw zoneRulesException(
402                         "Unexpected dateTimeRule.timeRuleType=" + dateTimeRule.getTimeRuleType()
403                                 + " in AnnualTimeZoneRule: " + annualTimeZoneRule);
404         }
405     }
406 
utcStartTime(long startTime, TimeArrayTimeZoneRule timeZoneRule)407     private long utcStartTime(long startTime, TimeArrayTimeZoneRule timeZoneRule) {
408         switch (timeZoneRule.getTimeType()) {
409             case DateTimeRule.UTC_TIME:
410                 return startTime;
411             case DateTimeRule.STANDARD_TIME:
412                 return startTime - timeZoneRule.getRawOffset();
413             case DateTimeRule.WALL_TIME:
414                 return startTime - timeZoneRule.getRawOffset() - timeZoneRule.getDSTSavings();
415             default:
416                 throw zoneRulesException("Unexpected timeType in " + timeZoneRule);
417         }
418     }
419 
zoneRulesException(String message)420     private ZoneRulesException zoneRulesException(String message) {
421         return new ZoneRulesException("Failed to build ZoneRules for " + timezone.getID() +
422                 ". " + message);
423     }
424 
localDateTime(long epochMillis, ZoneOffset zoneOffset)425     private static LocalDateTime localDateTime(long epochMillis, ZoneOffset zoneOffset) {
426         return Instant.ofEpochMilli(epochMillis)
427                 .atOffset(zoneOffset)
428                 .toLocalDateTime();
429     }
430 
dayOfWeek(DateTimeRule dateTimeRule)431     private static DayOfWeek dayOfWeek(DateTimeRule dateTimeRule) {
432         return DayOfWeek.SUNDAY.plus(dateTimeRule.getRuleDayOfWeek() - 1);
433     }
434 
standardOffset(TimeZoneRule timeZoneRule)435     private static ZoneOffset standardOffset(TimeZoneRule timeZoneRule) {
436         return toOffset(timeZoneRule.getRawOffset());
437     }
438 
fullOffset(TimeZoneRule timeZoneRule)439     private static ZoneOffset fullOffset(TimeZoneRule timeZoneRule) {
440         return toOffset(timeZoneRule.getRawOffset() + timeZoneRule.getDSTSavings());
441     }
442 
toOffset(int rawOffset)443     private static ZoneOffset toOffset(int rawOffset) {
444         return ZoneOffset.ofTotalSeconds(rawOffset / 1_000);
445     }
446 }
447