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