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 }