2 *******************************************************************************
3 * Copyright (C) 2007-2013, International Business Machines Corporation and *
4 * others. All Rights Reserved. *
5 *******************************************************************************
7 package com.ibm.icu.util;
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;
20 import com.ibm.icu.impl.Grego;
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>
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.
36 public class VTimeZone extends BasicTimeZone {
38 private static final long serialVersionUID = -6851467294127795902L;
41 * Create a <code>VTimeZone</code> instance by the time zone ID.
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.
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();
58 * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data.
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.
66 public static VTimeZone create(Reader reader) {
67 VTimeZone vtz = new VTimeZone();
68 if (vtz.load(reader)) {
79 public int getOffset(int era, int year, int month, int day, int dayOfWeek,
81 return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
89 public void getOffset(long date, boolean local, int[] offsets) {
90 tz.getOffset(date, local, offsets);
96 * @deprecated This API is ICU internal only.
99 public void getOffsetFromLocal(long date,
100 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
101 tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
109 public int getRawOffset() {
110 return tz.getRawOffset();
118 public boolean inDaylightTime(Date date) {
119 return tz.inDaylightTime(date);
127 public void setRawOffset(int offsetMillis) {
129 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
131 tz.setRawOffset(offsetMillis);
139 public boolean useDaylightTime() {
140 return tz.useDaylightTime();
148 public boolean observesDaylightTime() {
149 return tz.observesDaylightTime();
157 public boolean hasSameRules(TimeZone other) {
161 if (other instanceof VTimeZone) {
162 return tz.hasSameRules(((VTimeZone)other).tz);
164 return tz.hasSameRules(other);
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.
172 * @return The RFC2445 TZURL property value
176 public String getTZURL() {
181 * Sets the RFC2445 TZURL property value.
183 * @param url The TZURL property value.
187 public void setTZURL(String url) {
189 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
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.
199 * @return The Date represents the RFC2445 LAST-MODIFIED date.
203 public Date getLastModified() {
208 * Sets the date used for RFC2445 LAST-MODIFIED property value.
210 * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value.
214 public void setLastModified(Date date) {
216 throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
222 * Writes RFC2445 VTIMEZONE data for this time zone
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.
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)) {
235 bw.write(ICAL_TZURL);
240 } else if (line.startsWith(ICAL_LASTMOD + COLON)) {
241 if (lastmod != null) {
242 bw.write(ICAL_LASTMOD);
244 bw.write(getUTCDateTimeString(lastmod.getTime()));
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 + "]";
259 writeZone(writer, tz, customProperties);
264 * Writes RFC2445 VTIMEZONE data applicable for dates after
265 * the specified start time.
267 * @param writer The <code>Writer</code> used for the output
268 * @param start The start time
270 * @throws IOException If there were problems reading and writing to the writer.
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);
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]);
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 + "]";
289 writeZone(writer, rbtz, customProperties);
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.
301 * @param writer The <code>Writer</code> used for the output
302 * @param time The date
304 * @throws IOException If there were problems reading or writing to the writer.
308 public void writeSimple(Writer writer, long time) throws IOException {
309 // Extract simple rules
310 TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time);
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]);
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 + "]";
323 writeZone(writer, rbtz, customProperties);
326 // BasicTimeZone methods
333 public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
334 return tz.getNextTransition(base, inclusive);
342 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
343 return tz.getPreviousTransition(base, inclusive);
351 public boolean hasEquivalentTransitions(TimeZone other, long start, long end) {
355 return tz.hasEquivalentTransitions(other, start, end);
363 public TimeZoneRule[] getTimeZoneRules() {
364 return tz.getTimeZoneRules();
372 public TimeZoneRule[] getTimeZoneRules(long start) {
373 return tz.getTimeZoneRules(start);
381 public Object clone() {
385 return cloneAsThawed();
388 // private stuff ------------------------------------------------------
390 private BasicTimeZone tz;
391 private List<String> vtzlines;
392 private String olsonzid = null;
393 private String tzurl = null;
394 private Date lastmod = null;
396 private static String ICU_TZVERSION;
397 private static final String ICU_TZINFO_PROP = "X-TZINFO";
399 // Default DST savings
400 private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour
402 // Default time start
403 private static final long DEF_TZSTARTTIME = 0;
406 private static final long MIN_TIME = Long.MIN_VALUE;
407 private static final long MAX_TIME = Long.MAX_VALUE;
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
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";
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";
441 private static final String[] ICAL_DOW_NAMES =
442 {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
444 // Month length in regular year
445 private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
448 // Initialize ICU_TZVERSION
450 ICU_TZVERSION = TimeZone.getTZDataVersion();
451 } catch (MissingResourceException e) {
453 ICU_TZVERSION = null;
458 /* Hide the constructor */
459 private VTimeZone() {
462 private VTimeZone(String tzid) {
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.
476 private boolean load(Reader reader) {
477 // Read VTIMEZONE block into string array
479 vtzlines = new LinkedList<String>();
481 boolean start = false;
482 boolean success = false;
483 StringBuilder line = new StringBuilder();
485 int ch = reader.read();
488 if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) {
489 vtzlines.add(line.toString());
495 // CR, must be followed by LF by the definition in RFC2445
500 if (ch != 0x09 && ch != 0x20) {
501 // NOT followed by TAB/SP -> new line
503 if (line.length() > 0) {
504 vtzlines.add(line.toString());
509 line.append((char)ch);
518 if (line.toString().startsWith(ICAL_END_VTIMEZONE)) {
519 vtzlines.add(line.toString());
524 if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) {
525 vtzlines.add(line.toString());
532 line.append((char)ch);
539 } catch (IOException ioe) {
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
554 * Parse VTIMEZONE data and create a RuleBasedTimeZone
556 private boolean parse() {
558 if (vtzlines == null || vtzlines.size() == 0) {
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
579 for (String line : vtzlines) {
580 int valueSep = line.indexOf(COLON);
584 String name = line.substring(0, valueSep);
585 String value = line.substring(valueSep + 1);
589 if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) {
594 if (name.equals(ICAL_TZID)) {
596 } else if (name.equals(ICAL_TZURL)) {
598 } else if (name.equals(ICAL_LASTMOD)) {
599 // Always in 'Z' format, so the offset argument for the parse method
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
610 // initialize current zone properties
619 // BEGIN property other than STANDARD/DAYLIGHT
620 // must not be there.
624 } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) {
630 if (name.equals(ICAL_DTSTART)) {
632 } else if (name.equals(ICAL_TZNAME)) {
634 } else if (name.equals(ICAL_TZOFFSETFROM)) {
636 } else if (name.equals(ICAL_TZOFFSETTO)) {
638 } else if (name.equals(ICAL_RDATE)) {
639 // RDATE mixed with RRULE is not supported
645 dates = new LinkedList<String>();
647 // RDATE value may contain multiple date delimited
649 StringTokenizer st = new StringTokenizer(value, COMMA);
650 while (st.hasMoreTokens()) {
651 String date = st.nextToken();
654 } else if (name.equals(ICAL_RRULE)) {
655 // RRULE mixed with RDATE is not supported
656 if (!isRRULE && dates != null) {
659 } else if (dates == null) {
660 dates = new LinkedList<String>();
664 } else if (name.equals(ICAL_END)) {
665 // Mandatory properties
666 if (dtstart == null || from == null || to == null) {
670 // if tzname is not available, create one from tzid
671 if (tzname == null) {
672 tzname = getDefaultTZName(tzid, dst);
675 // create a time zone rule
676 TimeZoneRule rule = null;
683 // Parse TZOFFSETFROM/TZOFFSETTO
684 fromOffset = offsetStrToMillis(from);
685 toOffset = offsetStrToMillis(to);
688 // If daylight, use the previous offset as rawoffset if positive
689 if (toOffset - fromOffset > 0) {
690 rawOffset = fromOffset;
691 dstSavings = toOffset - fromOffset;
693 // This is rare case.. just use 1 hour DST savings
694 rawOffset = toOffset - DEF_DSTSAVINGS;
695 dstSavings = DEF_DSTSAVINGS;
698 rawOffset = toOffset;
703 start = parseDateTimeString(dtstart, fromOffset);
706 Date actualStart = null;
708 rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
710 rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
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;
724 if (fromOffset - toOffset == DEF_DSTSAVINGS) {
725 initialRawOffset = fromOffset - DEF_DSTSAVINGS;
726 initialDSTSavings = DEF_DSTSAVINGS;
728 initialRawOffset = fromOffset;
729 initialDSTSavings = 0;
734 } catch (IllegalArgumentException iae) {
735 // bad format - rule == null..
754 // Must have at least one rule
755 if (rules.size() == 0) {
759 // Create a initial rule
760 InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false),
761 initialRawOffset, initialDSTSavings);
763 // Finally, create the RuleBasedTimeZone
764 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule);
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) {
777 if (finalRuleCount > 2) {
778 // Too many final rules
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
789 // Normalize the final rule
790 AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx);
791 int tmpRaw = finalRule.getRawOffset();
792 int tmpDST = finalRule.getDSTSavings();
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) {
801 TimeZoneRule r = rules.get(i);
802 Date lastStart = r.getFinalStart(tmpRaw, tmpDST);
803 if (lastStart.after(start)) {
804 start = finalRule.getNextStart(lastStart.getTime(),
810 TimeZoneRule newRule;
811 if (start == finalStart) {
812 // Transform this into a single transition
813 newRule = new TimeArrayTimeZoneRule(
815 finalRule.getRawOffset(),
816 finalRule.getDSTSavings(),
817 new long[] {finalStart.getTime()},
818 DateTimeRule.UTC_TIME);
820 // Update the end year
821 int fields[] = Grego.timeToFields(start.getTime(), null);
822 newRule = new AnnualTimeZoneRule(
824 finalRule.getRawOffset(),
825 finalRule.getDSTSavings(),
827 finalRule.getStartYear(),
830 rules.set(finalRuleIdx, newRule);
834 for (TimeZoneRule r : rules) {
835 rbtz.addTransitionRule(r);
844 * Create a default TZNAME from TZID
846 private static String getDefaultTZName(String tzid, boolean isDST) {
848 return tzid + "(DST)";
850 return tzid + "(STD)";
854 * Create a TimeZoneRule by the RRULE definition
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) {
861 // Parse the first rule
862 String rrule = dates.get(0);
864 long until[] = new long[1];
865 int[] ruleFields = parseRRULE(rrule, until);
866 if (ruleFields == null) {
871 int month = ruleFields[0];
872 int dayOfWeek = ruleFields[1];
873 int nthDayOfWeek = ruleFields[2];
874 int dayOfMonth = ruleFields[3];
876 if (dates.size() == 1) {
878 if (ruleFields.length > 4) {
879 // Multiple BYMONTHDAY values
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
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
893 days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1;
894 firstDay = days[i] < firstDay ? days[i] : firstDay;
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) {
906 // days are not continuous
910 // Use DOW_GEQ_DOM rule with firstDay as the start date
911 dayOfMonth = firstDay;
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
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) {
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
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;
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);
945 // If UNTIL is newer than previous one, use the one
946 if (unt[0] > until[0]) {
950 // Check if BYMONTH + BYMONTHDAY + BYDAY rule
951 if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) {
954 // Count number of BYMONTHDAY
955 int count = fields.length - 3;
956 if (daysCount + count > 7) {
957 // We cannot support BYMONTHDAY more than 7
960 // Check if the same BYDAY is used. Otherwise, we cannot
962 if (fields[1] != dayOfWeek) {
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) {
971 anotherMonth = fields[0];
972 earliestMonth = anotherMonth;
973 // Reset earliest day
975 } else if (diff == 11 || diff == 1) {
977 anotherMonth = fields[0];
979 // The day range cannot exceed more than 2 months
982 } else if (fields[0] != month && fields[0] != anotherMonth) {
983 // The day range cannot exceed more than 2 months
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;
997 if (daysCount != 7) {
998 // Number of BYMONTHDAY entries must be 7
1001 month = earliestMonth;
1002 dayOfMonth = earliestDay;
1005 // Calculate start/end year and missing fields
1006 int[] dfields = Grego.timeToFields(start + fromOffset, null);
1007 int startYear = dfields[0];
1009 // If MYMONTH is not set, use the month of DTSTART
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];
1016 int timeInDay = dfields[5];
1018 int endYear = AnnualTimeZoneRule.MAX_YEAR;
1019 if (until[0] != MIN_TIME) {
1020 Grego.timeToFields(until[0], dfields);
1021 endYear = dfields[0];
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);
1037 // RRULE attributes are insufficient
1041 return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear);
1045 * Parse individual RRULE
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
1056 * null on any error cases, for exmaple, FREQ=YEARLY is not available
1058 * When UNTIL attribute is available, the time will be set to until[0],
1059 * otherwise, MIN_TIME
1061 private static int[] parseRRULE(String rrule, long[] until) {
1064 int nthDayOfWeek = 0;
1065 int[] dayOfMonth = null;
1067 long untilTime = MIN_TIME;
1068 boolean yearly = false;
1069 boolean parseError = false;
1070 StringTokenizer st= new StringTokenizer(rrule, SEMICOLON);
1072 while (st.hasMoreTokens()) {
1074 String prop = st.nextToken();
1075 int sep = prop.indexOf(EQUALS_SIGN);
1077 attr = prop.substring(0, sep);
1078 value = prop.substring(sep + 1);
1084 if (attr.equals(ICAL_FREQ)) {
1085 // only support YEARLY frequency type
1086 if (value.equals(ICAL_YEARLY)) {
1092 } else if (attr.equals(ICAL_UNTIL)) {
1093 // ISO8601 UTC format, for example, "20060315T020000Z"
1095 untilTime = parseDateTimeString(value, 0);
1096 } catch (IllegalArgumentException iae) {
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) {
1108 month = Integer.parseInt(value) - 1;
1109 if (month < 0 || month >= 12) {
1113 } catch (NumberFormatException nfe) {
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.
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) {
1131 if (value.charAt(0) == '+') {
1133 } else if (value.charAt(0) == '-') {
1135 } else if (length == 4) {
1140 int n = Integer.parseInt(value.substring(length - 3, length - 2));
1141 if (n == 0 || n > 4) {
1145 nthDayOfWeek = n * sign;
1146 } catch(NumberFormatException nfe) {
1150 value = value.substring(length - 2);
1153 for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) {
1154 if (value.equals(ICAL_DOW_NAMES[wday])) {
1158 if (wday < ICAL_DOW_NAMES.length) {
1159 // Sunday(1) - Saturday(7)
1160 dayOfWeek = wday + 1;
1165 } else if (attr.equals(ICAL_BYMONTHDAY)) {
1166 // Note: BYMONTHDAY may contain multiple days delimited by comma
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];
1174 while(days.hasMoreTokens()) {
1176 dayOfMonth[index++] = Integer.parseInt(days.nextToken());
1177 } catch (NumberFormatException nfe) {
1189 // FREQ=YEARLY must be set
1193 until[0] = untilTime;
1196 if (dayOfMonth == null) {
1197 results = new int[4];
1200 results = new int[3 + dayOfMonth.length];
1201 for (int i = 0; i < dayOfMonth.length; i++) {
1202 results[3 + i] = dayOfMonth[i];
1206 results[1] = dayOfWeek;
1207 results[2] = nthDayOfWeek;
1212 * Create a TimeZoneRule by the RDATE definition
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
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];
1224 times = new long[dates.size()];
1227 for (String date : dates) {
1228 times[idx++] = parseDateTimeString(date, fromOffset);
1230 } catch (IllegalArgumentException iae) {
1234 return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME);
1238 * Write the time zone rules in RFC2445 VTIMEZONE format
1240 private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException {
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]);
1254 String dstName = null;
1255 int dstFromOffset = 0;
1256 int dstFromDSTSavings = 0;
1257 int dstToOffset = 0;
1258 int dstStartYear = 0;
1260 int dstDayOfWeek = 0;
1261 int dstWeekInMonth = 0;
1262 int dstMillisInDay = 0;
1263 long dstStartTime = 0;
1264 long dstUntilTime = 0;
1266 AnnualTimeZoneRule finalDstRule = null;
1268 String stdName = null;
1269 int stdFromOffset = 0;
1270 int stdFromDSTSavings = 0;
1271 int stdToOffset = 0;
1272 int stdStartYear = 0;
1274 int stdDayOfWeek = 0;
1275 int stdWeekInMonth = 0;
1276 int stdMillisInDay = 0;
1277 long stdStartTime = 0;
1278 long stdUntilTime = 0;
1280 AnnualTimeZoneRule finalStdRule = null;
1282 int[] dtfields = new int[6];
1283 boolean hasTransitions = false;
1285 // Going through all transitions
1287 TimeZoneTransition tzt = basictz.getNextTransition(t, false);
1291 hasTransitions = true;
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;
1303 if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
1304 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
1305 finalDstRule = (AnnualTimeZoneRule)tzt.getTo();
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
1323 if (dstCount == 1) {
1324 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
1325 dstStartTime, true);
1327 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1328 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1333 // Reset this DST information
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;
1346 if (finalStdRule != null && finalDstRule != null) {
1350 if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
1351 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
1352 finalStdRule = (AnnualTimeZoneRule)tzt.getTo();
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
1370 if (stdCount == 1) {
1371 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
1372 stdStartTime, true);
1374 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1375 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1380 // Reset this STD information
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;
1393 if (finalStdRule != null && finalDstRule != null) {
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);
1406 if (finalDstRule == null) {
1407 if (dstCount == 1) {
1408 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
1409 dstStartTime, true);
1411 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1412 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1415 if (dstCount == 1) {
1416 writeFinalRule(w, true, finalDstRule,
1417 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
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);
1424 // Not equivalent rule - write out two different rules
1425 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
1426 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
1428 Date nextStart = finalDstRule.getNextStart(dstUntilTime,
1429 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, false);
1431 assert nextStart != null;
1432 if (nextStart != null) {
1433 writeFinalRule(w, true, finalDstRule,
1434 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, nextStart.getTime());
1441 if (finalStdRule == null) {
1442 if (stdCount == 1) {
1443 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
1444 stdStartTime, true);
1446 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1447 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1450 if (stdCount == 1) {
1451 writeFinalRule(w, false, finalStdRule,
1452 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
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);
1459 // Not equivalent rule - write out two different rules
1460 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
1461 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
1463 Date nextStart = finalStdRule.getNextStart(stdUntilTime,
1464 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, false);
1466 assert nextStart != null;
1467 if (nextStart != null) {
1468 writeFinalRule(w, false, finalStdRule,
1469 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, nextStart.getTime());
1481 * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent
1482 * to the DateTimerule.
1484 private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) {
1485 if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) {
1488 if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) {
1489 // Do not try to do more intelligent comparison for now.
1492 if (dtrule.getDateRuleType() == DateTimeRule.DOW
1493 && dtrule.getRuleWeekInMonth() == weekInMonth) {
1496 int ruleDOM = dtrule.getRuleDayOfMonth();
1497 if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) {
1498 if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) {
1501 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6
1502 && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) {
1506 if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) {
1507 if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) {
1510 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0
1511 && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) {
1519 * Write a single start time
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);
1525 writer.write(ICAL_RDATE);
1526 writer.write(COLON);
1527 writer.write(getDateTimeString(time + fromOffset));
1528 writer.write(NEWLINE);
1530 endZoneProps(writer, isDst);
1534 * Write start times defined by a DOM rule using VTIMEZONE RRULE
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);
1540 beginRRULE(writer, month);
1541 writer.write(ICAL_BYMONTHDAY);
1542 writer.write(EQUALS_SIGN);
1543 writer.write(Integer.toString(dayOfMonth));
1545 if (untilTime != MAX_TIME) {
1546 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1548 writer.write(NEWLINE);
1550 endZoneProps(writer, isDst);
1554 * Write start times defined by a DOW rule using VTIMEZONE RRULE
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);
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...
1566 if (untilTime != MAX_TIME) {
1567 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1569 writer.write(NEWLINE);
1571 endZoneProps(writer, isDst);
1575 * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE
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);
1589 // Otherwise, use BYMONTHDAY to include all possible dates
1590 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
1592 // Check if all days are in the same month
1593 int startDay = dayOfMonth;
1594 int currentMonthDays = 7;
1596 if (dayOfMonth <= 0) {
1597 // The start day is in previous month
1598 int prevMonthDays = 1 - dayOfMonth;
1599 currentMonthDays -= prevMonthDays;
1601 int prevMonth = (month - 1) < 0 ? 11 : month - 1;
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);
1608 // Start from 1 for the rest
1610 } else if (dayOfMonth + 6 > MONTHLENGTH[month]) {
1611 // Note: This code does not actually work well in February. For now, days in month in
1613 int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month];
1614 currentMonthDays -= nextMonthDays;
1616 int nextMonth = (month + 1) > 11 ? 0 : month + 1;
1618 writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
1620 writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset);
1621 endZoneProps(writer, isDst);
1626 * Called from writeZonePropsByDOW_GEQ_DOM
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 {
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;
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);
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));
1651 if (untilTime != MAX_TIME) {
1652 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
1654 writer.write(NEWLINE);
1658 * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE
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);
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);
1683 * Write the final time zone rule using RRULE, with no UNTIL attribute
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);
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
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));
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);
1706 case DateTimeRule.DOW:
1707 writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
1708 dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
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);
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);
1722 * Convert the rule to its equivalent rule using WALL_TIME mode
1724 private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) {
1725 if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
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;
1735 int month = -1, dom = 0, dow = 0, dtype = -1;
1739 wallt += Grego.MILLIS_PER_DAY;
1740 } else if (wallt >= Grego.MILLIS_PER_DAY) {
1742 wallt -= Grego.MILLIS_PER_DAY;
1745 month = rule.getRuleMonth();
1746 dom = rule.getRuleDayOfMonth();
1747 dow = rule.getRuleDayOfWeek();
1748 dtype = rule.getDateRuleType();
1751 if (dtype == DateTimeRule.DOW) {
1752 // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first
1753 int wim = rule.getRuleWeekInMonth();
1755 dtype = DateTimeRule.DOW_GEQ_DOM;
1756 dom = 7 * (wim - 1) + 1;
1758 dtype = DateTimeRule.DOW_LEQ_DOM;
1759 dom = MONTHLENGTH[month] + 7 * (wim + 1);
1763 // Shift one day before or after
1767 month = month < Calendar.JANUARY ? Calendar.DECEMBER : month;
1768 dom = MONTHLENGTH[month];
1769 } else if (dom > MONTHLENGTH[month]) {
1771 month = month > Calendar.DECEMBER ? Calendar.JANUARY : month;
1774 if (dtype != DateTimeRule.DOM) {
1775 // Adjust day of week
1777 if (dow < Calendar.SUNDAY) {
1778 dow = Calendar.SATURDAY;
1779 } else if (dow > Calendar.SATURDAY) {
1780 dow = Calendar.SUNDAY;
1784 // Create a new rule
1785 DateTimeRule modifiedRule;
1786 if (dtype == DateTimeRule.DOM) {
1787 modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME);
1789 modifiedRule = new DateTimeRule(month, dom, dow,
1790 (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME);
1792 return modifiedRule;
1796 * Write the opening section of zone properties
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);
1802 writer.write(ICAL_DAYLIGHT);
1804 writer.write(ICAL_STANDARD);
1806 writer.write(NEWLINE);
1809 writer.write(ICAL_TZOFFSETTO);
1810 writer.write(COLON);
1811 writer.write(millisToOffset(toOffset));
1812 writer.write(NEWLINE);
1815 writer.write(ICAL_TZOFFSETFROM);
1816 writer.write(COLON);
1817 writer.write(millisToOffset(fromOffset));
1818 writer.write(NEWLINE);
1821 writer.write(ICAL_TZNAME);
1822 writer.write(COLON);
1823 writer.write(tzname);
1824 writer.write(NEWLINE);
1827 writer.write(ICAL_DTSTART);
1828 writer.write(COLON);
1829 writer.write(getDateTimeString(startTime + fromOffset));
1830 writer.write(NEWLINE);
1834 * Writes the closing section of zone properties
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);
1841 writer.write(ICAL_DAYLIGHT);
1843 writer.write(ICAL_STANDARD);
1845 writer.write(NEWLINE);
1849 * Write the beginning part of RRULE line
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);
1865 * Append the UNTIL attribute after RRULE line
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);
1877 * Write the opening section of the VTIMEZONE block
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);
1894 if (lastmod != null) {
1895 writer.write(ICAL_LASTMOD);
1896 writer.write(COLON);
1897 writer.write(getUTCDateTimeString(lastmod.getTime()));
1898 writer.write(NEWLINE);
1903 * Write the closing section of the VTIMEZONE definition block
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);
1913 * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME
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));
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;
1930 sb.append(numToString(hour, 2));
1931 sb.append(numToString(min, 2));
1932 sb.append(numToString(sec, 2));
1933 return sb.toString();
1937 * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME
1939 private static String getUTCDateTimeString(long time) {
1940 return getDateTimeString(time) + "Z";
1944 * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and
1945 * #2 DATE WITH UTC TIME
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;
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"
1962 if (str.charAt(8) != 'T') {
1963 // charcter "T" must be used for separating date and time
1967 if (str.charAt(15) != 'Z') {
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) {
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) {
1996 throw new IllegalArgumentException("Invalid date time string format");
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);
2008 * Convert RFC2445 utc-offset string to milliseconds
2010 private static int offsetStrToMillis(String str) {
2011 boolean isValid = false;
2012 int sign = 0, hour = 0, min = 0, sec = 0;
2018 int length = str.length();
2019 if (length != 5 && length != 7) {
2020 // utf-offset must be 5 or 7 characters
2024 char s = str.charAt(0);
2027 } else if (s == '-') {
2030 // utf-offset must start with "+" or "-"
2035 hour = Integer.parseInt(str.substring(1, 3));
2036 min = Integer.parseInt(str.substring(3, 5));
2038 sec = Integer.parseInt(str.substring(5, 7));
2040 } catch (NumberFormatException nfe) {
2047 throw new IllegalArgumentException("Bad offset string");
2049 int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000;
2054 * Convert milliseconds to RFC2445 utc-offset string
2056 private static String millisToOffset(int millis) {
2057 StringBuilder sb = new StringBuilder(7);
2065 int t = millis / 1000;
2072 sb.append(numToString(hour, 2));
2073 sb.append(numToString(min, 2));
2074 sb.append(numToString(sec, 2));
2076 return sb.toString();
2080 * Format integer number
2082 private static String numToString(int num, int width) {
2083 String str = Integer.toString(num);
2084 int len = str.length();
2086 return str.substring(len - width, len);
2088 StringBuilder sb = new StringBuilder(width);
2089 for (int i = len; i < width; i++) {
2093 return sb.toString();
2097 private transient boolean isFrozen = false;
2103 public boolean isFrozen() {
2111 public TimeZone freeze() {
2120 public TimeZone cloneAsThawed() {
2121 VTimeZone vtz = (VTimeZone)super.cloneAsThawed();
2122 vtz.tz = (BasicTimeZone)tz.cloneAsThawed();
2123 vtz.isFrozen = false;