• 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.ChronoField.YEAR;
35 import static org.threeten.bp.temporal.TemporalAdjusters.nextOrSame;
36 import static org.threeten.bp.temporal.TemporalAdjusters.previousOrSame;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 
44 import org.threeten.bp.DateTimeException;
45 import org.threeten.bp.DayOfWeek;
46 import org.threeten.bp.LocalDate;
47 import org.threeten.bp.LocalDateTime;
48 import org.threeten.bp.LocalTime;
49 import org.threeten.bp.Month;
50 import org.threeten.bp.Year;
51 import org.threeten.bp.ZoneOffset;
52 import org.threeten.bp.chrono.IsoChronology;
53 import org.threeten.bp.jdk8.Jdk8Methods;
54 import org.threeten.bp.zone.ZoneOffsetTransitionRule.TimeDefinition;
55 
56 /**
57  * A mutable builder used to create all the rules for a historic time-zone.
58  * <p>
59  * The rules of a time-zone describe how the offset changes over time.
60  * The rules are created by building windows on the time-line within which
61  * the different rules apply. The rules may be one of two kinds:
62  * <p><ul>
63  * <li>Fixed savings - A single fixed amount of savings from the standard offset will apply.</li>
64  * <li>Rules - A set of one or more rules describe how daylight savings changes during the window.</li>
65  * </ul><p>
66  *
67  * <h3>Specification for implementors</h3>
68  * This class is a mutable builder used to create zone instances.
69  * It must only be used from a single thread.
70  * The created instances are immutable and thread-safe.
71  */
72 class ZoneRulesBuilder {
73 
74     /**
75      * The list of windows.
76      */
77     private List<TZWindow> windowList = new ArrayList<ZoneRulesBuilder.TZWindow>();
78     /**
79      * A map for deduplicating the output.
80      */
81     private Map<Object, Object> deduplicateMap;
82 
83     //-----------------------------------------------------------------------
84     /**
85      * Constructs an instance of the builder that can be used to create zone rules.
86      * <p>
87      * The builder is used by adding one or more windows representing portions
88      * of the time-line. The standard offset from UTC/Greenwich will be constant
89      * within a window, although two adjacent windows can have the same standard offset.
90      * <p>
91      * Within each window, there can either be a
92      * {@link #setFixedSavingsToWindow fixed savings amount} or a
93      * {@link #addRuleToWindow list of rules}.
94      */
ZoneRulesBuilder()95     public ZoneRulesBuilder() {
96     }
97 
98     //-----------------------------------------------------------------------
99     /**
100      * Adds a window to the builder that can be used to filter a set of rules.
101      * <p>
102      * This method defines and adds a window to the zone where the standard offset is specified.
103      * The window limits the effect of subsequent additions of transition rules
104      * or fixed savings. If neither rules or fixed savings are added to the window
105      * then the window will default to no savings.
106      * <p>
107      * Each window must be added sequentially, as the start instant of the window
108      * is derived from the until instant of the previous window.
109      *
110      * @param standardOffset  the standard offset, not null
111      * @param until  the date-time that the offset applies until, not null
112      * @param untilDefinition  the time type for the until date-time, not null
113      * @return this, for chaining
114      * @throws IllegalStateException if the window order is invalid
115      */
addWindow( ZoneOffset standardOffset, LocalDateTime until, TimeDefinition untilDefinition)116     public ZoneRulesBuilder addWindow(
117             ZoneOffset standardOffset,
118             LocalDateTime until,
119             TimeDefinition untilDefinition) {
120         Jdk8Methods.requireNonNull(standardOffset, "standardOffset");
121         Jdk8Methods.requireNonNull(until, "until");
122         Jdk8Methods.requireNonNull(untilDefinition, "untilDefinition");
123         TZWindow window = new TZWindow(standardOffset, until, untilDefinition);
124         if (windowList.size() > 0) {
125             TZWindow previous = windowList.get(windowList.size() - 1);
126             window.validateWindowOrder(previous);
127         }
128         windowList.add(window);
129         return this;
130     }
131 
132     /**
133      * Adds a window that applies until the end of time to the builder that can be
134      * used to filter a set of rules.
135      * <p>
136      * This method defines and adds a window to the zone where the standard offset is specified.
137      * The window limits the effect of subsequent additions of transition rules
138      * or fixed savings. If neither rules or fixed savings are added to the window
139      * then the window will default to no savings.
140      * <p>
141      * This must be added after all other windows.
142      * No more windows can be added after this one.
143      *
144      * @param standardOffset  the standard offset, not null
145      * @return this, for chaining
146      * @throws IllegalStateException if a forever window has already been added
147      */
addWindowForever(ZoneOffset standardOffset)148     public ZoneRulesBuilder addWindowForever(ZoneOffset standardOffset) {
149         return addWindow(standardOffset, LocalDateTime.MAX, TimeDefinition.WALL);
150     }
151 
152     //-----------------------------------------------------------------------
153     /**
154      * Sets the previously added window to have fixed savings.
155      * <p>
156      * Setting a window to have fixed savings simply means that a single daylight
157      * savings amount applies throughout the window. The window could be small,
158      * such as a single summer, or large, such as a multi-year daylight savings.
159      * <p>
160      * A window can either have fixed savings or rules but not both.
161      *
162      * @param fixedSavingAmountSecs  the amount of saving to use for the whole window, not null
163      * @return this, for chaining
164      * @throws IllegalStateException if no window has yet been added
165      * @throws IllegalStateException if the window already has rules
166      */
setFixedSavingsToWindow(int fixedSavingAmountSecs)167     public ZoneRulesBuilder setFixedSavingsToWindow(int fixedSavingAmountSecs) {
168         if (windowList.isEmpty()) {
169             throw new IllegalStateException("Must add a window before setting the fixed savings");
170         }
171         TZWindow window = windowList.get(windowList.size() - 1);
172         window.setFixedSavings(fixedSavingAmountSecs);
173         return this;
174     }
175 
176     //-----------------------------------------------------------------------
177     /**
178      * Adds a single transition rule to the current window.
179      * <p>
180      * This adds a rule such that the offset, expressed as a daylight savings amount,
181      * changes at the specified date-time.
182      *
183      * @param transitionDateTime  the date-time that the transition occurs as defined by timeDefintion, not null
184      * @param timeDefinition  the definition of how to convert local to actual time, not null
185      * @param savingAmountSecs  the amount of saving from the standard offset after the transition in seconds
186      * @return this, for chaining
187      * @throws IllegalStateException if no window has yet been added
188      * @throws IllegalStateException if the window already has fixed savings
189      * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
190      */
addRuleToWindow( LocalDateTime transitionDateTime, TimeDefinition timeDefinition, int savingAmountSecs)191     public ZoneRulesBuilder addRuleToWindow(
192             LocalDateTime transitionDateTime,
193             TimeDefinition timeDefinition,
194             int savingAmountSecs) {
195         Jdk8Methods.requireNonNull(transitionDateTime, "transitionDateTime");
196         return addRuleToWindow(
197                 transitionDateTime.getYear(), transitionDateTime.getYear(),
198                 transitionDateTime.getMonth(), transitionDateTime.getDayOfMonth(),
199                 null, transitionDateTime.toLocalTime(), false, timeDefinition, savingAmountSecs);
200     }
201 
202     /**
203      * Adds a single transition rule to the current window.
204      * <p>
205      * This adds a rule such that the offset, expressed as a daylight savings amount,
206      * changes at the specified date-time.
207      *
208      * @param year  the year of the transition, from MIN_VALUE to MAX_VALUE
209      * @param month  the month of the transition, not null
210      * @param dayOfMonthIndicator  the day-of-month of the transition, adjusted by dayOfWeek,
211      *   from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
212      * @param time  the time that the transition occurs as defined by timeDefintion, not null
213      * @param timeEndOfDay  whether midnight is at the end of day
214      * @param timeDefinition  the definition of how to convert local to actual time, not null
215      * @param savingAmountSecs  the amount of saving from the standard offset after the transition in seconds
216      * @return this, for chaining
217      * @throws DateTimeException if a date-time field is out of range
218      * @throws IllegalStateException if no window has yet been added
219      * @throws IllegalStateException if the window already has fixed savings
220      * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
221      */
addRuleToWindow( int year, Month month, int dayOfMonthIndicator, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAmountSecs)222     public ZoneRulesBuilder addRuleToWindow(
223             int year,
224             Month month,
225             int dayOfMonthIndicator,
226             LocalTime time,
227             boolean timeEndOfDay,
228             TimeDefinition timeDefinition,
229             int savingAmountSecs) {
230         return addRuleToWindow(year, year, month, dayOfMonthIndicator, null, time, timeEndOfDay, timeDefinition, savingAmountSecs);
231     }
232 
233     /**
234      * Adds a multi-year transition rule to the current window.
235      * <p>
236      * This adds a rule such that the offset, expressed as a daylight savings amount,
237      * changes at the specified date-time for each year in the range.
238      *
239      * @param startYear  the start year of the rule, from MIN_VALUE to MAX_VALUE
240      * @param endYear  the end year of the rule, from MIN_VALUE to MAX_VALUE
241      * @param month  the month of the transition, not null
242      * @param dayOfMonthIndicator  the day-of-month of the transition, adjusted by dayOfWeek,
243      *   from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
244      * @param dayOfWeek  the day-of-week to adjust to, null if day-of-month should not be adjusted
245      * @param time  the time that the transition occurs as defined by timeDefintion, not null
246      * @param timeEndOfDay  whether midnight is at the end of day
247      * @param timeDefinition  the definition of how to convert local to actual time, not null
248      * @param savingAmountSecs  the amount of saving from the standard offset after the transition in seconds
249      * @return this, for chaining
250      * @throws DateTimeException if a date-time field is out of range
251      * @throws IllegalArgumentException if the day of month indicator is invalid
252      * @throws IllegalArgumentException if the end of day midnight flag does not match the time
253      * @throws IllegalStateException if no window has yet been added
254      * @throws IllegalStateException if the window already has fixed savings
255      * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
256      */
addRuleToWindow( int startYear, int endYear, Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAmountSecs)257     public ZoneRulesBuilder addRuleToWindow(
258             int startYear,
259             int endYear,
260             Month month,
261             int dayOfMonthIndicator,
262             DayOfWeek dayOfWeek,
263             LocalTime time,
264             boolean timeEndOfDay,
265             TimeDefinition timeDefinition,
266             int savingAmountSecs) {
267         Jdk8Methods.requireNonNull(month, "month");
268         Jdk8Methods.requireNonNull(time, "time");
269         Jdk8Methods.requireNonNull(timeDefinition, "timeDefinition");
270         YEAR.checkValidValue(startYear);
271         YEAR.checkValidValue(endYear);
272         if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) {
273             throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
274         }
275         if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) {
276             throw new IllegalArgumentException("Time must be midnight when end of day flag is true");
277         }
278         if (windowList.isEmpty()) {
279             throw new IllegalStateException("Must add a window before adding a rule");
280         }
281         TZWindow window = windowList.get(windowList.size() - 1);
282         window.addRule(startYear, endYear, month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay ? 1 : 0, timeDefinition, savingAmountSecs);
283         return this;
284     }
285 
addRuleToWindow( int startYear, int endYear, Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, int adjustDays, TimeDefinition timeDefinition, int savingAmountSecs)286     ZoneRulesBuilder addRuleToWindow(
287             int startYear,
288             int endYear,
289             Month month,
290             int dayOfMonthIndicator,
291             DayOfWeek dayOfWeek,
292             LocalTime time,
293             int adjustDays,
294             TimeDefinition timeDefinition,
295             int savingAmountSecs) {
296         Jdk8Methods.requireNonNull(month, "month");
297         Jdk8Methods.requireNonNull(timeDefinition, "timeDefinition");
298         YEAR.checkValidValue(startYear);
299         YEAR.checkValidValue(endYear);
300         if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) {
301             throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero");
302         }
303         if (windowList.isEmpty()) {
304             throw new IllegalStateException("Must add a window before adding a rule");
305         }
306         TZWindow window = windowList.get(windowList.size() - 1);
307         window.addRule(startYear, endYear, month, dayOfMonthIndicator, dayOfWeek, time, adjustDays, timeDefinition, savingAmountSecs);
308         return this;
309     }
310 
311     //-----------------------------------------------------------------------
312     /**
313      * Completes the build converting the builder to a set of time-zone rules.
314      * <p>
315      * Calling this method alters the state of the builder.
316      * Further rules should not be added to this builder once this method is called.
317      *
318      * @param zoneId  the time-zone ID, not null
319      * @return the zone rules, not null
320      * @throws IllegalStateException if no windows have been added
321      * @throws IllegalStateException if there is only one rule defined as being forever for any given window
322      */
toRules(String zoneId)323     public ZoneRules toRules(String zoneId) {
324         return toRules(zoneId, new HashMap<Object, Object>());
325     }
326 
327     /**
328      * Completes the build converting the builder to a set of time-zone rules.
329      * <p>
330      * Calling this method alters the state of the builder.
331      * Further rules should not be added to this builder once this method is called.
332      *
333      * @param zoneId  the time-zone ID, not null
334      * @param deduplicateMap  a map for deduplicating the values, not null
335      * @return the zone rules, not null
336      * @throws IllegalStateException if no windows have been added
337      * @throws IllegalStateException if there is only one rule defined as being forever for any given window
338      */
toRules(String zoneId, Map<Object, Object> deduplicateMap)339     ZoneRules toRules(String zoneId, Map<Object, Object> deduplicateMap) {
340         Jdk8Methods.requireNonNull(zoneId, "zoneId");
341         this.deduplicateMap = deduplicateMap;
342         if (windowList.isEmpty()) {
343             throw new IllegalStateException("No windows have been added to the builder");
344         }
345 
346         final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<ZoneOffsetTransition>(4);
347         final List<ZoneOffsetTransition> transitionList = new ArrayList<ZoneOffsetTransition>(256);
348         final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<ZoneOffsetTransitionRule>(2);
349 
350         // initialize the standard offset calculation
351         final TZWindow firstWindow = windowList.get(0);
352         ZoneOffset loopStandardOffset = firstWindow.standardOffset;
353         int loopSavings = 0;
354         if (firstWindow.fixedSavingAmountSecs != null) {
355             loopSavings = firstWindow.fixedSavingAmountSecs;
356         }
357         final ZoneOffset firstWallOffset = deduplicate(ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + loopSavings));
358         LocalDateTime loopWindowStart = deduplicate(LocalDateTime.of(Year.MIN_VALUE, 1, 1, 0, 0));
359         ZoneOffset loopWindowOffset = firstWallOffset;
360 
361         // build the windows and rules to interesting data
362         for (TZWindow window : windowList) {
363             // tidy the state
364             window.tidy(loopWindowStart.getYear());
365 
366             // calculate effective savings at the start of the window
367             Integer effectiveSavings = window.fixedSavingAmountSecs;
368             if (effectiveSavings == null) {
369                 // apply rules from this window together with the standard offset and
370                 // savings from the last window to find the savings amount applicable
371                 // at start of this window
372                 effectiveSavings = 0;
373                 for (TZRule rule : window.ruleList) {
374                     ZoneOffsetTransition trans = rule.toTransition(loopStandardOffset, loopSavings);
375                     if (trans.toEpochSecond() > loopWindowStart.toEpochSecond(loopWindowOffset)) {
376                         // previous savings amount found, which could be the savings amount at
377                         // the instant that the window starts (hence isAfter)
378                         break;
379                     }
380                     effectiveSavings = rule.savingAmountSecs;
381                 }
382             }
383 
384             // check if standard offset changed, and update it
385             if (loopStandardOffset.equals(window.standardOffset) == false) {
386                 standardTransitionList.add(deduplicate(
387                     new ZoneOffsetTransition(
388                         LocalDateTime.ofEpochSecond(loopWindowStart.toEpochSecond(loopWindowOffset), 0, loopStandardOffset),
389                         loopStandardOffset, window.standardOffset)));
390                 loopStandardOffset = deduplicate(window.standardOffset);
391             }
392 
393             // check if the start of the window represents a transition
394             ZoneOffset effectiveWallOffset = deduplicate(ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + effectiveSavings));
395             if (loopWindowOffset.equals(effectiveWallOffset) == false) {
396                 ZoneOffsetTransition trans = deduplicate(
397                     new ZoneOffsetTransition(loopWindowStart, loopWindowOffset, effectiveWallOffset));
398                 transitionList.add(trans);
399             }
400             loopSavings = effectiveSavings;
401 
402             // apply rules within the window
403             for (TZRule rule : window.ruleList) {
404                 ZoneOffsetTransition trans = deduplicate(rule.toTransition(loopStandardOffset, loopSavings));
405                 if (trans.toEpochSecond() < loopWindowStart.toEpochSecond(loopWindowOffset) == false &&
406                         trans.toEpochSecond() < window.createDateTimeEpochSecond(loopSavings) &&
407                         trans.getOffsetBefore().equals(trans.getOffsetAfter()) == false) {
408                     transitionList.add(trans);
409                     loopSavings = rule.savingAmountSecs;
410                 }
411             }
412 
413             // calculate last rules
414             for (TZRule lastRule : window.lastRuleList) {
415                 ZoneOffsetTransitionRule transitionRule = deduplicate(lastRule.toTransitionRule(loopStandardOffset, loopSavings));
416                 lastTransitionRuleList.add(transitionRule);
417                 loopSavings = lastRule.savingAmountSecs;
418             }
419 
420             // finally we can calculate the true end of the window, passing it to the next window
421             loopWindowOffset = deduplicate(window.createWallOffset(loopSavings));
422             loopWindowStart = deduplicate(LocalDateTime.ofEpochSecond(
423                     window.createDateTimeEpochSecond(loopSavings), 0, loopWindowOffset));
424         }
425         return new StandardZoneRules(
426                 firstWindow.standardOffset, firstWallOffset, standardTransitionList,
427                 transitionList, lastTransitionRuleList);
428     }
429 
430     /**
431      * Deduplicates an object instance.
432      *
433      * @param <T> the generic type
434      * @param object  the object to deduplicate
435      * @return the deduplicated object
436      */
437     @SuppressWarnings("unchecked")
deduplicate(T object)438     <T> T deduplicate(T object) {
439         if (deduplicateMap.containsKey(object) == false) {
440             deduplicateMap.put(object, object);
441         }
442         return (T) deduplicateMap.get(object);
443     }
444 
445     //-----------------------------------------------------------------------
446     /**
447      * A definition of a window in the time-line.
448      * The window will have one standard offset and will either have a
449      * fixed DST savings or a set of rules.
450      */
451     class TZWindow {
452         /** The standard offset during the window, not null. */
453         private final ZoneOffset standardOffset;
454         /** The end local time, not null. */
455         private final LocalDateTime windowEnd;
456         /** The type of the end time, not null. */
457         private final TimeDefinition timeDefinition;
458 
459         /** The fixed amount of the saving to be applied during this window. */
460         private Integer fixedSavingAmountSecs;
461         /** The rules for the current window. */
462         private List<TZRule> ruleList = new ArrayList<TZRule>();
463         /** The latest year that the last year starts at. */
464         private int maxLastRuleStartYear = Year.MIN_VALUE;
465         /** The last rules. */
466         private List<TZRule> lastRuleList = new ArrayList<TZRule>();
467 
468         /**
469          * Constructor.
470          *
471          * @param standardOffset  the standard offset applicable during the window, not null
472          * @param windowEnd  the end of the window, relative to the time definition, null if forever
473          * @param timeDefinition  the time definition for calculating the true end, not null
474          */
TZWindow( ZoneOffset standardOffset, LocalDateTime windowEnd, TimeDefinition timeDefinition)475         TZWindow(
476                 ZoneOffset standardOffset,
477                 LocalDateTime windowEnd,
478                 TimeDefinition timeDefinition) {
479             super();
480             this.windowEnd = windowEnd;
481             this.timeDefinition = timeDefinition;
482             this.standardOffset = standardOffset;
483         }
484 
485         /**
486          * Sets the fixed savings amount for the window.
487          *
488          * @param fixedSavingAmount  the amount of daylight saving to apply throughout the window, may be null
489          * @throws IllegalStateException if the window already has rules
490          */
setFixedSavings(int fixedSavingAmount)491         void setFixedSavings(int fixedSavingAmount) {
492             if (ruleList.size() > 0 || lastRuleList.size() > 0) {
493                 throw new IllegalStateException("Window has DST rules, so cannot have fixed savings");
494             }
495             this.fixedSavingAmountSecs = fixedSavingAmount;
496         }
497 
498         /**
499          * Adds a rule to the current window.
500          *
501          * @param startYear  the start year of the rule, from MIN_VALUE to MAX_VALUE
502          * @param endYear  the end year of the rule, from MIN_VALUE to MAX_VALUE
503          * @param month  the month of the transition, not null
504          * @param dayOfMonthIndicator  the day-of-month of the transition, adjusted by dayOfWeek,
505          *   from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
506          * @param dayOfWeek  the day-of-week to adjust to, null if day-of-month should not be adjusted
507          * @param time  the time that the transition occurs as defined by timeDefintion, not null
508          * @param adjustDays  the time days adjustment
509          * @param timeDefinition  the definition of how to convert local to actual time, not null
510          * @param savingAmountSecs  the amount of saving from the standard offset in seconds
511          * @throws IllegalStateException if the window already has fixed savings
512          * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules
513          */
addRule( int startYear, int endYear, Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, int adjustDays, TimeDefinition timeDefinition, int savingAmountSecs)514         void addRule(
515                 int startYear,
516                 int endYear,
517                 Month month,
518                 int dayOfMonthIndicator,
519                 DayOfWeek dayOfWeek,
520                 LocalTime time,
521                 int adjustDays,
522                 TimeDefinition timeDefinition,
523                 int savingAmountSecs) {
524 
525             if (fixedSavingAmountSecs != null) {
526                 throw new IllegalStateException("Window has a fixed DST saving, so cannot have DST rules");
527             }
528             if (ruleList.size() >= 2000) {
529                 throw new IllegalStateException("Window has reached the maximum number of allowed rules");
530             }
531             boolean lastRule = false;
532             if (endYear == Year.MAX_VALUE) {
533                 lastRule = true;
534                 endYear = startYear;
535             }
536             int year = startYear;
537             while (year <= endYear) {
538                 TZRule rule = new TZRule(year, month, dayOfMonthIndicator, dayOfWeek, time, adjustDays, timeDefinition, savingAmountSecs);
539                 if (lastRule) {
540                     lastRuleList.add(rule);
541                     maxLastRuleStartYear = Math.max(startYear, maxLastRuleStartYear);
542                 } else {
543                     ruleList.add(rule);
544                 }
545                 year++;
546             }
547         }
548 
549         /**
550          * Validates that this window is after the previous one.
551          *
552          * @param previous  the previous window, not null
553          * @throws IllegalStateException if the window order is invalid
554          */
validateWindowOrder(TZWindow previous)555         void validateWindowOrder(TZWindow previous) {
556             if (windowEnd.isBefore(previous.windowEnd)) {
557                 throw new IllegalStateException("Windows must be added in date-time order: " +
558                         windowEnd + " < " + previous.windowEnd);
559             }
560         }
561 
562         /**
563          * Adds rules to make the last rules all start from the same year.
564          * Also add one more year to avoid weird case where penultimate year has odd offset.
565          *
566          * @param windowStartYear  the window start year
567          * @throws IllegalStateException if there is only one rule defined as being forever
568          */
tidy(int windowStartYear)569         void tidy(int windowStartYear) {
570             if (lastRuleList.size() == 1) {
571                 throw new IllegalStateException("Cannot have only one rule defined as being forever");
572             }
573 
574             // handle last rules
575             if (windowEnd.equals(LocalDateTime.MAX)) {
576                 // setup at least one real rule, which closes off other windows nicely
577                 maxLastRuleStartYear = Math.max(maxLastRuleStartYear, windowStartYear) + 1;
578                 for (TZRule lastRule : lastRuleList) {
579                     addRule(lastRule.year, maxLastRuleStartYear, lastRule.month, lastRule.dayOfMonthIndicator,
580                         lastRule.dayOfWeek, lastRule.time, lastRule.adjustDays, lastRule.timeDefinition, lastRule.savingAmountSecs);
581                     lastRule.year = maxLastRuleStartYear + 1;
582                 }
583                 if (maxLastRuleStartYear == Year.MAX_VALUE) {
584                     lastRuleList.clear();
585                 } else {
586                     maxLastRuleStartYear++;
587                 }
588             } else {
589                 // convert all within the endYear limit
590                 int endYear = windowEnd.getYear();
591                 for (TZRule lastRule : lastRuleList) {
592                     addRule(lastRule.year, endYear + 1, lastRule.month, lastRule.dayOfMonthIndicator,
593                         lastRule.dayOfWeek, lastRule.time, lastRule.adjustDays, lastRule.timeDefinition, lastRule.savingAmountSecs);
594                 }
595                 lastRuleList.clear();
596                 maxLastRuleStartYear = Year.MAX_VALUE;
597             }
598 
599             // ensure lists are sorted
600             Collections.sort(ruleList);
601             Collections.sort(lastRuleList);
602 
603             // default fixed savings to zero
604             if (ruleList.size() == 0 && fixedSavingAmountSecs == null) {
605                 fixedSavingAmountSecs = 0;
606             }
607         }
608 
609         /**
610          * Checks if the window is empty.
611          *
612          * @return true if the window is only a standard offset
613          */
isSingleWindowStandardOffset()614         boolean isSingleWindowStandardOffset() {
615             return windowEnd.equals(LocalDateTime.MAX) && timeDefinition == TimeDefinition.WALL &&
616                     fixedSavingAmountSecs == null && lastRuleList.isEmpty() && ruleList.isEmpty();
617         }
618 
619         /**
620          * Creates the wall offset for the local date-time at the end of the window.
621          *
622          * @param savingsSecs  the amount of savings in use in seconds
623          * @return the created date-time epoch second in the wall offset, not null
624          */
createWallOffset(int savingsSecs)625         ZoneOffset createWallOffset(int savingsSecs) {
626             return ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsSecs);
627         }
628 
629         /**
630          * Creates the offset date-time for the local date-time at the end of the window.
631          *
632          * @param savingsSecs  the amount of savings in use in seconds
633          * @return the created date-time epoch second in the wall offset, not null
634          */
createDateTimeEpochSecond(int savingsSecs)635         long createDateTimeEpochSecond(int savingsSecs) {
636             ZoneOffset wallOffset = createWallOffset(savingsSecs);
637             LocalDateTime ldt = timeDefinition.createDateTime(windowEnd, standardOffset, wallOffset);
638             return ldt.toEpochSecond(wallOffset);
639         }
640     }
641 
642     //-----------------------------------------------------------------------
643     /**
644      * A definition of the way a local time can be converted to an offset time.
645      */
646     class TZRule implements Comparable<TZRule> {
647         /** The year. */
648         private int year;
649         /** The month. */
650         private Month month;
651         /** The day-of-month. */
652         private int dayOfMonthIndicator;
653         /** The day-of-month. */
654         private DayOfWeek dayOfWeek;
655         /** The local time. */
656         private LocalTime time;
657         /** The local time days adjustment. */
658         private int adjustDays;
659         /** The type of the time. */
660         private TimeDefinition timeDefinition;
661         /** The amount of the saving to be applied after this point. */
662         private int savingAmountSecs;
663 
664         /**
665          * Constructor.
666          *
667          * @param year  the year
668          * @param month  the month, not null
669          * @param dayOfMonthIndicator  the day-of-month of the transition, adjusted by dayOfWeek,
670          *   from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month
671          * @param dayOfWeek  the day-of-week, null if day-of-month is exact
672          * @param time  the time, not null
673          * @param adjustDays  the time day adjustment
674          * @param timeDefinition  the time definition, not null
675          * @param savingAfterSecs  the savings amount in seconds
676          */
TZRule(int year, Month month, int dayOfMonthIndicator, DayOfWeek dayOfWeek, LocalTime time, int adjustDays, TimeDefinition timeDefinition, int savingAfterSecs)677         TZRule(int year, Month month, int dayOfMonthIndicator,
678                 DayOfWeek dayOfWeek, LocalTime time, int adjustDays,
679                 TimeDefinition timeDefinition, int savingAfterSecs) {
680             super();
681             this.year = year;
682             this.month = month;
683             this.dayOfMonthIndicator = dayOfMonthIndicator;
684             this.dayOfWeek = dayOfWeek;
685             this.time= time;
686             this.adjustDays= adjustDays;
687             this.timeDefinition = timeDefinition;
688             this.savingAmountSecs = savingAfterSecs;
689         }
690 
691         /**
692          * Converts this to a transition.
693          *
694          * @param standardOffset  the active standard offset, not null
695          * @param savingsBeforeSecs  the active savings in seconds
696          * @return the transition, not null
697          */
toTransition(ZoneOffset standardOffset, int savingsBeforeSecs)698         ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) {
699             // copy of code in ZoneOffsetTransitionRule to avoid infinite loop
700             LocalDate date = toLocalDate();
701             date = deduplicate(date);
702             LocalDateTime ldt = deduplicate(LocalDateTime.of(date.plusDays(adjustDays), time));
703             ZoneOffset wallOffset = deduplicate(ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs));
704             LocalDateTime dt = deduplicate(timeDefinition.createDateTime(ldt, standardOffset, wallOffset));
705             ZoneOffset offsetAfter = deduplicate(ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingAmountSecs));
706             return new ZoneOffsetTransition(dt, wallOffset, offsetAfter);
707         }
708 
709         /**
710          * Converts this to a transition rule.
711          *
712          * @param standardOffset  the active standard offset, not null
713          * @param savingsBeforeSecs  the active savings before the transition in seconds
714          * @return the transition, not null
715          */
toTransitionRule(ZoneOffset standardOffset, int savingsBeforeSecs)716         ZoneOffsetTransitionRule toTransitionRule(ZoneOffset standardOffset, int savingsBeforeSecs) {
717             // optimize stored format
718             if (dayOfMonthIndicator < 0) {
719                 if (month != Month.FEBRUARY) {
720                     dayOfMonthIndicator = month.maxLength() - 6;
721                 }
722             }
723 
724             // build rule
725             ZoneOffsetTransition trans = toTransition(standardOffset, savingsBeforeSecs);
726             return new ZoneOffsetTransitionRule(
727                     month, dayOfMonthIndicator, dayOfWeek, time, adjustDays, timeDefinition,
728                     standardOffset, trans.getOffsetBefore(), trans.getOffsetAfter());
729         }
730 
compareTo(TZRule other)731         public int compareTo(TZRule other) {
732             int cmp = year - other.year;
733             cmp = (cmp == 0 ? month.compareTo(other.month) : cmp);
734             if (cmp == 0) {
735                 // convert to date to handle dow/domIndicator/timeEndOfDay
736                 LocalDate thisDate = toLocalDate();
737                 LocalDate otherDate = other.toLocalDate();
738                 cmp = thisDate.compareTo(otherDate);
739             }
740             if (cmp != 0) {
741                 return cmp;
742             }
743             long timeSecs1 = time.toSecondOfDay() + adjustDays * 86400;
744             long timeSecs2 = other.time.toSecondOfDay() + other.adjustDays * 86400;
745             return timeSecs1 < timeSecs2 ? -1 : (timeSecs1 > timeSecs2 ? 1 : 0);
746         }
747 
toLocalDate()748         private LocalDate toLocalDate() {
749             LocalDate date;
750             if (dayOfMonthIndicator < 0) {
751                 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year));
752                 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonthIndicator);
753                 if (dayOfWeek != null) {
754                     date = date.with(previousOrSame(dayOfWeek));
755                 }
756             } else {
757                 date = LocalDate.of(year, month, dayOfMonthIndicator);
758                 if (dayOfWeek != null) {
759                     date = date.with(nextOrSame(dayOfWeek));
760                 }
761             }
762             return date;
763         }
764 
765         // no equals() or hashCode()
766     }
767 
768 }
769