1 /* 2 * Copyright (C) 2018 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 package com.android.libcore.timezone.tzlookup.zonetree; 17 18 import com.ibm.icu.text.TimeZoneNames; 19 import com.ibm.icu.util.BasicTimeZone; 20 import com.ibm.icu.util.TimeZone; 21 import com.ibm.icu.util.TimeZoneTransition; 22 23 import java.time.Instant; 24 import java.util.List; 25 import java.util.Objects; 26 27 /** 28 * A period of time when all time-zone related properties are expected to remain the same. 29 */ 30 final class ZoneOffsetPeriod { 31 /** The start of the period (inclusive) */ 32 private final Instant start; 33 /** The end of the period (exclusive) */ 34 private final Instant end; 35 /** The offset from UTC in milliseconds. */ 36 private final int rawOffsetMillis; 37 /** The additional offset to apply due to DST */ 38 private final int dstOffsetMillis; 39 /** A name for the time. */ 40 private final String name; 41 ZoneOffsetPeriod(Instant start, Instant end, int rawOffsetMillis, int dstOffsetMillis, String name)42 private ZoneOffsetPeriod(Instant start, Instant end, int rawOffsetMillis, int dstOffsetMillis, 43 String name) { 44 this.start = start; 45 this.end = end; 46 this.rawOffsetMillis = rawOffsetMillis; 47 this.dstOffsetMillis = dstOffsetMillis; 48 this.name = name; 49 } 50 51 /** 52 * Constructs an instance using ICU data. 53 */ create(TimeZoneNames timeZoneNames, BasicTimeZone timeZone, Instant minTime, Instant maxTime)54 public static ZoneOffsetPeriod create(TimeZoneNames timeZoneNames, BasicTimeZone timeZone, 55 Instant minTime, Instant maxTime) { 56 57 long startMillis = minTime.toEpochMilli(); 58 TimeZoneTransition transition = 59 timeZone.getNextTransition(startMillis, true /* inclusive */); 60 Instant end; 61 if (transition == null) { 62 // The zone has no transitions from start, so we create a ZoneOffsetPeriod 63 // from minTime to maxTime. 64 end = maxTime; 65 } else { 66 TimeZoneTransition nextTransition = 67 timeZone.getNextTransition(startMillis, false /* inclusive */); 68 if (nextTransition != null) { 69 long endTimeMillis = Math.min(nextTransition.getTime(), maxTime.toEpochMilli()); 70 end = Instant.ofEpochMilli(endTimeMillis); 71 } else { 72 // The zone has no next transition after minTime, so we create a ZoneOffsetPeriod 73 // from minTime to maxTime. 74 end = maxTime; 75 } 76 } 77 78 String longName = getNameAtTime(timeZoneNames, timeZone, startMillis); 79 int[] offsets = new int[2]; 80 timeZone.getOffset(startMillis, false /* local */, offsets); 81 return new ZoneOffsetPeriod(minTime, end, offsets[0], offsets[1], longName); 82 } 83 84 85 /** Splits a period in two at the specified instant, returning the generated periods. */ splitAtTime( ZoneOffsetPeriod toSplit, TimeZoneNames timeZoneNames, BasicTimeZone timeZone, Instant partitionInstant)86 public static ZoneOffsetPeriod[] splitAtTime( 87 ZoneOffsetPeriod toSplit, TimeZoneNames timeZoneNames, BasicTimeZone timeZone, 88 Instant partitionInstant) { 89 if (!partitionInstant.isAfter(toSplit.start) 90 || !partitionInstant.isBefore(toSplit.end)) { 91 throw new IllegalArgumentException(partitionInstant + " is not between " 92 + toSplit.start + " and " + toSplit.end); 93 } 94 // Work out the name at the split so the name is always the name at the beginning of the 95 // zone offset period. 96 String nameAtSplit = 97 getNameAtTime(timeZoneNames, timeZone, partitionInstant.toEpochMilli()); 98 int rawOffsetMillis = toSplit.rawOffsetMillis; 99 int dstOffsetMillis = toSplit.dstOffsetMillis; 100 return new ZoneOffsetPeriod[] { 101 new ZoneOffsetPeriod(toSplit.start, partitionInstant, rawOffsetMillis, 102 dstOffsetMillis, toSplit.name), 103 new ZoneOffsetPeriod(partitionInstant, toSplit.end, rawOffsetMillis, 104 dstOffsetMillis, nameAtSplit) 105 }; 106 } 107 getStartInstant()108 public Instant getStartInstant() { 109 return start; 110 } 111 getEndInstant()112 public Instant getEndInstant() { 113 return end; 114 } 115 getStartMillis()116 public long getStartMillis() { 117 return start.toEpochMilli(); 118 } 119 getEndMillis()120 public long getEndMillis() { 121 return end.toEpochMilli(); 122 } 123 getName()124 public String getName() { 125 return name; 126 } 127 getRawOffsetMillis()128 public int getRawOffsetMillis() { 129 return rawOffsetMillis; 130 } 131 getDstOffsetMillis()132 public int getDstOffsetMillis() { 133 return dstOffsetMillis; 134 } 135 136 @Override equals(Object o)137 public boolean equals(Object o) { 138 if (this == o) { 139 return true; 140 } 141 if (o == null || getClass() != o.getClass()) { 142 return false; 143 } 144 ZoneOffsetPeriod that = (ZoneOffsetPeriod) o; 145 return rawOffsetMillis == that.rawOffsetMillis && 146 dstOffsetMillis == that.dstOffsetMillis && 147 Objects.equals(start, that.start) && 148 Objects.equals(end, that.end) && 149 Objects.equals(name, that.name); 150 } 151 152 @Override hashCode()153 public int hashCode() { 154 return Objects.hash(start, end, rawOffsetMillis, dstOffsetMillis, name); 155 } 156 157 @Override toString()158 public String toString() { 159 return "ZoneOffsetPeriod{" + 160 "start=" + start + 161 ", end=" + end + 162 ", rawOffsetMillis=" + rawOffsetMillis + 163 ", dstOffsetMillis=" + dstOffsetMillis + 164 ", name='" + name + '\'' + 165 '}'; 166 } 167 168 /** 169 * A class for establishing when multiple periods are identical. 170 */ 171 static final class ZonePeriodsKey { 172 173 private final List<ZoneOffsetPeriod> periods; 174 ZonePeriodsKey(List<ZoneOffsetPeriod> periods)175 public ZonePeriodsKey(List<ZoneOffsetPeriod> periods) { 176 this.periods = periods; 177 } 178 179 @Override equals(Object o)180 public boolean equals(Object o) { 181 if (this == o) { 182 return true; 183 } 184 if (o == null || getClass() != o.getClass()) { 185 return false; 186 } 187 ZonePeriodsKey zoneKey = (ZonePeriodsKey) o; 188 return Objects.equals(periods, zoneKey.periods); 189 } 190 191 @Override hashCode()192 public int hashCode() { 193 return Objects.hash(periods); 194 } 195 196 @Override toString()197 public String toString() { 198 return "ZonePeriodsKey{" + 199 "periods=" + periods + 200 '}'; 201 } 202 } 203 getNameAtTime( TimeZoneNames timeZoneNames, BasicTimeZone timeZone, long startMillis)204 private static String getNameAtTime( 205 TimeZoneNames timeZoneNames, BasicTimeZone timeZone, long startMillis) { 206 int[] offsets = new int[2]; 207 timeZone.getOffset(startMillis, false /* local */, offsets); 208 String canonicalID = TimeZone.getCanonicalID(timeZone.getID()); 209 TimeZoneNames.NameType longNameType = offsets[1] == 0 210 ? TimeZoneNames.NameType.LONG_STANDARD : TimeZoneNames.NameType.LONG_DAYLIGHT; 211 return timeZoneNames.getDisplayName(canonicalID, longNameType, startMillis); 212 } 213 } 214