/* * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* * This file is available under and governed by the GNU General Public * License version 2 only, as published by the Free Software Foundation. * However, the following notice accompanied the original version of this * file: * * Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of JSR-310 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package build.tools.tzdb; import static build.tools.tzdb.Utils.*; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; /** * A mutable builder used to create all the rules for a historic time-zone. *

* The rules of a time-zone describe how the offset changes over time. * The rules are created by building windows on the time-line within which * the different rules apply. The rules may be one of two kinds: *

* *

Implementation notes

* This class is a mutable builder used to create zone instances. * It must only be used from a single thread. * The created instances are immutable and thread-safe. * * @since 1.8 */ public class ZoneRulesBuilder { /** * The list of windows. */ private List windowList = new ArrayList<>(); //----------------------------------------------------------------------- /** * Constructs an instance of the builder that can be used to create zone rules. *

* The builder is used by adding one or more windows representing portions * of the time-line. The standard offset from UTC/Greenwich will be constant * within a window, although two adjacent windows can have the same standard offset. *

* Within each window, there can either be a * {@link #setFixedSavingsToWindow fixed savings amount} or a * {@link #addRuleToWindow list of rules}. */ public ZoneRulesBuilder() { } //----------------------------------------------------------------------- /** * Adds a window to the builder that can be used to filter a set of rules. *

* This method defines and adds a window to the zone where the standard offset is specified. * The window limits the effect of subsequent additions of transition rules * or fixed savings. If neither rules or fixed savings are added to the window * then the window will default to no savings. *

* Each window must be added sequentially, as the start instant of the window * is derived from the until instant of the previous window. * * @param standardOffset the standard offset, not null * @param until the date-time that the offset applies until, not null * @param untilDefinition the time type for the until date-time, not null * @return this, for chaining * @throws IllegalStateException if the window order is invalid */ public ZoneRulesBuilder addWindow( ZoneOffset standardOffset, LocalDateTime until, TimeDefinition untilDefinition) { Objects.requireNonNull(standardOffset, "standardOffset"); Objects.requireNonNull(until, "until"); Objects.requireNonNull(untilDefinition, "untilDefinition"); TZWindow window = new TZWindow(standardOffset, until, untilDefinition); if (windowList.size() > 0) { TZWindow previous = windowList.get(windowList.size() - 1); window.validateWindowOrder(previous); } windowList.add(window); return this; } /** * Adds a window that applies until the end of time to the builder that can be * used to filter a set of rules. *

* This method defines and adds a window to the zone where the standard offset is specified. * The window limits the effect of subsequent additions of transition rules * or fixed savings. If neither rules or fixed savings are added to the window * then the window will default to no savings. *

* This must be added after all other windows. * No more windows can be added after this one. * * @param standardOffset the standard offset, not null * @return this, for chaining * @throws IllegalStateException if a forever window has already been added */ public ZoneRulesBuilder addWindowForever(ZoneOffset standardOffset) { return addWindow(standardOffset, LocalDateTime.MAX, TimeDefinition.WALL); } //----------------------------------------------------------------------- /** * Sets the previously added window to have fixed savings. *

* Setting a window to have fixed savings simply means that a single daylight * savings amount applies throughout the window. The window could be small, * such as a single summer, or large, such as a multi-year daylight savings. *

* A window can either have fixed savings or rules but not both. * * @param fixedSavingAmountSecs the amount of saving to use for the whole window, not null * @return this, for chaining * @throws IllegalStateException if no window has yet been added * @throws IllegalStateException if the window already has rules */ public ZoneRulesBuilder setFixedSavingsToWindow(int fixedSavingAmountSecs) { if (windowList.isEmpty()) { throw new IllegalStateException("Must add a window before setting the fixed savings"); } TZWindow window = windowList.get(windowList.size() - 1); window.setFixedSavings(fixedSavingAmountSecs); return this; } //----------------------------------------------------------------------- /** * Adds a single transition rule to the current window. *

* This adds a rule such that the offset, expressed as a daylight savings amount, * changes at the specified date-time. * * @param transitionDateTime the date-time that the transition occurs as defined by timeDefintion, not null * @param timeDefinition the definition of how to convert local to actual time, not null * @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds * @return this, for chaining * @throws IllegalStateException if no window has yet been added * @throws IllegalStateException if the window already has fixed savings * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules */ public ZoneRulesBuilder addRuleToWindow( LocalDateTime transitionDateTime, TimeDefinition timeDefinition, int savingAmountSecs) { Objects.requireNonNull(transitionDateTime, "transitionDateTime"); return addRuleToWindow( transitionDateTime.getYear(), transitionDateTime.getYear(), transitionDateTime.getMonth(), transitionDateTime.getDayOfMonth(), -1, transitionDateTime.getTime(), false, timeDefinition, savingAmountSecs); } /** * Adds a single transition rule to the current window. *

* This adds a rule such that the offset, expressed as a daylight savings amount, * changes at the specified date-time. * * @param year the year of the transition, from MIN_YEAR to MAX_YEAR * @param month the month of the transition, not null * @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek, * from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month * @param time the time that the transition occurs as defined by timeDefintion, not null * @param timeEndOfDay whether midnight is at the end of day * @param timeDefinition the definition of how to convert local to actual time, not null * @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds * @return this, for chaining * @throws DateTimeException if a date-time field is out of range * @throws IllegalStateException if no window has yet been added * @throws IllegalStateException if the window already has fixed savings * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules */ public ZoneRulesBuilder addRuleToWindow( int year, int month, int dayOfMonthIndicator, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAmountSecs) { return addRuleToWindow(year, year, month, dayOfMonthIndicator, -1, time, timeEndOfDay, timeDefinition, savingAmountSecs); } /** * Adds a multi-year transition rule to the current window. *

* This adds a rule such that the offset, expressed as a daylight savings amount, * changes at the specified date-time for each year in the range. * * @param startYear the start year of the rule, from MIN_YEAR to MAX_YEAR * @param endYear the end year of the rule, from MIN_YEAR to MAX_YEAR * @param month the month of the transition, from 1 to 12 * @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek, * from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month * @param dayOfWeek the day-of-week to adjust to, -1 if day-of-month should not be adjusted * @param time the time that the transition occurs as defined by timeDefintion, not null * @param timeEndOfDay whether midnight is at the end of day * @param timeDefinition the definition of how to convert local to actual time, not null * @param savingAmountSecs the amount of saving from the standard offset after the transition in seconds * @return this, for chaining * @throws DateTimeException if a date-time field is out of range * @throws IllegalArgumentException if the day of month indicator is invalid * @throws IllegalArgumentException if the end of day midnight flag does not match the time * @throws IllegalStateException if no window has yet been added * @throws IllegalStateException if the window already has fixed savings * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules */ public ZoneRulesBuilder addRuleToWindow( int startYear, int endYear, int month, int dayOfMonthIndicator, int dayOfWeek, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAmountSecs) { Objects.requireNonNull(time, "time"); Objects.requireNonNull(timeDefinition, "timeDefinition"); if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) { throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero"); } if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) { throw new IllegalArgumentException("Time must be midnight when end of day flag is true"); } if (windowList.isEmpty()) { throw new IllegalStateException("Must add a window before adding a rule"); } TZWindow window = windowList.get(windowList.size() - 1); window.addRule(startYear, endYear, month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, savingAmountSecs); return this; } //----------------------------------------------------------------------- /** * Completes the build converting the builder to a set of time-zone rules. *

* Calling this method alters the state of the builder. * Further rules should not be added to this builder once this method is called. * * @param zoneId the time-zone ID, not null * @return the zone rules, not null * @throws IllegalStateException if no windows have been added * @throws IllegalStateException if there is only one rule defined as being forever for any given window */ public ZoneRules toRules(String zoneId) { Objects.requireNonNull(zoneId, "zoneId"); if (windowList.isEmpty()) { throw new IllegalStateException("No windows have been added to the builder"); } final List standardTransitionList = new ArrayList<>(4); final List transitionList = new ArrayList<>(256); final List lastTransitionRuleList = new ArrayList<>(2); // initialize the standard offset calculation final TZWindow firstWindow = windowList.get(0); ZoneOffset loopStandardOffset = firstWindow.standardOffset; int loopSavings = 0; if (firstWindow.fixedSavingAmountSecs != null) { loopSavings = firstWindow.fixedSavingAmountSecs; } final ZoneOffset firstWallOffset = ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + loopSavings); LocalDateTime loopWindowStart = LocalDateTime.of(YEAR_MIN_VALUE, 1, 1, 0, 0); ZoneOffset loopWindowOffset = firstWallOffset; // build the windows and rules to interesting data for (TZWindow window : windowList) { // tidy the state window.tidy(loopWindowStart.getYear()); // calculate effective savings at the start of the window Integer effectiveSavings = window.fixedSavingAmountSecs; if (effectiveSavings == null) { // apply rules from this window together with the standard offset and // savings from the last window to find the savings amount applicable // at start of this window effectiveSavings = 0; for (TZRule rule : window.ruleList) { if (rule.toEpochSecond(loopStandardOffset, loopSavings) > loopWindowStart.toEpochSecond(loopWindowOffset)) { // previous savings amount found, which could be the savings amount at // the instant that the window starts (hence isAfter) break; } effectiveSavings = rule.savingAmountSecs; } } // check if standard offset changed, and update it if (loopStandardOffset.equals(window.standardOffset) == false) { standardTransitionList.add( new ZoneOffsetTransition( LocalDateTime.ofEpochSecond(loopWindowStart.toEpochSecond(loopWindowOffset), 0, loopStandardOffset), loopStandardOffset, window.standardOffset)); loopStandardOffset = window.standardOffset; } // check if the start of the window represents a transition ZoneOffset effectiveWallOffset = ZoneOffset.ofTotalSeconds(loopStandardOffset.getTotalSeconds() + effectiveSavings); if (loopWindowOffset.equals(effectiveWallOffset) == false) { transitionList.add(new ZoneOffsetTransition(loopWindowStart, loopWindowOffset, effectiveWallOffset)); } loopSavings = effectiveSavings; // apply rules within the window for (TZRule rule : window.ruleList) { if (rule.isTransition(loopSavings)) { ZoneOffsetTransition trans = rule.toTransition(loopStandardOffset, loopSavings); if (trans.toEpochSecond() < loopWindowStart.toEpochSecond(loopWindowOffset) == false && trans.toEpochSecond() < window.createDateTimeEpochSecond(loopSavings)) { transitionList.add(trans); loopSavings = rule.savingAmountSecs; } } } // calculate last rules for (TZRule lastRule : window.lastRuleList) { lastTransitionRuleList.add(lastRule.toTransitionRule(loopStandardOffset, loopSavings)); loopSavings = lastRule.savingAmountSecs; } // finally we can calculate the true end of the window, passing it to the next window loopWindowOffset = window.createWallOffset(loopSavings); loopWindowStart = LocalDateTime.ofEpochSecond( window.createDateTimeEpochSecond(loopSavings), 0, loopWindowOffset); } return new ZoneRules( firstWindow.standardOffset, firstWallOffset, standardTransitionList, transitionList, lastTransitionRuleList); } //----------------------------------------------------------------------- /** * A definition of a window in the time-line. * The window will have one standard offset and will either have a * fixed DST savings or a set of rules. */ class TZWindow { /** The standard offset during the window, not null. */ private final ZoneOffset standardOffset; /** The end local time, not null. */ private final LocalDateTime windowEnd; /** The type of the end time, not null. */ private final TimeDefinition timeDefinition; /** The fixed amount of the saving to be applied during this window. */ private Integer fixedSavingAmountSecs; /** The rules for the current window. */ private List ruleList = new ArrayList<>(); /** The latest year that the last year starts at. */ private int maxLastRuleStartYear = YEAR_MIN_VALUE; /** The last rules. */ private List lastRuleList = new ArrayList<>(); /** * Constructor. * * @param standardOffset the standard offset applicable during the window, not null * @param windowEnd the end of the window, relative to the time definition, null if forever * @param timeDefinition the time definition for calculating the true end, not null */ TZWindow( ZoneOffset standardOffset, LocalDateTime windowEnd, TimeDefinition timeDefinition) { super(); this.windowEnd = windowEnd; this.timeDefinition = timeDefinition; this.standardOffset = standardOffset; } /** * Sets the fixed savings amount for the window. * * @param fixedSavingAmount the amount of daylight saving to apply throughout the window, may be null * @throws IllegalStateException if the window already has rules */ void setFixedSavings(int fixedSavingAmount) { if (ruleList.size() > 0 || lastRuleList.size() > 0) { throw new IllegalStateException("Window has DST rules, so cannot have fixed savings"); } this.fixedSavingAmountSecs = fixedSavingAmount; } /** * Adds a rule to the current window. * * @param startYear the start year of the rule, from MIN_YEAR to MAX_YEAR * @param endYear the end year of the rule, from MIN_YEAR to MAX_YEAR * @param month the month of the transition, not null * @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek, * from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month * @param dayOfWeek the day-of-week to adjust to, null if day-of-month should not be adjusted * @param time the time that the transition occurs as defined by timeDefintion, not null * @param timeEndOfDay whether midnight is at the end of day * @param timeDefinition the definition of how to convert local to actual time, not null * @param savingAmountSecs the amount of saving from the standard offset in seconds * @throws IllegalStateException if the window already has fixed savings * @throws IllegalStateException if the window has reached the maximum capacity of 2000 rules */ void addRule( int startYear, int endYear, int month, int dayOfMonthIndicator, int dayOfWeek, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAmountSecs) { if (fixedSavingAmountSecs != null) { throw new IllegalStateException("Window has a fixed DST saving, so cannot have DST rules"); } if (ruleList.size() >= 2000) { throw new IllegalStateException("Window has reached the maximum number of allowed rules"); } boolean lastRule = false; if (endYear == YEAR_MAX_VALUE) { lastRule = true; endYear = startYear; } int year = startYear; while (year <= endYear) { TZRule rule = new TZRule(year, month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, savingAmountSecs); if (lastRule) { lastRuleList.add(rule); maxLastRuleStartYear = Math.max(startYear, maxLastRuleStartYear); } else { ruleList.add(rule); } year++; } } /** * Validates that this window is after the previous one. * * @param previous the previous window, not null * @throws IllegalStateException if the window order is invalid */ void validateWindowOrder(TZWindow previous) { if (windowEnd.compareTo(previous.windowEnd) < 0) { throw new IllegalStateException("Windows must be added in date-time order: " + windowEnd + " < " + previous.windowEnd); } } /** * Adds rules to make the last rules all start from the same year. * Also add one more year to avoid weird case where penultimate year has odd offset. * * @param windowStartYear the window start year * @throws IllegalStateException if there is only one rule defined as being forever */ void tidy(int windowStartYear) { if (lastRuleList.size() == 1) { throw new IllegalStateException("Cannot have only one rule defined as being forever"); } // handle last rules if (windowEnd.equals(LocalDateTime.MAX)) { // setup at least one real rule, which closes off other windows nicely maxLastRuleStartYear = Math.max(maxLastRuleStartYear, windowStartYear) + 1; for (TZRule lastRule : lastRuleList) { addRule(lastRule.year, maxLastRuleStartYear, lastRule.month, lastRule.dayOfMonthIndicator, lastRule.dayOfWeek, lastRule.time, lastRule.timeEndOfDay, lastRule.timeDefinition, lastRule.savingAmountSecs); lastRule.year = maxLastRuleStartYear + 1; } if (maxLastRuleStartYear == YEAR_MAX_VALUE) { lastRuleList.clear(); } else { maxLastRuleStartYear++; } } else { // convert all within the endYear limit int endYear = windowEnd.getYear(); for (TZRule lastRule : lastRuleList) { addRule(lastRule.year, endYear + 1, lastRule.month, lastRule.dayOfMonthIndicator, lastRule.dayOfWeek, lastRule.time, lastRule.timeEndOfDay, lastRule.timeDefinition, lastRule.savingAmountSecs); } lastRuleList.clear(); maxLastRuleStartYear = YEAR_MAX_VALUE; } // ensure lists are sorted Collections.sort(ruleList); Collections.sort(lastRuleList); // default fixed savings to zero if (ruleList.size() == 0 && fixedSavingAmountSecs == null) { fixedSavingAmountSecs = 0; } } /** * Checks if the window is empty. * * @return true if the window is only a standard offset */ boolean isSingleWindowStandardOffset() { return windowEnd.equals(LocalDateTime.MAX) && timeDefinition == TimeDefinition.WALL && fixedSavingAmountSecs == null && lastRuleList.isEmpty() && ruleList.isEmpty(); } /** * Creates the wall offset for the local date-time at the end of the window. * * @param savingsSecs the amount of savings in use in seconds * @return the created date-time epoch second in the wall offset, not null */ ZoneOffset createWallOffset(int savingsSecs) { return ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsSecs); } /** * Creates the offset date-time for the local date-time at the end of the window. * * @param savingsSecs the amount of savings in use in seconds * @return the created date-time epoch second in the wall offset, not null */ long createDateTimeEpochSecond(int savingsSecs) { ZoneOffset wallOffset = createWallOffset(savingsSecs); LocalDateTime ldt = timeDefinition.createDateTime(windowEnd, standardOffset, wallOffset); return ldt.toEpochSecond(wallOffset); } } //----------------------------------------------------------------------- /** * A definition of the way a local time can be converted to an offset time. */ class TZRule implements Comparable { private int year; private int month; private int dayOfMonthIndicator; private int dayOfWeek; private LocalTime time; private boolean timeEndOfDay; // Whether the local time is end of day. private TimeDefinition timeDefinition; // The type of the time. private int savingAmountSecs; // The amount of the saving to be applied after this point. /** * Constructor. * * @param year the year * @param month the month, value from 1 to 12 * @param dayOfMonthIndicator the day-of-month of the transition, adjusted by dayOfWeek, * from 1 to 31 adjusted later, or -1 to -28 adjusted earlier from the last day of the month * @param dayOfWeek the day-of-week, -1 if day-of-month is exact * @param time the time, not null * @param timeEndOfDay whether midnight is at the end of day * @param timeDefinition the time definition, not null * @param savingAfterSecs the savings amount in seconds */ TZRule(int year, int month, int dayOfMonthIndicator, int dayOfWeek, LocalTime time, boolean timeEndOfDay, TimeDefinition timeDefinition, int savingAfterSecs) { this.year = year; this.month = month; this.dayOfMonthIndicator = dayOfMonthIndicator; this.dayOfWeek = dayOfWeek; this.time = time; this.timeEndOfDay = timeEndOfDay; this.timeDefinition = timeDefinition; this.savingAmountSecs = savingAfterSecs; } /** * Converts this to a transition. * * @param standardOffset the active standard offset, not null * @param savingsBeforeSecs the active savings in seconds * @return the transition, not null */ ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) { // copy of code in ZoneOffsetTransitionRule to avoid infinite loop LocalDate date = toLocalDate(); LocalDateTime ldt = LocalDateTime.of(date, time); ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs); LocalDateTime dt = timeDefinition.createDateTime(ldt, standardOffset, wallOffset); ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingAmountSecs); return new ZoneOffsetTransition(dt, wallOffset, offsetAfter); } /** * Returns the apoch second of this rules with the specified * active standard offset and active savings * * @param standardOffset the active standard offset, not null * @param savingsBeforeSecs the active savings in seconds * @return the transition epoch second */ long toEpochSecond(ZoneOffset standardOffset, int savingsBeforeSecs) { LocalDateTime ldt = LocalDateTime.of(toLocalDate(), time); ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs); return timeDefinition.createDateTime(ldt, standardOffset, wallOffset) .toEpochSecond(wallOffset); } /** * Tests if this a real transition with the active savings in seconds * * @param savingsBeforeSecs the active savings in seconds * @return true, if savings in seconds changes */ boolean isTransition(int savingsBeforeSecs) { return savingAmountSecs != savingsBeforeSecs; } /** * Converts this to a transition rule. * * @param standardOffset the active standard offset, not null * @param savingsBeforeSecs the active savings before the transition in seconds * @return the transition, not null */ ZoneOffsetTransitionRule toTransitionRule(ZoneOffset standardOffset, int savingsBeforeSecs) { // optimize stored format if (dayOfMonthIndicator < 0) { if (month != 2) { // not Month.FEBRUARY dayOfMonthIndicator = maxLengthOfMonth(month) - 6; } } if (timeEndOfDay && dayOfMonthIndicator > 0 && (dayOfMonthIndicator == 28 && month == 2) == false) { LocalDate date = LocalDate.of(2004, month, dayOfMonthIndicator).plusDays(1); // leap-year month = date.getMonth(); dayOfMonthIndicator = date.getDayOfMonth(); if (dayOfWeek != -1) { dayOfWeek = plusDayOfWeek(dayOfWeek, 1); } timeEndOfDay = false; } // build rule return new ZoneOffsetTransitionRule( month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefinition, standardOffset, ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingsBeforeSecs), ZoneOffset.ofTotalSeconds(standardOffset.getTotalSeconds() + savingAmountSecs)); } public int compareTo(TZRule other) { int cmp = year - other.year; cmp = (cmp == 0 ? month - other.month : cmp); if (cmp == 0) { // convert to date to handle dow/domIndicator/timeEndOfDay LocalDate thisDate = toLocalDate(); LocalDate otherDate = other.toLocalDate(); cmp = thisDate.compareTo(otherDate); } cmp = (cmp == 0 ? time.compareTo(other.time) : cmp); return cmp; } private LocalDate toLocalDate() { LocalDate date; if (dayOfMonthIndicator < 0) { int monthLen = lengthOfMonth(month, isLeapYear(year)); date = LocalDate.of(year, month, monthLen + 1 + dayOfMonthIndicator); if (dayOfWeek != -1) { date = previousOrSame(date, dayOfWeek); } } else { date = LocalDate.of(year, month, dayOfMonthIndicator); if (dayOfWeek != -1) { date = nextOrSame(date, dayOfWeek); } } if (timeEndOfDay) { date = date.plusDays(1); } return date; } } }