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 &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