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