2 *******************************************************************************
\r
3 * Copyright (C) 2007-2010, International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 *******************************************************************************
\r
7 package com.ibm.icu.util;
\r
9 import java.io.BufferedWriter;
\r
10 import java.io.IOException;
\r
11 import java.io.Reader;
\r
12 import java.io.Writer;
\r
13 import java.util.ArrayList;
\r
14 import java.util.Date;
\r
15 import java.util.LinkedList;
\r
16 import java.util.List;
\r
17 import java.util.MissingResourceException;
\r
19 import com.ibm.icu.impl.Grego;
\r
22 * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE. You can create a
\r
23 * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>.
\r
24 * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule
\r
25 * in RFC2445 VTIMEZONE format. Also, you can create a <code>VTimeZone</code> instance
\r
26 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time
\r
27 * zone offset by the rules defined by the data.<br><br>
\r
29 * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to
\r
30 * decode or encode Non-ASCII text. Methods reading/writing VTIMEZONE data in this class
\r
31 * do nothing with MIME encoding.
\r
35 public class VTimeZone extends BasicTimeZone {
\r
37 private static final long serialVersionUID = -6851467294127795902L;
\r
40 * Create a <code>VTimeZone</code> instance by the time zone ID.
\r
42 * @param tzid The time zone ID, such as America/New_York
\r
43 * @return A <code>VTimeZone</code> initialized by the time zone ID, or null
\r
44 * when the ID is unknown.
\r
48 public static VTimeZone create(String tzid) {
\r
49 VTimeZone vtz = new VTimeZone();
\r
50 vtz.tz = (BasicTimeZone)TimeZone.getTimeZone(tzid, TimeZone.TIMEZONE_ICU);
\r
51 vtz.olsonzid = vtz.tz.getID();
\r
58 * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data.
\r
60 * @param reader The Reader for VTIMEZONE data input stream
\r
61 * @return A <code>VTimeZone</code> initialized by the VTIMEZONE data or
\r
62 * null if failed to load the rule from the VTIMEZONE data.
\r
66 public static VTimeZone create(Reader reader) {
\r
67 VTimeZone vtz = new VTimeZone();
\r
68 if (vtz.load(reader)) {
\r
78 public int getOffset(int era, int year, int month, int day, int dayOfWeek,
\r
80 return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
\r
87 public void getOffset(long date, boolean local, int[] offsets) {
\r
88 tz.getOffset(date, local, offsets);
\r
94 * @deprecated This API is ICU internal only.
\r
96 public void getOffsetFromLocal(long date,
\r
97 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
\r
98 tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
\r
105 public int getRawOffset() {
\r
106 return tz.getRawOffset();
\r
113 public boolean inDaylightTime(Date date) {
\r
114 return tz.inDaylightTime(date);
\r
121 public void setRawOffset(int offsetMillis) {
\r
122 tz.setRawOffset(offsetMillis);
\r
129 public boolean useDaylightTime() {
\r
130 return tz.useDaylightTime();
\r
137 public boolean hasSameRules(TimeZone other) {
\r
138 return tz.hasSameRules(other);
\r
142 * Gets the RFC2445 TZURL property value. When a <code>VTimeZone</code> instance was created from
\r
143 * VTIMEZONE data, the value is set by the TZURL property value in the data. Otherwise,
\r
144 * the initial value is null.
\r
146 * @return The RFC2445 TZURL property value
\r
150 public String getTZURL() {
\r
155 * Sets the RFC2445 TZURL property value.
\r
157 * @param url The TZURL property value.
\r
161 public void setTZURL(String url) {
\r
166 * Gets the RFC2445 LAST-MODIFIED property value. When a <code>VTimeZone</code> instance was created
\r
167 * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data.
\r
168 * Otherwise, the initial value is null.
\r
170 * @return The Date represents the RFC2445 LAST-MODIFIED date.
\r
174 public Date getLastModified() {
\r
179 * Sets the date used for RFC2445 LAST-MODIFIED property value.
\r
181 * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value.
\r
185 public void setLastModified(Date date) {
\r
190 * Writes RFC2445 VTIMEZONE data for this time zone
\r
192 * @param writer A <code>Writer</code> used for the output
\r
193 * @throws IOException If there were problems creating a buffered writer or writing to it.
\r
197 public void write(Writer writer) throws IOException {
\r
198 BufferedWriter bw = new BufferedWriter(writer);
\r
199 if (vtzlines != null) {
\r
200 for (String line : vtzlines) {
\r
201 if (line.startsWith(ICAL_TZURL + COLON)) {
\r
202 if (tzurl != null) {
\r
203 bw.write(ICAL_TZURL);
\r
208 } else if (line.startsWith(ICAL_LASTMOD + COLON)) {
\r
209 if (lastmod != null) {
\r
210 bw.write(ICAL_LASTMOD);
\r
212 bw.write(getUTCDateTimeString(lastmod.getTime()));
\r
222 String[] customProperties = null;
\r
223 if (olsonzid != null && ICU_TZVERSION != null) {
\r
224 customProperties = new String[1];
\r
225 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]";
\r
227 writeZone(writer, tz, customProperties);
\r
232 * Writes RFC2445 VTIMEZONE data applicable for dates after
\r
233 * the specified start time.
\r
235 * @param writer The <code>Writer</code> used for the output
\r
236 * @param start The start time
\r
238 * @throws IOException If there were problems reading and writing to the writer.
\r
242 public void write(Writer writer, long start) throws IOException {
\r
243 // Extract rules applicable to dates after the start time
\r
244 TimeZoneRule[] rules = tz.getTimeZoneRules(start);
\r
246 // Create a RuleBasedTimeZone with the subset rule
\r
247 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
\r
248 for (int i = 1; i < rules.length; i++) {
\r
249 rbtz.addTransitionRule(rules[i]);
\r
251 String[] customProperties = null;
\r
252 if (olsonzid != null && ICU_TZVERSION != null) {
\r
253 customProperties = new String[1];
\r
254 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
\r
255 "/Partial@" + start + "]";
\r
257 writeZone(writer, rbtz, customProperties);
\r
261 * Writes RFC2445 VTIMEZONE data applicable near the specified date.
\r
262 * Some common iCalendar implementations can only handle a single time
\r
263 * zone property or a pair of standard and daylight time properties using
\r
264 * BYDAY rule with day of week (such as BYDAY=1SUN). This method produce
\r
265 * the VTIMEZONE data which can be handled these implementations. The rules
\r
266 * produced by this method can be used only for calculating time zone offset
\r
267 * around the specified date.
\r
269 * @param writer The <code>Writer</code> used for the output
\r
270 * @param time The date
\r
272 * @throws IOException If there were problems reading or writing to the writer.
\r
276 public void writeSimple(Writer writer, long time) throws IOException {
\r
277 // Extract simple rules
\r
278 TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time);
\r
280 // Create a RuleBasedTimeZone with the subset rule
\r
281 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
\r
282 for (int i = 1; i < rules.length; i++) {
\r
283 rbtz.addTransitionRule(rules[i]);
\r
285 String[] customProperties = null;
\r
286 if (olsonzid != null && ICU_TZVERSION != null) {
\r
287 customProperties = new String[1];
\r
288 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
\r
289 "/Simple@" + time + "]";
\r
291 writeZone(writer, rbtz, customProperties);
\r
294 // BasicTimeZone methods
\r
300 public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
\r
301 return tz.getNextTransition(base, inclusive);
\r
308 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
\r
309 return tz.getPreviousTransition(base, inclusive);
\r
316 public boolean hasEquivalentTransitions(TimeZone other, long start, long end) {
\r
317 return tz.hasEquivalentTransitions(other, start, end);
\r
324 public TimeZoneRule[] getTimeZoneRules() {
\r
325 return tz.getTimeZoneRules();
\r
332 public TimeZoneRule[] getTimeZoneRules(long start) {
\r
333 return tz.getTimeZoneRules(start);
\r
340 public Object clone() {
\r
341 VTimeZone other = (VTimeZone)super.clone();
\r
342 other.tz = (BasicTimeZone)tz.clone();
\r
346 // private stuff ------------------------------------------------------
\r
348 private BasicTimeZone tz;
\r
349 private List<String> vtzlines;
\r
350 private String olsonzid = null;
\r
351 private String tzurl = null;
\r
352 private Date lastmod = null;
\r
354 private static String ICU_TZVERSION;
\r
355 private static final String ICU_TZINFO_PROP = "X-TZINFO";
\r
357 // Default DST savings
\r
358 private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour
\r
360 // Default time start
\r
361 private static final long DEF_TZSTARTTIME = 0;
\r
364 private static final long MIN_TIME = Long.MIN_VALUE;
\r
365 private static final long MAX_TIME = Long.MAX_VALUE;
\r
367 // Symbol characters used by RFC2445 VTIMEZONE
\r
368 private static final String COLON = ":";
\r
369 private static final String SEMICOLON = ";";
\r
370 private static final String EQUALS_SIGN = "=";
\r
371 private static final String COMMA = ",";
\r
372 private static final String NEWLINE = "\r\n"; // CRLF
\r
374 // RFC2445 VTIMEZONE tokens
\r
375 private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE";
\r
376 private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE";
\r
377 private static final String ICAL_BEGIN = "BEGIN";
\r
378 private static final String ICAL_END = "END";
\r
379 private static final String ICAL_VTIMEZONE = "VTIMEZONE";
\r
380 private static final String ICAL_TZID = "TZID";
\r
381 private static final String ICAL_STANDARD = "STANDARD";
\r
382 private static final String ICAL_DAYLIGHT = "DAYLIGHT";
\r
383 private static final String ICAL_DTSTART = "DTSTART";
\r
384 private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM";
\r
385 private static final String ICAL_TZOFFSETTO = "TZOFFSETTO";
\r
386 private static final String ICAL_RDATE = "RDATE";
\r
387 private static final String ICAL_RRULE = "RRULE";
\r
388 private static final String ICAL_TZNAME = "TZNAME";
\r
389 private static final String ICAL_TZURL = "TZURL";
\r
390 private static final String ICAL_LASTMOD = "LAST-MODIFIED";
\r
392 private static final String ICAL_FREQ = "FREQ";
\r
393 private static final String ICAL_UNTIL = "UNTIL";
\r
394 private static final String ICAL_YEARLY = "YEARLY";
\r
395 private static final String ICAL_BYMONTH = "BYMONTH";
\r
396 private static final String ICAL_BYDAY = "BYDAY";
\r
397 private static final String ICAL_BYMONTHDAY = "BYMONTHDAY";
\r
399 private static final String[] ICAL_DOW_NAMES =
\r
400 {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
\r
402 // Month length in regular year
\r
403 private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
\r
406 // Initialize ICU_TZVERSION
\r
408 ICU_TZVERSION = TimeZone.getTZDataVersion();
\r
409 } catch (MissingResourceException e) {
\r
411 ICU_TZVERSION = null;
\r
416 /* Hide the constructor */
\r
417 private VTimeZone() {
\r
421 * Read the input stream to locate the VTIMEZONE block and
\r
422 * parse the contents to initialize this VTimeZone object.
\r
423 * The reader skips other RFC2445 message headers. After
\r
424 * the parse is completed, the reader points at the beginning
\r
425 * of the header field just after the end of VTIMEZONE block.
\r
426 * When VTIMEZONE block is found and this object is successfully
\r
427 * initialized by the rules described in the data, this method
\r
428 * returns true. Otherwise, returns false.
\r
430 private boolean load(Reader reader) {
\r
431 // Read VTIMEZONE block into string array
\r
433 vtzlines = new LinkedList<String>();
\r
434 boolean eol = false;
\r
435 boolean start = false;
\r
436 boolean success = false;
\r
437 StringBuilder line = new StringBuilder();
\r
439 int ch = reader.read();
\r
442 if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) {
\r
443 vtzlines.add(line.toString());
\r
449 // CR, must be followed by LF by the definition in RFC2445
\r
454 if (ch != 0x09 && ch != 0x20) {
\r
455 // NOT followed by TAB/SP -> new line
\r
457 if (line.length() > 0) {
\r
458 vtzlines.add(line.toString());
\r
463 line.append((char)ch);
\r
472 if (line.toString().startsWith(ICAL_END_VTIMEZONE)) {
\r
473 vtzlines.add(line.toString());
\r
478 if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) {
\r
479 vtzlines.add(line.toString());
\r
486 line.append((char)ch);
\r
493 } catch (IOException ioe) {
\r
502 private static final int INI = 0; // Initial state
\r
503 private static final int VTZ = 1; // In VTIMEZONE
\r
504 private static final int TZI = 2; // In STANDARD or DAYLIGHT
\r
505 private static final int ERR = 3; // Error state
\r
508 * Parse VTIMEZONE data and create a RuleBasedTimeZone
\r
510 private boolean parse() {
\r
512 if (vtzlines == null || vtzlines.size() == 0) {
\r
518 String tzid = null;
\r
521 boolean dst = false; // current zone type
\r
522 String from = null; // current zone from offset
\r
523 String to = null; // current zone offset
\r
524 String tzname = null; // current zone name
\r
525 String dtstart = null; // current zone starts
\r
526 boolean isRRULE = false; // true if the rule is described by RRULE
\r
527 List<String> dates = null; // list of RDATE or RRULE strings
\r
528 List<TimeZoneRule> rules = new ArrayList<TimeZoneRule>(); // rule list
\r
529 int initialRawOffset = 0; // initial offset
\r
530 int initialDSTSavings = 0; // initial offset
\r
531 long firstStart = MAX_TIME; // the earliest rule start time
\r
533 for (String line : vtzlines) {
\r
534 int valueSep = line.indexOf(COLON);
\r
535 if (valueSep < 0) {
\r
538 String name = line.substring(0, valueSep);
\r
539 String value = line.substring(valueSep + 1);
\r
543 if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) {
\r
548 if (name.equals(ICAL_TZID)) {
\r
550 } else if (name.equals(ICAL_TZURL)) {
\r
552 } else if (name.equals(ICAL_LASTMOD)) {
\r
553 // Always in 'Z' format, so the offset argument for the parse method
\r
554 // can be any value.
\r
555 lastmod = new Date(parseDateTimeString(value, 0));
\r
556 } else if (name.equals(ICAL_BEGIN)) {
\r
557 boolean isDST = value.equals(ICAL_DAYLIGHT);
\r
558 if (value.equals(ICAL_STANDARD) || isDST) {
\r
559 // tzid must be ready at this point
\r
560 if (tzid == null) {
\r
564 // initialize current zone properties
\r
573 // BEGIN property other than STANDARD/DAYLIGHT
\r
574 // must not be there.
\r
578 } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) {
\r
584 if (name.equals(ICAL_DTSTART)) {
\r
586 } else if (name.equals(ICAL_TZNAME)) {
\r
588 } else if (name.equals(ICAL_TZOFFSETFROM)) {
\r
590 } else if (name.equals(ICAL_TZOFFSETTO)) {
\r
592 } else if (name.equals(ICAL_RDATE)) {
\r
593 // RDATE mixed with RRULE is not supported
\r
598 if (dates == null) {
\r
599 dates = new LinkedList<String>();
\r
601 // RDATE value may contain multiple date delimited
\r
603 StringTokenizer st = new StringTokenizer(value, COMMA);
\r
604 while (st.hasMoreTokens()) {
\r
605 String date = st.nextToken();
\r
608 } else if (name.equals(ICAL_RRULE)) {
\r
609 // RRULE mixed with RDATE is not supported
\r
610 if (!isRRULE && dates != null) {
\r
613 } else if (dates == null) {
\r
614 dates = new LinkedList<String>();
\r
618 } else if (name.equals(ICAL_END)) {
\r
619 // Mandatory properties
\r
620 if (dtstart == null || from == null || to == null) {
\r
624 // if tzname is not available, create one from tzid
\r
625 if (tzname == null) {
\r
626 tzname = getDefaultTZName(tzid, dst);
\r
629 // create a time zone rule
\r
630 TimeZoneRule rule = null;
\r
631 int fromOffset = 0;
\r
634 int dstSavings = 0;
\r
637 // Parse TZOFFSETFROM/TZOFFSETTO
\r
638 fromOffset = offsetStrToMillis(from);
\r
639 toOffset = offsetStrToMillis(to);
\r
642 // If daylight, use the previous offset as rawoffset if positive
\r
643 if (toOffset - fromOffset > 0) {
\r
644 rawOffset = fromOffset;
\r
645 dstSavings = toOffset - fromOffset;
\r
647 // This is rare case.. just use 1 hour DST savings
\r
648 rawOffset = toOffset - DEF_DSTSAVINGS;
\r
649 dstSavings = DEF_DSTSAVINGS;
\r
652 rawOffset = toOffset;
\r
657 start = parseDateTimeString(dtstart, fromOffset);
\r
660 Date actualStart = null;
\r
662 rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
\r
664 rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
\r
666 if (rule != null) {
\r
667 actualStart = rule.getFirstStart(fromOffset, 0);
\r
668 if (actualStart.getTime() < firstStart) {
\r
669 // save from offset information for the earliest rule
\r
670 firstStart = actualStart.getTime();
\r
671 // If this is STD, assume the time before this transtion
\r
672 // is DST when the difference is 1 hour. This might not be
\r
673 // accurate, but VTIMEZONE data does not have such info.
\r
674 if (dstSavings > 0) {
\r
675 initialRawOffset = fromOffset;
\r
676 initialDSTSavings = 0;
\r
678 if (fromOffset - toOffset == DEF_DSTSAVINGS) {
\r
679 initialRawOffset = fromOffset - DEF_DSTSAVINGS;
\r
680 initialDSTSavings = DEF_DSTSAVINGS;
\r
682 initialRawOffset = fromOffset;
\r
683 initialDSTSavings = 0;
\r
688 } catch (IllegalArgumentException iae) {
\r
689 // bad format - rule == null..
\r
692 if (rule == null) {
\r
702 if (state == ERR) {
\r
708 // Must have at least one rule
\r
709 if (rules.size() == 0) {
\r
713 // Create a initial rule
\r
714 InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false),
\r
715 initialRawOffset, initialDSTSavings);
\r
717 // Finally, create the RuleBasedTimeZone
\r
718 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule);
\r
720 int finalRuleIdx = -1;
\r
721 int finalRuleCount = 0;
\r
722 for (int i = 0; i < rules.size(); i++) {
\r
723 TimeZoneRule r = rules.get(i);
\r
724 if (r instanceof AnnualTimeZoneRule) {
\r
725 if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
731 if (finalRuleCount > 2) {
\r
732 // Too many final rules
\r
736 if (finalRuleCount == 1) {
\r
737 if (rules.size() == 1) {
\r
738 // Only one final rule, only governs the initial rule,
\r
739 // which is already initialized, thus, we do not need to
\r
740 // add this transition rule
\r
743 // Normalize the final rule
\r
744 AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx);
\r
745 int tmpRaw = finalRule.getRawOffset();
\r
746 int tmpDST = finalRule.getDSTSavings();
\r
748 // Find the last non-final rule
\r
749 Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings);
\r
750 Date start = finalStart;
\r
751 for (int i = 0; i < rules.size(); i++) {
\r
752 if (finalRuleIdx == i) {
\r
755 TimeZoneRule r = rules.get(i);
\r
756 Date lastStart = r.getFinalStart(tmpRaw, tmpDST);
\r
757 if (lastStart.after(start)) {
\r
758 start = finalRule.getNextStart(lastStart.getTime(),
\r
764 TimeZoneRule newRule;
\r
765 if (start == finalStart) {
\r
766 // Transform this into a single transition
\r
767 newRule = new TimeArrayTimeZoneRule(
\r
768 finalRule.getName(),
\r
769 finalRule.getRawOffset(),
\r
770 finalRule.getDSTSavings(),
\r
771 new long[] {finalStart.getTime()},
\r
772 DateTimeRule.UTC_TIME);
\r
774 // Update the end year
\r
775 int fields[] = Grego.timeToFields(start.getTime(), null);
\r
776 newRule = new AnnualTimeZoneRule(
\r
777 finalRule.getName(),
\r
778 finalRule.getRawOffset(),
\r
779 finalRule.getDSTSavings(),
\r
780 finalRule.getRule(),
\r
781 finalRule.getStartYear(),
\r
784 rules.set(finalRuleIdx, newRule);
\r
788 for (TimeZoneRule r : rules) {
\r
789 rbtz.addTransitionRule(r);
\r
798 * Create a default TZNAME from TZID
\r
800 private static String getDefaultTZName(String tzid, boolean isDST) {
\r
802 return tzid + "(DST)";
\r
804 return tzid + "(STD)";
\r
808 * Create a TimeZoneRule by the RRULE definition
\r
810 private static TimeZoneRule createRuleByRRULE(String tzname,
\r
811 int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
\r
812 if (dates == null || dates.size() == 0) {
\r
815 // Parse the first rule
\r
816 String rrule = dates.get(0);
\r
818 long until[] = new long[1];
\r
819 int[] ruleFields = parseRRULE(rrule, until);
\r
820 if (ruleFields == null) {
\r
825 int month = ruleFields[0];
\r
826 int dayOfWeek = ruleFields[1];
\r
827 int nthDayOfWeek = ruleFields[2];
\r
828 int dayOfMonth = ruleFields[3];
\r
830 if (dates.size() == 1) {
\r
832 if (ruleFields.length > 4) {
\r
833 // Multiple BYMONTHDAY values
\r
835 if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) {
\r
836 // Only support the rule using 7 continuous days
\r
837 // BYMONTH and BYDAY must be set at the same time
\r
840 int firstDay = 31; // max possible number of dates in a month
\r
841 int days[] = new int[7];
\r
842 for (int i = 0; i < 7; i++) {
\r
843 days[i] = ruleFields[3 + i];
\r
844 // Resolve negative day numbers. A negative day number should
\r
845 // not be used in February, but if we see such case, we use 28
\r
847 days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1;
\r
848 firstDay = days[i] < firstDay ? days[i] : firstDay;
\r
850 // Make sure days are continuous
\r
851 for (int i = 1; i < 7; i++) {
\r
852 boolean found = false;
\r
853 for (int j = 0; j < 7; j++) {
\r
854 if (days[j] == firstDay + i) {
\r
860 // days are not continuous
\r
864 // Use DOW_GEQ_DOM rule with firstDay as the start date
\r
865 dayOfMonth = firstDay;
\r
868 // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines.
\r
869 // Otherwise, not supported.
\r
870 if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) {
\r
871 // This is not the case
\r
874 // Parse the rest of rules if number of rules is not exceeding 7.
\r
875 // We can only support 7 continuous days starting from a day of month.
\r
876 if (dates.size() > 7) {
\r
880 // Note: To check valid date range across multiple rule is a little
\r
881 // bit complicated. For now, this code is not doing strict range
\r
882 // checking across month boundary
\r
884 int earliestMonth = month;
\r
885 int daysCount = ruleFields.length - 3;
\r
886 int earliestDay = 31;
\r
887 for (int i = 0; i < daysCount; i++) {
\r
888 int dom = ruleFields[3 + i];
\r
889 dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1;
\r
890 earliestDay = dom < earliestDay ? dom : earliestDay;
\r
893 int anotherMonth = -1;
\r
894 for (int i = 1; i < dates.size(); i++) {
\r
895 rrule = dates.get(i);
\r
896 long[] unt = new long[1];
\r
897 int[] fields = parseRRULE(rrule, unt);
\r
899 // If UNTIL is newer than previous one, use the one
\r
900 if (unt[0] > until[0]) {
\r
904 // Check if BYMONTH + BYMONTHDAY + BYDAY rule
\r
905 if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) {
\r
908 // Count number of BYMONTHDAY
\r
909 int count = fields.length - 3;
\r
910 if (daysCount + count > 7) {
\r
911 // We cannot support BYMONTHDAY more than 7
\r
914 // Check if the same BYDAY is used. Otherwise, we cannot
\r
915 // support the rule
\r
916 if (fields[1] != dayOfWeek) {
\r
919 // Check if the month is same or right next to the primary month
\r
920 if (fields[0] != month) {
\r
921 if (anotherMonth == -1) {
\r
922 int diff = fields[0] - month;
\r
923 if (diff == -11 || diff == -1) {
\r
925 anotherMonth = fields[0];
\r
926 earliestMonth = anotherMonth;
\r
927 // Reset earliest day
\r
929 } else if (diff == 11 || diff == 1) {
\r
931 anotherMonth = fields[0];
\r
933 // The day range cannot exceed more than 2 months
\r
936 } else if (fields[0] != month && fields[0] != anotherMonth) {
\r
937 // The day range cannot exceed more than 2 months
\r
941 // If ealier month, go through days to find the earliest day
\r
942 if (fields[0] == earliestMonth) {
\r
943 for (int j = 0; j < count; j++) {
\r
944 int dom = fields[3 + j];
\r
945 dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1;
\r
946 earliestDay = dom < earliestDay ? dom : earliestDay;
\r
949 daysCount += count;
\r
951 if (daysCount != 7) {
\r
952 // Number of BYMONTHDAY entries must be 7
\r
955 month = earliestMonth;
\r
956 dayOfMonth = earliestDay;
\r
959 // Calculate start/end year and missing fields
\r
960 int[] dfields = Grego.timeToFields(start + fromOffset, null);
\r
961 int startYear = dfields[0];
\r
963 // If MYMONTH is not set, use the month of DTSTART
\r
964 month = dfields[1];
\r
966 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) {
\r
967 // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY
\r
968 dayOfMonth = dfields[2];
\r
970 int timeInDay = dfields[5];
\r
972 int endYear = AnnualTimeZoneRule.MAX_YEAR;
\r
973 if (until[0] != MIN_TIME) {
\r
974 Grego.timeToFields(until[0], dfields);
\r
975 endYear = dfields[0];
\r
978 // Create the AnnualDateTimeRule
\r
979 DateTimeRule adtr = null;
\r
980 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
\r
981 // Day in month rule, for example, 15th day in the month
\r
982 adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME);
\r
983 } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) {
\r
984 // Nth day of week rule, for example, last Sunday
\r
985 adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME);
\r
986 } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
\r
987 // First day of week after day of month rule, for example,
\r
988 // first Sunday after 15th day in the month
\r
989 adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME);
\r
991 // RRULE attributes are insufficient
\r
995 return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear);
\r
999 * Parse individual RRULE
\r
1003 * int[0] month calculated by BYMONTH - 1, or -1 when not found
\r
1004 * int[1] day of week in BYDAY, or 0 when not found
\r
1005 * int[2] day of week ordinal number in BYDAY, or 0 when not found
\r
1006 * int[i >= 3] day of month, which could be multiple values, or 0 when not found
\r
1010 * null on any error cases, for exmaple, FREQ=YEARLY is not available
\r
1012 * When UNTIL attribute is available, the time will be set to until[0],
\r
1013 * otherwise, MIN_TIME
\r
1015 private static int[] parseRRULE(String rrule, long[] until) {
\r
1017 int dayOfWeek = 0;
\r
1018 int nthDayOfWeek = 0;
\r
1019 int[] dayOfMonth = null;
\r
1021 long untilTime = MIN_TIME;
\r
1022 boolean yearly = false;
\r
1023 boolean parseError = false;
\r
1024 StringTokenizer st= new StringTokenizer(rrule, SEMICOLON);
\r
1026 while (st.hasMoreTokens()) {
\r
1027 String attr, value;
\r
1028 String prop = st.nextToken();
\r
1029 int sep = prop.indexOf(EQUALS_SIGN);
\r
1031 attr = prop.substring(0, sep);
\r
1032 value = prop.substring(sep + 1);
\r
1034 parseError = true;
\r
1038 if (attr.equals(ICAL_FREQ)) {
\r
1039 // only support YEARLY frequency type
\r
1040 if (value.equals(ICAL_YEARLY)) {
\r
1043 parseError = true;
\r
1046 } else if (attr.equals(ICAL_UNTIL)) {
\r
1047 // ISO8601 UTC format, for example, "20060315T020000Z"
\r
1049 untilTime = parseDateTimeString(value, 0);
\r
1050 } catch (IllegalArgumentException iae) {
\r
1051 parseError = true;
\r
1054 } else if (attr.equals(ICAL_BYMONTH)) {
\r
1055 // Note: BYMONTH may contain multiple months, but only single month make sense for
\r
1056 // VTIMEZONE property.
\r
1057 if (value.length() > 2) {
\r
1058 parseError = true;
\r
1062 month = Integer.parseInt(value) - 1;
\r
1063 if (month < 0 || month >= 12) {
\r
1064 parseError = true;
\r
1067 } catch (NumberFormatException nfe) {
\r
1068 parseError = true;
\r
1071 } else if (attr.equals(ICAL_BYDAY)) {
\r
1072 // Note: BYDAY may contain multiple day of week separated by comma. It is unlikely used for
\r
1073 // VTIMEZONE property. We do not support the case.
\r
1075 // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday
\r
1076 // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday
\r
1077 int length = value.length();
\r
1078 if (length < 2 || length > 4) {
\r
1079 parseError = true;
\r
1083 // Nth day of week
\r
1085 if (value.charAt(0) == '+') {
\r
1087 } else if (value.charAt(0) == '-') {
\r
1089 } else if (length == 4) {
\r
1090 parseError = true;
\r
1094 int n = Integer.parseInt(value.substring(length - 3, length - 2));
\r
1095 if (n == 0 || n > 4) {
\r
1096 parseError = true;
\r
1099 nthDayOfWeek = n * sign;
\r
1100 } catch(NumberFormatException nfe) {
\r
1101 parseError = true;
\r
1104 value = value.substring(length - 2);
\r
1107 for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) {
\r
1108 if (value.equals(ICAL_DOW_NAMES[wday])) {
\r
1112 if (wday < ICAL_DOW_NAMES.length) {
\r
1113 // Sunday(1) - Saturday(7)
\r
1114 dayOfWeek = wday + 1;
\r
1116 parseError = true;
\r
1119 } else if (attr.equals(ICAL_BYMONTHDAY)) {
\r
1120 // Note: BYMONTHDAY may contain multiple days delimited by comma
\r
1122 // A value of BYMONTHDAY could be negative, for example, -1 means
\r
1123 // the last day in a month
\r
1124 StringTokenizer days = new StringTokenizer(value, COMMA);
\r
1125 int count = days.countTokens();
\r
1126 dayOfMonth = new int[count];
\r
1128 while(days.hasMoreTokens()) {
\r
1130 dayOfMonth[index++] = Integer.parseInt(days.nextToken());
\r
1131 } catch (NumberFormatException nfe) {
\r
1132 parseError = true;
\r
1143 // FREQ=YEARLY must be set
\r
1147 until[0] = untilTime;
\r
1150 if (dayOfMonth == null) {
\r
1151 results = new int[4];
\r
1154 results = new int[3 + dayOfMonth.length];
\r
1155 for (int i = 0; i < dayOfMonth.length; i++) {
\r
1156 results[3 + i] = dayOfMonth[i];
\r
1159 results[0] = month;
\r
1160 results[1] = dayOfWeek;
\r
1161 results[2] = nthDayOfWeek;
\r
1166 * Create a TimeZoneRule by the RDATE definition
\r
1168 private static TimeZoneRule createRuleByRDATE(String tzname,
\r
1169 int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
\r
1170 // Create an array of transition times
\r
1172 if (dates == null || dates.size() == 0) {
\r
1173 // When no RDATE line is provided, use start (DTSTART)
\r
1174 // as the transition time
\r
1175 times = new long[1];
\r
1178 times = new long[dates.size()];
\r
1181 for (String date : dates) {
\r
1182 times[idx++] = parseDateTimeString(date, fromOffset);
\r
1184 } catch (IllegalArgumentException iae) {
\r
1188 return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME);
\r
1192 * Write the time zone rules in RFC2445 VTIMEZONE format
\r
1194 private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException {
\r
1195 // Write the header
\r
1198 if (customProperties != null && customProperties.length > 0) {
\r
1199 for (int i = 0; i < customProperties.length; i++) {
\r
1200 if (customProperties[i] != null) {
\r
1201 w.write(customProperties[i]);
\r
1207 long t = MIN_TIME;
\r
1208 String dstName = null;
\r
1209 int dstFromOffset = 0;
\r
1210 int dstFromDSTSavings = 0;
\r
1211 int dstToOffset = 0;
\r
1212 int dstStartYear = 0;
\r
1214 int dstDayOfWeek = 0;
\r
1215 int dstWeekInMonth = 0;
\r
1216 int dstMillisInDay = 0;
\r
1217 long dstStartTime = 0;
\r
1218 long dstUntilTime = 0;
\r
1220 AnnualTimeZoneRule finalDstRule = null;
\r
1222 String stdName = null;
\r
1223 int stdFromOffset = 0;
\r
1224 int stdFromDSTSavings = 0;
\r
1225 int stdToOffset = 0;
\r
1226 int stdStartYear = 0;
\r
1228 int stdDayOfWeek = 0;
\r
1229 int stdWeekInMonth = 0;
\r
1230 int stdMillisInDay = 0;
\r
1231 long stdStartTime = 0;
\r
1232 long stdUntilTime = 0;
\r
1234 AnnualTimeZoneRule finalStdRule = null;
\r
1236 int[] dtfields = new int[6];
\r
1237 boolean hasTransitions = false;
\r
1239 // Going through all transitions
\r
1241 TimeZoneTransition tzt = basictz.getNextTransition(t, false);
\r
1242 if (tzt == null) {
\r
1245 hasTransitions = true;
\r
1246 t = tzt.getTime();
\r
1247 String name = tzt.getTo().getName();
\r
1248 boolean isDst = (tzt.getTo().getDSTSavings() != 0);
\r
1249 int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
\r
1250 int fromDSTSavings = tzt.getFrom().getDSTSavings();
\r
1251 int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
\r
1252 Grego.timeToFields(tzt.getTime() + fromOffset, dtfields);
\r
1253 int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]);
\r
1254 int year = dtfields[0];
\r
1255 boolean sameRule = false;
\r
1257 if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
\r
1258 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
1259 finalDstRule = (AnnualTimeZoneRule)tzt.getTo();
\r
1262 if (dstCount > 0) {
\r
1263 if (year == dstStartYear + dstCount
\r
1264 && name.equals(dstName)
\r
1265 && dstFromOffset == fromOffset
\r
1266 && dstToOffset == toOffset
\r
1267 && dstMonth == dtfields[1]
\r
1268 && dstDayOfWeek == dtfields[3]
\r
1269 && dstWeekInMonth == weekInMonth
\r
1270 && dstMillisInDay == dtfields[5]) {
\r
1271 // Update until time
\r
1277 if (dstCount == 1) {
\r
1278 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
\r
1279 dstStartTime, true);
\r
1281 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1282 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1287 // Reset this DST information
\r
1289 dstFromOffset = fromOffset;
\r
1290 dstFromDSTSavings = fromDSTSavings;
\r
1291 dstToOffset = toOffset;
\r
1292 dstStartYear = year;
\r
1293 dstMonth = dtfields[1];
\r
1294 dstDayOfWeek = dtfields[3];
\r
1295 dstWeekInMonth = weekInMonth;
\r
1296 dstMillisInDay = dtfields[5];
\r
1297 dstStartTime = dstUntilTime = t;
\r
1300 if (finalStdRule != null && finalDstRule != null) {
\r
1304 if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
\r
1305 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
1306 finalStdRule = (AnnualTimeZoneRule)tzt.getTo();
\r
1309 if (stdCount > 0) {
\r
1310 if (year == stdStartYear + stdCount
\r
1311 && name.equals(stdName)
\r
1312 && stdFromOffset == fromOffset
\r
1313 && stdToOffset == toOffset
\r
1314 && stdMonth == dtfields[1]
\r
1315 && stdDayOfWeek == dtfields[3]
\r
1316 && stdWeekInMonth == weekInMonth
\r
1317 && stdMillisInDay == dtfields[5]) {
\r
1318 // Update until time
\r
1324 if (stdCount == 1) {
\r
1325 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
\r
1326 stdStartTime, true);
\r
1328 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1329 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1334 // Reset this STD information
\r
1336 stdFromOffset = fromOffset;
\r
1337 stdFromDSTSavings = fromDSTSavings;
\r
1338 stdToOffset = toOffset;
\r
1339 stdStartYear = year;
\r
1340 stdMonth = dtfields[1];
\r
1341 stdDayOfWeek = dtfields[3];
\r
1342 stdWeekInMonth = weekInMonth;
\r
1343 stdMillisInDay = dtfields[5];
\r
1344 stdStartTime = stdUntilTime = t;
\r
1347 if (finalStdRule != null && finalDstRule != null) {
\r
1352 if (!hasTransitions) {
\r
1353 // No transition - put a single non transition RDATE
\r
1354 int offset = basictz.getOffset(0 /* any time */);
\r
1355 boolean isDst = (offset != basictz.getRawOffset());
\r
1356 writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst),
\r
1357 offset, offset, DEF_TZSTARTTIME - offset, false);
\r
1359 if (dstCount > 0) {
\r
1360 if (finalDstRule == null) {
\r
1361 if (dstCount == 1) {
\r
1362 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
\r
1363 dstStartTime, true);
\r
1365 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1366 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1369 if (dstCount == 1) {
\r
1370 writeFinalRule(w, true, finalDstRule,
\r
1371 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
\r
1373 // Use a single rule if possible
\r
1374 if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) {
\r
1375 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1376 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME);
\r
1378 // Not equivalent rule - write out two different rules
\r
1379 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1380 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1381 writeFinalRule(w, true, finalDstRule,
\r
1382 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
\r
1387 if (stdCount > 0) {
\r
1388 if (finalStdRule == null) {
\r
1389 if (stdCount == 1) {
\r
1390 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
\r
1391 stdStartTime, true);
\r
1393 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1394 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1397 if (stdCount == 1) {
\r
1398 writeFinalRule(w, false, finalStdRule,
\r
1399 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
\r
1401 // Use a single rule if possible
\r
1402 if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) {
\r
1403 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1404 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME);
\r
1406 // Not equivalent rule - write out two different rules
\r
1407 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1408 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1409 writeFinalRule(w, false, finalStdRule,
\r
1410 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
\r
1420 * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent
\r
1421 * to the DateTimerule.
\r
1423 private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) {
\r
1424 if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) {
\r
1427 if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) {
\r
1428 // Do not try to do more intelligent comparison for now.
\r
1431 if (dtrule.getDateRuleType() == DateTimeRule.DOW
\r
1432 && dtrule.getRuleWeekInMonth() == weekInMonth) {
\r
1435 int ruleDOM = dtrule.getRuleDayOfMonth();
\r
1436 if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) {
\r
1437 if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) {
\r
1440 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6
\r
1441 && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) {
\r
1445 if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) {
\r
1446 if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) {
\r
1449 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0
\r
1450 && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) {
\r
1458 * Write a single start time
\r
1460 private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname,
\r
1461 int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException {
\r
1462 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time);
\r
1464 writer.write(ICAL_RDATE);
\r
1465 writer.write(COLON);
\r
1466 writer.write(getDateTimeString(time + fromOffset));
\r
1467 writer.write(NEWLINE);
\r
1469 endZoneProps(writer, isDst);
\r
1473 * Write start times defined by a DOM rule using VTIMEZONE RRULE
\r
1475 private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1476 int month, int dayOfMonth, long startTime, long untilTime) throws IOException {
\r
1477 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1479 beginRRULE(writer, month);
\r
1480 writer.write(ICAL_BYMONTHDAY);
\r
1481 writer.write(EQUALS_SIGN);
\r
1482 writer.write(Integer.toString(dayOfMonth));
\r
1484 if (untilTime != MAX_TIME) {
\r
1485 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1487 writer.write(NEWLINE);
\r
1489 endZoneProps(writer, isDst);
\r
1493 * Write start times defined by a DOW rule using VTIMEZONE RRULE
\r
1495 private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1496 int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1497 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1499 beginRRULE(writer, month);
\r
1500 writer.write(ICAL_BYDAY);
\r
1501 writer.write(EQUALS_SIGN);
\r
1502 writer.write(Integer.toString(weekInMonth)); // -4, -3, -2, -1, 1, 2, 3, 4
\r
1503 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU...
\r
1505 if (untilTime != MAX_TIME) {
\r
1506 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1508 writer.write(NEWLINE);
\r
1510 endZoneProps(writer, isDst);
\r
1514 * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE
\r
1516 private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1517 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1518 // Check if this rule can be converted to DOW rule
\r
1519 if (dayOfMonth%7 == 1) {
\r
1520 // Can be represented by DOW rule
\r
1521 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1522 month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime);
\r
1523 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) {
\r
1524 // Can be represented by DOW rule with negative week number
\r
1525 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1526 month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime);
\r
1528 // Otherwise, use BYMONTHDAY to include all possible dates
\r
1529 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1531 // Check if all days are in the same month
\r
1532 int startDay = dayOfMonth;
\r
1533 int currentMonthDays = 7;
\r
1535 if (dayOfMonth <= 0) {
\r
1536 // The start day is in previous month
\r
1537 int prevMonthDays = 1 - dayOfMonth;
\r
1538 currentMonthDays -= prevMonthDays;
\r
1540 int prevMonth = (month - 1) < 0 ? 11 : month - 1;
\r
1542 // Note: When a rule is separated into two, UNTIL attribute needs to be
\r
1543 // calculated for each of them. For now, we skip this, because we basically use this method
\r
1544 // only for final rules, which does not have the UNTIL attribute
\r
1545 writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
\r
1547 // Start from 1 for the rest
\r
1549 } else if (dayOfMonth + 6 > MONTHLENGTH[month]) {
\r
1550 // Note: This code does not actually work well in February. For now, days in month in
\r
1552 int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month];
\r
1553 currentMonthDays -= nextMonthDays;
\r
1555 int nextMonth = (month + 1) > 11 ? 0 : month + 1;
\r
1557 writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
\r
1559 writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset);
\r
1560 endZoneProps(writer, isDst);
\r
1565 * Called from writeZonePropsByDOW_GEQ_DOM
\r
1567 private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month,
\r
1568 int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException {
\r
1570 int startDayNum = dayOfMonth;
\r
1571 boolean isFeb = (month == Calendar.FEBRUARY);
\r
1572 if (dayOfMonth < 0 && !isFeb) {
\r
1573 // Use positive number if possible
\r
1574 startDayNum = MONTHLENGTH[month] + dayOfMonth + 1;
\r
1576 beginRRULE(writer, month);
\r
1577 writer.write(ICAL_BYDAY);
\r
1578 writer.write(EQUALS_SIGN);
\r
1579 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU...
\r
1580 writer.write(SEMICOLON);
\r
1581 writer.write(ICAL_BYMONTHDAY);
\r
1582 writer.write(EQUALS_SIGN);
\r
1584 writer.write(Integer.toString(startDayNum));
\r
1585 for (int i = 1; i < numDays; i++) {
\r
1586 writer.write(COMMA);
\r
1587 writer.write(Integer.toString(startDayNum + i));
\r
1590 if (untilTime != MAX_TIME) {
\r
1591 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1593 writer.write(NEWLINE);
\r
1597 * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE
\r
1599 private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1600 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1601 // Check if this rule can be converted to DOW rule
\r
1602 if (dayOfMonth%7 == 0) {
\r
1603 // Can be represented by DOW rule
\r
1604 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1605 month, dayOfMonth/7, dayOfWeek, startTime, untilTime);
\r
1606 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){
\r
1607 // Can be represented by DOW rule with negative week number
\r
1608 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1609 month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime);
\r
1610 } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) {
\r
1611 // Specical case for February
\r
1612 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1613 Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime);
\r
1615 // Otherwise, convert this to DOW_GEQ_DOM rule
\r
1616 writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset,
\r
1617 month, dayOfMonth - 6, dayOfWeek, startTime, untilTime);
\r
1622 * Write the final time zone rule using RRULE, with no UNTIL attribute
\r
1624 private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule,
\r
1625 int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{
\r
1626 DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings);
\r
1627 int toOffset = rule.getRawOffset() + rule.getDSTSavings();
\r
1628 switch (dtrule.getDateRuleType()) {
\r
1629 case DateTimeRule.DOM:
\r
1630 writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1631 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME);
\r
1633 case DateTimeRule.DOW:
\r
1634 writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1635 dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1637 case DateTimeRule.DOW_GEQ_DOM:
\r
1638 writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1639 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1641 case DateTimeRule.DOW_LEQ_DOM:
\r
1642 writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1643 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1649 * Convert the rule to its equivalent rule using WALL_TIME mode
\r
1651 private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) {
\r
1652 if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
\r
1655 int wallt = rule.getRuleMillisInDay();
\r
1656 if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) {
\r
1657 wallt += (rawOffset + dstSavings);
\r
1658 } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) {
\r
1659 wallt += dstSavings;
\r
1662 int month = -1, dom = 0, dow = 0, dtype = -1;
\r
1666 wallt += Grego.MILLIS_PER_DAY;
\r
1667 } else if (wallt >= Grego.MILLIS_PER_DAY) {
\r
1669 wallt -= Grego.MILLIS_PER_DAY;
\r
1672 month = rule.getRuleMonth();
\r
1673 dom = rule.getRuleDayOfMonth();
\r
1674 dow = rule.getRuleDayOfWeek();
\r
1675 dtype = rule.getDateRuleType();
\r
1677 if (dshift != 0) {
\r
1678 if (dtype == DateTimeRule.DOW) {
\r
1679 // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first
\r
1680 int wim = rule.getRuleWeekInMonth();
\r
1682 dtype = DateTimeRule.DOW_GEQ_DOM;
\r
1683 dom = 7 * (wim - 1) + 1;
\r
1685 dtype = DateTimeRule.DOW_LEQ_DOM;
\r
1686 dom = MONTHLENGTH[month] + 7 * (wim + 1);
\r
1690 // Shift one day before or after
\r
1694 month = month < Calendar.JANUARY ? Calendar.DECEMBER : month;
\r
1695 dom = MONTHLENGTH[month];
\r
1696 } else if (dom > MONTHLENGTH[month]) {
\r
1698 month = month > Calendar.DECEMBER ? Calendar.JANUARY : month;
\r
1701 if (dtype != DateTimeRule.DOM) {
\r
1702 // Adjust day of week
\r
1704 if (dow < Calendar.SUNDAY) {
\r
1705 dow = Calendar.SATURDAY;
\r
1706 } else if (dow > Calendar.SATURDAY) {
\r
1707 dow = Calendar.SUNDAY;
\r
1711 // Create a new rule
\r
1712 DateTimeRule modifiedRule;
\r
1713 if (dtype == DateTimeRule.DOM) {
\r
1714 modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME);
\r
1716 modifiedRule = new DateTimeRule(month, dom, dow,
\r
1717 (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME);
\r
1719 return modifiedRule;
\r
1723 * Write the opening section of zone properties
\r
1725 private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException {
\r
1726 writer.write(ICAL_BEGIN);
\r
1727 writer.write(COLON);
\r
1729 writer.write(ICAL_DAYLIGHT);
\r
1731 writer.write(ICAL_STANDARD);
\r
1733 writer.write(NEWLINE);
\r
1736 writer.write(ICAL_TZOFFSETTO);
\r
1737 writer.write(COLON);
\r
1738 writer.write(millisToOffset(toOffset));
\r
1739 writer.write(NEWLINE);
\r
1742 writer.write(ICAL_TZOFFSETFROM);
\r
1743 writer.write(COLON);
\r
1744 writer.write(millisToOffset(fromOffset));
\r
1745 writer.write(NEWLINE);
\r
1748 writer.write(ICAL_TZNAME);
\r
1749 writer.write(COLON);
\r
1750 writer.write(tzname);
\r
1751 writer.write(NEWLINE);
\r
1754 writer.write(ICAL_DTSTART);
\r
1755 writer.write(COLON);
\r
1756 writer.write(getDateTimeString(startTime + fromOffset));
\r
1757 writer.write(NEWLINE);
\r
1761 * Writes the closing section of zone properties
\r
1763 private static void endZoneProps(Writer writer, boolean isDst) throws IOException{
\r
1764 // END:STANDARD or END:DAYLIGHT
\r
1765 writer.write(ICAL_END);
\r
1766 writer.write(COLON);
\r
1768 writer.write(ICAL_DAYLIGHT);
\r
1770 writer.write(ICAL_STANDARD);
\r
1772 writer.write(NEWLINE);
\r
1776 * Write the beginning part of RRULE line
\r
1778 private static void beginRRULE(Writer writer, int month) throws IOException {
\r
1779 writer.write(ICAL_RRULE);
\r
1780 writer.write(COLON);
\r
1781 writer.write(ICAL_FREQ);
\r
1782 writer.write(EQUALS_SIGN);
\r
1783 writer.write(ICAL_YEARLY);
\r
1784 writer.write(SEMICOLON);
\r
1785 writer.write(ICAL_BYMONTH);
\r
1786 writer.write(EQUALS_SIGN);
\r
1787 writer.write(Integer.toString(month + 1));
\r
1788 writer.write(SEMICOLON);
\r
1792 * Append the UNTIL attribute after RRULE line
\r
1794 private static void appendUNTIL(Writer writer, String until) throws IOException {
\r
1795 if (until != null) {
\r
1796 writer.write(SEMICOLON);
\r
1797 writer.write(ICAL_UNTIL);
\r
1798 writer.write(EQUALS_SIGN);
\r
1799 writer.write(until);
\r
1804 * Write the opening section of the VTIMEZONE block
\r
1806 private void writeHeader(Writer writer)throws IOException {
\r
1807 writer.write(ICAL_BEGIN);
\r
1808 writer.write(COLON);
\r
1809 writer.write(ICAL_VTIMEZONE);
\r
1810 writer.write(NEWLINE);
\r
1811 writer.write(ICAL_TZID);
\r
1812 writer.write(COLON);
\r
1813 writer.write(tz.getID());
\r
1814 writer.write(NEWLINE);
\r
1815 if (tzurl != null) {
\r
1816 writer.write(ICAL_TZURL);
\r
1817 writer.write(COLON);
\r
1818 writer.write(tzurl);
\r
1819 writer.write(NEWLINE);
\r
1821 if (lastmod != null) {
\r
1822 writer.write(ICAL_LASTMOD);
\r
1823 writer.write(COLON);
\r
1824 writer.write(getUTCDateTimeString(lastmod.getTime()));
\r
1825 writer.write(NEWLINE);
\r
1830 * Write the closing section of the VTIMEZONE definition block
\r
1832 private static void writeFooter(Writer writer) throws IOException {
\r
1833 writer.write(ICAL_END);
\r
1834 writer.write(COLON);
\r
1835 writer.write(ICAL_VTIMEZONE);
\r
1836 writer.write(NEWLINE);
\r
1840 * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME
\r
1842 private static String getDateTimeString(long time) {
\r
1843 int[] fields = Grego.timeToFields(time, null);
\r
1844 StringBuilder sb = new StringBuilder(15);
\r
1845 sb.append(numToString(fields[0], 4));
\r
1846 sb.append(numToString(fields[1] + 1, 2));
\r
1847 sb.append(numToString(fields[2], 2));
\r
1850 int t = fields[5];
\r
1851 int hour = t / Grego.MILLIS_PER_HOUR;
\r
1852 t %= Grego.MILLIS_PER_HOUR;
\r
1853 int min = t / Grego.MILLIS_PER_MINUTE;
\r
1854 t %= Grego.MILLIS_PER_MINUTE;
\r
1855 int sec = t / Grego.MILLIS_PER_SECOND;
\r
1857 sb.append(numToString(hour, 2));
\r
1858 sb.append(numToString(min, 2));
\r
1859 sb.append(numToString(sec, 2));
\r
1860 return sb.toString();
\r
1864 * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME
\r
1866 private static String getUTCDateTimeString(long time) {
\r
1867 return getDateTimeString(time) + "Z";
\r
1871 * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and
\r
1872 * #2 DATE WITH UTC TIME
\r
1874 private static long parseDateTimeString(String str, int offset) {
\r
1875 int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
\r
1876 boolean isUTC = false;
\r
1877 boolean isValid = false;
\r
1879 if (str == null) {
\r
1883 int length = str.length();
\r
1884 if (length != 15 && length != 16) {
\r
1885 // FORM#1 15 characters, such as "20060317T142115"
\r
1886 // FORM#2 16 characters, such as "20060317T142115Z"
\r
1889 if (str.charAt(8) != 'T') {
\r
1890 // charcter "T" must be used for separating date and time
\r
1893 if (length == 16) {
\r
1894 if (str.charAt(15) != 'Z') {
\r
1902 year = Integer.parseInt(str.substring(0, 4));
\r
1903 month = Integer.parseInt(str.substring(4, 6)) - 1; // 0-based
\r
1904 day = Integer.parseInt(str.substring(6, 8));
\r
1905 hour = Integer.parseInt(str.substring(9, 11));
\r
1906 min = Integer.parseInt(str.substring(11, 13));
\r
1907 sec = Integer.parseInt(str.substring(13, 15));
\r
1908 } catch (NumberFormatException nfe) {
\r
1912 // check valid range
\r
1913 int maxDayOfMonth = Grego.monthLength(year, month);
\r
1914 if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth ||
\r
1915 hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) {
\r
1923 throw new IllegalArgumentException("Invalid date time string format");
\r
1925 // Calculate the time
\r
1926 long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY;
\r
1927 time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND);
\r
1935 * Convert RFC2445 utc-offset string to milliseconds
\r
1937 private static int offsetStrToMillis(String str) {
\r
1938 boolean isValid = false;
\r
1939 int sign = 0, hour = 0, min = 0, sec = 0;
\r
1942 if (str == null) {
\r
1945 int length = str.length();
\r
1946 if (length != 5 && length != 7) {
\r
1947 // utf-offset must be 5 or 7 characters
\r
1951 char s = str.charAt(0);
\r
1954 } else if (s == '-') {
\r
1957 // utf-offset must start with "+" or "-"
\r
1962 hour = Integer.parseInt(str.substring(1, 3));
\r
1963 min = Integer.parseInt(str.substring(3, 5));
\r
1964 if (length == 7) {
\r
1965 sec = Integer.parseInt(str.substring(5, 7));
\r
1967 } catch (NumberFormatException nfe) {
\r
1974 throw new IllegalArgumentException("Bad offset string");
\r
1976 int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000;
\r
1981 * Convert milliseconds to RFC2445 utc-offset string
\r
1983 private static String millisToOffset(int millis) {
\r
1984 StringBuilder sb = new StringBuilder(7);
\r
1985 if (millis >= 0) {
\r
1991 int hour, min, sec;
\r
1992 int t = millis / 1000;
\r
1995 t = (t - sec) / 60;
\r
1999 sb.append(numToString(hour, 2));
\r
2000 sb.append(numToString(min, 2));
\r
2001 sb.append(numToString(sec, 2));
\r
2003 return sb.toString();
\r
2007 * Format integer number
\r
2009 private static String numToString(int num, int width) {
\r
2010 String str = Integer.toString(num);
\r
2011 int len = str.length();
\r
2012 if (len >= width) {
\r
2013 return str.substring(len - width, len);
\r
2015 StringBuilder sb = new StringBuilder(width);
\r
2016 for (int i = len; i < width; i++) {
\r
2020 return sb.toString();
\r