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