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 }