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