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