1 /* 2 * Copyright (c) 2014, 2018, 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 package build.tools.tzdb; 28 29 import java.io.IOException; 30 import java.nio.charset.StandardCharsets; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.nio.file.Paths; 34 import java.util.ArrayList; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Map.Entry; 39 import java.util.NavigableMap; 40 import java.util.Objects; 41 import java.util.Set; 42 import java.util.TreeMap; 43 import java.util.TreeSet; 44 import java.util.concurrent.ConcurrentSkipListMap; 45 import java.time.*; 46 import java.time.Year; 47 import java.time.chrono.IsoChronology; 48 import java.time.temporal.TemporalAdjusters; 49 import java.time.zone.ZoneOffsetTransition; 50 import java.time.zone.ZoneOffsetTransitionRule; 51 import java.time.zone.ZoneOffsetTransitionRule.TimeDefinition; 52 import java.time.zone.ZoneRulesException; 53 54 /** 55 * Compile and build time-zone rules from IANA timezone data 56 * 57 * @author Xueming Shen 58 * @author Stephen Colebourne 59 * @author Michael Nascimento Santos 60 * 61 * @since 9 62 */ 63 64 class TzdbZoneRulesProvider { 65 66 /** 67 * Creates an instance. 68 * 69 * @throws ZoneRulesException if unable to load 70 */ 71 public TzdbZoneRulesProvider(List<Path> files) { 72 try { 73 load(files); 74 } catch (Exception ex) { 75 throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex); 76 } 77 } 78 79 public Set<String> getZoneIds() { 80 return new TreeSet(regionIds); 81 } 82 83 public Map<String, String> getAliasMap() { 84 return links; 85 } 86 87 public ZoneRules getZoneRules(String zoneId) { 88 Object obj = zones.get(zoneId); 89 if (obj == null) { 90 String zoneId0 = zoneId; 91 if (links.containsKey(zoneId)) { 92 zoneId = links.get(zoneId); 93 obj = zones.get(zoneId); 94 } 95 if (obj == null) { 96 // Timezone link can be located in 'backward' file and it 97 // can refer to another link, so we need to check for 98 // link one more time, before throwing an exception 99 String zoneIdBack = zoneId; 100 if (links.containsKey(zoneId)) { 101 zoneId = links.get(zoneId); 102 obj = zones.get(zoneId); 103 } 104 if (obj == null) { 105 throw new ZoneRulesException("Unknown time-zone ID: " + zoneIdBack); 106 } 107 } 108 } 109 if (obj instanceof ZoneRules) { 110 return (ZoneRules)obj; 111 } 112 try { 113 ZoneRules zrules = buildRules(zoneId, (List<ZoneLine>)obj); 114 zones.put(zoneId, zrules); 115 return zrules; 116 } catch (Exception ex) { 117 throw new ZoneRulesException( 118 "Invalid binary time-zone data: TZDB:" + zoneId, ex); 119 } 120 } 121 122 ////////////////////////////////////////////////////////////////////// 123 124 /** 125 * All the regions that are available. 126 */ 127 private List<String> regionIds = new ArrayList<>(600); 128 129 /** 130 * Zone region to rules mapping 131 */ 132 private final Map<String, Object> zones = new ConcurrentSkipListMap<>(); 133 134 /** 135 * compatibility list 136 */ 137 private static Set<String> excludedZones; 138 static { 139 // (1) exclude EST, HST and MST. They are supported 140 // via the short-id mapping 141 // (2) remove UTC and GMT 142 // (3) remove ROC, which is not supported in j.u.tz 143 excludedZones = new TreeSet<>(); 144 excludedZones.add("EST"); 145 excludedZones.add("HST"); 146 excludedZones.add("MST"); 147 excludedZones.add("GMT+0"); 148 excludedZones.add("GMT-0"); 149 excludedZones.add("ROC"); 150 } 151 152 private Map<String, String> links = new TreeMap<>(); 153 private Map<String, List<RuleLine>> rules = new TreeMap<>(); 154 155 private void load(List<Path> files) throws IOException { 156 157 for (Path file : files) { 158 List<ZoneLine> openZone = null; 159 try { 160 for (String line : Files.readAllLines(file, StandardCharsets.ISO_8859_1)) { 161 if (line.length() == 0 || line.charAt(0) == '#') { 162 continue; 163 } 164 //StringIterator itr = new StringIterator(line); 165 String[] tokens = split(line); 166 if (openZone != null && // continuing zone line 167 Character.isWhitespace(line.charAt(0)) && 168 tokens.length > 0) { 169 ZoneLine zLine = new ZoneLine(); 170 openZone.add(zLine); 171 if (zLine.parse(tokens, 0)) { 172 openZone = null; 173 } 174 continue; 175 } 176 if (line.startsWith("Zone")) { // parse Zone line 177 String name = tokens[1]; 178 if (excludedZones.contains(name)){ 179 continue; 180 } 181 if (zones.containsKey(name)) { 182 throw new IllegalArgumentException( 183 "Duplicated zone name in file: " + name + 184 ", line: [" + line + "]"); 185 } 186 openZone = new ArrayList<>(10); 187 zones.put(name, openZone); 188 regionIds.add(name); 189 ZoneLine zLine = new ZoneLine(); 190 openZone.add(zLine); 191 if (zLine.parse(tokens, 2)) { 192 openZone = null; 193 } 194 } else if (line.startsWith("Rule")) { // parse Rule line 195 String name = tokens[1]; 196 if (!rules.containsKey(name)) { 197 rules.put(name, new ArrayList<RuleLine>(10)); 198 } 199 rules.get(name).add(new RuleLine().parse(tokens)); 200 } else if (line.startsWith("Link")) { // parse link line 201 if (tokens.length >= 3) { 202 String realId = tokens[1]; 203 String aliasId = tokens[2]; 204 if (excludedZones.contains(aliasId)){ 205 continue; 206 } 207 links.put(aliasId, realId); 208 regionIds.add(aliasId); 209 } else { 210 throw new IllegalArgumentException( 211 "Invalid Link line in file" + 212 file + ", line: [" + line + "]"); 213 } 214 } else { 215 // skip unknown line 216 } 217 } 218 219 } catch (Exception ex) { 220 throw new RuntimeException("Failed while processing file [" + file + 221 "]", ex); 222 } 223 } 224 } 225 226 private String[] split(String str) { 227 int off = 0; 228 int end = str.length(); 229 ArrayList<String> list = new ArrayList<>(10); 230 while (off < end) { 231 char c = str.charAt(off); 232 if (c == '\t' || c == ' ') { 233 off++; 234 continue; 235 } 236 if (c == '#') { // comment 237 break; 238 } 239 int start = off; 240 while (off < end) { 241 c = str.charAt(off); 242 if (c == ' ' || c == '\t') { 243 break; 244 } 245 off++; 246 } 247 if (start != off) { 248 list.add(str.substring(start, off)); 249 } 250 } 251 return list.toArray(new String[list.size()]); 252 } 253 254 /** 255 * Class representing a month-day-time in the TZDB file. 256 */ 257 private static abstract class MonthDayTime { 258 /** The month of the cutover. */ 259 Month month = Month.JANUARY; 260 261 /** The day-of-month of the cutover. */ 262 int dayOfMonth = 1; 263 264 /** Whether to adjust forwards. */ 265 boolean adjustForwards = true; 266 267 /** The day-of-week of the cutover. */ 268 DayOfWeek dayOfWeek; 269 270 /** The time of the cutover, in second of day */ 271 int secsOfDay = 0; 272 273 /** Whether this is midnight end of day. */ 274 boolean endOfDay; 275 /** The time of the cutover. */ 276 277 TimeDefinition timeDefinition = TimeDefinition.WALL; 278 279 void adjustToForwards(int year) { 280 if (adjustForwards == false && dayOfMonth > 0) { 281 // weekDay<=monthDay case, don't have it in tzdb data for now 282 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6); 283 dayOfMonth = adjustedDate.getDayOfMonth(); 284 month = adjustedDate.getMonth(); 285 adjustForwards = true; 286 } 287 } 288 289 LocalDateTime toDateTime(int year) { 290 LocalDate date; 291 if (dayOfMonth < 0) { 292 int monthLen = month.length(IsoChronology.INSTANCE.isLeapYear(year)); 293 date = LocalDate.of(year, month, monthLen + 1 + dayOfMonth); 294 if (dayOfWeek != null) { 295 date = date.with(TemporalAdjusters.previousOrSame(dayOfWeek)); 296 } 297 } else { 298 date = LocalDate.of(year, month, dayOfMonth); 299 if (dayOfWeek != null) { 300 date = date.with(TemporalAdjusters.nextOrSame(dayOfWeek)); 301 } 302 } 303 if (endOfDay) { 304 date = date.plusDays(1); 305 } 306 return LocalDateTime.of(date, LocalTime.ofSecondOfDay(secsOfDay)); 307 } 308 309 /** 310 * Parses the MonthDaytime segment of a tzdb line. 311 */ 312 private void parse(String[] tokens, int off) { 313 month = parseMonth(tokens[off++]); 314 if (off < tokens.length) { 315 String dayRule = tokens[off++]; 316 if (dayRule.startsWith("last")) { 317 dayOfMonth = -1; 318 dayOfWeek = parseDayOfWeek(dayRule.substring(4)); 319 adjustForwards = false; 320 } else { 321 int index = dayRule.indexOf(">="); 322 if (index > 0) { 323 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index)); 324 dayRule = dayRule.substring(index + 2); 325 } else { 326 index = dayRule.indexOf("<="); 327 if (index > 0) { 328 dayOfWeek = parseDayOfWeek(dayRule.substring(0, index)); 329 adjustForwards = false; 330 dayRule = dayRule.substring(index + 2); 331 } 332 } 333 dayOfMonth = Integer.parseInt(dayRule); 334 if (dayOfMonth < -28 || dayOfMonth > 31 || dayOfMonth == 0) { 335 throw new IllegalArgumentException( 336 "Day of month indicator must be between -28 and 31 inclusive excluding zero"); 337 } 338 } 339 if (off < tokens.length) { 340 String timeStr = tokens[off++]; 341 secsOfDay = parseSecs(timeStr); 342 if (secsOfDay == 86400) { 343 // time must be midnight when end of day flag is true 344 endOfDay = true; 345 secsOfDay = 0; 346 } 347 timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1)); 348 } 349 } 350 } 351 352 int parseYear(String year, int defaultYear) { 353 switch (year.toLowerCase()) { 354 case "min": return 1900; 355 case "max": return Year.MAX_VALUE; 356 case "only": return defaultYear; 357 } 358 return Integer.parseInt(year); 359 } 360 361 Month parseMonth(String mon) { 362 switch (mon) { 363 case "Jan": return Month.JANUARY; 364 case "Feb": return Month.FEBRUARY; 365 case "Mar": return Month.MARCH; 366 case "Apr": return Month.APRIL; 367 case "May": return Month.MAY; 368 case "Jun": return Month.JUNE; 369 case "Jul": return Month.JULY; 370 case "Aug": return Month.AUGUST; 371 case "Sep": return Month.SEPTEMBER; 372 case "Oct": return Month.OCTOBER; 373 case "Nov": return Month.NOVEMBER; 374 case "Dec": return Month.DECEMBER; 375 } 376 throw new IllegalArgumentException("Unknown month: " + mon); 377 } 378 379 DayOfWeek parseDayOfWeek(String dow) { 380 switch (dow) { 381 case "Mon": return DayOfWeek.MONDAY; 382 case "Tue": return DayOfWeek.TUESDAY; 383 case "Wed": return DayOfWeek.WEDNESDAY; 384 case "Thu": return DayOfWeek.THURSDAY; 385 case "Fri": return DayOfWeek.FRIDAY; 386 case "Sat": return DayOfWeek.SATURDAY; 387 case "Sun": return DayOfWeek.SUNDAY; 388 } 389 throw new IllegalArgumentException("Unknown day-of-week: " + dow); 390 } 391 392 String parseOptional(String str) { 393 return str.equals("-") ? null : str; 394 } 395 396 static final boolean isDigit(char c) { 397 return c >= '0' && c <= '9'; 398 } 399 400 private int parseSecs(String time) { 401 if (time.equals("-")) { 402 return 0; 403 } 404 // faster hack 405 int secs = 0; 406 int sign = 1; 407 int off = 0; 408 int len = time.length(); 409 if (off < len && time.charAt(off) == '-') { 410 sign = -1; 411 off++; 412 } 413 char c0, c1; 414 if (off < len && isDigit(c0 = time.charAt(off++))) { 415 int hour = c0 - '0'; 416 if (off < len && isDigit(c1 = time.charAt(off))) { 417 hour = hour * 10 + c1 - '0'; 418 off++; 419 } 420 secs = hour * 60 * 60; 421 if (off < len && time.charAt(off++) == ':') { 422 if (off + 1 < len && 423 isDigit(c0 = time.charAt(off++)) && 424 isDigit(c1 = time.charAt(off++))) { 425 // minutes 426 secs += ((c0 - '0') * 10 + c1 - '0') * 60; 427 if (off < len && time.charAt(off++) == ':') { 428 if (off + 1 < len && 429 isDigit(c0 = time.charAt(off++)) && 430 isDigit(c1 = time.charAt(off++))) { 431 // seconds 432 secs += ((c0 - '0') * 10 + c1 - '0'); 433 } 434 } 435 } 436 437 } 438 return secs * sign; 439 } 440 throw new IllegalArgumentException("[" + time + "]"); 441 } 442 443 int parseOffset(String str) { 444 int secs = parseSecs(str); 445 if (Math.abs(secs) > 18 * 60 * 60) { 446 throw new IllegalArgumentException( 447 "Zone offset not in valid range: -18:00 to +18:00"); 448 } 449 return secs; 450 } 451 452 int parsePeriod(String str) { 453 return parseSecs(str); 454 } 455 456 TimeDefinition parseTimeDefinition(char c) { 457 switch (c) { 458 case 's': 459 case 'S': 460 // standard time 461 return TimeDefinition.STANDARD; 462 case 'u': 463 case 'U': 464 case 'g': 465 case 'G': 466 case 'z': 467 case 'Z': 468 // UTC 469 return TimeDefinition.UTC; 470 case 'w': 471 case 'W': 472 default: 473 // wall time 474 return TimeDefinition.WALL; 475 } 476 } 477 } 478 479 /** 480 * Class representing a rule line in the TZDB file. 481 */ 482 private static class RuleLine extends MonthDayTime { 483 /** The start year. */ 484 int startYear; 485 486 /** The end year. */ 487 int endYear; 488 489 /** The amount of savings, in seconds. */ 490 int savingsAmount; 491 492 /** The text name of the zone. */ 493 String text; 494 495 /** 496 * Converts this to a transition rule. 497 * 498 * @param standardOffset the active standard offset, not null 499 * @param savingsBeforeSecs the active savings before the transition in seconds 500 * @return the transition, not null 501 */ 502 ZoneOffsetTransitionRule toTransitionRule(ZoneOffset stdOffset, int savingsBefore) { 503 // rule shared by different zones, so don't change it 504 Month month = this.month; 505 int dayOfMonth = this.dayOfMonth; 506 DayOfWeek dayOfWeek = this.dayOfWeek; 507 boolean endOfDay = this.endOfDay; 508 509 // optimize stored format 510 if (dayOfMonth < 0) { 511 if (month != Month.FEBRUARY) { // not Month.FEBRUARY 512 dayOfMonth = month.maxLength() - 6; 513 } 514 } 515 if (endOfDay && dayOfMonth > 0 && 516 (dayOfMonth == 28 && month == Month.FEBRUARY) == false) { 517 LocalDate date = LocalDate.of(2004, month, dayOfMonth).plusDays(1); // leap-year 518 month = date.getMonth(); 519 dayOfMonth = date.getDayOfMonth(); 520 if (dayOfWeek != null) { 521 dayOfWeek = dayOfWeek.plus(1); 522 } 523 endOfDay = false; 524 } 525 // build rule 526 return ZoneOffsetTransitionRule.of( 527 //month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, 528 month, dayOfMonth, dayOfWeek, 529 LocalTime.ofSecondOfDay(secsOfDay), endOfDay, timeDefinition, 530 stdOffset, 531 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsBefore), 532 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savingsAmount)); 533 } 534 535 RuleLine parse(String[] tokens) { 536 startYear = parseYear(tokens[2], 0); 537 endYear = parseYear(tokens[3], startYear); 538 if (startYear > endYear) { 539 throw new IllegalArgumentException( 540 "Invalid <Rule> line/Year order invalid:" + startYear + " > " + endYear); 541 } 542 //parseOptional(s.next()); // type is unused 543 super.parse(tokens, 5); // monthdaytime parsing 544 savingsAmount = parsePeriod(tokens[8]); 545 //rule.text = parseOptional(s.next()); 546 return this; 547 } 548 } 549 550 /** 551 * Class representing a linked set of zone lines in the TZDB file. 552 */ 553 private static class ZoneLine extends MonthDayTime { 554 /** The standard offset. */ 555 int stdOffsetSecs; 556 557 /** The fixed savings amount. */ 558 int fixedSavingsSecs = 0; 559 560 /** The savings rule. */ 561 String savingsRule; 562 563 /** The text name of the zone. */ 564 String text; 565 566 /** The cutover year */ 567 int year = Year.MAX_VALUE; 568 569 /** The cutover date time */ 570 LocalDateTime ldt; 571 572 /** The cutover date/time in epoch seconds/UTC */ 573 long ldtSecs = Long.MIN_VALUE; 574 575 LocalDateTime toDateTime() { 576 if (ldt == null) { 577 ldt = toDateTime(year); 578 } 579 return ldt; 580 } 581 582 /** 583 * Creates the date-time epoch second in the wall offset for the local 584 * date-time at the end of the window. 585 * 586 * @param savingsSecs the amount of savings in use in seconds 587 * @return the created date-time epoch second in the wall offset, not null 588 */ 589 long toDateTimeEpochSecond(int savingsSecs) { 590 if (ldtSecs == Long.MIN_VALUE) { 591 ldtSecs = toDateTime().toEpochSecond(ZoneOffset.UTC); 592 } 593 switch(timeDefinition) { 594 case UTC: return ldtSecs; 595 case STANDARD: return ldtSecs - stdOffsetSecs; 596 default: return ldtSecs - (stdOffsetSecs + savingsSecs); // WALL 597 } 598 } 599 600 boolean parse(String[] tokens, int off) { 601 stdOffsetSecs = parseOffset(tokens[off++]); 602 savingsRule = parseOptional(tokens[off++]); 603 if (savingsRule != null && savingsRule.length() > 0 && 604 (savingsRule.charAt(0) == '-' || isDigit(savingsRule.charAt(0)))) { 605 try { 606 fixedSavingsSecs = parsePeriod(savingsRule); 607 savingsRule = null; 608 } catch (Exception ex) { 609 fixedSavingsSecs = 0; 610 } 611 } 612 text = tokens[off++]; 613 if (off < tokens.length) { 614 year = Integer.parseInt(tokens[off++]); 615 if (off < tokens.length) { 616 super.parse(tokens, off); // MonthDayTime 617 } 618 return false; 619 } else { 620 return true; 621 } 622 } 623 } 624 625 /** 626 * Class representing a rule line in the TZDB file for a particular year. 627 */ 628 private static class TransRule implements Comparable<TransRule> 629 { 630 private int year; 631 private RuleLine rule; 632 633 /** The trans date/time */ 634 private LocalDateTime ldt; 635 636 /** The trans date/time in epoch seconds (assume UTC) */ 637 long ldtSecs; 638 639 TransRule(int year, RuleLine rule) { 640 this.year = year; 641 this.rule = rule; 642 this.ldt = rule.toDateTime(year); 643 this.ldtSecs = ldt.toEpochSecond(ZoneOffset.UTC); 644 } 645 646 ZoneOffsetTransition toTransition(ZoneOffset standardOffset, int savingsBeforeSecs) { 647 // copy of code in ZoneOffsetTransitionRule to avoid infinite loop 648 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds( 649 standardOffset.getTotalSeconds() + savingsBeforeSecs); 650 ZoneOffset offsetAfter = ZoneOffset.ofTotalSeconds( 651 standardOffset.getTotalSeconds() + rule.savingsAmount); 652 LocalDateTime dt = rule.timeDefinition 653 .createDateTime(ldt, standardOffset, wallOffset); 654 return ZoneOffsetTransition.of(dt, wallOffset, offsetAfter); 655 } 656 657 long toEpochSecond(ZoneOffset stdOffset, int savingsBeforeSecs) { 658 switch(rule.timeDefinition) { 659 case UTC: return ldtSecs; 660 case STANDARD: return ldtSecs - stdOffset.getTotalSeconds(); 661 default: return ldtSecs - (stdOffset.getTotalSeconds() + savingsBeforeSecs); // WALL 662 } 663 } 664 665 /** 666 * Tests if this a real transition with the active savings in seconds 667 * 668 * @param savingsBefore the active savings in seconds 669 * @return true, if savings changes 670 */ 671 boolean isTransition(int savingsBefore) { 672 return rule.savingsAmount != savingsBefore; 673 } 674 675 public int compareTo(TransRule other) { 676 return (ldtSecs < other.ldtSecs)? -1 : ((ldtSecs == other.ldtSecs) ? 0 : 1); 677 } 678 } 679 680 private ZoneRules buildRules(String zoneId, List<ZoneLine> zones) { 681 if (zones.isEmpty()) { 682 throw new IllegalStateException("No available zone window"); 683 } 684 final List<ZoneOffsetTransition> standardTransitionList = new ArrayList<>(4); 685 final List<ZoneOffsetTransition> transitionList = new ArrayList<>(256); 686 final List<ZoneOffsetTransitionRule> lastTransitionRuleList = new ArrayList<>(2); 687 688 final ZoneLine zone0 = zones.get(0); 689 // initialize the standard offset, wallOffset and savings for loop 690 691 //ZoneOffset stdOffset = zone0.standardOffset; 692 ZoneOffset stdOffset = ZoneOffset.ofTotalSeconds(zone0.stdOffsetSecs); 693 694 int savings = zone0.fixedSavingsSecs; 695 ZoneOffset wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings); 696 697 // start ldt of each zone window 698 LocalDateTime zoneStart = LocalDateTime.MIN; 699 700 // first stanard offset 701 ZoneOffset firstStdOffset = stdOffset; 702 // first wall offset 703 ZoneOffset firstWallOffset = wallOffset; 704 705 for (ZoneLine zone : zones) { 706 // check if standard offset changed, update it if yes 707 ZoneOffset stdOffsetPrev = stdOffset; // for effectiveSavings check 708 if (zone.stdOffsetSecs != stdOffset.getTotalSeconds()) { 709 ZoneOffset stdOffsetNew = ZoneOffset.ofTotalSeconds(zone.stdOffsetSecs); 710 standardTransitionList.add( 711 ZoneOffsetTransition.of( 712 LocalDateTime.ofEpochSecond(zoneStart.toEpochSecond(wallOffset), 713 0, 714 stdOffset), 715 stdOffset, 716 stdOffsetNew)); 717 stdOffset = stdOffsetNew; 718 } 719 720 LocalDateTime zoneEnd; 721 if (zone.year == Year.MAX_VALUE) { 722 zoneEnd = LocalDateTime.MAX; 723 } else { 724 zoneEnd = zone.toDateTime(); 725 } 726 if (zoneEnd.compareTo(zoneStart) < 0) { 727 throw new IllegalStateException("Windows must be in date-time order: " + 728 zoneEnd + " < " + zoneStart); 729 } 730 // calculate effective savings at the start of the window 731 List<TransRule> trules = null; 732 List<TransRule> lastRules = null; 733 734 int effectiveSavings = zone.fixedSavingsSecs; 735 if (zone.savingsRule != null) { 736 List<RuleLine> tzdbRules = rules.get(zone.savingsRule); 737 if (tzdbRules == null) { 738 throw new IllegalArgumentException("<Rule> not found: " + 739 zone.savingsRule); 740 } 741 trules = new ArrayList<>(256); 742 lastRules = new ArrayList<>(2); 743 int lastRulesStartYear = Year.MIN_VALUE; 744 745 // merge the rules to transitions 746 for (RuleLine rule : tzdbRules) { 747 if (rule.startYear > zoneEnd.getYear()) { 748 // rules will not be used for this zone entry 749 continue; 750 } 751 rule.adjustToForwards(2004); // irrelevant, treat as leap year 752 753 int startYear = rule.startYear; 754 int endYear = rule.endYear; 755 if (zoneEnd.equals(LocalDateTime.MAX)) { 756 if (endYear == Year.MAX_VALUE) { 757 endYear = startYear; 758 lastRules.add(new TransRule(endYear, rule)); 759 } 760 lastRulesStartYear = Math.max(startYear, lastRulesStartYear); 761 } else { 762 if (endYear == Year.MAX_VALUE) { 763 //endYear = zoneEnd.getYear(); 764 endYear = zone.year; 765 } 766 } 767 int year = startYear; 768 while (year <= endYear) { 769 trules.add(new TransRule(year, rule)); 770 year++; 771 } 772 } 773 774 // last rules, fill the gap years between different last rules 775 if (zoneEnd.equals(LocalDateTime.MAX)) { 776 lastRulesStartYear = Math.max(lastRulesStartYear, zoneStart.getYear()) + 1; 777 for (TransRule rule : lastRules) { 778 if (rule.year <= lastRulesStartYear) { 779 int year = rule.year; 780 while (year <= lastRulesStartYear) { 781 trules.add(new TransRule(year, rule.rule)); 782 year++; 783 } 784 rule.year = lastRulesStartYear; 785 rule.ldt = rule.rule.toDateTime(year); 786 rule.ldtSecs = rule.ldt.toEpochSecond(ZoneOffset.UTC); 787 } 788 } 789 Collections.sort(lastRules); 790 } 791 // sort the merged rules 792 Collections.sort(trules); 793 794 effectiveSavings = 0; 795 for (TransRule rule : trules) { 796 if (rule.toEpochSecond(stdOffsetPrev, savings) > 797 zoneStart.toEpochSecond(wallOffset)) { 798 // previous savings amount found, which could be the 799 // savings amount at the instant that the window starts 800 // (hence isAfter) 801 break; 802 } 803 effectiveSavings = rule.rule.savingsAmount; 804 } 805 } 806 // check if the start of the window represents a transition 807 ZoneOffset effectiveWallOffset = 808 ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + effectiveSavings); 809 810 if (!wallOffset.equals(effectiveWallOffset)) { 811 transitionList.add(ZoneOffsetTransition.of(zoneStart, 812 wallOffset, 813 effectiveWallOffset)); 814 } 815 savings = effectiveSavings; 816 // apply rules within the window 817 if (trules != null) { 818 long zoneStartEpochSecs = zoneStart.toEpochSecond(wallOffset); 819 for (TransRule trule : trules) { 820 if (trule.isTransition(savings)) { 821 long epochSecs = trule.toEpochSecond(stdOffset, savings); 822 if (epochSecs < zoneStartEpochSecs || 823 epochSecs >= zone.toDateTimeEpochSecond(savings)) { 824 continue; 825 } 826 transitionList.add(trule.toTransition(stdOffset, savings)); 827 savings = trule.rule.savingsAmount; 828 } 829 } 830 } 831 if (lastRules != null) { 832 for (TransRule trule : lastRules) { 833 lastTransitionRuleList.add(trule.rule.toTransitionRule(stdOffset, savings)); 834 savings = trule.rule.savingsAmount; 835 } 836 } 837 838 // finally we can calculate the true end of the window, passing it to the next window 839 wallOffset = ZoneOffset.ofTotalSeconds(stdOffset.getTotalSeconds() + savings); 840 zoneStart = LocalDateTime.ofEpochSecond(zone.toDateTimeEpochSecond(savings), 841 0, 842 wallOffset); 843 } 844 return new ZoneRules(firstStdOffset, 845 firstWallOffset, 846 standardTransitionList, 847 transitionList, 848 lastTransitionRuleList); 849 } 850 851 }