ZoneOffsetPeriod.java revision 181282f9128922e72b0b7091a1ffe54d23e0a896
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 */
16package com.android.libcore.timezone.tzlookup.zonetree;
17
18import com.ibm.icu.text.TimeZoneNames;
19import com.ibm.icu.util.BasicTimeZone;
20import com.ibm.icu.util.TimeZone;
21import com.ibm.icu.util.TimeZoneTransition;
22
23import java.time.Instant;
24import java.util.List;
25import java.util.Objects;
26
27/**
28 * A period of time when all time-zone related properties are expected to remain the same.
29 */
30final 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