001 /* 002 * Copyright 2001-2005 Stephen Colebourne 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.joda.time.tz; 017 018 import java.io.BufferedReader; 019 import java.io.DataOutputStream; 020 import java.io.File; 021 import java.io.FileInputStream; 022 import java.io.FileOutputStream; 023 import java.io.FileReader; 024 import java.io.IOException; 025 import java.io.InputStream; 026 import java.io.OutputStream; 027 import java.util.ArrayList; 028 import java.util.HashMap; 029 import java.util.Iterator; 030 import java.util.List; 031 import java.util.Locale; 032 import java.util.Map; 033 import java.util.StringTokenizer; 034 import java.util.TreeMap; 035 036 import org.joda.time.Chronology; 037 import org.joda.time.DateTime; 038 import org.joda.time.DateTimeField; 039 import org.joda.time.DateTimeZone; 040 import org.joda.time.LocalDate; 041 import org.joda.time.MutableDateTime; 042 import org.joda.time.chrono.ISOChronology; 043 import org.joda.time.chrono.LenientChronology; 044 import org.joda.time.format.DateTimeFormatter; 045 import org.joda.time.format.ISODateTimeFormat; 046 047 /** 048 * Compiles Olson ZoneInfo database files into binary files for each time zone 049 * in the database. {@link DateTimeZoneBuilder} is used to construct and encode 050 * compiled data files. {@link ZoneInfoProvider} loads the encoded files and 051 * converts them back into {@link DateTimeZone} objects. 052 * <p> 053 * Although this tool is similar to zic, the binary formats are not 054 * compatible. The latest Olson database files may be obtained 055 * <a href="http://www.twinsun.com/tz/tz-link.htm">here</a>. 056 * <p> 057 * ZoneInfoCompiler is mutable and not thread-safe, although the main method 058 * may be safely invoked by multiple threads. 059 * 060 * @author Brian S O'Neill 061 * @since 1.0 062 */ 063 public class ZoneInfoCompiler { 064 static DateTimeOfYear cStartOfYear; 065 066 static Chronology cLenientISO; 067 068 /** 069 * Launches the ZoneInfoCompiler tool. 070 * 071 * <pre> 072 * Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files> 073 * where possible options include: 074 * -src <directory> Specify where to read source files 075 * -dst <directory> Specify where to write generated files 076 * </pre> 077 */ 078 public static void main(String[] args) throws Exception { 079 if (args.length == 0) { 080 printUsage(); 081 return; 082 } 083 084 File inputDir = null; 085 File outputDir = null; 086 087 int i; 088 for (i=0; i<args.length; i++) { 089 try { 090 if ("-src".equals(args[i])) { 091 inputDir = new File(args[++i]); 092 } else if ("-dst".equals(args[i])) { 093 outputDir = new File(args[++i]); 094 } else if ("-?".equals(args[i])) { 095 printUsage(); 096 return; 097 } else { 098 break; 099 } 100 } catch (IndexOutOfBoundsException e) { 101 printUsage(); 102 return; 103 } 104 } 105 106 if (i >= args.length) { 107 printUsage(); 108 return; 109 } 110 111 File[] sources = new File[args.length - i]; 112 for (int j=0; i<args.length; i++,j++) { 113 sources[j] = inputDir == null ? new File(args[i]) : new File(inputDir, args[i]); 114 } 115 116 ZoneInfoCompiler zic = new ZoneInfoCompiler(); 117 zic.compile(outputDir, sources); 118 } 119 120 private static void printUsage() { 121 System.out.println("Usage: java org.joda.time.tz.ZoneInfoCompiler <options> <source files>"); 122 System.out.println("where possible options include:"); 123 System.out.println(" -src <directory> Specify where to read source files"); 124 System.out.println(" -dst <directory> Specify where to write generated files"); 125 } 126 127 static DateTimeOfYear getStartOfYear() { 128 if (cStartOfYear == null) { 129 cStartOfYear = new DateTimeOfYear(); 130 } 131 return cStartOfYear; 132 } 133 134 static Chronology getLenientISOChronology() { 135 if (cLenientISO == null) { 136 cLenientISO = LenientChronology.getInstance(ISOChronology.getInstanceUTC()); 137 } 138 return cLenientISO; 139 } 140 141 /** 142 * @param zimap maps string ids to DateTimeZone objects. 143 */ 144 static void writeZoneInfoMap(DataOutputStream dout, Map zimap) throws IOException { 145 // Build the string pool. 146 Map idToIndex = new HashMap(zimap.size()); 147 TreeMap indexToId = new TreeMap(); 148 149 Iterator it = zimap.entrySet().iterator(); 150 short count = 0; 151 while (it.hasNext()) { 152 Map.Entry entry = (Map.Entry)it.next(); 153 String id = (String)entry.getKey(); 154 if (!idToIndex.containsKey(id)) { 155 Short index = new Short(count); 156 idToIndex.put(id, index); 157 indexToId.put(index, id); 158 if (++count == 0) { 159 throw new InternalError("Too many time zone ids"); 160 } 161 } 162 id = ((DateTimeZone)entry.getValue()).getID(); 163 if (!idToIndex.containsKey(id)) { 164 Short index = new Short(count); 165 idToIndex.put(id, index); 166 indexToId.put(index, id); 167 if (++count == 0) { 168 throw new InternalError("Too many time zone ids"); 169 } 170 } 171 } 172 173 // Write the string pool, ordered by index. 174 dout.writeShort(indexToId.size()); 175 it = indexToId.values().iterator(); 176 while (it.hasNext()) { 177 dout.writeUTF((String)it.next()); 178 } 179 180 // Write the mappings. 181 dout.writeShort(zimap.size()); 182 it = zimap.entrySet().iterator(); 183 while (it.hasNext()) { 184 Map.Entry entry = (Map.Entry)it.next(); 185 String id = (String)entry.getKey(); 186 dout.writeShort(((Short)idToIndex.get(id)).shortValue()); 187 id = ((DateTimeZone)entry.getValue()).getID(); 188 dout.writeShort(((Short)idToIndex.get(id)).shortValue()); 189 } 190 } 191 192 static int parseYear(String str, int def) { 193 str = str.toLowerCase(); 194 if (str.equals("minimum") || str.equals("min")) { 195 return Integer.MIN_VALUE; 196 } else if (str.equals("maximum") || str.equals("max")) { 197 return Integer.MAX_VALUE; 198 } else if (str.equals("only")) { 199 return def; 200 } 201 return Integer.parseInt(str); 202 } 203 204 static int parseMonth(String str) { 205 DateTimeField field = ISOChronology.getInstanceUTC().monthOfYear(); 206 return field.get(field.set(0, str, Locale.ENGLISH)); 207 } 208 209 static int parseDayOfWeek(String str) { 210 DateTimeField field = ISOChronology.getInstanceUTC().dayOfWeek(); 211 return field.get(field.set(0, str, Locale.ENGLISH)); 212 } 213 214 static String parseOptional(String str) { 215 return (str.equals("-")) ? null : str; 216 } 217 218 static int parseTime(String str) { 219 DateTimeFormatter p = ISODateTimeFormat.hourMinuteSecondFraction(); 220 MutableDateTime mdt = new MutableDateTime(0, getLenientISOChronology()); 221 int pos = 0; 222 if (str.startsWith("-")) { 223 pos = 1; 224 } 225 int newPos = p.parseInto(mdt, str, pos); 226 if (newPos == ~pos) { 227 throw new IllegalArgumentException(str); 228 } 229 int millis = (int)mdt.getMillis(); 230 if (pos == 1) { 231 millis = -millis; 232 } 233 return millis; 234 } 235 236 static char parseZoneChar(char c) { 237 switch (c) { 238 case 's': case 'S': 239 // Standard time 240 return 's'; 241 case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z': 242 // UTC 243 return 'u'; 244 case 'w': case 'W': default: 245 // Wall time 246 return 'w'; 247 } 248 } 249 250 /** 251 * @return false if error. 252 */ 253 static boolean test(String id, DateTimeZone tz) { 254 if (!id.equals(tz.getID())) { 255 return true; 256 } 257 258 // Test to ensure that reported transitions are not duplicated. 259 260 long millis = ISOChronology.getInstanceUTC().year().set(0, 1850); 261 long end = ISOChronology.getInstanceUTC().year().set(0, 2050); 262 263 int offset = tz.getOffset(millis); 264 String key = tz.getNameKey(millis); 265 266 List transitions = new ArrayList(); 267 268 while (true) { 269 long next = tz.nextTransition(millis); 270 if (next == millis || next > end) { 271 break; 272 } 273 274 millis = next; 275 276 int nextOffset = tz.getOffset(millis); 277 String nextKey = tz.getNameKey(millis); 278 279 if (offset == nextOffset 280 && key.equals(nextKey)) { 281 System.out.println("*d* Error in " + tz.getID() + " " 282 + new DateTime(millis, 283 ISOChronology.getInstanceUTC())); 284 return false; 285 } 286 287 if (nextKey == null || (nextKey.length() < 3 && !"??".equals(nextKey))) { 288 System.out.println("*s* Error in " + tz.getID() + " " 289 + new DateTime(millis, 290 ISOChronology.getInstanceUTC()) 291 + ", nameKey=" + nextKey); 292 return false; 293 } 294 295 transitions.add(new Long(millis)); 296 297 offset = nextOffset; 298 key = nextKey; 299 } 300 301 // Now verify that reverse transitions match up. 302 303 millis = ISOChronology.getInstanceUTC().year().set(0, 2050); 304 end = ISOChronology.getInstanceUTC().year().set(0, 1850); 305 306 for (int i=transitions.size(); --i>= 0; ) { 307 long prev = tz.previousTransition(millis); 308 if (prev == millis || prev < end) { 309 break; 310 } 311 312 millis = prev; 313 314 long trans = ((Long)transitions.get(i)).longValue(); 315 316 if (trans - 1 != millis) { 317 System.out.println("*r* Error in " + tz.getID() + " " 318 + new DateTime(millis, 319 ISOChronology.getInstanceUTC()) + " != " 320 + new DateTime(trans - 1, 321 ISOChronology.getInstanceUTC())); 322 323 return false; 324 } 325 } 326 327 return true; 328 } 329 330 // Maps names to RuleSets. 331 private Map iRuleSets; 332 333 // List of Zone objects. 334 private List iZones; 335 336 // List String pairs to link. 337 private List iLinks; 338 339 public ZoneInfoCompiler() { 340 iRuleSets = new HashMap(); 341 iZones = new ArrayList(); 342 iLinks = new ArrayList(); 343 } 344 345 /** 346 * Returns a map of ids to DateTimeZones. 347 * 348 * @param outputDir optional directory to write compiled data files to 349 * @param sources optional list of source files to parse 350 */ 351 public Map compile(File outputDir, File[] sources) throws IOException { 352 if (sources != null) { 353 for (int i=0; i<sources.length; i++) { 354 BufferedReader in = new BufferedReader(new FileReader(sources[i])); 355 parseDataFile(in); 356 in.close(); 357 } 358 } 359 360 if (outputDir != null) { 361 if (!outputDir.exists()) { 362 throw new IOException("Destination directory doesn't exist: " + outputDir); 363 } 364 if (!outputDir.isDirectory()) { 365 throw new IOException("Destination is not a directory: " + outputDir); 366 } 367 } 368 369 Map map = new TreeMap(); 370 371 for (int i=0; i<iZones.size(); i++) { 372 Zone zone = (Zone)iZones.get(i); 373 DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); 374 zone.addToBuilder(builder, iRuleSets); 375 final DateTimeZone original = builder.toDateTimeZone(zone.iName, true); 376 DateTimeZone tz = original; 377 if (test(tz.getID(), tz)) { 378 map.put(tz.getID(), tz); 379 if (outputDir != null) { 380 System.out.println("Writing " + tz.getID()); 381 File file = new File(outputDir, tz.getID()); 382 if (!file.getParentFile().exists()) { 383 file.getParentFile().mkdirs(); 384 } 385 OutputStream out = new FileOutputStream(file); 386 builder.writeTo(zone.iName, out); 387 out.close(); 388 389 // Test if it can be read back. 390 InputStream in = new FileInputStream(file); 391 DateTimeZone tz2 = DateTimeZoneBuilder.readFrom(in, tz.getID()); 392 in.close(); 393 394 if (!original.equals(tz2)) { 395 System.out.println("*e* Error in " + tz.getID() + 396 ": Didn't read properly from file"); 397 } 398 } 399 } 400 } 401 402 for (int pass=0; pass<2; pass++) { 403 for (int i=0; i<iLinks.size(); i += 2) { 404 String id = (String)iLinks.get(i); 405 String alias = (String)iLinks.get(i + 1); 406 DateTimeZone tz = (DateTimeZone)map.get(id); 407 if (tz == null) { 408 if (pass > 0) { 409 System.out.println("Cannot find time zone '" + id + 410 "' to link alias '" + alias + "' to"); 411 } 412 } else { 413 map.put(alias, tz); 414 } 415 } 416 } 417 418 if (outputDir != null) { 419 System.out.println("Writing ZoneInfoMap"); 420 File file = new File(outputDir, "ZoneInfoMap"); 421 if (!file.getParentFile().exists()) { 422 file.getParentFile().mkdirs(); 423 } 424 425 OutputStream out = new FileOutputStream(file); 426 DataOutputStream dout = new DataOutputStream(out); 427 // Sort and filter out any duplicates that match case. 428 Map zimap = new TreeMap(String.CASE_INSENSITIVE_ORDER); 429 zimap.putAll(map); 430 writeZoneInfoMap(dout, zimap); 431 dout.close(); 432 } 433 434 return map; 435 } 436 437 public void parseDataFile(BufferedReader in) throws IOException { 438 Zone zone = null; 439 String line; 440 while ((line = in.readLine()) != null) { 441 String trimmed = line.trim(); 442 if (trimmed.length() == 0 || trimmed.charAt(0) == '#') { 443 continue; 444 } 445 446 int index = line.indexOf('#'); 447 if (index >= 0) { 448 line = line.substring(0, index); 449 } 450 451 //System.out.println(line); 452 453 StringTokenizer st = new StringTokenizer(line, " \t"); 454 455 if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) { 456 if (zone != null) { 457 // Zone continuation 458 zone.chain(st); 459 } 460 continue; 461 } else { 462 if (zone != null) { 463 iZones.add(zone); 464 } 465 zone = null; 466 } 467 468 if (st.hasMoreTokens()) { 469 String token = st.nextToken(); 470 if (token.equalsIgnoreCase("Rule")) { 471 Rule r = new Rule(st); 472 RuleSet rs = (RuleSet)iRuleSets.get(r.iName); 473 if (rs == null) { 474 rs = new RuleSet(r); 475 iRuleSets.put(r.iName, rs); 476 } else { 477 rs.addRule(r); 478 } 479 } else if (token.equalsIgnoreCase("Zone")) { 480 zone = new Zone(st); 481 } else if (token.equalsIgnoreCase("Link")) { 482 iLinks.add(st.nextToken()); 483 iLinks.add(st.nextToken()); 484 } else { 485 System.out.println("Unknown line: " + line); 486 } 487 } 488 } 489 490 if (zone != null) { 491 iZones.add(zone); 492 } 493 } 494 495 static class DateTimeOfYear { 496 public final int iMonthOfYear; 497 public final int iDayOfMonth; 498 public final int iDayOfWeek; 499 public final boolean iAdvanceDayOfWeek; 500 public final int iMillisOfDay; 501 public final char iZoneChar; 502 503 DateTimeOfYear() { 504 iMonthOfYear = 1; 505 iDayOfMonth = 1; 506 iDayOfWeek = 0; 507 iAdvanceDayOfWeek = false; 508 iMillisOfDay = 0; 509 iZoneChar = 'w'; 510 } 511 512 DateTimeOfYear(StringTokenizer st) { 513 int month = 1; 514 int day = 1; 515 int dayOfWeek = 0; 516 int millis = 0; 517 boolean advance = false; 518 char zoneChar = 'w'; 519 520 if (st.hasMoreTokens()) { 521 month = parseMonth(st.nextToken()); 522 523 if (st.hasMoreTokens()) { 524 String str = st.nextToken(); 525 if (str.startsWith("last")) { 526 day = -1; 527 dayOfWeek = parseDayOfWeek(str.substring(4)); 528 advance = false; 529 } else { 530 try { 531 day = Integer.parseInt(str); 532 dayOfWeek = 0; 533 advance = false; 534 } catch (NumberFormatException e) { 535 int index = str.indexOf(">="); 536 if (index > 0) { 537 day = Integer.parseInt(str.substring(index + 2)); 538 dayOfWeek = parseDayOfWeek(str.substring(0, index)); 539 advance = true; 540 } else { 541 index = str.indexOf("<="); 542 if (index > 0) { 543 day = Integer.parseInt(str.substring(index + 2)); 544 dayOfWeek = parseDayOfWeek(str.substring(0, index)); 545 advance = false; 546 } else { 547 throw new IllegalArgumentException(str); 548 } 549 } 550 } 551 } 552 553 if (st.hasMoreTokens()) { 554 str = st.nextToken(); 555 zoneChar = parseZoneChar(str.charAt(str.length() - 1)); 556 if (str.equals("24:00")) { 557 LocalDate date = (day == -1 ? 558 new LocalDate(2001, month, 1).plusMonths(1) : 559 new LocalDate(2001, month, day).plusDays(1)); 560 advance = (day != -1); 561 month = date.getMonthOfYear(); 562 day = date.getDayOfMonth(); 563 dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1; 564 } else { 565 millis = parseTime(str); 566 } 567 } 568 } 569 } 570 571 iMonthOfYear = month; 572 iDayOfMonth = day; 573 iDayOfWeek = dayOfWeek; 574 iAdvanceDayOfWeek = advance; 575 iMillisOfDay = millis; 576 iZoneChar = zoneChar; 577 } 578 579 /** 580 * Adds a recurring savings rule to the builder. 581 */ 582 public void addRecurring(DateTimeZoneBuilder builder, String nameKey, 583 int saveMillis, int fromYear, int toYear) 584 { 585 builder.addRecurringSavings(nameKey, saveMillis, 586 fromYear, toYear, 587 iZoneChar, 588 iMonthOfYear, 589 iDayOfMonth, 590 iDayOfWeek, 591 iAdvanceDayOfWeek, 592 iMillisOfDay); 593 } 594 595 /** 596 * Adds a cutover to the builder. 597 */ 598 public void addCutover(DateTimeZoneBuilder builder, int year) { 599 builder.addCutover(year, 600 iZoneChar, 601 iMonthOfYear, 602 iDayOfMonth, 603 iDayOfWeek, 604 iAdvanceDayOfWeek, 605 iMillisOfDay); 606 } 607 608 public String toString() { 609 return 610 "MonthOfYear: " + iMonthOfYear + "\n" + 611 "DayOfMonth: " + iDayOfMonth + "\n" + 612 "DayOfWeek: " + iDayOfWeek + "\n" + 613 "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" + 614 "MillisOfDay: " + iMillisOfDay + "\n" + 615 "ZoneChar: " + iZoneChar + "\n"; 616 } 617 } 618 619 private static class Rule { 620 public final String iName; 621 public final int iFromYear; 622 public final int iToYear; 623 public final String iType; 624 public final DateTimeOfYear iDateTimeOfYear; 625 public final int iSaveMillis; 626 public final String iLetterS; 627 628 Rule(StringTokenizer st) { 629 iName = st.nextToken().intern(); 630 iFromYear = parseYear(st.nextToken(), 0); 631 iToYear = parseYear(st.nextToken(), iFromYear); 632 if (iToYear < iFromYear) { 633 throw new IllegalArgumentException(); 634 } 635 iType = parseOptional(st.nextToken()); 636 iDateTimeOfYear = new DateTimeOfYear(st); 637 iSaveMillis = parseTime(st.nextToken()); 638 iLetterS = parseOptional(st.nextToken()); 639 } 640 641 /** 642 * Adds a recurring savings rule to the builder. 643 */ 644 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) { 645 String nameKey = formatName(nameFormat); 646 iDateTimeOfYear.addRecurring 647 (builder, nameKey, iSaveMillis, iFromYear, iToYear); 648 } 649 650 private String formatName(String nameFormat) { 651 int index = nameFormat.indexOf('/'); 652 if (index > 0) { 653 if (iSaveMillis == 0) { 654 // Extract standard name. 655 return nameFormat.substring(0, index).intern(); 656 } else { 657 return nameFormat.substring(index + 1).intern(); 658 } 659 } 660 index = nameFormat.indexOf("%s"); 661 if (index < 0) { 662 return nameFormat; 663 } 664 String left = nameFormat.substring(0, index); 665 String right = nameFormat.substring(index + 2); 666 String name; 667 if (iLetterS == null) { 668 name = left.concat(right); 669 } else { 670 name = left + iLetterS + right; 671 } 672 return name.intern(); 673 } 674 675 public String toString() { 676 return 677 "[Rule]\n" + 678 "Name: " + iName + "\n" + 679 "FromYear: " + iFromYear + "\n" + 680 "ToYear: " + iToYear + "\n" + 681 "Type: " + iType + "\n" + 682 iDateTimeOfYear + 683 "SaveMillis: " + iSaveMillis + "\n" + 684 "LetterS: " + iLetterS + "\n"; 685 } 686 } 687 688 private static class RuleSet { 689 private List iRules; 690 691 RuleSet(Rule rule) { 692 iRules = new ArrayList(); 693 iRules.add(rule); 694 } 695 696 void addRule(Rule rule) { 697 if (!(rule.iName.equals(((Rule)iRules.get(0)).iName))) { 698 throw new IllegalArgumentException("Rule name mismatch"); 699 } 700 iRules.add(rule); 701 } 702 703 /** 704 * Adds recurring savings rules to the builder. 705 */ 706 public void addRecurring(DateTimeZoneBuilder builder, String nameFormat) { 707 for (int i=0; i<iRules.size(); i++) { 708 Rule rule = (Rule)iRules.get(i); 709 rule.addRecurring(builder, nameFormat); 710 } 711 } 712 } 713 714 private static class Zone { 715 public final String iName; 716 public final int iOffsetMillis; 717 public final String iRules; 718 public final String iFormat; 719 public final int iUntilYear; 720 public final DateTimeOfYear iUntilDateTimeOfYear; 721 722 private Zone iNext; 723 724 Zone(StringTokenizer st) { 725 this(st.nextToken(), st); 726 } 727 728 private Zone(String name, StringTokenizer st) { 729 iName = name.intern(); 730 iOffsetMillis = parseTime(st.nextToken()); 731 iRules = parseOptional(st.nextToken()); 732 iFormat = st.nextToken().intern(); 733 734 int year = Integer.MAX_VALUE; 735 DateTimeOfYear dtOfYear = getStartOfYear(); 736 737 if (st.hasMoreTokens()) { 738 year = Integer.parseInt(st.nextToken()); 739 if (st.hasMoreTokens()) { 740 dtOfYear = new DateTimeOfYear(st); 741 } 742 } 743 744 iUntilYear = year; 745 iUntilDateTimeOfYear = dtOfYear; 746 } 747 748 void chain(StringTokenizer st) { 749 if (iNext != null) { 750 iNext.chain(st); 751 } else { 752 iNext = new Zone(iName, st); 753 } 754 } 755 756 /* 757 public DateTimeZone buildDateTimeZone(Map ruleSets) { 758 DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); 759 addToBuilder(builder, ruleSets); 760 return builder.toDateTimeZone(iName); 761 } 762 */ 763 764 /** 765 * Adds zone info to the builder. 766 */ 767 public void addToBuilder(DateTimeZoneBuilder builder, Map ruleSets) { 768 addToBuilder(this, builder, ruleSets); 769 } 770 771 private static void addToBuilder(Zone zone, 772 DateTimeZoneBuilder builder, 773 Map ruleSets) 774 { 775 for (; zone != null; zone = zone.iNext) { 776 builder.setStandardOffset(zone.iOffsetMillis); 777 778 if (zone.iRules == null) { 779 builder.setFixedSavings(zone.iFormat, 0); 780 } else { 781 try { 782 // Check if iRules actually just refers to a savings. 783 int saveMillis = parseTime(zone.iRules); 784 builder.setFixedSavings(zone.iFormat, saveMillis); 785 } 786 catch (Exception e) { 787 RuleSet rs = (RuleSet)ruleSets.get(zone.iRules); 788 if (rs == null) { 789 throw new IllegalArgumentException 790 ("Rules not found: " + zone.iRules); 791 } 792 rs.addRecurring(builder, zone.iFormat); 793 } 794 } 795 796 if (zone.iUntilYear == Integer.MAX_VALUE) { 797 break; 798 } 799 800 zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear); 801 } 802 } 803 804 public String toString() { 805 String str = 806 "[Zone]\n" + 807 "Name: " + iName + "\n" + 808 "OffsetMillis: " + iOffsetMillis + "\n" + 809 "Rules: " + iRules + "\n" + 810 "Format: " + iFormat + "\n" + 811 "UntilYear: " + iUntilYear + "\n" + 812 iUntilDateTimeOfYear; 813 814 if (iNext == null) { 815 return str; 816 } 817 818 return str + "...\n" + iNext.toString(); 819 } 820 } 821 } 822