Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/jdk17u
Path: blob/master/src/java.base/share/classes/java/time/zone/ZoneRules.java
67794 views
1
/*
2
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
3
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4
*
5
* This code is free software; you can redistribute it and/or modify it
6
* under the terms of the GNU General Public License version 2 only, as
7
* published by the Free Software Foundation. Oracle designates this
8
* particular file as subject to the "Classpath" exception as provided
9
* by Oracle in the LICENSE file that accompanied this code.
10
*
11
* This code is distributed in the hope that it will be useful, but WITHOUT
12
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14
* version 2 for more details (a copy is included in the LICENSE file that
15
* accompanied this code).
16
*
17
* You should have received a copy of the GNU General Public License version
18
* 2 along with this work; if not, write to the Free Software Foundation,
19
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20
*
21
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22
* or visit www.oracle.com if you need additional information or have any
23
* questions.
24
*/
25
26
/*
27
* This file is available under and governed by the GNU General Public
28
* License version 2 only, as published by the Free Software Foundation.
29
* However, the following notice accompanied the original version of this
30
* file:
31
*
32
* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
33
*
34
* All rights reserved.
35
*
36
* Redistribution and use in source and binary forms, with or without
37
* modification, are permitted provided that the following conditions are met:
38
*
39
* * Redistributions of source code must retain the above copyright notice,
40
* this list of conditions and the following disclaimer.
41
*
42
* * Redistributions in binary form must reproduce the above copyright notice,
43
* this list of conditions and the following disclaimer in the documentation
44
* and/or other materials provided with the distribution.
45
*
46
* * Neither the name of JSR-310 nor the names of its contributors
47
* may be used to endorse or promote products derived from this software
48
* without specific prior written permission.
49
*
50
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
51
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
52
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
53
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
54
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
55
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
56
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
57
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
58
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
59
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61
*/
62
package java.time.zone;
63
64
import java.io.DataInput;
65
import java.io.DataOutput;
66
import java.io.IOException;
67
import java.io.InvalidObjectException;
68
import java.io.ObjectInputStream;
69
import java.io.Serializable;
70
import java.time.Duration;
71
import java.time.Instant;
72
import java.time.LocalDateTime;
73
import java.time.ZoneId;
74
import java.time.ZoneOffset;
75
import java.time.Year;
76
import java.util.ArrayList;
77
import java.util.Arrays;
78
import java.util.Collections;
79
import java.util.List;
80
import java.util.Objects;
81
import java.util.concurrent.ConcurrentHashMap;
82
import java.util.concurrent.ConcurrentMap;
83
84
/**
85
* The rules defining how the zone offset varies for a single time-zone.
86
* <p>
87
* The rules model all the historic and future transitions for a time-zone.
88
* {@link ZoneOffsetTransition} is used for known transitions, typically historic.
89
* {@link ZoneOffsetTransitionRule} is used for future transitions that are based
90
* on the result of an algorithm.
91
* <p>
92
* The rules are loaded via {@link ZoneRulesProvider} using a {@link ZoneId}.
93
* The same rules may be shared internally between multiple zone IDs.
94
* <p>
95
* Serializing an instance of {@code ZoneRules} will store the entire set of rules.
96
* It does not store the zone ID as it is not part of the state of this object.
97
* <p>
98
* A rule implementation may or may not store full information about historic
99
* and future transitions, and the information stored is only as accurate as
100
* that supplied to the implementation by the rules provider.
101
* Applications should treat the data provided as representing the best information
102
* available to the implementation of this rule.
103
*
104
* @implSpec
105
* This class is immutable and thread-safe.
106
*
107
* @since 1.8
108
*/
109
public final class ZoneRules implements Serializable {
110
111
/**
112
* Serialization version.
113
*/
114
private static final long serialVersionUID = 3044319355680032515L;
115
/**
116
* The last year to have its transitions cached.
117
*/
118
private static final int LAST_CACHED_YEAR = 2100;
119
120
/**
121
* The transitions between standard offsets (epoch seconds), sorted.
122
*/
123
private final long[] standardTransitions;
124
/**
125
* The standard offsets.
126
*/
127
private final ZoneOffset[] standardOffsets;
128
/**
129
* The transitions between instants (epoch seconds), sorted.
130
*/
131
private final long[] savingsInstantTransitions;
132
/**
133
* The transitions between local date-times, sorted.
134
* This is a paired array, where the first entry is the start of the transition
135
* and the second entry is the end of the transition.
136
*/
137
private final LocalDateTime[] savingsLocalTransitions;
138
/**
139
* The wall offsets.
140
*/
141
private final ZoneOffset[] wallOffsets;
142
/**
143
* The last rule.
144
*/
145
private final ZoneOffsetTransitionRule[] lastRules;
146
/**
147
* The map of recent transitions.
148
*/
149
private final transient ConcurrentMap<Integer, ZoneOffsetTransition[]> lastRulesCache =
150
new ConcurrentHashMap<Integer, ZoneOffsetTransition[]>();
151
/**
152
* The zero-length long array.
153
*/
154
private static final long[] EMPTY_LONG_ARRAY = new long[0];
155
/**
156
* The zero-length lastrules array.
157
*/
158
private static final ZoneOffsetTransitionRule[] EMPTY_LASTRULES =
159
new ZoneOffsetTransitionRule[0];
160
/**
161
* The zero-length ldt array.
162
*/
163
private static final LocalDateTime[] EMPTY_LDT_ARRAY = new LocalDateTime[0];
164
/**
165
* The number of days in a 400 year cycle.
166
*/
167
private static final int DAYS_PER_CYCLE = 146097;
168
/**
169
* The number of days from year zero to year 1970.
170
* There are five 400 year cycles from year zero to 2000.
171
* There are 7 leap years from 1970 to 2000.
172
*/
173
private static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);
174
175
/**
176
* Obtains an instance of a ZoneRules.
177
*
178
* @param baseStandardOffset the standard offset to use before legal rules were set, not null
179
* @param baseWallOffset the wall offset to use before legal rules were set, not null
180
* @param standardOffsetTransitionList the list of changes to the standard offset, not null
181
* @param transitionList the list of transitions, not null
182
* @param lastRules the recurring last rules, size 16 or less, not null
183
* @return the zone rules, not null
184
*/
185
public static ZoneRules of(ZoneOffset baseStandardOffset,
186
ZoneOffset baseWallOffset,
187
List<ZoneOffsetTransition> standardOffsetTransitionList,
188
List<ZoneOffsetTransition> transitionList,
189
List<ZoneOffsetTransitionRule> lastRules) {
190
Objects.requireNonNull(baseStandardOffset, "baseStandardOffset");
191
Objects.requireNonNull(baseWallOffset, "baseWallOffset");
192
Objects.requireNonNull(standardOffsetTransitionList, "standardOffsetTransitionList");
193
Objects.requireNonNull(transitionList, "transitionList");
194
Objects.requireNonNull(lastRules, "lastRules");
195
return new ZoneRules(baseStandardOffset, baseWallOffset,
196
standardOffsetTransitionList, transitionList, lastRules);
197
}
198
199
/**
200
* Obtains an instance of ZoneRules that has fixed zone rules.
201
*
202
* @param offset the offset this fixed zone rules is based on, not null
203
* @return the zone rules, not null
204
* @see #isFixedOffset()
205
*/
206
public static ZoneRules of(ZoneOffset offset) {
207
Objects.requireNonNull(offset, "offset");
208
return new ZoneRules(offset);
209
}
210
211
/**
212
* Creates an instance.
213
*
214
* @param baseStandardOffset the standard offset to use before legal rules were set, not null
215
* @param baseWallOffset the wall offset to use before legal rules were set, not null
216
* @param standardOffsetTransitionList the list of changes to the standard offset, not null
217
* @param transitionList the list of transitions, not null
218
* @param lastRules the recurring last rules, size 16 or less, not null
219
*/
220
ZoneRules(ZoneOffset baseStandardOffset,
221
ZoneOffset baseWallOffset,
222
List<ZoneOffsetTransition> standardOffsetTransitionList,
223
List<ZoneOffsetTransition> transitionList,
224
List<ZoneOffsetTransitionRule> lastRules) {
225
super();
226
227
// convert standard transitions
228
229
this.standardTransitions = new long[standardOffsetTransitionList.size()];
230
231
this.standardOffsets = new ZoneOffset[standardOffsetTransitionList.size() + 1];
232
this.standardOffsets[0] = baseStandardOffset;
233
for (int i = 0; i < standardOffsetTransitionList.size(); i++) {
234
this.standardTransitions[i] = standardOffsetTransitionList.get(i).toEpochSecond();
235
this.standardOffsets[i + 1] = standardOffsetTransitionList.get(i).getOffsetAfter();
236
}
237
238
// convert savings transitions to locals
239
List<LocalDateTime> localTransitionList = new ArrayList<>();
240
List<ZoneOffset> localTransitionOffsetList = new ArrayList<>();
241
localTransitionOffsetList.add(baseWallOffset);
242
for (ZoneOffsetTransition trans : transitionList) {
243
if (trans.isGap()) {
244
localTransitionList.add(trans.getDateTimeBefore());
245
localTransitionList.add(trans.getDateTimeAfter());
246
} else {
247
localTransitionList.add(trans.getDateTimeAfter());
248
localTransitionList.add(trans.getDateTimeBefore());
249
}
250
localTransitionOffsetList.add(trans.getOffsetAfter());
251
}
252
this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);
253
this.wallOffsets = localTransitionOffsetList.toArray(new ZoneOffset[localTransitionOffsetList.size()]);
254
255
// convert savings transitions to instants
256
this.savingsInstantTransitions = new long[transitionList.size()];
257
for (int i = 0; i < transitionList.size(); i++) {
258
this.savingsInstantTransitions[i] = transitionList.get(i).toEpochSecond();
259
}
260
261
// last rules
262
Object[] temp = lastRules.toArray();
263
ZoneOffsetTransitionRule[] rulesArray = Arrays.copyOf(temp, temp.length, ZoneOffsetTransitionRule[].class);
264
if (rulesArray.length > 16) {
265
throw new IllegalArgumentException("Too many transition rules");
266
}
267
this.lastRules = rulesArray;
268
}
269
270
/**
271
* Constructor.
272
*
273
* @param standardTransitions the standard transitions, not null
274
* @param standardOffsets the standard offsets, not null
275
* @param savingsInstantTransitions the standard transitions, not null
276
* @param wallOffsets the wall offsets, not null
277
* @param lastRules the recurring last rules, size 15 or less, not null
278
*/
279
private ZoneRules(long[] standardTransitions,
280
ZoneOffset[] standardOffsets,
281
long[] savingsInstantTransitions,
282
ZoneOffset[] wallOffsets,
283
ZoneOffsetTransitionRule[] lastRules) {
284
super();
285
286
this.standardTransitions = standardTransitions;
287
this.standardOffsets = standardOffsets;
288
this.savingsInstantTransitions = savingsInstantTransitions;
289
this.wallOffsets = wallOffsets;
290
this.lastRules = lastRules;
291
292
if (savingsInstantTransitions.length == 0) {
293
this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
294
} else {
295
// convert savings transitions to locals
296
List<LocalDateTime> localTransitionList = new ArrayList<>();
297
for (int i = 0; i < savingsInstantTransitions.length; i++) {
298
ZoneOffset before = wallOffsets[i];
299
ZoneOffset after = wallOffsets[i + 1];
300
ZoneOffsetTransition trans = new ZoneOffsetTransition(savingsInstantTransitions[i], before, after);
301
if (trans.isGap()) {
302
localTransitionList.add(trans.getDateTimeBefore());
303
localTransitionList.add(trans.getDateTimeAfter());
304
} else {
305
localTransitionList.add(trans.getDateTimeAfter());
306
localTransitionList.add(trans.getDateTimeBefore());
307
}
308
}
309
this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);
310
}
311
}
312
313
/**
314
* Creates an instance of ZoneRules that has fixed zone rules.
315
*
316
* @param offset the offset this fixed zone rules is based on, not null
317
* @see #isFixedOffset()
318
*/
319
private ZoneRules(ZoneOffset offset) {
320
this.standardOffsets = new ZoneOffset[1];
321
this.standardOffsets[0] = offset;
322
this.standardTransitions = EMPTY_LONG_ARRAY;
323
this.savingsInstantTransitions = EMPTY_LONG_ARRAY;
324
this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
325
this.wallOffsets = standardOffsets;
326
this.lastRules = EMPTY_LASTRULES;
327
}
328
329
/**
330
* Defend against malicious streams.
331
*
332
* @param s the stream to read
333
* @throws InvalidObjectException always
334
*/
335
private void readObject(ObjectInputStream s) throws InvalidObjectException {
336
throw new InvalidObjectException("Deserialization via serialization delegate");
337
}
338
339
/**
340
* Writes the object using a
341
* <a href="{@docRoot}/serialized-form.html#java.time.zone.Ser">dedicated serialized form</a>.
342
* @serialData
343
* <pre style="font-size:1.0em">{@code
344
*
345
* out.writeByte(1); // identifies a ZoneRules
346
* out.writeInt(standardTransitions.length);
347
* for (long trans : standardTransitions) {
348
* Ser.writeEpochSec(trans, out);
349
* }
350
* for (ZoneOffset offset : standardOffsets) {
351
* Ser.writeOffset(offset, out);
352
* }
353
* out.writeInt(savingsInstantTransitions.length);
354
* for (long trans : savingsInstantTransitions) {
355
* Ser.writeEpochSec(trans, out);
356
* }
357
* for (ZoneOffset offset : wallOffsets) {
358
* Ser.writeOffset(offset, out);
359
* }
360
* out.writeByte(lastRules.length);
361
* for (ZoneOffsetTransitionRule rule : lastRules) {
362
* rule.writeExternal(out);
363
* }
364
* }
365
* </pre>
366
* <p>
367
* Epoch second values used for offsets are encoded in a variable
368
* length form to make the common cases put fewer bytes in the stream.
369
* <pre style="font-size:1.0em">{@code
370
*
371
* static void writeEpochSec(long epochSec, DataOutput out) throws IOException {
372
* if (epochSec >= -4575744000L && epochSec < 10413792000L && epochSec % 900 == 0) { // quarter hours between 1825 and 2300
373
* int store = (int) ((epochSec + 4575744000L) / 900);
374
* out.writeByte((store >>> 16) & 255);
375
* out.writeByte((store >>> 8) & 255);
376
* out.writeByte(store & 255);
377
* } else {
378
* out.writeByte(255);
379
* out.writeLong(epochSec);
380
* }
381
* }
382
* }
383
* </pre>
384
* <p>
385
* ZoneOffset values are encoded in a variable length form so the
386
* common cases put fewer bytes in the stream.
387
* <pre style="font-size:1.0em">{@code
388
*
389
* static void writeOffset(ZoneOffset offset, DataOutput out) throws IOException {
390
* final int offsetSecs = offset.getTotalSeconds();
391
* int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127; // compress to -72 to +72
392
* out.writeByte(offsetByte);
393
* if (offsetByte == 127) {
394
* out.writeInt(offsetSecs);
395
* }
396
* }
397
*}
398
* </pre>
399
* @return the replacing object, not null
400
*/
401
private Object writeReplace() {
402
return new Ser(Ser.ZRULES, this);
403
}
404
405
/**
406
* Writes the state to the stream.
407
*
408
* @param out the output stream, not null
409
* @throws IOException if an error occurs
410
*/
411
void writeExternal(DataOutput out) throws IOException {
412
out.writeInt(standardTransitions.length);
413
for (long trans : standardTransitions) {
414
Ser.writeEpochSec(trans, out);
415
}
416
for (ZoneOffset offset : standardOffsets) {
417
Ser.writeOffset(offset, out);
418
}
419
out.writeInt(savingsInstantTransitions.length);
420
for (long trans : savingsInstantTransitions) {
421
Ser.writeEpochSec(trans, out);
422
}
423
for (ZoneOffset offset : wallOffsets) {
424
Ser.writeOffset(offset, out);
425
}
426
out.writeByte(lastRules.length);
427
for (ZoneOffsetTransitionRule rule : lastRules) {
428
rule.writeExternal(out);
429
}
430
}
431
432
/**
433
* Reads the state from the stream. The 1,024 limit to the lengths
434
* of stdTrans and savSize is intended to be the size well enough
435
* to accommodate the max number of transitions in current tzdb data
436
* (203 for Asia/Tehran).
437
*
438
* @param in the input stream, not null
439
* @return the created object, not null
440
* @throws IOException if an error occurs
441
*/
442
static ZoneRules readExternal(DataInput in) throws IOException, ClassNotFoundException {
443
int stdSize = in.readInt();
444
if (stdSize > 1024) {
445
throw new InvalidObjectException("Too many transitions");
446
}
447
long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY
448
: new long[stdSize];
449
for (int i = 0; i < stdSize; i++) {
450
stdTrans[i] = Ser.readEpochSec(in);
451
}
452
ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1];
453
for (int i = 0; i < stdOffsets.length; i++) {
454
stdOffsets[i] = Ser.readOffset(in);
455
}
456
int savSize = in.readInt();
457
if (savSize > 1024) {
458
throw new InvalidObjectException("Too many saving offsets");
459
}
460
long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY
461
: new long[savSize];
462
for (int i = 0; i < savSize; i++) {
463
savTrans[i] = Ser.readEpochSec(in);
464
}
465
ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1];
466
for (int i = 0; i < savOffsets.length; i++) {
467
savOffsets[i] = Ser.readOffset(in);
468
}
469
int ruleSize = in.readByte();
470
if (ruleSize > 16) {
471
throw new InvalidObjectException("Too many transition rules");
472
}
473
ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ?
474
EMPTY_LASTRULES : new ZoneOffsetTransitionRule[ruleSize];
475
for (int i = 0; i < ruleSize; i++) {
476
rules[i] = ZoneOffsetTransitionRule.readExternal(in);
477
}
478
return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules);
479
}
480
481
/**
482
* Checks of the zone rules are fixed, such that the offset never varies.
483
*
484
* @return true if the time-zone is fixed and the offset never changes
485
*/
486
public boolean isFixedOffset() {
487
return standardOffsets[0].equals(wallOffsets[0]) &&
488
standardTransitions.length == 0 &&
489
savingsInstantTransitions.length == 0 &&
490
lastRules.length == 0;
491
}
492
493
/**
494
* Gets the offset applicable at the specified instant in these rules.
495
* <p>
496
* The mapping from an instant to an offset is simple, there is only
497
* one valid offset for each instant.
498
* This method returns that offset.
499
*
500
* @param instant the instant to find the offset for, not null, but null
501
* may be ignored if the rules have a single offset for all instants
502
* @return the offset, not null
503
*/
504
public ZoneOffset getOffset(Instant instant) {
505
if (savingsInstantTransitions.length == 0) {
506
return wallOffsets[0];
507
}
508
long epochSec = instant.getEpochSecond();
509
// check if using last rules
510
if (lastRules.length > 0 &&
511
epochSec > savingsInstantTransitions[savingsInstantTransitions.length - 1]) {
512
int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
513
ZoneOffsetTransition[] transArray = findTransitionArray(year);
514
ZoneOffsetTransition trans = null;
515
for (int i = 0; i < transArray.length; i++) {
516
trans = transArray[i];
517
if (epochSec < trans.toEpochSecond()) {
518
return trans.getOffsetBefore();
519
}
520
}
521
return trans.getOffsetAfter();
522
}
523
524
// using historic rules
525
int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);
526
if (index < 0) {
527
// switch negative insert position to start of matched range
528
index = -index - 2;
529
}
530
return wallOffsets[index + 1];
531
}
532
533
/**
534
* Gets a suitable offset for the specified local date-time in these rules.
535
* <p>
536
* The mapping from a local date-time to an offset is not straightforward.
537
* There are three cases:
538
* <ul>
539
* <li>Normal, with one valid offset. For the vast majority of the year, the normal
540
* case applies, where there is a single valid offset for the local date-time.</li>
541
* <li>Gap, with zero valid offsets. This is when clocks jump forward typically
542
* due to the spring daylight savings change from "winter" to "summer".
543
* In a gap there are local date-time values with no valid offset.</li>
544
* <li>Overlap, with two valid offsets. This is when clocks are set back typically
545
* due to the autumn daylight savings change from "summer" to "winter".
546
* In an overlap there are local date-time values with two valid offsets.</li>
547
* </ul>
548
* Thus, for any given local date-time there can be zero, one or two valid offsets.
549
* This method returns the single offset in the Normal case, and in the Gap or Overlap
550
* case it returns the offset before the transition.
551
* <p>
552
* Since, in the case of Gap and Overlap, the offset returned is a "best" value, rather
553
* than the "correct" value, it should be treated with care. Applications that care
554
* about the correct offset should use a combination of this method,
555
* {@link #getValidOffsets(LocalDateTime)} and {@link #getTransition(LocalDateTime)}.
556
*
557
* @param localDateTime the local date-time to query, not null, but null
558
* may be ignored if the rules have a single offset for all instants
559
* @return the best available offset for the local date-time, not null
560
*/
561
public ZoneOffset getOffset(LocalDateTime localDateTime) {
562
Object info = getOffsetInfo(localDateTime);
563
if (info instanceof ZoneOffsetTransition) {
564
return ((ZoneOffsetTransition) info).getOffsetBefore();
565
}
566
return (ZoneOffset) info;
567
}
568
569
/**
570
* Gets the offset applicable at the specified local date-time in these rules.
571
* <p>
572
* The mapping from a local date-time to an offset is not straightforward.
573
* There are three cases:
574
* <ul>
575
* <li>Normal, with one valid offset. For the vast majority of the year, the normal
576
* case applies, where there is a single valid offset for the local date-time.</li>
577
* <li>Gap, with zero valid offsets. This is when clocks jump forward typically
578
* due to the spring daylight savings change from "winter" to "summer".
579
* In a gap there are local date-time values with no valid offset.</li>
580
* <li>Overlap, with two valid offsets. This is when clocks are set back typically
581
* due to the autumn daylight savings change from "summer" to "winter".
582
* In an overlap there are local date-time values with two valid offsets.</li>
583
* </ul>
584
* Thus, for any given local date-time there can be zero, one or two valid offsets.
585
* This method returns that list of valid offsets, which is a list of size 0, 1 or 2.
586
* In the case where there are two offsets, the earlier offset is returned at index 0
587
* and the later offset at index 1.
588
* <p>
589
* There are various ways to handle the conversion from a {@code LocalDateTime}.
590
* One technique, using this method, would be:
591
* <pre>
592
* List&lt;ZoneOffset&gt; validOffsets = rules.getValidOffsets(localDT);
593
* if (validOffsets.size() == 1) {
594
* // Normal case: only one valid offset
595
* zoneOffset = validOffsets.get(0);
596
* } else {
597
* // Gap or Overlap: determine what to do from transition (which will be non-null)
598
* ZoneOffsetTransition trans = rules.getTransition(localDT);
599
* }
600
* </pre>
601
* <p>
602
* In theory, it is possible for there to be more than two valid offsets.
603
* This would happen if clocks to be put back more than once in quick succession.
604
* This has never happened in the history of time-zones and thus has no special handling.
605
* However, if it were to happen, then the list would return more than 2 entries.
606
*
607
* @param localDateTime the local date-time to query for valid offsets, not null, but null
608
* may be ignored if the rules have a single offset for all instants
609
* @return the list of valid offsets, may be immutable, not null
610
*/
611
public List<ZoneOffset> getValidOffsets(LocalDateTime localDateTime) {
612
// should probably be optimized
613
Object info = getOffsetInfo(localDateTime);
614
if (info instanceof ZoneOffsetTransition) {
615
return ((ZoneOffsetTransition) info).getValidOffsets();
616
}
617
return Collections.singletonList((ZoneOffset) info);
618
}
619
620
/**
621
* Gets the offset transition applicable at the specified local date-time in these rules.
622
* <p>
623
* The mapping from a local date-time to an offset is not straightforward.
624
* There are three cases:
625
* <ul>
626
* <li>Normal, with one valid offset. For the vast majority of the year, the normal
627
* case applies, where there is a single valid offset for the local date-time.</li>
628
* <li>Gap, with zero valid offsets. This is when clocks jump forward typically
629
* due to the spring daylight savings change from "winter" to "summer".
630
* In a gap there are local date-time values with no valid offset.</li>
631
* <li>Overlap, with two valid offsets. This is when clocks are set back typically
632
* due to the autumn daylight savings change from "summer" to "winter".
633
* In an overlap there are local date-time values with two valid offsets.</li>
634
* </ul>
635
* A transition is used to model the cases of a Gap or Overlap.
636
* The Normal case will return null.
637
* <p>
638
* There are various ways to handle the conversion from a {@code LocalDateTime}.
639
* One technique, using this method, would be:
640
* <pre>
641
* ZoneOffsetTransition trans = rules.getTransition(localDT);
642
* if (trans != null) {
643
* // Gap or Overlap: determine what to do from transition
644
* } else {
645
* // Normal case: only one valid offset
646
* zoneOffset = rule.getOffset(localDT);
647
* }
648
* </pre>
649
*
650
* @param localDateTime the local date-time to query for offset transition, not null, but null
651
* may be ignored if the rules have a single offset for all instants
652
* @return the offset transition, null if the local date-time is not in transition
653
*/
654
public ZoneOffsetTransition getTransition(LocalDateTime localDateTime) {
655
Object info = getOffsetInfo(localDateTime);
656
return (info instanceof ZoneOffsetTransition ? (ZoneOffsetTransition) info : null);
657
}
658
659
private Object getOffsetInfo(LocalDateTime dt) {
660
if (savingsLocalTransitions.length == 0) {
661
return wallOffsets[0];
662
}
663
// check if using last rules
664
if (lastRules.length > 0 &&
665
dt.isAfter(savingsLocalTransitions[savingsLocalTransitions.length - 1])) {
666
ZoneOffsetTransition[] transArray = findTransitionArray(dt.getYear());
667
Object info = null;
668
for (ZoneOffsetTransition trans : transArray) {
669
info = findOffsetInfo(dt, trans);
670
if (info instanceof ZoneOffsetTransition || info.equals(trans.getOffsetBefore())) {
671
return info;
672
}
673
}
674
return info;
675
}
676
677
// using historic rules
678
int index = Arrays.binarySearch(savingsLocalTransitions, dt);
679
if (index == -1) {
680
// before first transition
681
return wallOffsets[0];
682
}
683
if (index < 0) {
684
// switch negative insert position to start of matched range
685
index = -index - 2;
686
} else if (index < savingsLocalTransitions.length - 1 &&
687
savingsLocalTransitions[index].equals(savingsLocalTransitions[index + 1])) {
688
// handle overlap immediately following gap
689
index++;
690
}
691
if ((index & 1) == 0) {
692
// gap or overlap
693
LocalDateTime dtBefore = savingsLocalTransitions[index];
694
LocalDateTime dtAfter = savingsLocalTransitions[index + 1];
695
ZoneOffset offsetBefore = wallOffsets[index / 2];
696
ZoneOffset offsetAfter = wallOffsets[index / 2 + 1];
697
if (offsetAfter.getTotalSeconds() > offsetBefore.getTotalSeconds()) {
698
// gap
699
return new ZoneOffsetTransition(dtBefore, offsetBefore, offsetAfter);
700
} else {
701
// overlap
702
return new ZoneOffsetTransition(dtAfter, offsetBefore, offsetAfter);
703
}
704
} else {
705
// normal (neither gap or overlap)
706
return wallOffsets[index / 2 + 1];
707
}
708
}
709
710
/**
711
* Finds the offset info for a local date-time and transition.
712
*
713
* @param dt the date-time, not null
714
* @param trans the transition, not null
715
* @return the offset info, not null
716
*/
717
private Object findOffsetInfo(LocalDateTime dt, ZoneOffsetTransition trans) {
718
LocalDateTime localTransition = trans.getDateTimeBefore();
719
if (trans.isGap()) {
720
if (dt.isBefore(localTransition)) {
721
return trans.getOffsetBefore();
722
}
723
if (dt.isBefore(trans.getDateTimeAfter())) {
724
return trans;
725
} else {
726
return trans.getOffsetAfter();
727
}
728
} else {
729
if (dt.isBefore(localTransition) == false) {
730
return trans.getOffsetAfter();
731
}
732
if (dt.isBefore(trans.getDateTimeAfter())) {
733
return trans.getOffsetBefore();
734
} else {
735
return trans;
736
}
737
}
738
}
739
740
/**
741
* Finds the appropriate transition array for the given year.
742
*
743
* @param year the year, not null
744
* @return the transition array, not null
745
*/
746
private ZoneOffsetTransition[] findTransitionArray(int year) {
747
Integer yearObj = year; // should use Year class, but this saves a class load
748
ZoneOffsetTransition[] transArray = lastRulesCache.get(yearObj);
749
if (transArray != null) {
750
return transArray;
751
}
752
ZoneOffsetTransitionRule[] ruleArray = lastRules;
753
transArray = new ZoneOffsetTransition[ruleArray.length];
754
for (int i = 0; i < ruleArray.length; i++) {
755
transArray[i] = ruleArray[i].createTransition(year);
756
}
757
if (year < LAST_CACHED_YEAR) {
758
lastRulesCache.putIfAbsent(yearObj, transArray);
759
}
760
return transArray;
761
}
762
763
/**
764
* Gets the standard offset for the specified instant in this zone.
765
* <p>
766
* This provides access to historic information on how the standard offset
767
* has changed over time.
768
* The standard offset is the offset before any daylight saving time is applied.
769
* This is typically the offset applicable during winter.
770
*
771
* @param instant the instant to find the offset information for, not null, but null
772
* may be ignored if the rules have a single offset for all instants
773
* @return the standard offset, not null
774
*/
775
public ZoneOffset getStandardOffset(Instant instant) {
776
if (standardTransitions.length == 0) {
777
return standardOffsets[0];
778
}
779
long epochSec = instant.getEpochSecond();
780
int index = Arrays.binarySearch(standardTransitions, epochSec);
781
if (index < 0) {
782
// switch negative insert position to start of matched range
783
index = -index - 2;
784
}
785
return standardOffsets[index + 1];
786
}
787
788
/**
789
* Gets the amount of daylight savings in use for the specified instant in this zone.
790
* <p>
791
* This provides access to historic information on how the amount of daylight
792
* savings has changed over time.
793
* This is the difference between the standard offset and the actual offset.
794
* Typically the amount is zero during winter and one hour during summer.
795
* Time-zones are second-based, so the nanosecond part of the duration will be zero.
796
* <p>
797
* This default implementation calculates the duration from the
798
* {@link #getOffset(java.time.Instant) actual} and
799
* {@link #getStandardOffset(java.time.Instant) standard} offsets.
800
*
801
* @param instant the instant to find the daylight savings for, not null, but null
802
* may be ignored if the rules have a single offset for all instants
803
* @return the difference between the standard and actual offset, not null
804
*/
805
public Duration getDaylightSavings(Instant instant) {
806
if (isFixedOffset()) {
807
return Duration.ZERO;
808
}
809
ZoneOffset standardOffset = getStandardOffset(instant);
810
ZoneOffset actualOffset = getOffset(instant);
811
return Duration.ofSeconds(actualOffset.getTotalSeconds() - standardOffset.getTotalSeconds());
812
}
813
814
/**
815
* Checks if the specified instant is in daylight savings.
816
* <p>
817
* This checks if the standard offset and the actual offset are the same
818
* for the specified instant.
819
* If they are not, it is assumed that daylight savings is in operation.
820
* <p>
821
* This default implementation compares the {@link #getOffset(java.time.Instant) actual}
822
* and {@link #getStandardOffset(java.time.Instant) standard} offsets.
823
*
824
* @param instant the instant to find the offset information for, not null, but null
825
* may be ignored if the rules have a single offset for all instants
826
* @return the standard offset, not null
827
*/
828
public boolean isDaylightSavings(Instant instant) {
829
return (getStandardOffset(instant).equals(getOffset(instant)) == false);
830
}
831
832
/**
833
* Checks if the offset date-time is valid for these rules.
834
* <p>
835
* To be valid, the local date-time must not be in a gap and the offset
836
* must match one of the valid offsets.
837
* <p>
838
* This default implementation checks if {@link #getValidOffsets(java.time.LocalDateTime)}
839
* contains the specified offset.
840
*
841
* @param localDateTime the date-time to check, not null, but null
842
* may be ignored if the rules have a single offset for all instants
843
* @param offset the offset to check, null returns false
844
* @return true if the offset date-time is valid for these rules
845
*/
846
public boolean isValidOffset(LocalDateTime localDateTime, ZoneOffset offset) {
847
return getValidOffsets(localDateTime).contains(offset);
848
}
849
850
/**
851
* Gets the next transition after the specified instant.
852
* <p>
853
* This returns details of the next transition after the specified instant.
854
* For example, if the instant represents a point where "Summer" daylight savings time
855
* applies, then the method will return the transition to the next "Winter" time.
856
*
857
* @param instant the instant to get the next transition after, not null, but null
858
* may be ignored if the rules have a single offset for all instants
859
* @return the next transition after the specified instant, null if this is after the last transition
860
*/
861
public ZoneOffsetTransition nextTransition(Instant instant) {
862
if (savingsInstantTransitions.length == 0) {
863
return null;
864
}
865
long epochSec = instant.getEpochSecond();
866
// check if using last rules
867
if (epochSec >= savingsInstantTransitions[savingsInstantTransitions.length - 1]) {
868
if (lastRules.length == 0) {
869
return null;
870
}
871
// search year the instant is in
872
int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
873
ZoneOffsetTransition[] transArray = findTransitionArray(year);
874
for (ZoneOffsetTransition trans : transArray) {
875
if (epochSec < trans.toEpochSecond()) {
876
return trans;
877
}
878
}
879
// use first from following year
880
if (year < Year.MAX_VALUE) {
881
transArray = findTransitionArray(year + 1);
882
return transArray[0];
883
}
884
return null;
885
}
886
887
// using historic rules
888
int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);
889
if (index < 0) {
890
index = -index - 1; // switched value is the next transition
891
} else {
892
index += 1; // exact match, so need to add one to get the next
893
}
894
return new ZoneOffsetTransition(savingsInstantTransitions[index], wallOffsets[index], wallOffsets[index + 1]);
895
}
896
897
/**
898
* Gets the previous transition before the specified instant.
899
* <p>
900
* This returns details of the previous transition before the specified instant.
901
* For example, if the instant represents a point where "summer" daylight saving time
902
* applies, then the method will return the transition from the previous "winter" time.
903
*
904
* @param instant the instant to get the previous transition after, not null, but null
905
* may be ignored if the rules have a single offset for all instants
906
* @return the previous transition before the specified instant, null if this is before the first transition
907
*/
908
public ZoneOffsetTransition previousTransition(Instant instant) {
909
if (savingsInstantTransitions.length == 0) {
910
return null;
911
}
912
long epochSec = instant.getEpochSecond();
913
if (instant.getNano() > 0 && epochSec < Long.MAX_VALUE) {
914
epochSec += 1; // allow rest of method to only use seconds
915
}
916
917
// check if using last rules
918
long lastHistoric = savingsInstantTransitions[savingsInstantTransitions.length - 1];
919
if (lastRules.length > 0 && epochSec > lastHistoric) {
920
// search year the instant is in
921
ZoneOffset lastHistoricOffset = wallOffsets[wallOffsets.length - 1];
922
int year = findYear(epochSec, lastHistoricOffset);
923
ZoneOffsetTransition[] transArray = findTransitionArray(year);
924
for (int i = transArray.length - 1; i >= 0; i--) {
925
if (epochSec > transArray[i].toEpochSecond()) {
926
return transArray[i];
927
}
928
}
929
// use last from preceding year
930
int lastHistoricYear = findYear(lastHistoric, lastHistoricOffset);
931
if (--year > lastHistoricYear) {
932
transArray = findTransitionArray(year);
933
return transArray[transArray.length - 1];
934
}
935
// drop through
936
}
937
938
// using historic rules
939
int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);
940
if (index < 0) {
941
index = -index - 1;
942
}
943
if (index <= 0) {
944
return null;
945
}
946
return new ZoneOffsetTransition(savingsInstantTransitions[index - 1], wallOffsets[index - 1], wallOffsets[index]);
947
}
948
949
private int findYear(long epochSecond, ZoneOffset offset) {
950
long localSecond = epochSecond + offset.getTotalSeconds();
951
long zeroDay = Math.floorDiv(localSecond, 86400) + DAYS_0000_TO_1970;
952
953
// find the march-based year
954
zeroDay -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle
955
long adjust = 0;
956
if (zeroDay < 0) {
957
// adjust negative years to positive for calculation
958
long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;
959
adjust = adjustCycles * 400;
960
zeroDay += -adjustCycles * DAYS_PER_CYCLE;
961
}
962
long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;
963
long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
964
if (doyEst < 0) {
965
// fix estimate
966
yearEst--;
967
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
968
}
969
yearEst += adjust; // reset any negative year
970
int marchDoy0 = (int) doyEst;
971
972
// convert march-based values back to january-based
973
int marchMonth0 = (marchDoy0 * 5 + 2) / 153;
974
yearEst += marchMonth0 / 10;
975
976
// Cap to the max value
977
return (int)Math.min(yearEst, Year.MAX_VALUE);
978
}
979
980
/**
981
* Gets the complete list of fully defined transitions.
982
* <p>
983
* The complete set of transitions for this rules instance is defined by this method
984
* and {@link #getTransitionRules()}. This method returns those transitions that have
985
* been fully defined. These are typically historical, but may be in the future.
986
* <p>
987
* The list will be empty for fixed offset rules and for any time-zone where there has
988
* only ever been a single offset. The list will also be empty if the transition rules are unknown.
989
*
990
* @return an immutable list of fully defined transitions, not null
991
*/
992
public List<ZoneOffsetTransition> getTransitions() {
993
List<ZoneOffsetTransition> list = new ArrayList<>();
994
for (int i = 0; i < savingsInstantTransitions.length; i++) {
995
list.add(new ZoneOffsetTransition(savingsInstantTransitions[i], wallOffsets[i], wallOffsets[i + 1]));
996
}
997
return Collections.unmodifiableList(list);
998
}
999
1000
/**
1001
* Gets the list of transition rules for years beyond those defined in the transition list.
1002
* <p>
1003
* The complete set of transitions for this rules instance is defined by this method
1004
* and {@link #getTransitions()}. This method returns instances of {@link ZoneOffsetTransitionRule}
1005
* that define an algorithm for when transitions will occur.
1006
* <p>
1007
* For any given {@code ZoneRules}, this list contains the transition rules for years
1008
* beyond those years that have been fully defined. These rules typically refer to future
1009
* daylight saving time rule changes.
1010
* <p>
1011
* If the zone defines daylight savings into the future, then the list will normally
1012
* be of size two and hold information about entering and exiting daylight savings.
1013
* If the zone does not have daylight savings, or information about future changes
1014
* is uncertain, then the list will be empty.
1015
* <p>
1016
* The list will be empty for fixed offset rules and for any time-zone where there is no
1017
* daylight saving time. The list will also be empty if the transition rules are unknown.
1018
*
1019
* @return an immutable list of transition rules, not null
1020
*/
1021
public List<ZoneOffsetTransitionRule> getTransitionRules() {
1022
return List.of(lastRules);
1023
}
1024
1025
/**
1026
* Checks if this set of rules equals another.
1027
* <p>
1028
* Two rule sets are equal if they will always result in the same output
1029
* for any given input instant or local date-time.
1030
* Rules from two different groups may return false even if they are in fact the same.
1031
* <p>
1032
* This definition should result in implementations comparing their entire state.
1033
*
1034
* @param otherRules the other rules, null returns false
1035
* @return true if this rules is the same as that specified
1036
*/
1037
@Override
1038
public boolean equals(Object otherRules) {
1039
if (this == otherRules) {
1040
return true;
1041
}
1042
return (otherRules instanceof ZoneRules other)
1043
&& Arrays.equals(standardTransitions, other.standardTransitions)
1044
&& Arrays.equals(standardOffsets, other.standardOffsets)
1045
&& Arrays.equals(savingsInstantTransitions, other.savingsInstantTransitions)
1046
&& Arrays.equals(wallOffsets, other.wallOffsets)
1047
&& Arrays.equals(lastRules, other.lastRules);
1048
}
1049
1050
/**
1051
* Returns a suitable hash code given the definition of {@code #equals}.
1052
*
1053
* @return the hash code
1054
*/
1055
@Override
1056
public int hashCode() {
1057
return Arrays.hashCode(standardTransitions) ^
1058
Arrays.hashCode(standardOffsets) ^
1059
Arrays.hashCode(savingsInstantTransitions) ^
1060
Arrays.hashCode(wallOffsets) ^
1061
Arrays.hashCode(lastRules);
1062
}
1063
1064
/**
1065
* Returns a string describing this object.
1066
*
1067
* @return a string for debugging, not null
1068
*/
1069
@Override
1070
public String toString() {
1071
return "ZoneRules[currentStandardOffset=" + standardOffsets[standardOffsets.length - 1] + "]";
1072
}
1073
1074
}
1075
1076