• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp.zone;
33 
34 import static org.threeten.bp.temporal.TemporalAdjusters.nextOrSame;
35 import static org.threeten.bp.temporal.TemporalAdjusters.previousOrSame;
36 
37 import java.io.DataInput;
38 import java.io.DataOutput;
39 import java.io.IOException;
40 import java.io.Serializable;
41 
42 import org.threeten.bp.DayOfWeek;
43 import org.threeten.bp.LocalDate;
44 import org.threeten.bp.LocalDateTime;
45 import org.threeten.bp.LocalTime;
46 import org.threeten.bp.Month;
47 import org.threeten.bp.ZoneOffset;
48 import org.threeten.bp.chrono.IsoChronology;
49 import org.threeten.bp.jdk8.Jdk8Methods;
50 
51 /**
52  * A rule expressing how to create a transition.
53  * <p>
54  * This class allows rules for identifying future transitions to be expressed.
55  * A rule might be written in many forms:
56  * <p><ul>
57  * <li>the 16th March
58  * <li>the Sunday on or after the 16th March
59  * <li>the Sunday on or before the 16th March
60  * <li>the last Sunday in February
61  * </ul><p>
62  * These different rule types can be expressed and queried.
63  *
64  * <h3>Specification for implementors</h3>
65  * This class is immutable and thread-safe.
66  */
67 public final class ZoneOffsetTransitionRule implements Serializable {
68 
69     /**
70      * Serialization version.
71      */
72     private static final long serialVersionUID = 6889046316657758795L;
73     /**
74      * The number of seconds per day.
75      */
76     private static final int SECS_PER_DAY = 86400;
77 
78     /**
79      * The month of the month-day of the first day of the cutover week.
80      * The actual date will be adjusted by the dowChange field.
81      */
82     private final Month month;
83     /**
84      * The day-of-month of the month-day of the cutover week.
85      * If positive, it is the start of the week where the cutover can occur.
86      * If negative, it represents the end of the week where cutover can occur.
87      * The value is the number of days from the end of the month, such that
88      * {@code -1} is the last day of the month, {@code -2} is the second
89      * to last day, and so on.
90      */
91     private final byte dom;
92     /**
93      * The cutover day-of-week, null to retain the day-of-month.
94      */
95     private final DayOfWeek dow;
96     /**
97      * The cutover time in the 'before' offset.
98      */
99     private final LocalTime time;
100     /**
101      * The number of days to adjust by.
102      */
103     private final int adjustDays;
104     /**
105      * The definition of how the local time should be interpreted.
106      */
107     private final TimeDefinition timeDefinition;
108     /**
109      * The standard offset at the cutover.
110      */
111     private final ZoneOffset standardOffset;
112     /**
113      * The offset before the cutover.
114      */
115     private final ZoneOffset offsetBefore;
116     /**
117      * The offset after the cutover.
118      */
119     private final ZoneOffset offsetAfter;
120 
121     /**
122      * Obtains an instance defining the yearly rule to create transitions between two offsets.
123      * <p>
124      * Applications should normally obtain an instance from {@link ZoneRules}.
125      * This factory is only intended for use when creating {@link ZoneRules}.
126      *
127      * @param month  the month of the month-day of the first day of the cutover week, not null
128      * @param dayOfMonthIndicator  the day of the month-day of the cutover week, positive if the week is that
129      *  day or later, negative if the week is that day or earlier, counting from the last day of the month,
130      *  from -28 to 31 excluding 0
131      * @param dayOfWeek  the required day-of-week, null if the month-day should not be changed
132      * @param time  the cutover time in the 'before' offset, not null
133      * @param timeEndOfDay  whether the time is midnight at the end of day
134      * @param timeDefnition  how to interpret the cutover
135      * @param standardOffset  the standard offset in force at the cutover, not null
136      * @param offsetBefore  the offset before the cutover, not null
137      * @param offsetAfter  the offset after the cutover, not null
138      * @return the rule, not null
139      * @throws IllegalArgumentException if the day of month indicator is invalid
140      * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight
141      */
of( Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefnition, ZoneOffset standardOffset, ZoneOffset offsetBefore, ZoneOffset offsetAfter)142     public static ZoneOffsetTransitionRule of(
143             Month month,
144             int dayOfMonthIndicator,
145             DayOfWeek dayOfWeek,
146             LocalTime time,
147             boolean timeEndOfDay,
148             TimeDefinition timeDefnition,
149             ZoneOffset standardOffset,
150             ZoneOffset offsetBefore,
151             ZoneOffset offsetAfter) {
152         Jdk8Methods.requireNonNull(month, "month");
153         Jdk8Methods.requireNonNull(time, "time");
154         Jdk8Methods.requireNonNull(timeDefnition, "timeDefnition");
155         Jdk8Methods.requireNonNull(standardOffset, "standardOffset");
156         Jdk8Methods.requireNonNull(offsetBefore, "offsetBefore");
157         Jdk8Methods.requireNonNull(offsetAfter, "offsetAfter");
158         if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) {
159             throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
160         }
161         if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) {
162             throw new IllegalArgumentException("Time must be midnight when end of day flag is true");
163         }
164         return new ZoneOffsetTransitionRule(month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay ? 1 : 0, timeDefnition, standardOffset, offsetBefore, offsetAfter);
165     }
166 
167     /**
168      * Creates an instance defining the yearly rule to create transitions between two offsets.
169      *
170      * @param month  the month of the month-day of the first day of the cutover week, not null
171      * @param dayOfMonthIndicator  the day of the month-day of the cutover week, positive if the week is that
172      *  day or later, negative if the week is that day or earlier, counting from the last day of the month,
173      *  from -28 to 31 excluding 0
174      * @param dayOfWeek  the required day-of-week, null if the month-day should not be changed
175      * @param time  the cutover time in the 'before' offset, not null
176      * @param adjustDays  the time days adjustment
177      * @param timeDefnition  how to interpret the cutover
178      * @param standardOffset  the standard offset in force at the cutover, not null
179      * @param offsetBefore  the offset before the cutover, not null
180      * @param offsetAfter  the offset after the cutover, not null
181      * @throws IllegalArgumentException if the day of month indicator is invalid
182      * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight
183      */
ZoneOffsetTransitionRule( Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, int adjustDays, TimeDefinition timeDefnition, ZoneOffset standardOffset, ZoneOffset offsetBefore, ZoneOffset offsetAfter)184     ZoneOffsetTransitionRule(
185             Month month,
186             int dayOfMonthIndicator,
187             DayOfWeek dayOfWeek,
188             LocalTime time,
189             int adjustDays,
190             TimeDefinition timeDefnition,
191             ZoneOffset standardOffset,
192             ZoneOffset offsetBefore,
193             ZoneOffset offsetAfter) {
194         this.month = month;
195         this.dom = (byte) dayOfMonthIndicator;
196         this.dow = dayOfWeek;
197         this.time = time;
198         this.adjustDays = adjustDays;
199         this.timeDefinition = timeDefnition;
200         this.standardOffset = standardOffset;
201         this.offsetBefore = offsetBefore;
202         this.offsetAfter = offsetAfter;
203     }
204 
205     //-----------------------------------------------------------------------
206     /**
207      * Uses a serialization delegate.
208      *
209      * @return the replacing object, not null
210      */
writeReplace()211     private Object writeReplace() {
212         return new Ser(Ser.ZOTRULE, this);
213     }
214 
215     /**
216      * Writes the state to the stream.
217      *
218      * @param out  the output stream, not null
219      * @throws IOException if an error occurs
220      */
writeExternal(DataOutput out)221     void writeExternal(DataOutput out) throws IOException {
222         final int timeSecs = time.toSecondOfDay() + adjustDays * SECS_PER_DAY;
223         final int stdOffset = standardOffset.getTotalSeconds();
224         final int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset;
225         final int afterDiff = offsetAfter.getTotalSeconds() - stdOffset;
226         final int timeByte = (timeSecs % 3600 == 0 && timeSecs <= SECS_PER_DAY ?
227                 (timeSecs == SECS_PER_DAY ? 24 : time.getHour()) : 31);
228         final int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255);
229         final int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3);
230         final int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3);
231         final int dowByte = (dow == null ? 0 : dow.getValue());
232         int b = (month.getValue() << 28) +          // 4 bits
233                 ((dom + 32) << 22) +                // 6 bits
234                 (dowByte << 19) +                   // 3 bits
235                 (timeByte << 14) +                  // 5 bits
236                 (timeDefinition.ordinal() << 12) +  // 2 bits
237                 (stdOffsetByte << 4) +              // 8 bits
238                 (beforeByte << 2) +                 // 2 bits
239                 afterByte;                          // 2 bits
240         out.writeInt(b);
241         if (timeByte == 31) {
242             out.writeInt(timeSecs);
243         }
244         if (stdOffsetByte == 255) {
245             out.writeInt(stdOffset);
246         }
247         if (beforeByte == 3) {
248             out.writeInt(offsetBefore.getTotalSeconds());
249         }
250         if (afterByte == 3) {
251             out.writeInt(offsetAfter.getTotalSeconds());
252         }
253     }
254 
255     /**
256      * Reads the state from the stream.
257      *
258      * @param in  the input stream, not null
259      * @return the created object, not null
260      * @throws IOException if an error occurs
261      */
readExternal(DataInput in)262     static ZoneOffsetTransitionRule readExternal(DataInput in) throws IOException {
263         int data = in.readInt();
264         Month month = Month.of(data >>> 28);
265         int dom = ((data & (63 << 22)) >>> 22) - 32;
266         int dowByte = (data & (7 << 19)) >>> 19;
267         DayOfWeek dow = dowByte == 0 ? null : DayOfWeek.of(dowByte);
268         int timeByte = (data & (31 << 14)) >>> 14;
269         TimeDefinition defn = TimeDefinition.values()[(data & (3 << 12)) >>> 12];
270         int stdByte = (data & (255 << 4)) >>> 4;
271         int beforeByte = (data & (3 << 2)) >>> 2;
272         int afterByte = (data & 3);
273         int timeOfDaysSecs = (timeByte == 31 ? in.readInt() : timeByte * 3600);
274         ZoneOffset std = (stdByte == 255 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds((stdByte - 128) * 900));
275         ZoneOffset before = (beforeByte == 3 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + beforeByte * 1800));
276         ZoneOffset after = (afterByte == 3 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + afterByte * 1800));
277         // only bit of validation that we need to copy from public of() method
278         if (dom < -28 || dom > 31 || dom == 0) {
279             throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
280         }
281         LocalTime time = LocalTime.ofSecondOfDay(Jdk8Methods.floorMod(timeOfDaysSecs, SECS_PER_DAY));
282         int adjustDays = Jdk8Methods.floorDiv(timeOfDaysSecs, SECS_PER_DAY);
283         return new ZoneOffsetTransitionRule(month, dom, dow, time, adjustDays, defn, std, before, after);
284     }
285 
286     //-----------------------------------------------------------------------
287     /**
288      * Gets the month of the transition.
289      * <p>
290      * If the rule defines an exact date then the month is the month of that date.
291      * <p>
292      * If the rule defines a week where the transition might occur, then the month
293      * if the month of either the earliest or latest possible date of the cutover.
294      *
295      * @return the month of the transition, not null
296      */
getMonth()297     public Month getMonth() {
298         return month;
299     }
300 
301     /**
302      * Gets the indicator of the day-of-month of the transition.
303      * <p>
304      * If the rule defines an exact date then the day is the month of that date.
305      * <p>
306      * If the rule defines a week where the transition might occur, then the day
307      * defines either the start of the end of the transition week.
308      * <p>
309      * If the value is positive, then it represents a normal day-of-month, and is the
310      * earliest possible date that the transition can be.
311      * The date may refer to 29th February which should be treated as 1st March in non-leap years.
312      * <p>
313      * If the value is negative, then it represents the number of days back from the
314      * end of the month where {@code -1} is the last day of the month.
315      * In this case, the day identified is the latest possible date that the transition can be.
316      *
317      * @return the day-of-month indicator, from -28 to 31 excluding 0
318      */
getDayOfMonthIndicator()319     public int getDayOfMonthIndicator() {
320         return dom;
321     }
322 
323     /**
324      * Gets the day-of-week of the transition.
325      * <p>
326      * If the rule defines an exact date then this returns null.
327      * <p>
328      * If the rule defines a week where the cutover might occur, then this method
329      * returns the day-of-week that the month-day will be adjusted to.
330      * If the day is positive then the adjustment is later.
331      * If the day is negative then the adjustment is earlier.
332      *
333      * @return the day-of-week that the transition occurs, null if the rule defines an exact date
334      */
getDayOfWeek()335     public DayOfWeek getDayOfWeek() {
336         return dow;
337     }
338 
339     /**
340      * Gets the local time of day of the transition which must be checked with
341      * {@link #isMidnightEndOfDay()}.
342      * <p>
343      * The time is converted into an instant using the time definition.
344      *
345      * @return the local time of day of the transition, not null
346      */
getLocalTime()347     public LocalTime getLocalTime() {
348         return time;
349     }
350 
351     /**
352      * Is the transition local time midnight at the end of day.
353      * <p>
354      * The transition may be represented as occurring at 24:00.
355      *
356      * @return whether a local time of midnight is at the start or end of the day
357      */
isMidnightEndOfDay()358     public boolean isMidnightEndOfDay() {
359         return adjustDays == 1 && time.equals(LocalTime.MIDNIGHT);
360     }
361 
362     /**
363      * Gets the time definition, specifying how to convert the time to an instant.
364      * <p>
365      * The local time can be converted to an instant using the standard offset,
366      * the wall offset or UTC.
367      *
368      * @return the time definition, not null
369      */
getTimeDefinition()370     public TimeDefinition getTimeDefinition() {
371         return timeDefinition;
372     }
373 
374     /**
375      * Gets the standard offset in force at the transition.
376      *
377      * @return the standard offset, not null
378      */
getStandardOffset()379     public ZoneOffset getStandardOffset() {
380         return standardOffset;
381     }
382 
383     /**
384      * Gets the offset before the transition.
385      *
386      * @return the offset before, not null
387      */
getOffsetBefore()388     public ZoneOffset getOffsetBefore() {
389         return offsetBefore;
390     }
391 
392     /**
393      * Gets the offset after the transition.
394      *
395      * @return the offset after, not null
396      */
getOffsetAfter()397     public ZoneOffset getOffsetAfter() {
398         return offsetAfter;
399     }
400 
401     //-----------------------------------------------------------------------
402     /**
403      * Creates a transition instance for the specified year.
404      * <p>
405      * Calculations are performed using the ISO-8601 chronology.
406      *
407      * @param year  the year to create a transition for, not null
408      * @return the transition instance, not null
409      */
createTransition(int year)410     public ZoneOffsetTransition createTransition(int year) {
411         LocalDate date;
412         if (dom < 0) {
413             date = LocalDate.of(year, month, month.length(IsoChronology.INSTANCE.isLeapYear(year)) + 1 + dom);
414             if (dow != null) {
415                 date = date.with(previousOrSame(dow));
416             }
417         } else {
418             date = LocalDate.of(year, month, dom);
419             if (dow != null) {
420                 date = date.with(nextOrSame(dow));
421             }
422         }
423         LocalDateTime localDT = LocalDateTime.of(date.plusDays(adjustDays), time);
424         LocalDateTime transition = timeDefinition.createDateTime(localDT, standardOffset, offsetBefore);
425         return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter);
426     }
427 
428     //-----------------------------------------------------------------------
429     /**
430      * Checks if this object equals another.
431      * <p>
432      * The entire state of the object is compared.
433      *
434      * @param otherRule  the other object to compare to, null returns false
435      * @return true if equal
436      */
437     @Override
equals(Object otherRule)438     public boolean equals(Object otherRule) {
439         if (otherRule == this) {
440             return true;
441         }
442         if (otherRule instanceof ZoneOffsetTransitionRule) {
443             ZoneOffsetTransitionRule other = (ZoneOffsetTransitionRule) otherRule;
444             return month == other.month && dom == other.dom && dow == other.dow &&
445                 timeDefinition == other.timeDefinition &&
446                 adjustDays == other.adjustDays &&
447                 time.equals(other.time) &&
448                 standardOffset.equals(other.standardOffset) &&
449                 offsetBefore.equals(other.offsetBefore) &&
450                 offsetAfter.equals(other.offsetAfter);
451         }
452         return false;
453     }
454 
455     /**
456      * Returns a suitable hash code.
457      *
458      * @return the hash code
459      */
460     @Override
hashCode()461     public int hashCode() {
462         int hash = ((time.toSecondOfDay() + adjustDays) << 15) +
463                 (month.ordinal() << 11) + ((dom + 32) << 5) +
464                 ((dow == null ? 7 : dow.ordinal()) << 2) + (timeDefinition.ordinal());
465         return hash ^ standardOffset.hashCode() ^
466                 offsetBefore.hashCode() ^ offsetAfter.hashCode();
467     }
468 
469     //-----------------------------------------------------------------------
470     /**
471      * Returns a string describing this object.
472      *
473      * @return a string for debugging, not null
474      */
475     @Override
toString()476     public String toString() {
477         StringBuilder buf = new StringBuilder();
478         buf.append("TransitionRule[")
479             .append(offsetBefore.compareTo(offsetAfter) > 0 ? "Gap " : "Overlap ")
480             .append(offsetBefore).append(" to ").append(offsetAfter).append(", ");
481         if (dow != null) {
482             if (dom == -1) {
483                 buf.append(dow.name()).append(" on or before last day of ").append(month.name());
484             } else if (dom < 0) {
485                 buf.append(dow.name()).append(" on or before last day minus ").append(-dom - 1).append(" of ").append(month.name());
486             } else {
487                 buf.append(dow.name()).append(" on or after ").append(month.name()).append(' ').append(dom);
488             }
489         } else {
490             buf.append(month.name()).append(' ').append(dom);
491         }
492         buf.append(" at ");
493         if (adjustDays == 0) {
494             buf.append(time);
495         } else {
496             long timeOfDaysMins = time.toSecondOfDay() / 60 + adjustDays * 24 * 60;
497             appendZeroPad(buf, Jdk8Methods.floorDiv(timeOfDaysMins, 60));
498             buf.append(':');
499             appendZeroPad(buf, Jdk8Methods.floorMod(timeOfDaysMins, 60));
500         }
501         buf.append(" ").append(timeDefinition)
502             .append(", standard offset ").append(standardOffset)
503             .append(']');
504         return buf.toString();
505     }
506 
appendZeroPad(StringBuilder sb, long number)507     private void appendZeroPad(StringBuilder sb, long number) {
508         if (number < 10) {
509             sb.append(0);
510         }
511         sb.append(number);
512     }
513 
514     //-----------------------------------------------------------------------
515     /**
516      * A definition of the way a local time can be converted to the actual
517      * transition date-time.
518      * <p>
519      * Time zone rules are expressed in one of three ways:
520      * <p><ul>
521      * <li>Relative to UTC</li>
522      * <li>Relative to the standard offset in force</li>
523      * <li>Relative to the wall offset (what you would see on a clock on the wall)</li>
524      * </ul><p>
525      */
526     public static enum TimeDefinition {
527         /** The local date-time is expressed in terms of the UTC offset. */
528         UTC,
529         /** The local date-time is expressed in terms of the wall offset. */
530         WALL,
531         /** The local date-time is expressed in terms of the standard offset. */
532         STANDARD;
533 
534         /**
535          * Converts the specified local date-time to the local date-time actually
536          * seen on a wall clock.
537          * <p>
538          * This method converts using the type of this enum.
539          * The output is defined relative to the 'before' offset of the transition.
540          * <p>
541          * The UTC type uses the UTC offset.
542          * The STANDARD type uses the standard offset.
543          * The WALL type returns the input date-time.
544          * The result is intended for use with the wall-offset.
545          *
546          * @param dateTime  the local date-time, not null
547          * @param standardOffset  the standard offset, not null
548          * @param wallOffset  the wall offset, not null
549          * @return the date-time relative to the wall/before offset, not null
550          */
createDateTime(LocalDateTime dateTime, ZoneOffset standardOffset, ZoneOffset wallOffset)551         public LocalDateTime createDateTime(LocalDateTime dateTime, ZoneOffset standardOffset, ZoneOffset wallOffset) {
552             switch (this) {
553                 case UTC: {
554                     int difference = wallOffset.getTotalSeconds() - ZoneOffset.UTC.getTotalSeconds();
555                     return dateTime.plusSeconds(difference);
556                 }
557                 case STANDARD: {
558                     int difference = wallOffset.getTotalSeconds() - standardOffset.getTotalSeconds();
559                     return dateTime.plusSeconds(difference);
560                 }
561                 default:  // WALL
562                     return dateTime;
563             }
564         }
565     }
566 
567 }
568