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