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