1// © 2016 and later: Unicode, Inc. and others.
2// License & terms of use: http://www.unicode.org/copyright.html#License
3/*
4 *******************************************************************************
5 * Copyright (C) 2007-2015, International Business Machines Corporation and    *
6 * others. All Rights Reserved.                                                *
7 *******************************************************************************
8 */
9package com.ibm.icu.util;
10
11import java.io.BufferedWriter;
12import java.io.IOException;
13import java.io.Reader;
14import java.io.Writer;
15import java.util.ArrayList;
16import java.util.Date;
17import java.util.LinkedList;
18import java.util.List;
19import java.util.MissingResourceException;
20import java.util.StringTokenizer;
21
22import com.ibm.icu.impl.Grego;
23
24/**
25 * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE.  You can create a
26 * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>.
27 * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule
28 * in RFC2445 VTIMEZONE format.  Also, you can create a <code>VTimeZone</code> instance
29 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time
30 * zone offset by the rules defined by the data.<br><br>
31 *
32 * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to
33 * decode or encode Non-ASCII text.  Methods reading/writing VTIMEZONE data in this class
34 * do nothing with MIME encoding.
35 *
36 * @stable ICU 3.8
37 */
38public class VTimeZone extends BasicTimeZone {
39
40    private static final long serialVersionUID = -6851467294127795902L;
41
42    /**
43     * Create a <code>VTimeZone</code> instance by the time zone ID.
44     *
45     * @param tzid The time zone ID, such as America/New_York
46     * @return A <code>VTimeZone</code> initialized by the time zone ID, or null
47     * when the ID is unknown.
48     *
49     * @stable ICU 3.8
50     */
51    public static VTimeZone create(String tzid) {
52        BasicTimeZone basicTimeZone = TimeZone.getFrozenICUTimeZone(tzid, true);
53        if (basicTimeZone == null) {
54            return null;
55        }
56        VTimeZone vtz = new VTimeZone(tzid);
57        vtz.tz = (BasicTimeZone) basicTimeZone.cloneAsThawed();
58        vtz.olsonzid = vtz.tz.getID();
59
60        return vtz;
61    }
62
63    /**
64     * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data.
65     *
66     * @param reader The Reader for VTIMEZONE data input stream
67     * @return A <code>VTimeZone</code> initialized by the VTIMEZONE data or
68     * null if failed to load the rule from the VTIMEZONE data.
69     *
70     * @stable ICU 3.8
71     */
72    public static VTimeZone create(Reader reader) {
73        VTimeZone vtz = new VTimeZone();
74        if (vtz.load(reader)) {
75            return vtz;
76        }
77        return null;
78    }
79
80    /**
81     * {@inheritDoc}
82     * @stable ICU 3.8
83     */
84    @Override
85    public int getOffset(int era, int year, int month, int day, int dayOfWeek,
86            int milliseconds) {
87        return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
88    }
89
90    /**
91     * {@inheritDoc}
92     * @stable ICU 3.8
93     */
94    @Override
95    public void getOffset(long date, boolean local, int[] offsets) {
96        tz.getOffset(date, local, offsets);
97    }
98
99    /**
100     * {@inheritDoc}
101     * @internal
102     * @deprecated This API is ICU internal only.
103     */
104    @Deprecated
105    @Override
106    public void getOffsetFromLocal(long date,
107            int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
108        tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
109    }
110
111    /**
112     * {@inheritDoc}
113     * @stable ICU 3.8
114     */
115    @Override
116    public int getRawOffset() {
117        return tz.getRawOffset();
118    }
119
120    /**
121     * {@inheritDoc}
122     * @stable ICU 3.8
123     */
124    @Override
125    public boolean inDaylightTime(Date date) {
126        return tz.inDaylightTime(date);
127    }
128
129    /**
130     * {@inheritDoc}
131     * @stable ICU 3.8
132     */
133    @Override
134    public void setRawOffset(int offsetMillis) {
135        if (isFrozen()) {
136            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
137        }
138        tz.setRawOffset(offsetMillis);
139    }
140
141    /**
142     * {@inheritDoc}
143     * @stable ICU 3.8
144     */
145    @Override
146    public boolean useDaylightTime() {
147        return tz.useDaylightTime();
148    }
149
150    /**
151     * {@inheritDoc}
152     * @stable ICU 49
153     */
154    @Override
155    public boolean observesDaylightTime() {
156        return tz.observesDaylightTime();
157    }
158
159    /**
160     * {@inheritDoc}
161     * @stable ICU 3.8
162     */
163    @Override
164    public boolean hasSameRules(TimeZone other) {
165        if (this == other) {
166            return true;
167        }
168        if (other instanceof VTimeZone) {
169            return tz.hasSameRules(((VTimeZone)other).tz);
170        }
171        return tz.hasSameRules(other);
172    }
173
174    /**
175     * Gets the RFC2445 TZURL property value.  When a <code>VTimeZone</code> instance was created from
176     * VTIMEZONE data, the value is set by the TZURL property value in the data.  Otherwise,
177     * the initial value is null.
178     *
179     * @return The RFC2445 TZURL property value
180     *
181     * @stable ICU 3.8
182     */
183    public String getTZURL() {
184        return tzurl;
185    }
186
187    /**
188     * Sets the RFC2445 TZURL property value.
189     *
190     * @param url The TZURL property value.
191     *
192     * @stable ICU 3.8
193     */
194    public void setTZURL(String url) {
195        if (isFrozen()) {
196            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
197        }
198        tzurl = url;
199    }
200
201    /**
202     * Gets the RFC2445 LAST-MODIFIED property value.  When a <code>VTimeZone</code> instance was created
203     * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data.
204     * Otherwise, the initial value is null.
205     *
206     * @return The Date represents the RFC2445 LAST-MODIFIED date.
207     *
208     * @stable ICU 3.8
209     */
210    public Date getLastModified() {
211        return lastmod;
212    }
213
214    /**
215     * Sets the date used for RFC2445 LAST-MODIFIED property value.
216     *
217     * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value.
218     *
219     * @stable ICU 3.8
220     */
221    public void setLastModified(Date date) {
222        if (isFrozen()) {
223            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
224        }
225        lastmod = date;
226    }
227
228    /**
229     * Writes RFC2445 VTIMEZONE data for this time zone
230     *
231     * @param writer A <code>Writer</code> used for the output
232     * @throws IOException If there were problems creating a buffered writer or writing to it.
233     *
234     * @stable ICU 3.8
235     */
236    public void write(Writer writer) throws IOException {
237        BufferedWriter bw = new BufferedWriter(writer);
238        if (vtzlines != null) {
239            for (String line : vtzlines) {
240                if (line.startsWith(ICAL_TZURL + COLON)) {
241                    if (tzurl != null) {
242                        bw.write(ICAL_TZURL);
243                        bw.write(COLON);
244                        bw.write(tzurl);
245                        bw.write(NEWLINE);
246                    }
247                } else if (line.startsWith(ICAL_LASTMOD + COLON)) {
248                    if (lastmod != null) {
249                        bw.write(ICAL_LASTMOD);
250                        bw.write(COLON);
251                        bw.write(getUTCDateTimeString(lastmod.getTime()));
252                        bw.write(NEWLINE);
253                    }
254                } else {
255                    bw.write(line);
256                    bw.write(NEWLINE);
257                }
258            }
259            bw.flush();
260        } else {
261            String[] customProperties = null;
262            if (olsonzid != null && ICU_TZVERSION != null) {
263                customProperties = new String[1];
264                customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]";
265            }
266            writeZone(writer, tz, customProperties);
267        }
268    }
269
270    /**
271     * Writes RFC2445 VTIMEZONE data applicable for dates after
272     * the specified start time.
273     *
274     * @param writer    The <code>Writer</code> used for the output
275     * @param start     The start time
276     *
277     * @throws IOException If there were problems reading and writing to the writer.
278     *
279     * @stable ICU 3.8
280     */
281    public void write(Writer writer, long start) throws IOException {
282        // Extract rules applicable to dates after the start time
283        TimeZoneRule[] rules = tz.getTimeZoneRules(start);
284
285        // Create a RuleBasedTimeZone with the subset rule
286        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
287        for (int i = 1; i < rules.length; i++) {
288            rbtz.addTransitionRule(rules[i]);
289        }
290        String[] customProperties = null;
291        if (olsonzid != null && ICU_TZVERSION != null) {
292            customProperties = new String[1];
293            customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
294                "/Partial@" + start + "]";
295        }
296        writeZone(writer, rbtz, customProperties);
297    }
298
299    /**
300     * Writes RFC2445 VTIMEZONE data applicable near the specified date.
301     * Some common iCalendar implementations can only handle a single time
302     * zone property or a pair of standard and daylight time properties using
303     * BYDAY rule with day of week (such as BYDAY=1SUN).  This method produce
304     * the VTIMEZONE data which can be handled these implementations.  The rules
305     * produced by this method can be used only for calculating time zone offset
306     * around the specified date.
307     *
308     * @param writer    The <code>Writer</code> used for the output
309     * @param time      The date
310     *
311     * @throws IOException If there were problems reading or writing to the writer.
312     *
313     * @stable ICU 3.8
314     */
315    public void writeSimple(Writer writer, long time) throws IOException {
316        // Extract simple rules
317        TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time);
318
319        // Create a RuleBasedTimeZone with the subset rule
320        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
321        for (int i = 1; i < rules.length; i++) {
322            rbtz.addTransitionRule(rules[i]);
323        }
324        String[] customProperties = null;
325        if (olsonzid != null && ICU_TZVERSION != null) {
326            customProperties = new String[1];
327            customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
328                "/Simple@" + time + "]";
329        }
330        writeZone(writer, rbtz, customProperties);
331    }
332
333    // BasicTimeZone methods
334
335    /**
336     * {@inheritDoc}
337     * @stable ICU 3.8
338     */
339    @Override
340    public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
341        return tz.getNextTransition(base, inclusive);
342    }
343
344    /**
345     * {@inheritDoc}
346     * @stable ICU 3.8
347     */
348    @Override
349    public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
350        return tz.getPreviousTransition(base, inclusive);
351    }
352
353    /**
354     * {@inheritDoc}
355     * @stable ICU 3.8
356     */
357    @Override
358    public boolean hasEquivalentTransitions(TimeZone other, long start, long end) {
359        if (this == other) {
360            return true;
361        }
362        return tz.hasEquivalentTransitions(other, start, end);
363    }
364
365    /**
366     * {@inheritDoc}
367     * @stable ICU 3.8
368     */
369    @Override
370    public TimeZoneRule[] getTimeZoneRules() {
371        return tz.getTimeZoneRules();
372    }
373
374    /**
375     * {@inheritDoc}
376     * @stable ICU 3.8
377     */
378    @Override
379    public TimeZoneRule[] getTimeZoneRules(long start) {
380        return tz.getTimeZoneRules(start);
381    }
382
383    /**
384     * {@inheritDoc}
385     * @stable ICU 3.8
386     */
387    @Override
388    public Object clone() {
389        if (isFrozen()) {
390            return this;
391        }
392        return cloneAsThawed();
393    }
394
395    // private stuff ------------------------------------------------------
396
397    private BasicTimeZone tz;
398    private List<String> vtzlines;
399    private String olsonzid = null;
400    private String tzurl = null;
401    private Date lastmod = null;
402
403    private static String ICU_TZVERSION;
404    private static final String ICU_TZINFO_PROP = "X-TZINFO";
405
406    // Default DST savings
407    private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour
408
409    // Default time start
410    private static final long DEF_TZSTARTTIME = 0;
411
412    // minimum/max
413    private static final long MIN_TIME = Long.MIN_VALUE;
414    private static final long MAX_TIME = Long.MAX_VALUE;
415
416    // Symbol characters used by RFC2445 VTIMEZONE
417    private static final String COLON = ":";
418    private static final String SEMICOLON = ";";
419    private static final String EQUALS_SIGN = "=";
420    private static final String COMMA = ",";
421    private static final String NEWLINE = "\r\n";   // CRLF
422
423    // RFC2445 VTIMEZONE tokens
424    private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE";
425    private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE";
426    private static final String ICAL_BEGIN = "BEGIN";
427    private static final String ICAL_END = "END";
428    private static final String ICAL_VTIMEZONE = "VTIMEZONE";
429    private static final String ICAL_TZID = "TZID";
430    private static final String ICAL_STANDARD = "STANDARD";
431    private static final String ICAL_DAYLIGHT = "DAYLIGHT";
432    private static final String ICAL_DTSTART = "DTSTART";
433    private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM";
434    private static final String ICAL_TZOFFSETTO = "TZOFFSETTO";
435    private static final String ICAL_RDATE = "RDATE";
436    private static final String ICAL_RRULE = "RRULE";
437    private static final String ICAL_TZNAME = "TZNAME";
438    private static final String ICAL_TZURL = "TZURL";
439    private static final String ICAL_LASTMOD = "LAST-MODIFIED";
440
441    private static final String ICAL_FREQ = "FREQ";
442    private static final String ICAL_UNTIL = "UNTIL";
443    private static final String ICAL_YEARLY = "YEARLY";
444    private static final String ICAL_BYMONTH = "BYMONTH";
445    private static final String ICAL_BYDAY = "BYDAY";
446    private static final String ICAL_BYMONTHDAY = "BYMONTHDAY";
447
448    private static final String[] ICAL_DOW_NAMES =
449    {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
450
451    // Month length in regular year
452    private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
453
454    static {
455        // Initialize ICU_TZVERSION
456        try {
457            ICU_TZVERSION = TimeZone.getTZDataVersion();
458        } catch (MissingResourceException e) {
459            ///CLOVER:OFF
460            ICU_TZVERSION = null;
461            ///CLOVER:ON
462        }
463    }
464
465    /* Hide the constructor */
466    private VTimeZone() {
467    }
468
469    private VTimeZone(String tzid) {
470        super(tzid);
471    }
472
473    /*
474     * Read the input stream to locate the VTIMEZONE block and
475     * parse the contents to initialize this VTimeZone object.
476     * The reader skips other RFC2445 message headers.  After
477     * the parse is completed, the reader points at the beginning
478     * of the header field just after the end of VTIMEZONE block.
479     * When VTIMEZONE block is found and this object is successfully
480     * initialized by the rules described in the data, this method
481     * returns true.  Otherwise, returns false.
482     */
483    private boolean load(Reader reader) {
484        // Read VTIMEZONE block into string array
485        try {
486            vtzlines = new LinkedList<String>();
487            boolean eol = false;
488            boolean start = false;
489            boolean success = false;
490            StringBuilder line = new StringBuilder();
491            while (true) {
492                int ch = reader.read();
493                if (ch == -1) {
494                    // end of file
495                    if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) {
496                        vtzlines.add(line.toString());
497                        success = true;
498                    }
499                    break;
500                }
501                if (ch == 0x0D) {
502                    // CR, must be followed by LF by the definition in RFC2445
503                    continue;
504                }
505
506                if (eol) {
507                    if (ch != 0x09 && ch != 0x20) {
508                        // NOT followed by TAB/SP -> new line
509                        if (start) {
510                            if (line.length() > 0) {
511                                vtzlines.add(line.toString());
512                            }
513                        }
514                        line.setLength(0);
515                        if (ch != 0x0A) {
516                            line.append((char)ch);
517                        }
518                    }
519                    eol = false;
520                } else {
521                    if (ch == 0x0A) {
522                        // LF
523                        eol = true;
524                        if (start) {
525                            if (line.toString().startsWith(ICAL_END_VTIMEZONE)) {
526                                vtzlines.add(line.toString());
527                                success = true;
528                                break;
529                            }
530                        } else {
531                            if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) {
532                                vtzlines.add(line.toString());
533                                line.setLength(0);
534                                start = true;
535                                eol = false;
536                            }
537                        }
538                    } else {
539                        line.append((char)ch);
540                    }
541                }
542            }
543            if (!success) {
544                return false;
545            }
546        } catch (IOException ioe) {
547            ///CLOVER:OFF
548            return false;
549            ///CLOVER:ON
550        }
551        return parse();
552    }
553
554    // parser state
555    private static final int INI = 0;   // Initial state
556    private static final int VTZ = 1;   // In VTIMEZONE
557    private static final int TZI = 2;   // In STANDARD or DAYLIGHT
558    private static final int ERR = 3;   // Error state
559
560    /*
561     * Parse VTIMEZONE data and create a RuleBasedTimeZone
562     */
563    private boolean parse() {
564        ///CLOVER:OFF
565        if (vtzlines == null || vtzlines.size() == 0) {
566            return false;
567        }
568        ///CLOVER:ON
569
570        // timezone ID
571        String tzid = null;
572
573        int state = INI;
574        boolean dst = false;    // current zone type
575        String from = null;     // current zone from offset
576        String to = null;       // current zone offset
577        String tzname = null;   // current zone name
578        String dtstart = null;  // current zone starts
579        boolean isRRULE = false;    // true if the rule is described by RRULE
580        List<String> dates = null;  // list of RDATE or RRULE strings
581        List<TimeZoneRule> rules = new ArrayList<TimeZoneRule>();   // rule list
582        int initialRawOffset = 0;  // initial offset
583        int initialDSTSavings = 0;  // initial offset
584        long firstStart = MAX_TIME; // the earliest rule start time
585
586        for (String line : vtzlines) {
587            int valueSep = line.indexOf(COLON);
588            if (valueSep < 0) {
589                continue;
590            }
591            String name = line.substring(0, valueSep);
592            String value = line.substring(valueSep + 1);
593
594            switch (state) {
595            case INI:
596                if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) {
597                    state = VTZ;
598                }
599                break;
600            case VTZ:
601                if (name.equals(ICAL_TZID)) {
602                    tzid = value;
603                } else if (name.equals(ICAL_TZURL)) {
604                    tzurl = value;
605                } else if (name.equals(ICAL_LASTMOD)) {
606                    // Always in 'Z' format, so the offset argument for the parse method
607                    // can be any value.
608                    lastmod = new Date(parseDateTimeString(value, 0));
609                } else if (name.equals(ICAL_BEGIN)) {
610                    boolean isDST = value.equals(ICAL_DAYLIGHT);
611                    if (value.equals(ICAL_STANDARD) || isDST) {
612                        // tzid must be ready at this point
613                        if (tzid == null) {
614                            state = ERR;
615                            break;
616                        }
617                        // initialize current zone properties
618                        dates = null;
619                        isRRULE = false;
620                        from = null;
621                        to = null;
622                        tzname = null;
623                        dst = isDST;
624                        state = TZI;
625                    } else {
626                        // BEGIN property other than STANDARD/DAYLIGHT
627                        // must not be there.
628                        state = ERR;
629                        break;
630                    }
631                } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) {
632                    break;
633                }
634                break;
635
636            case TZI:
637                if (name.equals(ICAL_DTSTART)) {
638                    dtstart = value;
639                } else if (name.equals(ICAL_TZNAME)) {
640                    tzname = value;
641                } else if (name.equals(ICAL_TZOFFSETFROM)) {
642                    from = value;
643                } else if (name.equals(ICAL_TZOFFSETTO)) {
644                    to = value;
645                } else if (name.equals(ICAL_RDATE)) {
646                    // RDATE mixed with RRULE is not supported
647                    if (isRRULE) {
648                        state = ERR;
649                        break;
650                    }
651                    if (dates == null) {
652                        dates = new LinkedList<String>();
653                    }
654                    // RDATE value may contain multiple date delimited
655                    // by comma
656                    StringTokenizer st = new StringTokenizer(value, COMMA);
657                    while (st.hasMoreTokens()) {
658                        String date = st.nextToken();
659                        dates.add(date);
660                    }
661                } else if (name.equals(ICAL_RRULE)) {
662                    // RRULE mixed with RDATE is not supported
663                    if (!isRRULE && dates != null) {
664                        state = ERR;
665                        break;
666                    } else if (dates == null) {
667                        dates = new LinkedList<String>();
668                    }
669                    isRRULE = true;
670                    dates.add(value);
671                } else if (name.equals(ICAL_END)) {
672                    // Mandatory properties
673                    if (dtstart == null || from == null || to == null) {
674                        state = ERR;
675                        break;
676                    }
677                    // if tzname is not available, create one from tzid
678                    if (tzname == null) {
679                        tzname = getDefaultTZName(tzid, dst);
680                    }
681
682                    // create a time zone rule
683                    TimeZoneRule rule = null;
684                    int fromOffset = 0;
685                    int toOffset = 0;
686                    int rawOffset = 0;
687                    int dstSavings = 0;
688                    long start = 0;
689                    try {
690                        // Parse TZOFFSETFROM/TZOFFSETTO
691                        fromOffset = offsetStrToMillis(from);
692                        toOffset = offsetStrToMillis(to);
693
694                        if (dst) {
695                            // If daylight, use the previous offset as rawoffset if positive
696                            if (toOffset - fromOffset > 0) {
697                                rawOffset = fromOffset;
698                                dstSavings = toOffset - fromOffset;
699                            } else {
700                                // This is rare case..  just use 1 hour DST savings
701                                rawOffset = toOffset - DEF_DSTSAVINGS;
702                                dstSavings = DEF_DSTSAVINGS;
703                            }
704                        } else {
705                            rawOffset = toOffset;
706                            dstSavings = 0;
707                        }
708
709                        // start time
710                        start = parseDateTimeString(dtstart, fromOffset);
711
712                        // Create the rule
713                        Date actualStart = null;
714                        if (isRRULE) {
715                            rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
716                        } else {
717                            rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
718                        }
719                        if (rule != null) {
720                            actualStart = rule.getFirstStart(fromOffset, 0);
721                            if (actualStart.getTime() < firstStart) {
722                                // save from offset information for the earliest rule
723                                firstStart = actualStart.getTime();
724                                // If this is STD, assume the time before this transtion
725                                // is DST when the difference is 1 hour.  This might not be
726                                // accurate, but VTIMEZONE data does not have such info.
727                                if (dstSavings > 0) {
728                                    initialRawOffset = fromOffset;
729                                    initialDSTSavings = 0;
730                                } else {
731                                    if (fromOffset - toOffset == DEF_DSTSAVINGS) {
732                                        initialRawOffset = fromOffset - DEF_DSTSAVINGS;
733                                        initialDSTSavings = DEF_DSTSAVINGS;
734                                    } else {
735                                        initialRawOffset = fromOffset;
736                                        initialDSTSavings = 0;
737                                    }
738                                }
739                            }
740                        }
741                    } catch (IllegalArgumentException iae) {
742                        // bad format - rule == null..
743                    }
744
745                    if (rule == null) {
746                        state = ERR;
747                        break;
748                    }
749                    rules.add(rule);
750                    state = VTZ;
751                }
752                break;
753            }
754
755            if (state == ERR) {
756                vtzlines = null;
757                return false;
758            }
759        }
760
761        // Must have at least one rule
762        if (rules.size() == 0) {
763            return false;
764        }
765
766        // Create a initial rule
767        InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false),
768                initialRawOffset, initialDSTSavings);
769
770        // Finally, create the RuleBasedTimeZone
771        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule);
772
773        int finalRuleIdx = -1;
774        int finalRuleCount = 0;
775        for (int i = 0; i < rules.size(); i++) {
776            TimeZoneRule r = rules.get(i);
777            if (r instanceof AnnualTimeZoneRule) {
778                if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
779                    finalRuleCount++;
780                    finalRuleIdx = i;
781                }
782            }
783        }
784        if (finalRuleCount > 2) {
785            // Too many final rules
786            return false;
787        }
788
789        if (finalRuleCount == 1) {
790            if (rules.size() == 1) {
791                // Only one final rule, only governs the initial rule,
792                // which is already initialized, thus, we do not need to
793                // add this transition rule
794                rules.clear();
795            } else {
796                // Normalize the final rule
797                AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx);
798                int tmpRaw = finalRule.getRawOffset();
799                int tmpDST = finalRule.getDSTSavings();
800
801                // Find the last non-final rule
802                Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings);
803                Date start = finalStart;
804                for (int i = 0; i < rules.size(); i++) {
805                    if (finalRuleIdx == i) {
806                        continue;
807                    }
808                    TimeZoneRule r = rules.get(i);
809                    Date lastStart = r.getFinalStart(tmpRaw, tmpDST);
810                    if (lastStart.after(start)) {
811                        start = finalRule.getNextStart(lastStart.getTime(),
812                                r.getRawOffset(),
813                                r.getDSTSavings(),
814                                false);
815                    }
816                }
817                TimeZoneRule newRule;
818                if (start == finalStart) {
819                    // Transform this into a single transition
820                    newRule = new TimeArrayTimeZoneRule(
821                            finalRule.getName(),
822                            finalRule.getRawOffset(),
823                            finalRule.getDSTSavings(),
824                            new long[] {finalStart.getTime()},
825                            DateTimeRule.UTC_TIME);
826                } else {
827                    // Update the end year
828                    int fields[] = Grego.timeToFields(start.getTime(), null);
829                    newRule = new AnnualTimeZoneRule(
830                            finalRule.getName(),
831                            finalRule.getRawOffset(),
832                            finalRule.getDSTSavings(),
833                            finalRule.getRule(),
834                            finalRule.getStartYear(),
835                            fields[0]);
836                }
837                rules.set(finalRuleIdx, newRule);
838            }
839        }
840
841        for (TimeZoneRule r : rules) {
842            rbtz.addTransitionRule(r);
843        }
844
845        tz = rbtz;
846        setID(tzid);
847        return true;
848    }
849
850    /*
851     * Create a default TZNAME from TZID
852     */
853    private static String getDefaultTZName(String tzid, boolean isDST) {
854        if (isDST) {
855            return tzid + "(DST)";
856        }
857        return tzid + "(STD)";
858    }
859
860    /*
861     * Create a TimeZoneRule by the RRULE definition
862     */
863    private static TimeZoneRule createRuleByRRULE(String tzname,
864            int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
865        if (dates == null || dates.size() == 0) {
866            return null;
867        }
868        // Parse the first rule
869        String rrule = dates.get(0);
870
871        long until[] = new long[1];
872        int[] ruleFields = parseRRULE(rrule, until);
873        if (ruleFields == null) {
874            // Invalid RRULE
875            return null;
876        }
877
878        int month = ruleFields[0];
879        int dayOfWeek = ruleFields[1];
880        int nthDayOfWeek = ruleFields[2];
881        int dayOfMonth = ruleFields[3];
882
883        if (dates.size() == 1) {
884            // No more rules
885            if (ruleFields.length > 4) {
886                // Multiple BYMONTHDAY values
887
888                if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) {
889                    // Only support the rule using 7 continuous days
890                    // BYMONTH and BYDAY must be set at the same time
891                    return null;
892                }
893                int firstDay = 31; // max possible number of dates in a month
894                int days[] = new int[7];
895                for (int i = 0; i < 7; i++) {
896                    days[i] = ruleFields[3 + i];
897                    // Resolve negative day numbers.  A negative day number should
898                    // not be used in February, but if we see such case, we use 28
899                    // as the base.
900                    days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1;
901                    firstDay = days[i] < firstDay ? days[i] : firstDay;
902                }
903                // Make sure days are continuous
904                for (int i = 1; i < 7; i++) {
905                    boolean found = false;
906                    for (int j = 0; j < 7; j++) {
907                        if (days[j] == firstDay + i) {
908                            found = true;
909                            break;
910                        }
911                    }
912                    if (!found) {
913                        // days are not continuous
914                        return null;
915                    }
916                }
917                // Use DOW_GEQ_DOM rule with firstDay as the start date
918                dayOfMonth = firstDay;
919            }
920        } else {
921            // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines.
922            // Otherwise, not supported.
923            if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) {
924                // This is not the case
925                return null;
926            }
927            // Parse the rest of rules if number of rules is not exceeding 7.
928            // We can only support 7 continuous days starting from a day of month.
929            if (dates.size() > 7) {
930                return null;
931            }
932
933            // Note: To check valid date range across multiple rule is a little
934            // bit complicated.  For now, this code is not doing strict range
935            // checking across month boundary
936
937            int earliestMonth = month;
938            int daysCount = ruleFields.length - 3;
939            int earliestDay = 31;
940            for (int i = 0; i < daysCount; i++) {
941                int dom = ruleFields[3 + i];
942                dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1;
943                earliestDay = dom < earliestDay ? dom : earliestDay;
944            }
945
946            int anotherMonth = -1;
947            for (int i = 1; i < dates.size(); i++) {
948                rrule = dates.get(i);
949                long[] unt = new long[1];
950                int[] fields = parseRRULE(rrule, unt);
951
952                // If UNTIL is newer than previous one, use the one
953                if (unt[0] > until[0]) {
954                    until = unt;
955                }
956
957                // Check if BYMONTH + BYMONTHDAY + BYDAY rule
958                if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) {
959                    return null;
960                }
961                // Count number of BYMONTHDAY
962                int count = fields.length - 3;
963                if (daysCount + count > 7) {
964                    // We cannot support BYMONTHDAY more than 7
965                    return null;
966                }
967                // Check if the same BYDAY is used.  Otherwise, we cannot
968                // support the rule
969                if (fields[1] != dayOfWeek) {
970                    return null;
971                }
972                // Check if the month is same or right next to the primary month
973                if (fields[0] != month) {
974                    if (anotherMonth == -1) {
975                        int diff = fields[0] - month;
976                        if (diff == -11 || diff == -1) {
977                            // Previous month
978                            anotherMonth = fields[0];
979                            earliestMonth = anotherMonth;
980                            // Reset earliest day
981                            earliestDay = 31;
982                        } else if (diff == 11 || diff == 1) {
983                            // Next month
984                            anotherMonth = fields[0];
985                        } else {
986                            // The day range cannot exceed more than 2 months
987                            return null;
988                        }
989                    } else if (fields[0] != month && fields[0] != anotherMonth) {
990                        // The day range cannot exceed more than 2 months
991                        return null;
992                    }
993                }
994                // If ealier month, go through days to find the earliest day
995                if (fields[0] == earliestMonth) {
996                    for (int j = 0; j < count; j++) {
997                        int dom = fields[3 + j];
998                        dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1;
999                        earliestDay = dom < earliestDay ? dom : earliestDay;
1000                    }
1001                }
1002                daysCount += count;
1003            }
1004            if (daysCount != 7) {
1005                // Number of BYMONTHDAY entries must be 7
1006                return null;
1007            }
1008            month = earliestMonth;
1009            dayOfMonth = earliestDay;
1010        }
1011
1012        // Calculate start/end year and missing fields
1013        int[] dfields = Grego.timeToFields(start + fromOffset, null);
1014        int startYear = dfields[0];
1015        if (month == -1) {
1016            // If MYMONTH is not set, use the month of DTSTART
1017            month = dfields[1];
1018        }
1019        if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) {
1020            // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY
1021            dayOfMonth = dfields[2];
1022        }
1023        int timeInDay = dfields[5];
1024
1025        int endYear = AnnualTimeZoneRule.MAX_YEAR;
1026        if (until[0] != MIN_TIME) {
1027            Grego.timeToFields(until[0], dfields);
1028            endYear = dfields[0];
1029        }
1030
1031        // Create the AnnualDateTimeRule
1032        DateTimeRule adtr = null;
1033        if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
1034            // Day in month rule, for example, 15th day in the month
1035            adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME);
1036        } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) {
1037            // Nth day of week rule, for example, last Sunday
1038            adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME);
1039        } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
1040            // First day of week after day of month rule, for example,
1041            // first Sunday after 15th day in the month
1042            adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME);
1043        } else {
1044            // RRULE attributes are insufficient
1045            return null;
1046        }
1047
1048        return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear);
1049    }
1050
1051    /*
1052     * Parse individual RRULE
1053     *
1054     * On return -
1055     *
1056     * int[0] month calculated by BYMONTH - 1, or -1 when not found
1057     * int[1] day of week in BYDAY, or 0 when not found
1058     * int[2] day of week ordinal number in BYDAY, or 0 when not found
1059     * int[i >= 3] day of month, which could be multiple values, or 0 when not found
1060     *
1061     *  or
1062     *
1063     * null on any error cases, for exmaple, FREQ=YEARLY is not available
1064     *
1065     * When UNTIL attribute is available, the time will be set to until[0],
1066     * otherwise, MIN_TIME
1067     */
1068    private static int[] parseRRULE(String rrule, long[] until) {
1069        int month = -1;
1070        int dayOfWeek = 0;
1071        int nthDayOfWeek = 0;
1072        int[] dayOfMonth = null;
1073
1074        long untilTime = MIN_TIME;
1075        boolean yearly = false;
1076        boolean parseError = false;
1077        StringTokenizer st= new StringTokenizer(rrule, SEMICOLON);
1078
1079        while (st.hasMoreTokens()) {
1080            String attr, value;
1081            String prop = st.nextToken();
1082            int sep = prop.indexOf(EQUALS_SIGN);
1083            if (sep != -1) {
1084                attr = prop.substring(0, sep);
1085                value = prop.substring(sep + 1);
1086            } else {
1087                parseError = true;
1088                break;
1089            }
1090
1091            if (attr.equals(ICAL_FREQ)) {
1092                // only support YEARLY frequency type
1093                if (value.equals(ICAL_YEARLY)) {
1094                    yearly = true;
1095                } else {
1096                    parseError = true;
1097                    break;
1098                }
1099            } else if (attr.equals(ICAL_UNTIL)) {
1100                // ISO8601 UTC format, for example, "20060315T020000Z"
1101                try {
1102                    untilTime = parseDateTimeString(value, 0);
1103                } catch (IllegalArgumentException iae) {
1104                    parseError = true;
1105                    break;
1106                }
1107            } else if (attr.equals(ICAL_BYMONTH)) {
1108                // Note: BYMONTH may contain multiple months, but only single month make sense for
1109                // VTIMEZONE property.
1110                if (value.length() > 2) {
1111                    parseError = true;
1112                    break;
1113                }
1114                try {
1115                    month = Integer.parseInt(value) - 1;
1116                    if (month < 0 || month >= 12) {
1117                        parseError = true;
1118                        break;
1119                    }
1120                } catch (NumberFormatException nfe) {
1121                    parseError = true;
1122                    break;
1123                }
1124            } else if (attr.equals(ICAL_BYDAY)) {
1125                // Note: BYDAY may contain multiple day of week separated by comma.  It is unlikely used for
1126                // VTIMEZONE property.  We do not support the case.
1127
1128                // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday
1129                // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday
1130                int length = value.length();
1131                if (length < 2 || length > 4) {
1132                    parseError = true;
1133                    break;
1134                }
1135                if (length > 2) {
1136                    // Nth day of week
1137                    int sign = 1;
1138                    if (value.charAt(0) == '+') {
1139                        sign = 1;
1140                    } else if (value.charAt(0) == '-') {
1141                        sign = -1;
1142                    } else if (length == 4) {
1143                        parseError = true;
1144                        break;
1145                    }
1146                    try {
1147                        int n = Integer.parseInt(value.substring(length - 3, length - 2));
1148                        if (n == 0 || n > 4) {
1149                            parseError = true;
1150                            break;
1151                        }
1152                        nthDayOfWeek = n * sign;
1153                    } catch(NumberFormatException nfe) {
1154                        parseError = true;
1155                        break;
1156                    }
1157                    value = value.substring(length - 2);
1158                }
1159                int wday;
1160                for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) {
1161                    if (value.equals(ICAL_DOW_NAMES[wday])) {
1162                        break;
1163                    }
1164                }
1165                if (wday < ICAL_DOW_NAMES.length) {
1166                    // Sunday(1) - Saturday(7)
1167                    dayOfWeek = wday + 1;
1168                } else {
1169                    parseError = true;
1170                    break;
1171                }
1172            } else if (attr.equals(ICAL_BYMONTHDAY)) {
1173                // Note: BYMONTHDAY may contain multiple days delimited by comma
1174                //
1175                // A value of BYMONTHDAY could be negative, for example, -1 means
1176                // the last day in a month
1177                StringTokenizer days = new StringTokenizer(value, COMMA);
1178                int count = days.countTokens();
1179                dayOfMonth = new int[count];
1180                int index = 0;
1181                while(days.hasMoreTokens()) {
1182                    try {
1183                        dayOfMonth[index++] = Integer.parseInt(days.nextToken());
1184                    } catch (NumberFormatException nfe) {
1185                        parseError = true;
1186                        break;
1187                    }
1188                }
1189            }
1190        }
1191
1192        if (parseError) {
1193            return null;
1194        }
1195        if (!yearly) {
1196            // FREQ=YEARLY must be set
1197            return null;
1198        }
1199
1200        until[0] = untilTime;
1201
1202        int[] results;
1203        if (dayOfMonth == null) {
1204            results = new int[4];
1205            results[3] = 0;
1206        } else {
1207            results = new int[3 + dayOfMonth.length];
1208            for (int i = 0; i < dayOfMonth.length; i++) {
1209                results[3 + i] = dayOfMonth[i];
1210            }
1211        }
1212        results[0] = month;
1213        results[1] = dayOfWeek;
1214        results[2] = nthDayOfWeek;
1215        return results;
1216    }
1217
1218    /*
1219     * Create a TimeZoneRule by the RDATE definition
1220     */
1221    private static TimeZoneRule createRuleByRDATE(String tzname,
1222            int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
1223        // Create an array of transition times
1224        long[] times;
1225        if (dates == null || dates.size() == 0) {
1226            // When no RDATE line is provided, use start (DTSTART)
1227            // as the transition time
1228            times = new long[1];
1229            times[0] = start;
1230        } else {
1231            times = new long[dates.size()];
1232            int idx = 0;
1233            try {
1234                for (String date : dates) {
1235                    times[idx++] = parseDateTimeString(date, fromOffset);
1236                }
1237            } catch (IllegalArgumentException iae) {
1238                return null;
1239            }
1240        }
1241        return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME);
1242    }
1243
1244    /*
1245     * Write the time zone rules in RFC2445 VTIMEZONE format
1246     */
1247    private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException {
1248        // Write the header
1249        writeHeader(w);
1250
1251        if (customProperties != null && customProperties.length > 0) {
1252            for (int i = 0; i < customProperties.length; i++) {
1253                if (customProperties[i] != null) {
1254                    w.write(customProperties[i]);
1255                    w.write(NEWLINE);
1256                }
1257            }
1258        }
1259
1260        long t = MIN_TIME;
1261        String dstName = null;
1262        int dstFromOffset = 0;
1263        int dstFromDSTSavings = 0;
1264        int dstToOffset = 0;
1265        int dstStartYear = 0;
1266        int dstMonth = 0;
1267        int dstDayOfWeek = 0;
1268        int dstWeekInMonth = 0;
1269        int dstMillisInDay = 0;
1270        long dstStartTime = 0;
1271        long dstUntilTime = 0;
1272        int dstCount = 0;
1273        AnnualTimeZoneRule finalDstRule = null;
1274
1275        String stdName = null;
1276        int stdFromOffset = 0;
1277        int stdFromDSTSavings = 0;
1278        int stdToOffset = 0;
1279        int stdStartYear = 0;
1280        int stdMonth = 0;
1281        int stdDayOfWeek = 0;
1282        int stdWeekInMonth = 0;
1283        int stdMillisInDay = 0;
1284        long stdStartTime = 0;
1285        long stdUntilTime = 0;
1286        int stdCount = 0;
1287        AnnualTimeZoneRule finalStdRule = null;
1288
1289        int[] dtfields = new int[6];
1290        boolean hasTransitions = false;
1291
1292        // Going through all transitions
1293        while(true) {
1294            TimeZoneTransition tzt = basictz.getNextTransition(t, false);
1295            if (tzt == null) {
1296                break;
1297            }
1298            hasTransitions = true;
1299            t = tzt.getTime();
1300            String name = tzt.getTo().getName();
1301            boolean isDst = (tzt.getTo().getDSTSavings() != 0);
1302            int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
1303            int fromDSTSavings = tzt.getFrom().getDSTSavings();
1304            int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
1305            Grego.timeToFields(tzt.getTime() + fromOffset, dtfields);
1306            int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]);
1307            int year = dtfields[0];
1308            boolean sameRule = false;
1309            if (isDst) {
1310                if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
1311                    if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
1312                        finalDstRule = (AnnualTimeZoneRule)tzt.getTo();
1313                    }
1314                }
1315                if (dstCount > 0) {
1316                    if (year == dstStartYear + dstCount
1317                            && name.equals(dstName)
1318                            && dstFromOffset == fromOffset
1319                            && dstToOffset == toOffset
1320                            && dstMonth == dtfields[1]
1321                            && dstDayOfWeek == dtfields[3]
1322                            && dstWeekInMonth == weekInMonth
1323                            && dstMillisInDay == dtfields[5]) {
1324                        // Update until time
1325                        dstUntilTime = t;
1326                        dstCount++;
1327                        sameRule = true;
1328                    }
1329                    if (!sameRule) {
1330                        if (dstCount == 1) {
1331                            writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
1332                                    dstStartTime, true);
1333                        } else {
1334                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1335                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1336                        }
1337                    }
1338                }
1339                if (!sameRule) {
1340                    // Reset this DST information
1341                    dstName = name;
1342                    dstFromOffset = fromOffset;
1343                    dstFromDSTSavings = fromDSTSavings;
1344                    dstToOffset = toOffset;
1345                    dstStartYear = year;
1346                    dstMonth = dtfields[1];
1347                    dstDayOfWeek = dtfields[3];
1348                    dstWeekInMonth = weekInMonth;
1349                    dstMillisInDay = dtfields[5];
1350                    dstStartTime = dstUntilTime = t;
1351                    dstCount = 1;
1352                }
1353                if (finalStdRule != null && finalDstRule != null) {
1354                    break;
1355                }
1356            } else {
1357                if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
1358                    if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
1359                        finalStdRule = (AnnualTimeZoneRule)tzt.getTo();
1360                    }
1361                }
1362                if (stdCount > 0) {
1363                    if (year == stdStartYear + stdCount
1364                            && name.equals(stdName)
1365                            && stdFromOffset == fromOffset
1366                            && stdToOffset == toOffset
1367                            && stdMonth == dtfields[1]
1368                            && stdDayOfWeek == dtfields[3]
1369                            && stdWeekInMonth == weekInMonth
1370                            && stdMillisInDay == dtfields[5]) {
1371                        // Update until time
1372                        stdUntilTime = t;
1373                        stdCount++;
1374                        sameRule = true;
1375                    }
1376                    if (!sameRule) {
1377                        if (stdCount == 1) {
1378                            writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
1379                                    stdStartTime, true);
1380                        } else {
1381                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1382                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1383                        }
1384                    }
1385                }
1386                if (!sameRule) {
1387                    // Reset this STD information
1388                    stdName = name;
1389                    stdFromOffset = fromOffset;
1390                    stdFromDSTSavings = fromDSTSavings;
1391                    stdToOffset = toOffset;
1392                    stdStartYear = year;
1393                    stdMonth = dtfields[1];
1394                    stdDayOfWeek = dtfields[3];
1395                    stdWeekInMonth = weekInMonth;
1396                    stdMillisInDay = dtfields[5];
1397                    stdStartTime = stdUntilTime = t;
1398                    stdCount = 1;
1399                }
1400                if (finalStdRule != null && finalDstRule != null) {
1401                    break;
1402                }
1403            }
1404        }
1405        if (!hasTransitions) {
1406            // No transition - put a single non transition RDATE
1407            int offset = basictz.getOffset(0 /* any time */);
1408            boolean isDst = (offset != basictz.getRawOffset());
1409            writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst),
1410                    offset, offset, DEF_TZSTARTTIME - offset, false);
1411        } else {
1412            if (dstCount > 0) {
1413                if (finalDstRule == null) {
1414                    if (dstCount == 1) {
1415                        writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
1416                                dstStartTime, true);
1417                    } else {
1418                        writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1419                                dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1420                    }
1421                } else {
1422                    if (dstCount == 1) {
1423                        writeFinalRule(w, true, finalDstRule,
1424                                dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
1425                    } else {
1426                        // Use a single rule if possible
1427                        if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) {
1428                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1429                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME);
1430                        } else {
1431                            // Not equivalent rule - write out two different rules
1432                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1433                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1434
1435                            Date nextStart = finalDstRule.getNextStart(dstUntilTime,
1436                                    dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, false);
1437
1438                            assert nextStart != null;
1439                            if (nextStart != null) {
1440                                writeFinalRule(w, true, finalDstRule,
1441                                        dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, nextStart.getTime());
1442                            }
1443                        }
1444                    }
1445                }
1446            }
1447            if (stdCount > 0) {
1448                if (finalStdRule == null) {
1449                    if (stdCount == 1) {
1450                        writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
1451                                stdStartTime, true);
1452                    } else {
1453                        writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1454                                stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1455                    }
1456                } else {
1457                    if (stdCount == 1) {
1458                        writeFinalRule(w, false, finalStdRule,
1459                                stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
1460                    } else {
1461                        // Use a single rule if possible
1462                        if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) {
1463                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1464                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME);
1465                        } else {
1466                            // Not equivalent rule - write out two different rules
1467                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1468                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1469
1470                            Date nextStart = finalStdRule.getNextStart(stdUntilTime,
1471                                    stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, false);
1472
1473                            assert nextStart != null;
1474                            if (nextStart != null) {
1475                                writeFinalRule(w, false, finalStdRule,
1476                                        stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, nextStart.getTime());
1477
1478                            }
1479                        }
1480                    }
1481                }
1482            }
1483        }
1484        writeFooter(w);
1485    }
1486
1487    /*
1488     * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent
1489     * to the DateTimerule.
1490     */
1491    private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) {
1492        if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) {
1493            return false;
1494        }
1495        if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) {
1496            // Do not try to do more intelligent comparison for now.
1497            return false;
1498        }
1499        if (dtrule.getDateRuleType() == DateTimeRule.DOW
1500                && dtrule.getRuleWeekInMonth() == weekInMonth) {
1501            return true;
1502        }
1503        int ruleDOM = dtrule.getRuleDayOfMonth();
1504        if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) {
1505            if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) {
1506                return true;
1507            }
1508            if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6
1509                    && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) {
1510                return true;
1511            }
1512        }
1513        if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) {
1514            if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) {
1515                return true;
1516            }
1517            if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0
1518                    && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) {
1519                return true;
1520            }
1521        }
1522        return false;
1523    }
1524
1525    /*
1526     * Write a single start time
1527     */
1528    private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname,
1529            int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException {
1530        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time);
1531        if (withRDATE) {
1532            writer.write(ICAL_RDATE);
1533            writer.write(COLON);
1534            writer.write(getDateTimeString(time + fromOffset));
1535            writer.write(NEWLINE);
1536        }
1537        endZoneProps(writer, isDst);
1538    }
1539
1540    /*
1541     * Write start times defined by a DOM rule using VTIMEZONE RRULE
1542     */
1543    private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
1544            int month, int dayOfMonth, long startTime, long untilTime) throws IOException {
1545        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
1546
1547        beginRRULE(writer, month);
1548        writer.write(ICAL_BYMONTHDAY);
1549        writer.write(EQUALS_SIGN);
1550        writer.write(Integer.toString(dayOfMonth));
1551
1552        if (untilTime != MAX_TIME) {
1553            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1554        }
1555        writer.write(NEWLINE);
1556
1557        endZoneProps(writer, isDst);
1558    }
1559
1560    /*
1561     * Write start times defined by a DOW rule using VTIMEZONE RRULE
1562     */
1563    private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
1564            int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
1565        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
1566
1567        beginRRULE(writer, month);
1568        writer.write(ICAL_BYDAY);
1569        writer.write(EQUALS_SIGN);
1570        writer.write(Integer.toString(weekInMonth));    // -4, -3, -2, -1, 1, 2, 3, 4
1571        writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]);    // SU, MO, TU...
1572
1573        if (untilTime != MAX_TIME) {
1574            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1575        }
1576        writer.write(NEWLINE);
1577
1578        endZoneProps(writer, isDst);
1579    }
1580
1581    /*
1582     * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE
1583     */
1584    private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
1585            int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
1586        // Check if this rule can be converted to DOW rule
1587        if (dayOfMonth%7 == 1) {
1588            // Can be represented by DOW rule
1589            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
1590                    month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime);
1591        } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) {
1592            // Can be represented by DOW rule with negative week number
1593            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
1594                    month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime);
1595        } else {
1596            // Otherwise, use BYMONTHDAY to include all possible dates
1597            beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
1598
1599            // Check if all days are in the same month
1600            int startDay = dayOfMonth;
1601            int currentMonthDays = 7;
1602
1603            if (dayOfMonth <= 0) {
1604                // The start day is in previous month
1605                int prevMonthDays = 1 - dayOfMonth;
1606                currentMonthDays -= prevMonthDays;
1607
1608                int prevMonth = (month - 1) < 0 ? 11 : month - 1;
1609
1610                // Note: When a rule is separated into two, UNTIL attribute needs to be
1611                // calculated for each of them.  For now, we skip this, because we basically use this method
1612                // only for final rules, which does not have the UNTIL attribute
1613                writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
1614
1615                // Start from 1 for the rest
1616                startDay = 1;
1617            } else if (dayOfMonth + 6 > MONTHLENGTH[month]) {
1618                // Note: This code does not actually work well in February.  For now, days in month in
1619                // non-leap year.
1620                int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month];
1621                currentMonthDays -= nextMonthDays;
1622
1623                int nextMonth = (month + 1) > 11 ? 0 : month + 1;
1624
1625                writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
1626            }
1627            writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset);
1628            endZoneProps(writer, isDst);
1629        }
1630    }
1631
1632    /*
1633     * Called from writeZonePropsByDOW_GEQ_DOM
1634     */
1635    private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month,
1636            int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException {
1637
1638        int startDayNum = dayOfMonth;
1639        boolean isFeb = (month == Calendar.FEBRUARY);
1640        if (dayOfMonth < 0 && !isFeb) {
1641            // Use positive number if possible
1642            startDayNum = MONTHLENGTH[month] + dayOfMonth + 1;
1643        }
1644        beginRRULE(writer, month);
1645        writer.write(ICAL_BYDAY);
1646        writer.write(EQUALS_SIGN);
1647        writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]);    // SU, MO, TU...
1648        writer.write(SEMICOLON);
1649        writer.write(ICAL_BYMONTHDAY);
1650        writer.write(EQUALS_SIGN);
1651
1652        writer.write(Integer.toString(startDayNum));
1653        for (int i = 1; i < numDays; i++) {
1654            writer.write(COMMA);
1655            writer.write(Integer.toString(startDayNum + i));
1656        }
1657
1658        if (untilTime != MAX_TIME) {
1659            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1660        }
1661        writer.write(NEWLINE);
1662    }
1663
1664    /*
1665     * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE
1666     */
1667    private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
1668            int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
1669        // Check if this rule can be converted to DOW rule
1670        if (dayOfMonth%7 == 0) {
1671            // Can be represented by DOW rule
1672            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
1673                    month, dayOfMonth/7, dayOfWeek, startTime, untilTime);
1674        } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){
1675            // Can be represented by DOW rule with negative week number
1676            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
1677                    month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime);
1678        } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) {
1679            // Specical case for February
1680            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
1681                    Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime);
1682        } else {
1683            // Otherwise, convert this to DOW_GEQ_DOM rule
1684            writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset,
1685                    month, dayOfMonth - 6, dayOfWeek, startTime, untilTime);
1686        }
1687    }
1688
1689    /*
1690     * Write the final time zone rule using RRULE, with no UNTIL attribute
1691     */
1692    private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule,
1693            int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{
1694        DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings);
1695
1696        // If the rule's mills in a day is out of range, adjust start time.
1697        // Olson tzdata supports 24:00 of a day, but VTIMEZONE does not.
1698        // See ticket#7008/#7518
1699
1700        int timeInDay = dtrule.getRuleMillisInDay();
1701        if (timeInDay < 0) {
1702            startTime = startTime + (0 - timeInDay);
1703        } else if (timeInDay >= Grego.MILLIS_PER_DAY) {
1704            startTime = startTime - (timeInDay - (Grego.MILLIS_PER_DAY - 1));
1705        }
1706
1707        int toOffset = rule.getRawOffset() + rule.getDSTSavings();
1708        switch (dtrule.getDateRuleType()) {
1709        case DateTimeRule.DOM:
1710            writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
1711                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME);
1712            break;
1713        case DateTimeRule.DOW:
1714            writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
1715                    dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
1716            break;
1717        case DateTimeRule.DOW_GEQ_DOM:
1718            writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
1719                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
1720            break;
1721        case DateTimeRule.DOW_LEQ_DOM:
1722            writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
1723                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
1724            break;
1725        }
1726    }
1727
1728    /*
1729     * Convert the rule to its equivalent rule using WALL_TIME mode
1730     */
1731    private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) {
1732        if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
1733            return rule;
1734        }
1735        int wallt = rule.getRuleMillisInDay();
1736        if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) {
1737            wallt += (rawOffset + dstSavings);
1738        } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) {
1739            wallt += dstSavings;
1740        }
1741
1742        int month = -1, dom = 0, dow = 0, dtype = -1;
1743        int dshift = 0;
1744        if (wallt < 0) {
1745            dshift = -1;
1746            wallt += Grego.MILLIS_PER_DAY;
1747        } else if (wallt >= Grego.MILLIS_PER_DAY) {
1748            dshift = 1;
1749            wallt -= Grego.MILLIS_PER_DAY;
1750        }
1751
1752        month = rule.getRuleMonth();
1753        dom = rule.getRuleDayOfMonth();
1754        dow = rule.getRuleDayOfWeek();
1755        dtype = rule.getDateRuleType();
1756
1757        if (dshift != 0) {
1758            if (dtype == DateTimeRule.DOW) {
1759                // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first
1760                int wim = rule.getRuleWeekInMonth();
1761                if (wim > 0) {
1762                    dtype = DateTimeRule.DOW_GEQ_DOM;
1763                    dom = 7 * (wim - 1) + 1;
1764                } else {
1765                    dtype = DateTimeRule.DOW_LEQ_DOM;
1766                    dom = MONTHLENGTH[month] + 7 * (wim + 1);
1767                }
1768
1769            }
1770            // Shift one day before or after
1771            dom += dshift;
1772            if (dom == 0) {
1773                month--;
1774                month = month < Calendar.JANUARY ? Calendar.DECEMBER : month;
1775                dom = MONTHLENGTH[month];
1776            } else if (dom > MONTHLENGTH[month]) {
1777                month++;
1778                month = month > Calendar.DECEMBER ? Calendar.JANUARY : month;
1779                dom = 1;
1780            }
1781            if (dtype != DateTimeRule.DOM) {
1782                // Adjust day of week
1783                dow += dshift;
1784                if (dow < Calendar.SUNDAY) {
1785                    dow = Calendar.SATURDAY;
1786                } else if (dow > Calendar.SATURDAY) {
1787                    dow = Calendar.SUNDAY;
1788                }
1789            }
1790        }
1791        // Create a new rule
1792        DateTimeRule modifiedRule;
1793        if (dtype == DateTimeRule.DOM) {
1794            modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME);
1795        } else {
1796            modifiedRule = new DateTimeRule(month, dom, dow,
1797                    (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME);
1798        }
1799        return modifiedRule;
1800    }
1801
1802    /*
1803     * Write the opening section of zone properties
1804     */
1805    private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException {
1806        writer.write(ICAL_BEGIN);
1807        writer.write(COLON);
1808        if (isDst) {
1809            writer.write(ICAL_DAYLIGHT);
1810        } else {
1811            writer.write(ICAL_STANDARD);
1812        }
1813        writer.write(NEWLINE);
1814
1815        // TZOFFSETTO
1816        writer.write(ICAL_TZOFFSETTO);
1817        writer.write(COLON);
1818        writer.write(millisToOffset(toOffset));
1819        writer.write(NEWLINE);
1820
1821        // TZOFFSETFROM
1822        writer.write(ICAL_TZOFFSETFROM);
1823        writer.write(COLON);
1824        writer.write(millisToOffset(fromOffset));
1825        writer.write(NEWLINE);
1826
1827        // TZNAME
1828        writer.write(ICAL_TZNAME);
1829        writer.write(COLON);
1830        writer.write(tzname);
1831        writer.write(NEWLINE);
1832
1833        // DTSTART
1834        writer.write(ICAL_DTSTART);
1835        writer.write(COLON);
1836        writer.write(getDateTimeString(startTime + fromOffset));
1837        writer.write(NEWLINE);
1838    }
1839
1840    /*
1841     * Writes the closing section of zone properties
1842     */
1843    private static void endZoneProps(Writer writer, boolean isDst) throws IOException{
1844        // END:STANDARD or END:DAYLIGHT
1845        writer.write(ICAL_END);
1846        writer.write(COLON);
1847        if (isDst) {
1848            writer.write(ICAL_DAYLIGHT);
1849        } else {
1850            writer.write(ICAL_STANDARD);
1851        }
1852        writer.write(NEWLINE);
1853    }
1854
1855    /*
1856     * Write the beginning part of RRULE line
1857     */
1858    private static void beginRRULE(Writer writer, int month) throws IOException {
1859        writer.write(ICAL_RRULE);
1860        writer.write(COLON);
1861        writer.write(ICAL_FREQ);
1862        writer.write(EQUALS_SIGN);
1863        writer.write(ICAL_YEARLY);
1864        writer.write(SEMICOLON);
1865        writer.write(ICAL_BYMONTH);
1866        writer.write(EQUALS_SIGN);
1867        writer.write(Integer.toString(month + 1));
1868        writer.write(SEMICOLON);
1869    }
1870
1871    /*
1872     * Append the UNTIL attribute after RRULE line
1873     */
1874    private static void appendUNTIL(Writer writer, String until) throws IOException {
1875        if (until != null) {
1876            writer.write(SEMICOLON);
1877            writer.write(ICAL_UNTIL);
1878            writer.write(EQUALS_SIGN);
1879            writer.write(until);
1880        }
1881    }
1882
1883    /*
1884     * Write the opening section of the VTIMEZONE block
1885     */
1886    private void writeHeader(Writer writer)throws IOException {
1887        writer.write(ICAL_BEGIN);
1888        writer.write(COLON);
1889        writer.write(ICAL_VTIMEZONE);
1890        writer.write(NEWLINE);
1891        writer.write(ICAL_TZID);
1892        writer.write(COLON);
1893        writer.write(tz.getID());
1894        writer.write(NEWLINE);
1895        if (tzurl != null) {
1896            writer.write(ICAL_TZURL);
1897            writer.write(COLON);
1898            writer.write(tzurl);
1899            writer.write(NEWLINE);
1900        }
1901        if (lastmod != null) {
1902            writer.write(ICAL_LASTMOD);
1903            writer.write(COLON);
1904            writer.write(getUTCDateTimeString(lastmod.getTime()));
1905            writer.write(NEWLINE);
1906        }
1907    }
1908
1909    /*
1910     * Write the closing section of the VTIMEZONE definition block
1911     */
1912    private static void writeFooter(Writer writer) throws IOException {
1913        writer.write(ICAL_END);
1914        writer.write(COLON);
1915        writer.write(ICAL_VTIMEZONE);
1916        writer.write(NEWLINE);
1917    }
1918
1919    /*
1920     * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME
1921     */
1922    private static String getDateTimeString(long time) {
1923        int[] fields = Grego.timeToFields(time, null);
1924        StringBuilder sb = new StringBuilder(15);
1925        sb.append(numToString(fields[0], 4));
1926        sb.append(numToString(fields[1] + 1, 2));
1927        sb.append(numToString(fields[2], 2));
1928        sb.append('T');
1929
1930        int t = fields[5];
1931        int hour = t / Grego.MILLIS_PER_HOUR;
1932        t %= Grego.MILLIS_PER_HOUR;
1933        int min = t / Grego.MILLIS_PER_MINUTE;
1934        t %= Grego.MILLIS_PER_MINUTE;
1935        int sec = t / Grego.MILLIS_PER_SECOND;
1936
1937        sb.append(numToString(hour, 2));
1938        sb.append(numToString(min, 2));
1939        sb.append(numToString(sec, 2));
1940        return sb.toString();
1941    }
1942
1943    /*
1944     * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME
1945     */
1946    private static String getUTCDateTimeString(long time) {
1947        return getDateTimeString(time) + "Z";
1948    }
1949
1950    /*
1951     * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and
1952     * #2 DATE WITH UTC TIME
1953     */
1954    private static long parseDateTimeString(String str, int offset) {
1955        int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
1956        boolean isUTC = false;
1957        boolean isValid = false;
1958        do {
1959            if (str == null) {
1960                break;
1961            }
1962
1963            int length = str.length();
1964            if (length != 15 && length != 16) {
1965                // FORM#1 15 characters, such as "20060317T142115"
1966                // FORM#2 16 characters, such as "20060317T142115Z"
1967                break;
1968            }
1969            if (str.charAt(8) != 'T') {
1970                // charcter "T" must be used for separating date and time
1971                break;
1972            }
1973            if (length == 16) {
1974                if (str.charAt(15) != 'Z') {
1975                    // invalid format
1976                    break;
1977                }
1978                isUTC = true;
1979            }
1980
1981            try {
1982                year = Integer.parseInt(str.substring(0, 4));
1983                month = Integer.parseInt(str.substring(4, 6)) - 1;  // 0-based
1984                day = Integer.parseInt(str.substring(6, 8));
1985                hour = Integer.parseInt(str.substring(9, 11));
1986                min = Integer.parseInt(str.substring(11, 13));
1987                sec = Integer.parseInt(str.substring(13, 15));
1988            } catch (NumberFormatException nfe) {
1989                break;
1990            }
1991
1992            // check valid range
1993            int maxDayOfMonth = Grego.monthLength(year, month);
1994            if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth ||
1995                    hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) {
1996                break;
1997            }
1998
1999            isValid = true;
2000        } while(false);
2001
2002        if (!isValid) {
2003            throw new IllegalArgumentException("Invalid date time string format");
2004        }
2005        // Calculate the time
2006        long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY;
2007        time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND);
2008        if (!isUTC) {
2009            time -= offset;
2010        }
2011        return time;
2012    }
2013
2014    /*
2015     * Convert RFC2445 utc-offset string to milliseconds
2016     */
2017    private static int offsetStrToMillis(String str) {
2018        boolean isValid = false;
2019        int sign = 0, hour = 0, min = 0, sec = 0;
2020
2021        do {
2022            if (str == null) {
2023                break;
2024            }
2025            int length = str.length();
2026            if (length != 5 && length != 7) {
2027                // utf-offset must be 5 or 7 characters
2028                break;
2029            }
2030            // sign
2031            char s = str.charAt(0);
2032            if (s == '+') {
2033                sign = 1;
2034            } else if (s == '-') {
2035                sign = -1;
2036            } else {
2037                // utf-offset must start with "+" or "-"
2038                break;
2039            }
2040
2041            try {
2042                hour = Integer.parseInt(str.substring(1, 3));
2043                min = Integer.parseInt(str.substring(3, 5));
2044                if (length == 7) {
2045                    sec = Integer.parseInt(str.substring(5, 7));
2046                }
2047            } catch (NumberFormatException nfe) {
2048                break;
2049            }
2050            isValid = true;
2051        } while(false);
2052
2053        if (!isValid) {
2054            throw new IllegalArgumentException("Bad offset string");
2055        }
2056        int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000;
2057        return millis;
2058    }
2059
2060    /*
2061     * Convert milliseconds to RFC2445 utc-offset string
2062     */
2063    private static String millisToOffset(int millis) {
2064        StringBuilder sb = new StringBuilder(7);
2065        if (millis >= 0) {
2066            sb.append('+');
2067        } else {
2068            sb.append('-');
2069            millis = -millis;
2070        }
2071        int hour, min, sec;
2072        int t = millis / 1000;
2073
2074        sec = t % 60;
2075        t = (t - sec) / 60;
2076        min = t % 60;
2077        hour = t / 60;
2078
2079        sb.append(numToString(hour, 2));
2080        sb.append(numToString(min, 2));
2081        sb.append(numToString(sec, 2));
2082
2083        return sb.toString();
2084    }
2085
2086    /*
2087     * Format integer number
2088     */
2089    private static String numToString(int num, int width) {
2090        String str = Integer.toString(num);
2091        int len = str.length();
2092        if (len >= width) {
2093            return str.substring(len - width, len);
2094        }
2095        StringBuilder sb = new StringBuilder(width);
2096        for (int i = len; i < width; i++) {
2097            sb.append('0');
2098        }
2099        sb.append(str);
2100        return sb.toString();
2101    }
2102
2103    // Freezable stuffs
2104    private volatile transient boolean isFrozen = false;
2105
2106    /**
2107     * {@inheritDoc}
2108     * @stable ICU 49
2109     */
2110    public boolean isFrozen() {
2111        return isFrozen;
2112    }
2113
2114    /**
2115     * {@inheritDoc}
2116     * @stable ICU 49
2117     */
2118    public TimeZone freeze() {
2119        isFrozen = true;
2120        return this;
2121    }
2122
2123    /**
2124     * {@inheritDoc}
2125     * @stable ICU 49
2126     */
2127    public TimeZone cloneAsThawed() {
2128        VTimeZone vtz = (VTimeZone)super.cloneAsThawed();
2129        vtz.tz = (BasicTimeZone)tz.cloneAsThawed();
2130        vtz.isFrozen = false;
2131        return vtz;
2132    }
2133}
2134