Home | History | Annotate | Download | only in zonetree
      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 
     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      */
     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. */
     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 
    108     public Instant getStartInstant() {
    109         return start;
    110     }
    111 
    112     public Instant getEndInstant() {
    113         return end;
    114     }
    115 
    116     public long getStartMillis() {
    117         return start.toEpochMilli();
    118     }
    119 
    120     public long getEndMillis() {
    121         return end.toEpochMilli();
    122     }
    123 
    124     public String getName() {
    125         return name;
    126     }
    127 
    128     public int getRawOffsetMillis() {
    129         return rawOffsetMillis;
    130     }
    131 
    132     public int getDstOffsetMillis() {
    133         return dstOffsetMillis;
    134     }
    135 
    136     @Override
    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
    153     public int hashCode() {
    154         return Objects.hash(start, end, rawOffsetMillis, dstOffsetMillis, name);
    155     }
    156 
    157     @Override
    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 
    175         public ZonePeriodsKey(List<ZoneOffsetPeriod> periods) {
    176             this.periods = periods;
    177         }
    178 
    179         @Override
    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
    192         public int hashCode() {
    193             return Objects.hash(periods);
    194         }
    195 
    196         @Override
    197         public String toString() {
    198             return "ZonePeriodsKey{" +
    199                     "periods=" + periods +
    200                     '}';
    201         }
    202     }
    203 
    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