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 */
016package org.joda.time.tz;
017
018import java.io.BufferedReader;
019import java.io.DataOutputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileOutputStream;
023import java.io.FileReader;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Locale;
032import java.util.Map;
033import java.util.StringTokenizer;
034import java.util.TreeMap;
035
036import org.joda.time.Chronology;
037import org.joda.time.DateTime;
038import org.joda.time.DateTimeField;
039import org.joda.time.DateTimeZone;
040import org.joda.time.LocalDate;
041import org.joda.time.MutableDateTime;
042import org.joda.time.chrono.ISOChronology;
043import org.joda.time.chrono.LenientChronology;
044import org.joda.time.format.DateTimeFormatter;
045import 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 */
063public 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 &lt;options&gt; &lt;source files&gt;
073     * where possible options include:
074     *   -src &lt;directory&gt;    Specify where to read source files
075     *   -dst &lt;directory&gt;    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