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