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 }