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