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