1 /* 2 * Copyright (c) 2012, 2013, 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 * Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos 28 * 29 * All rights reserved. 30 * 31 * Redistribution and use in source and binary forms, with or without 32 * modification, are permitted provided that the following conditions are met: 33 * 34 * * Redistributions of source code must retain the above copyright notice, 35 * this list of conditions and the following disclaimer. 36 * 37 * * Redistributions in binary form must reproduce the above copyright notice, 38 * this list of conditions and the following disclaimer in the documentation 39 * and/or other materials provided with the distribution. 40 * 41 * * Neither the name of JSR-310 nor the names of its contributors 42 * may be used to endorse or promote products derived from this software 43 * without specific prior written permission. 44 * 45 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 46 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 47 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 48 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 49 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 50 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 51 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 52 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 53 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 54 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 55 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 56 */ 57 package build.tools.tzdb; 58 59 import static build.tools.tzdb.Utils.*; 60 61 import java.io.ByteArrayOutputStream; 62 import java.io.DataOutputStream; 63 import java.nio.charset.StandardCharsets; 64 import java.nio.file.Files; 65 import java.nio.file.Path; 66 import java.nio.file.Paths; 67 import java.text.ParsePosition; 68 import java.util.ArrayList; 69 import java.util.Arrays; 70 import java.util.HashMap; 71 import java.util.HashSet; 72 import java.util.List; 73 import java.util.Map; 74 import java.util.NoSuchElementException; 75 import java.util.Scanner; 76 import java.util.SortedMap; 77 import java.util.TreeMap; 78 import java.util.regex.Matcher; 79 import java.util.regex.MatchResult; 80 import java.util.regex.Pattern; 81 82 /** 83 * A compiler that reads a set of TZDB time-zone files and builds a single 84 * combined TZDB data file. 85 * 86 * @since 1.8 87 */ 88 public final class TzdbZoneRulesCompiler { 89 90 public static void main(String[] args) { 91 new TzdbZoneRulesCompiler().compile(args); 92 } 93 94 private void compile(String[] args) { 95 if (args.length < 2) { 96 outputHelp(); 97 return; 98 } 99 Path srcDir = null; 100 Path dstFile = null; 101 String version = null; 102 // parse args/options 103 int i; 104 for (i = 0; i < args.length; i++) { 105 String arg = args[i]; 106 if (!arg.startsWith("-")) { 107 break; 108 } 109 if ("-srcdir".equals(arg)) { 110 if (srcDir == null && ++i < args.length) { 111 srcDir = Paths.get(args[i]); 112 continue; 113 } 114 } else if ("-dstfile".equals(arg)) { 115 if (dstFile == null && ++i < args.length) { 116 dstFile = Paths.get(args[i]); 117 continue; 118 } 119 } else if ("-verbose".equals(arg)) { 120 if (!verbose) { 121 verbose = true; 122 continue; 123 } 124 } else if (!"-help".equals(arg)) { 125 System.out.println("Unrecognised option: " + arg); 126 } 127 outputHelp(); 128 return; 129 } 130 // check source directory 131 if (srcDir == null) { 132 System.err.println("Source directory must be specified using -srcdir"); 133 System.exit(1); 134 } 135 if (!Files.isDirectory(srcDir)) { 136 System.err.println("Source does not exist or is not a directory: " + srcDir); 137 System.exit(1); 138 } 139 // parse source file names 140 if (i == args.length) { 141 i = 0; 142 args = new String[] {"africa", "antarctica", "asia", "australasia", "europe", 143 "northamerica","southamerica", "backward", "etcetera" }; 144 System.out.println("Source filenames not specified, using default set ( "); 145 for (String name : args) { 146 System.out.printf(name + " "); 147 } 148 System.out.println(")"); 149 } 150 // source files in this directory 151 List<Path> srcFiles = new ArrayList<>(); 152 for (; i < args.length; i++) { 153 Path file = srcDir.resolve(args[i]); 154 if (Files.exists(file)) { 155 srcFiles.add(file); 156 } else { 157 System.err.println("Source directory does not contain source file: " + args[i]); 158 System.exit(1); 159 } 160 } 161 // check destination file 162 if (dstFile == null) { 163 dstFile = srcDir.resolve("tzdb.dat"); 164 } else { 165 Path parent = dstFile.getParent(); 166 if (parent != null && !Files.exists(parent)) { 167 System.err.println("Destination directory does not exist: " + parent); 168 System.exit(1); 169 } 170 } 171 try { 172 // get tzdb source version 173 Matcher m = Pattern.compile("tzdata(?<ver>[0-9]{4}[A-z])") 174 .matcher(new String(Files.readAllBytes(srcDir.resolve("VERSION")), 175 "ISO-8859-1")); 176 if (m.find()) { 177 version = m.group("ver"); 178 } else { 179 System.exit(1); 180 System.err.println("Source directory does not contain file: VERSION"); 181 } 182 printVerbose("Compiling TZDB version " + version); 183 // parse source files 184 for (Path file : srcFiles) { 185 printVerbose("Parsing file: " + file); 186 parseFile(file); 187 } 188 // build zone rules 189 printVerbose("Building rules"); 190 buildZoneRules(); 191 // output to file 192 printVerbose("Outputting tzdb file: " + dstFile); 193 outputFile(dstFile, version, builtZones, links); 194 } catch (Exception ex) { 195 System.out.println("Failed: " + ex.toString()); 196 ex.printStackTrace(); 197 System.exit(1); 198 } 199 System.exit(0); 200 } 201 202 /** 203 * Output usage text for the command line. 204 */ 205 private static void outputHelp() { 206 System.out.println("Usage: TzdbZoneRulesCompiler <options> <tzdb source filenames>"); 207 System.out.println("where options include:"); 208 System.out.println(" -srcdir <directory> Where to find tzdb source directory (required)"); 209 System.out.println(" -dstfile <file> Where to output generated file (default srcdir/tzdb.dat)"); 210 System.out.println(" -help Print this usage message"); 211 System.out.println(" -verbose Output verbose information during compilation"); 212 System.out.println(" The source directory must contain the unpacked tzdb files, such as asia or europe"); 213 } 214 215 /** 216 * Outputs the file. 217 */ 218 private void outputFile(Path dstFile, String version, 219 SortedMap<String, ZoneRules> builtZones, 220 Map<String, String> links) { 221 try (DataOutputStream out = new DataOutputStream(Files.newOutputStream(dstFile))) { 222 // file version 223 out.writeByte(1); 224 // group 225 out.writeUTF("TZDB"); 226 // versions 227 out.writeShort(1); 228 out.writeUTF(version); 229 // regions 230 String[] regionArray = builtZones.keySet().toArray(new String[builtZones.size()]); 231 out.writeShort(regionArray.length); 232 for (String regionId : regionArray) { 233 out.writeUTF(regionId); 234 } 235 // rules -- hashset -> remove the dup 236 List<ZoneRules> rulesList = new ArrayList<>(new HashSet<>(builtZones.values())); 237 out.writeShort(rulesList.size()); 238 ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); 239 for (ZoneRules rules : rulesList) { 240 baos.reset(); 241 DataOutputStream dataos = new DataOutputStream(baos); 242 rules.writeExternal(dataos); 243 dataos.close(); 244 byte[] bytes = baos.toByteArray(); 245 out.writeShort(bytes.length); 246 out.write(bytes); 247 } 248 // link version-region-rules 249 out.writeShort(builtZones.size()); 250 for (Map.Entry<String, ZoneRules> entry : builtZones.entrySet()) { 251 int regionIndex = Arrays.binarySearch(regionArray, entry.getKey()); 252 int rulesIndex = rulesList.indexOf(entry.getValue()); 253 out.writeShort(regionIndex); 254 out.writeShort(rulesIndex); 255 } 256 // alias-region 257 out.writeShort(links.size()); 258 for (Map.Entry<String, String> entry : links.entrySet()) { 259 int aliasIndex = Arrays.binarySearch(regionArray, entry.getKey()); 260 int regionIndex = Arrays.binarySearch(regionArray, entry.getValue()); 261 out.writeShort(aliasIndex); 262 out.writeShort(regionIndex); 263 } 264 out.flush(); 265 } catch (Exception ex) { 266 System.out.println("Failed: " + ex.toString()); 267 ex.printStackTrace(); 268 System.exit(1); 269 } 270 } 271 272 private static final Pattern YEAR = Pattern.compile("(?i)(?<min>min)|(?<max>max)|(?<only>only)|(?<year>[0-9]+)"); 273 private static final Pattern MONTH = Pattern.compile("(?i)(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)"); 274 private static final Matcher DOW = Pattern.compile("(?i)(mon)|(tue)|(wed)|(thu)|(fri)|(sat)|(sun)").matcher(""); 275 private static final Matcher TIME = Pattern.compile("(?<neg>-)?+(?<hour>[0-9]{1,2})(:(?<minute>[0-5][0-9]))?+(:(?<second>[0-5][0-9]))?+").matcher(""); 276 277 /** The TZDB rules. */ 278 private final Map<String, List<TZDBRule>> rules = new HashMap<>(); 279 280 /** The TZDB zones. */ 281 private final Map<String, List<TZDBZone>> zones = new HashMap<>(); 282 283 /** The TZDB links. */ 284 private final Map<String, String> links = new HashMap<>(); 285 286 /** The built zones. */ 287 private final SortedMap<String, ZoneRules> builtZones = new TreeMap<>(); 288 289 /** Whether to output verbose messages. */ 290 private boolean verbose; 291 292 /** 293 * private contructor 294 */ 295 private TzdbZoneRulesCompiler() { 296 } 297 298 /** 299 * Parses a source file. 300 * 301 * @param file the file being read, not null 302 * @throws Exception if an error occurs 303 */ 304 private void parseFile(Path file) throws Exception { 305 int lineNumber = 1; 306 String line = null; 307 try { 308 List<String> lines = Files.readAllLines(file, StandardCharsets.ISO_8859_1); 309 List<TZDBZone> openZone = null; 310 for (; lineNumber < lines.size(); lineNumber++) { 311 line = lines.get(lineNumber); 312 int index = line.indexOf('#'); // remove comments (doesn't handle # in quotes) 313 if (index >= 0) { 314 line = line.substring(0, index); 315 } 316 if (line.trim().length() == 0) { // ignore blank lines 317 continue; 318 } 319 Scanner s = new Scanner(line); 320 if (openZone != null && Character.isWhitespace(line.charAt(0)) && s.hasNext()) { 321 if (parseZoneLine(s, openZone)) { 322 openZone = null; 323 } 324 } else { 325 if (s.hasNext()) { 326 String first = s.next(); 327 if (first.equals("Zone")) { 328 openZone = new ArrayList<>(); 329 try { 330 zones.put(s.next(), openZone); 331 if (parseZoneLine(s, openZone)) { 332 openZone = null; 333 } 334 } catch (NoSuchElementException x) { 335 printVerbose("Invalid Zone line in file: " + file + ", line: " + line); 336 throw new IllegalArgumentException("Invalid Zone line"); 337 } 338 } else { 339 openZone = null; 340 if (first.equals("Rule")) { 341 try { 342 parseRuleLine(s); 343 } catch (NoSuchElementException x) { 344 printVerbose("Invalid Rule line in file: " + file + ", line: " + line); 345 throw new IllegalArgumentException("Invalid Rule line"); 346 } 347 } else if (first.equals("Link")) { 348 try { 349 String realId = s.next(); 350 String aliasId = s.next(); 351 links.put(aliasId, realId); 352 } catch (NoSuchElementException x) { 353 printVerbose("Invalid Link line in file: " + file + ", line: " + line); 354 throw new IllegalArgumentException("Invalid Link line"); 355 } 356 357 } else { 358 throw new IllegalArgumentException("Unknown line"); 359 } 360 } 361 } 362 } 363 } 364 } catch (Exception ex) { 365 throw new Exception("Failed while parsing file '" + file + "' on line " + lineNumber + " '" + line + "'", ex); 366 } 367 } 368 369 /** 370 * Parses a Rule line. 371 * 372 * @param s the line scanner, not null 373 */ 374 private void parseRuleLine(Scanner s) { 375 TZDBRule rule = new TZDBRule(); 376 String name = s.next(); 377 if (rules.containsKey(name) == false) { 378 rules.put(name, new ArrayList<TZDBRule>()); 379 } 380 rules.get(name).add(rule); 381 rule.startYear = parseYear(s, 0); 382 rule.endYear = parseYear(s, rule.startYear); 383 if (rule.startYear > rule.endYear) { 384 throw new IllegalArgumentException("Year order invalid: " + rule.startYear + " > " + rule.endYear); 385 } 386 parseOptional(s.next()); // type is unused 387 parseMonthDayTime(s, rule); 388 rule.savingsAmount = parsePeriod(s.next()); 389 rule.text = parseOptional(s.next()); 390 } 391 392 /** 393 * Parses a Zone line. 394 * 395 * @param s the line scanner, not null 396 * @return true if the zone is complete 397 */ 398 private boolean parseZoneLine(Scanner s, List<TZDBZone> zoneList) { 399 TZDBZone zone = new TZDBZone(); 400 zoneList.add(zone); 401 zone.standardOffset = parseOffset(s.next()); 402 String savingsRule = parseOptional(s.next()); 403 if (savingsRule == null) { 404 zone.fixedSavingsSecs = 0; 405 zone.savingsRule = null; 406 } else { 407 try { 408 zone.fixedSavingsSecs = parsePeriod(savingsRule); 409 zone.savingsRule = null; 410 } catch (Exception ex) { 411 zone.fixedSavingsSecs = null; 412 zone.savingsRule = savingsRule; 413 } 414 } 415 zone.text = s.next(); 416 if (s.hasNext()) { 417 zone.year = Integer.parseInt(s.next()); 418 if (s.hasNext()) { 419 parseMonthDayTime(s, zone); 420 } 421 return false; 422 } else { 423 return true; 424 } 425 } 426 427 /** 428 * Parses a Rule line. 429 * 430 * @param s the line scanner, not null 431 * @param mdt the object to parse into, not null 432 */ 433 private void parseMonthDayTime(Scanner s, TZDBMonthDayTime mdt) { 434 mdt.month = parseMonth(s); 435 if (s.hasNext()) { 436 String dayRule = s.next(); 437 if (dayRule.startsWith("last")) { 438 mdt.dayOfMonth = -1; 439 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(4)); 440 mdt.adjustForwards = false; 441 } else { 442 int index = dayRule.indexOf(">="); 443 if (index > 0) { 444 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index)); 445 dayRule = dayRule.substring(index + 2); 446 } else { 447 index = dayRule.indexOf("<="); 448 if (index > 0) { 449 mdt.dayOfWeek = parseDayOfWeek(dayRule.substring(0, index)); 450 mdt.adjustForwards = false; 451 dayRule = dayRule.substring(index + 2); 452 } 453 } 454 mdt.dayOfMonth = Integer.parseInt(dayRule); 455 } 456 if (s.hasNext()) { 457 String timeStr = s.next(); 458 int secsOfDay = parseSecs(timeStr); 459 if (secsOfDay == 86400) { 460 mdt.endOfDay = true; 461 secsOfDay = 0; 462 } 463 LocalTime time = LocalTime.ofSecondOfDay(secsOfDay); 464 mdt.time = time; 465 mdt.timeDefinition = parseTimeDefinition(timeStr.charAt(timeStr.length() - 1)); 466 } 467 } 468 } 469 470 private int parseYear(Scanner s, int defaultYear) { 471 if (s.hasNext(YEAR)) { 472 s.next(YEAR); 473 MatchResult mr = s.match(); 474 if (mr.group(1) != null) { 475 return 1900; // systemv has min 476 } else if (mr.group(2) != null) { 477 return YEAR_MAX_VALUE; 478 } else if (mr.group(3) != null) { 479 return defaultYear; 480 } 481 return Integer.parseInt(mr.group(4)); 482 /* 483 if (mr.group("min") != null) { 484 //return YEAR_MIN_VALUE; 485 return 1900; // systemv has min 486 } else if (mr.group("max") != null) { 487 return YEAR_MAX_VALUE; 488 } else if (mr.group("only") != null) { 489 return defaultYear; 490 } 491 return Integer.parseInt(mr.group("year")); 492 */ 493 } 494 throw new IllegalArgumentException("Unknown year: " + s.next()); 495 } 496 497 private int parseMonth(Scanner s) { 498 if (s.hasNext(MONTH)) { 499 s.next(MONTH); 500 for (int moy = 1; moy < 13; moy++) { 501 if (s.match().group(moy) != null) { 502 return moy; 503 } 504 } 505 } 506 throw new IllegalArgumentException("Unknown month: " + s.next()); 507 } 508 509 private int parseDayOfWeek(String str) { 510 if (DOW.reset(str).matches()) { 511 for (int dow = 1; dow < 8; dow++) { 512 if (DOW.group(dow) != null) { 513 return dow; 514 } 515 } 516 } 517 throw new IllegalArgumentException("Unknown day-of-week: " + str); 518 } 519 520 private String parseOptional(String str) { 521 return str.equals("-") ? null : str; 522 } 523 524 private int parseSecs(String str) { 525 if (str.equals("-")) { 526 return 0; 527 } 528 try { 529 if (TIME.reset(str).find()) { 530 int secs = Integer.parseInt(TIME.group("hour")) * 60 * 60; 531 if (TIME.group("minute") != null) { 532 secs += Integer.parseInt(TIME.group("minute")) * 60; 533 } 534 if (TIME.group("second") != null) { 535 secs += Integer.parseInt(TIME.group("second")); 536 } 537 if (TIME.group("neg") != null) { 538 secs = -secs; 539 } 540 return secs; 541 } 542 } catch (NumberFormatException x) {} 543 throw new IllegalArgumentException(str); 544 } 545 546 private ZoneOffset parseOffset(String str) { 547 int secs = parseSecs(str); 548 return ZoneOffset.ofTotalSeconds(secs); 549 } 550 551 private int parsePeriod(String str) { 552 return parseSecs(str); 553 } 554 555 private TimeDefinition parseTimeDefinition(char c) { 556 switch (c) { 557 case 's': 558 case 'S': 559 // standard time 560 return TimeDefinition.STANDARD; 561 case 'u': 562 case 'U': 563 case 'g': 564 case 'G': 565 case 'z': 566 case 'Z': 567 // UTC 568 return TimeDefinition.UTC; 569 case 'w': 570 case 'W': 571 default: 572 // wall time 573 return TimeDefinition.WALL; 574 } 575 } 576 577 /** 578 * Build the rules, zones and links into real zones. 579 * 580 * @throws Exception if an error occurs 581 */ 582 private void buildZoneRules() throws Exception { 583 // build zones 584 for (String zoneId : zones.keySet()) { 585 printVerbose("Building zone " + zoneId); 586 List<TZDBZone> tzdbZones = zones.get(zoneId); 587 ZoneRulesBuilder bld = new ZoneRulesBuilder(); 588 for (TZDBZone tzdbZone : tzdbZones) { 589 bld = tzdbZone.addToBuilder(bld, rules); 590 } 591 builtZones.put(zoneId, bld.toRules(zoneId)); 592 } 593 594 // build aliases 595 for (String aliasId : links.keySet()) { 596 String realId = links.get(aliasId); 597 printVerbose("Linking alias " + aliasId + " to " + realId); 598 ZoneRules realRules = builtZones.get(realId); 599 if (realRules == null) { 600 realId = links.get(realId); // try again (handle alias liked to alias) 601 printVerbose("Relinking alias " + aliasId + " to " + realId); 602 realRules = builtZones.get(realId); 603 if (realRules == null) { 604 throw new IllegalArgumentException("Alias '" + aliasId + "' links to invalid zone '" + realId); 605 } 606 links.put(aliasId, realId); 607 } 608 builtZones.put(aliasId, realRules); 609 } 610 // remove UTC and GMT 611 // builtZones.remove("UTC"); 612 // builtZones.remove("GMT"); 613 // builtZones.remove("GMT0"); 614 builtZones.remove("GMT+0"); 615 builtZones.remove("GMT-0"); 616 links.remove("GMT+0"); 617 links.remove("GMT-0"); 618 // remove ROC, which is not supported in j.u.tz 619 builtZones.remove("ROC"); 620 links.remove("ROC"); 621 // remove EST, HST and MST. They are supported via 622 // the short-id mapping 623 builtZones.remove("EST"); 624 builtZones.remove("HST"); 625 builtZones.remove("MST"); 626 } 627 628 /** 629 * Prints a verbose message. 630 * 631 * @param message the message, not null 632 */ 633 private void printVerbose(String message) { 634 if (verbose) { 635 System.out.println(message); 636 } 637 } 638 639 /** 640 * Class representing a month-day-time in the TZDB file. 641 */ 642 abstract class TZDBMonthDayTime { 643 /** The month of the cutover. */ 644 int month = 1; 645 /** The day-of-month of the cutover. */ 646 int dayOfMonth = 1; 647 /** Whether to adjust forwards. */ 648 boolean adjustForwards = true; 649 /** The day-of-week of the cutover. */ 650 int dayOfWeek = -1; 651 /** The time of the cutover. */ 652 LocalTime time = LocalTime.MIDNIGHT; 653 /** Whether this is midnight end of day. */ 654 boolean endOfDay; 655 /** The time of the cutover. */ 656 TimeDefinition timeDefinition = TimeDefinition.WALL; 657 void adjustToFowards(int year) { 658 if (adjustForwards == false && dayOfMonth > 0) { 659 LocalDate adjustedDate = LocalDate.of(year, month, dayOfMonth).minusDays(6); 660 dayOfMonth = adjustedDate.getDayOfMonth(); 661 month = adjustedDate.getMonth(); 662 adjustForwards = true; 663 } 664 } 665 } 666 667 /** 668 * Class representing a rule line in the TZDB file. 669 */ 670 final class TZDBRule extends TZDBMonthDayTime { 671 /** The start year. */ 672 int startYear; 673 /** The end year. */ 674 int endYear; 675 /** The amount of savings. */ 676 int savingsAmount; 677 /** The text name of the zone. */ 678 String text; 679 680 void addToBuilder(ZoneRulesBuilder bld) { 681 adjustToFowards(2004); // irrelevant, treat as leap year 682 bld.addRuleToWindow(startYear, endYear, month, dayOfMonth, dayOfWeek, time, endOfDay, timeDefinition, savingsAmount); 683 } 684 } 685 686 /** 687 * Class representing a linked set of zone lines in the TZDB file. 688 */ 689 final class TZDBZone extends TZDBMonthDayTime { 690 /** The standard offset. */ 691 ZoneOffset standardOffset; 692 /** The fixed savings amount. */ 693 Integer fixedSavingsSecs; 694 /** The savings rule. */ 695 String savingsRule; 696 /** The text name of the zone. */ 697 String text; 698 /** The year of the cutover. */ 699 int year = YEAR_MAX_VALUE; 700 701 ZoneRulesBuilder addToBuilder(ZoneRulesBuilder bld, Map<String, List<TZDBRule>> rules) { 702 if (year != YEAR_MAX_VALUE) { 703 bld.addWindow(standardOffset, toDateTime(year), timeDefinition); 704 } else { 705 bld.addWindowForever(standardOffset); 706 } 707 if (fixedSavingsSecs != null) { 708 bld.setFixedSavingsToWindow(fixedSavingsSecs); 709 } else { 710 List<TZDBRule> tzdbRules = rules.get(savingsRule); 711 if (tzdbRules == null) { 712 throw new IllegalArgumentException("Rule not found: " + savingsRule); 713 } 714 for (TZDBRule tzdbRule : tzdbRules) { 715 tzdbRule.addToBuilder(bld); 716 } 717 } 718 return bld; 719 } 720 721 private LocalDateTime toDateTime(int year) { 722 adjustToFowards(year); 723 LocalDate date; 724 if (dayOfMonth == -1) { 725 dayOfMonth = lengthOfMonth(month, isLeapYear(year)); 726 date = LocalDate.of(year, month, dayOfMonth); 727 if (dayOfWeek != -1) { 728 date = previousOrSame(date, dayOfWeek); 729 } 730 } else { 731 date = LocalDate.of(year, month, dayOfMonth); 732 if (dayOfWeek != -1) { 733 date = nextOrSame(date, dayOfWeek); 734 } 735 } 736 LocalDateTime ldt = LocalDateTime.of(date, time); 737 if (endOfDay) { 738 ldt = ldt.plusDays(1); 739 } 740 return ldt; 741 } 742 } 743 }