2 *******************************************************************************
\r
3 * Copyright (C) 2007-2008, 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.Date;
\r
14 import java.util.Iterator;
\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
20 import com.ibm.icu.util.DateTimeRule;
\r
23 * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE. You can create a
\r
24 * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>.
\r
25 * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule
\r
26 * in RFC2445 VTIMEZONE format. Also, you can create a <code>VTimeZone</code> instance
\r
27 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time
\r
28 * zone offset by the rules defined by the data.<br><br>
\r
30 * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to
\r
31 * decode or encode Non-ASCII text. Methods reading/writing VTIMEZONE data in this class
\r
32 * do nothing with MIME encoding.
\r
36 public class VTimeZone extends BasicTimeZone {
\r
38 private static final long serialVersionUID = -6851467294127795902L;
\r
41 * Create a <code>VTimeZone</code> instance by the time zone ID.
\r
43 * @param tzid The time zone ID, such as America/New_York
\r
44 * @return A <code>VTimeZone</code> initialized by the time zone ID, or null
\r
45 * when the ID is unknown.
\r
49 public static VTimeZone create(String tzid) {
\r
50 VTimeZone vtz = new VTimeZone();
\r
51 vtz.tz = (BasicTimeZone)TimeZone.getTimeZone(tzid, TimeZone.TIMEZONE_ICU);
\r
52 vtz.olsonzid = vtz.tz.getID();
\r
59 * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data.
\r
61 * @param reader The Reader for VTIMEZONE data input stream
\r
62 * @return A <code>VTimeZone</code> initialized by the VTIMEZONE data or
\r
63 * null if failed to load the rule from the VTIMEZONE data.
\r
67 public static VTimeZone create(Reader reader) {
\r
68 VTimeZone vtz = new VTimeZone();
\r
69 if (vtz.load(reader)) {
\r
79 public int getOffset(int era, int year, int month, int day, int dayOfWeek,
\r
81 return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
\r
88 public void getOffset(long date, boolean local, int[] offsets) {
\r
89 tz.getOffset(date, local, offsets);
\r
95 * @deprecated This API is ICU internal only.
\r
97 public void getOffsetFromLocal(long date,
\r
98 int nonExistingTimeOpt, int duplicatedTimeOpt, int[] offsets) {
\r
99 tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
\r
106 public int getRawOffset() {
\r
107 return tz.getRawOffset();
\r
114 public boolean inDaylightTime(Date date) {
\r
115 return tz.inDaylightTime(date);
\r
122 public void setRawOffset(int offsetMillis) {
\r
123 tz.setRawOffset(offsetMillis);
\r
130 public boolean useDaylightTime() {
\r
131 return tz.useDaylightTime();
\r
138 public boolean hasSameRules(TimeZone other) {
\r
139 return tz.hasSameRules(other);
\r
143 * Gets the RFC2445 TZURL property value. When a <code>VTimeZone</code> instance was created from
\r
144 * VTIMEZONE data, the value is set by the TZURL property value in the data. Otherwise,
\r
145 * the initial value is null.
\r
147 * @return The RFC2445 TZURL property value
\r
151 public String getTZURL() {
\r
156 * Sets the RFC2445 TZURL property value.
\r
158 * @param url The TZURL property value.
\r
162 public void setTZURL(String url) {
\r
167 * Gets the RFC2445 LAST-MODIFIED property value. When a <code>VTimeZone</code> instance was created
\r
168 * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data.
\r
169 * Otherwise, the initial value is null.
\r
171 * @return The Date represents the RFC2445 LAST-MODIFIED date.
\r
175 public Date getLastModified() {
\r
180 * Sets the date used for RFC2445 LAST-MODIFIED property value.
\r
182 * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value.
\r
186 public void setLastModified(Date date) {
\r
191 * Writes RFC2445 VTIMEZONE data for this time zone
\r
193 * @param writer A <code>Writer</code> used for the output
\r
194 * @throws IOException
\r
198 public void write(Writer writer) throws IOException {
\r
199 BufferedWriter bw = new BufferedWriter(writer);
\r
200 if (vtzlines != null) {
\r
201 Iterator it = vtzlines.iterator();
\r
202 while (it.hasNext()) {
\r
203 String line = (String)it.next();
\r
204 if (line.startsWith(ICAL_TZURL + COLON)) {
\r
205 if (tzurl != null) {
\r
206 bw.write(ICAL_TZURL);
\r
211 } else if (line.startsWith(ICAL_LASTMOD + COLON)) {
\r
212 if (lastmod != null) {
\r
213 bw.write(ICAL_LASTMOD);
\r
215 bw.write(getUTCDateTimeString(lastmod.getTime()));
\r
225 String[] customProperties = null;
\r
226 if (olsonzid != null && ICU_TZVERSION != null) {
\r
227 customProperties = new String[1];
\r
228 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]";
\r
230 writeZone(writer, tz, customProperties);
\r
235 * Writes RFC2445 VTIMEZONE data applicable for dates after
\r
236 * the specified start time.
\r
238 * @param writer The <code>Writer</code> used for the output
\r
239 * @param start The start time
\r
241 * @throws IOException
\r
245 public void write(Writer writer, long start) throws IOException {
\r
246 // Extract rules applicable to dates after the start time
\r
247 TimeZoneRule[] rules = tz.getTimeZoneRules(start);
\r
249 // Create a RuleBasedTimeZone with the subset rule
\r
250 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
\r
251 for (int i = 1; i < rules.length; i++) {
\r
252 rbtz.addTransitionRule(rules[i]);
\r
254 String[] customProperties = null;
\r
255 if (olsonzid != null && ICU_TZVERSION != null) {
\r
256 customProperties = new String[1];
\r
257 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
\r
258 "/Partial@" + start + "]";
\r
260 writeZone(writer, rbtz, customProperties);
\r
264 * Writes RFC2445 VTIMEZONE data applicable near the specified date.
\r
265 * Some common iCalendar implementations can only handle a single time
\r
266 * zone property or a pair of standard and daylight time properties using
\r
267 * BYDAY rule with day of week (such as BYDAY=1SUN). This method produce
\r
268 * the VTIMEZONE data which can be handled these implementations. The rules
\r
269 * produced by this method can be used only for calculating time zone offset
\r
270 * around the specified date.
\r
272 * @param writer The <code>Writer</code> used for the output
\r
273 * @param time The date
\r
275 * @throws IOException
\r
279 public void writeSimple(Writer writer, long time) throws IOException {
\r
280 // Extract simple rules
\r
281 TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time);
\r
283 // Create a RuleBasedTimeZone with the subset rule
\r
284 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
\r
285 for (int i = 1; i < rules.length; i++) {
\r
286 rbtz.addTransitionRule(rules[i]);
\r
288 String[] customProperties = null;
\r
289 if (olsonzid != null && ICU_TZVERSION != null) {
\r
290 customProperties = new String[1];
\r
291 customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
\r
292 "/Simple@" + time + "]";
\r
294 writeZone(writer, rbtz, customProperties);
\r
297 // BasicTimeZone methods
\r
303 public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
\r
304 return tz.getNextTransition(base, inclusive);
\r
311 public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
\r
312 return tz.getPreviousTransition(base, inclusive);
\r
319 public boolean hasEquivalentTransitions(TimeZone other, long start, long end) {
\r
320 return tz.hasEquivalentTransitions(other, start, end);
\r
327 public TimeZoneRule[] getTimeZoneRules() {
\r
328 return tz.getTimeZoneRules();
\r
335 public TimeZoneRule[] getTimeZoneRules(long start) {
\r
336 return tz.getTimeZoneRules(start);
\r
343 public Object clone() {
\r
344 VTimeZone other = (VTimeZone)super.clone();
\r
345 other.tz = (BasicTimeZone)tz.clone();
\r
349 // private stuff ------------------------------------------------------
\r
351 private BasicTimeZone tz;
\r
352 private List vtzlines;
\r
353 private String olsonzid = null;
\r
354 private String tzurl = null;
\r
355 private Date lastmod = null;
\r
357 private static String ICU_TZVERSION;
\r
358 private static final String ICU_TZINFO_PROP = "X-TZINFO";
\r
360 // Default DST savings
\r
361 private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour
\r
363 // Default time start
\r
364 private static final long DEF_TZSTARTTIME = 0;
\r
367 private static final long MIN_TIME = Long.MIN_VALUE;
\r
368 private static final long MAX_TIME = Long.MAX_VALUE;
\r
370 // Symbol characters used by RFC2445 VTIMEZONE
\r
371 private static final String COLON = ":";
\r
372 private static final String SEMICOLON = ";";
\r
373 private static final String EQUALS_SIGN = "=";
\r
374 private static final String COMMA = ",";
\r
375 private static final String NEWLINE = "\r\n"; // CRLF
\r
377 // RFC2445 VTIMEZONE tokens
\r
378 private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE";
\r
379 private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE";
\r
380 private static final String ICAL_BEGIN = "BEGIN";
\r
381 private static final String ICAL_END = "END";
\r
382 private static final String ICAL_VTIMEZONE = "VTIMEZONE";
\r
383 private static final String ICAL_TZID = "TZID";
\r
384 private static final String ICAL_STANDARD = "STANDARD";
\r
385 private static final String ICAL_DAYLIGHT = "DAYLIGHT";
\r
386 private static final String ICAL_DTSTART = "DTSTART";
\r
387 private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM";
\r
388 private static final String ICAL_TZOFFSETTO = "TZOFFSETTO";
\r
389 private static final String ICAL_RDATE = "RDATE";
\r
390 private static final String ICAL_RRULE = "RRULE";
\r
391 private static final String ICAL_TZNAME = "TZNAME";
\r
392 private static final String ICAL_TZURL = "TZURL";
\r
393 private static final String ICAL_LASTMOD = "LAST-MODIFIED";
\r
395 private static final String ICAL_FREQ = "FREQ";
\r
396 private static final String ICAL_UNTIL = "UNTIL";
\r
397 private static final String ICAL_YEARLY = "YEARLY";
\r
398 private static final String ICAL_BYMONTH = "BYMONTH";
\r
399 private static final String ICAL_BYDAY = "BYDAY";
\r
400 private static final String ICAL_BYMONTHDAY = "BYMONTHDAY";
\r
402 private static final String[] ICAL_DOW_NAMES =
\r
403 {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
\r
405 // Month length in regular year
\r
406 private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
\r
409 // Initialize ICU_TZVERSION
\r
411 UResourceBundle tzbundle = UResourceBundle.getBundleInstance(
\r
412 "com/ibm/icu/impl/data/icudt" + VersionInfo.ICU_DATA_VERSION, "zoneinfo");
\r
413 ICU_TZVERSION = tzbundle.getString("TZVersion");
\r
414 } catch (MissingResourceException e) {
\r
416 ICU_TZVERSION = null;
\r
421 /* Hide the constructor */
\r
422 private VTimeZone() {
\r
426 * Read the input stream to locate the VTIMEZONE block and
\r
427 * parse the contents to initialize this VTimeZone object.
\r
428 * The reader skips other RFC2445 message headers. After
\r
429 * the parse is completed, the reader points at the beginning
\r
430 * of the header field just after the end of VTIMEZONE block.
\r
431 * When VTIMEZONE block is found and this object is successfully
\r
432 * initialized by the rules described in the data, this method
\r
433 * returns true. Otherwise, returns false.
\r
435 private boolean load(Reader reader) {
\r
436 // Read VTIMEZONE block into string array
\r
438 vtzlines = new LinkedList();
\r
439 boolean eol = false;
\r
440 boolean start = false;
\r
441 boolean success = false;
\r
442 StringBuffer line = new StringBuffer();
\r
444 int ch = reader.read();
\r
447 if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) {
\r
448 vtzlines.add(line.toString());
\r
454 // CR, must be followed by LF by the definition in RFC2445
\r
459 if (ch != 0x09 && ch != 0x20) {
\r
460 // NOT followed by TAB/SP -> new line
\r
462 if (line.length() > 0) {
\r
463 vtzlines.add(line.toString());
\r
468 line.append((char)ch);
\r
477 if (line.toString().startsWith(ICAL_END_VTIMEZONE)) {
\r
478 vtzlines.add(line.toString());
\r
483 if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) {
\r
484 vtzlines.add(line.toString());
\r
491 line.append((char)ch);
\r
498 } catch (IOException ioe) {
\r
507 private static final int INI = 0; // Initial state
\r
508 private static final int VTZ = 1; // In VTIMEZONE
\r
509 private static final int TZI = 2; // In STANDARD or DAYLIGHT
\r
510 private static final int ERR = 3; // Error state
\r
513 * Parse VTIMEZONE data and create a RuleBasedTimeZone
\r
515 private boolean parse() {
\r
517 if (vtzlines == null || vtzlines.size() == 0) {
\r
523 String tzid = null;
\r
526 boolean dst = false; // current zone type
\r
527 String from = null; // current zone from offset
\r
528 String to = null; // current zone offset
\r
529 String tzname = null; // current zone name
\r
530 String dtstart = null; // current zone starts
\r
531 boolean isRRULE = false;// true if the rule is described by RRULE
\r
532 List dates = null; // list of RDATE or RRULE strings
\r
533 List rules = new LinkedList(); // rule list
\r
534 int initialRawOffset = 0; // initial offset
\r
535 int initialDSTSavings = 0; // initial offset
\r
536 long firstStart = MAX_TIME; // the earliest rule start time
\r
538 Iterator it = vtzlines.iterator();
\r
540 while (it.hasNext()) {
\r
541 String line = (String)it.next();
\r
543 int valueSep = line.indexOf(COLON);
\r
544 if (valueSep < 0) {
\r
547 String name = line.substring(0, valueSep);
\r
548 String value = line.substring(valueSep + 1);
\r
552 if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) {
\r
557 if (name.equals(ICAL_TZID)) {
\r
559 } else if (name.equals(ICAL_TZURL)) {
\r
561 } else if (name.equals(ICAL_LASTMOD)) {
\r
562 // Always in 'Z' format, so the offset argument for the parse method
\r
563 // can be any value.
\r
564 lastmod = new Date(parseDateTimeString(value, 0));
\r
565 } else if (name.equals(ICAL_BEGIN)) {
\r
566 boolean isDST = value.equals(ICAL_DAYLIGHT);
\r
567 if (value.equals(ICAL_STANDARD) || isDST) {
\r
568 // tzid must be ready at this point
\r
569 if (tzid == null) {
\r
573 // initialize current zone properties
\r
582 // BEGIN property other than STANDARD/DAYLIGHT
\r
583 // must not be there.
\r
587 } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) {
\r
593 if (name.equals(ICAL_DTSTART)) {
\r
595 } else if (name.equals(ICAL_TZNAME)) {
\r
597 } else if (name.equals(ICAL_TZOFFSETFROM)) {
\r
599 } else if (name.equals(ICAL_TZOFFSETTO)) {
\r
601 } else if (name.equals(ICAL_RDATE)) {
\r
602 // RDATE mixed with RRULE is not supported
\r
607 if (dates == null) {
\r
608 dates = new LinkedList();
\r
610 // RDATE value may contain multiple date delimited
\r
612 StringTokenizer st = new StringTokenizer(value, COMMA);
\r
613 while (st.hasMoreTokens()) {
\r
614 String date = st.nextToken();
\r
617 } else if (name.equals(ICAL_RRULE)) {
\r
618 // RRULE mixed with RDATE is not supported
\r
619 if (!isRRULE && dates != null) {
\r
622 } else if (dates == null) {
\r
623 dates = new LinkedList();
\r
627 } else if (name.equals(ICAL_END)) {
\r
628 // Mandatory properties
\r
629 if (dtstart == null || from == null || to == null) {
\r
633 // if tzname is not available, create one from tzid
\r
634 if (tzname == null) {
\r
635 tzname = getDefaultTZName(tzid, dst);
\r
638 // create a time zone rule
\r
639 TimeZoneRule rule = null;
\r
640 int fromOffset = 0;
\r
643 int dstSavings = 0;
\r
646 // Parse TZOFFSETFROM/TZOFFSETTO
\r
647 fromOffset = offsetStrToMillis(from);
\r
648 toOffset = offsetStrToMillis(to);
\r
651 // If daylight, use the previous offset as rawoffset if positive
\r
652 if (toOffset - fromOffset > 0) {
\r
653 rawOffset = fromOffset;
\r
654 dstSavings = toOffset - fromOffset;
\r
656 // This is rare case.. just use 1 hour DST savings
\r
657 rawOffset = toOffset - DEF_DSTSAVINGS;
\r
658 dstSavings = DEF_DSTSAVINGS;
\r
661 rawOffset = toOffset;
\r
666 start = parseDateTimeString(dtstart, fromOffset);
\r
669 Date actualStart = null;
\r
671 rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
\r
673 rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
\r
675 if (rule != null) {
\r
676 actualStart = rule.getFirstStart(fromOffset, 0);
\r
677 if (actualStart.getTime() < firstStart) {
\r
678 // save from offset information for the earliest rule
\r
679 firstStart = actualStart.getTime();
\r
680 // If this is STD, assume the time before this transtion
\r
681 // is DST when the difference is 1 hour. This might not be
\r
682 // accurate, but VTIMEZONE data does not have such info.
\r
683 if (dstSavings > 0) {
\r
684 initialRawOffset = fromOffset;
\r
685 initialDSTSavings = 0;
\r
687 if (fromOffset - toOffset == DEF_DSTSAVINGS) {
\r
688 initialRawOffset = fromOffset - DEF_DSTSAVINGS;
\r
689 initialDSTSavings = DEF_DSTSAVINGS;
\r
691 initialRawOffset = fromOffset;
\r
692 initialDSTSavings = 0;
\r
697 } catch (IllegalArgumentException iae) {
\r
698 // bad format - rule == null..
\r
701 if (rule == null) {
\r
711 if (state == ERR) {
\r
717 // Must have at least one rule
\r
718 if (rules.size() == 0) {
\r
722 // Create a initial rule
\r
723 InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false),
\r
724 initialRawOffset, initialDSTSavings);
\r
726 // Finally, create the RuleBasedTimeZone
\r
727 RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule);
\r
729 int finalRuleIdx = -1;
\r
730 int finalRuleCount = 0;
\r
731 for (int i = 0; i < rules.size(); i++) {
\r
732 TimeZoneRule r = (TimeZoneRule)rules.get(i);
\r
733 if (r instanceof AnnualTimeZoneRule) {
\r
734 if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
740 if (finalRuleCount > 2) {
\r
741 // Too many final rules
\r
745 if (finalRuleCount == 1) {
\r
746 if (rules.size() == 1) {
\r
747 // Only one final rule, only governs the initial rule,
\r
748 // which is already initialized, thus, we do not need to
\r
749 // add this transition rule
\r
752 // Normalize the final rule
\r
753 AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx);
\r
754 int tmpRaw = finalRule.getRawOffset();
\r
755 int tmpDST = finalRule.getDSTSavings();
\r
757 // Find the last non-final rule
\r
758 Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings);
\r
759 Date start = finalStart;
\r
760 for (int i = 0; i < rules.size(); i++) {
\r
761 if (finalRuleIdx == i) {
\r
764 TimeZoneRule r = (TimeZoneRule)rules.get(i);
\r
765 Date lastStart = r.getFinalStart(tmpRaw, tmpDST);
\r
766 if (lastStart.after(start)) {
\r
767 start = finalRule.getNextStart(lastStart.getTime(),
\r
773 TimeZoneRule newRule;
\r
774 if (start == finalStart) {
\r
775 // Transform this into a single transition
\r
776 newRule = new TimeArrayTimeZoneRule(
\r
777 finalRule.getName(),
\r
778 finalRule.getRawOffset(),
\r
779 finalRule.getDSTSavings(),
\r
780 new long[] {finalStart.getTime()},
\r
781 DateTimeRule.UTC_TIME);
\r
783 // Update the end year
\r
784 int fields[] = Grego.timeToFields(start.getTime(), null);
\r
785 newRule = new AnnualTimeZoneRule(
\r
786 finalRule.getName(),
\r
787 finalRule.getRawOffset(),
\r
788 finalRule.getDSTSavings(),
\r
789 finalRule.getRule(),
\r
790 finalRule.getStartYear(),
\r
793 rules.set(finalRuleIdx, newRule);
\r
797 Iterator rit = rules.iterator();
\r
798 while(rit.hasNext()) {
\r
799 rbtz.addTransitionRule((TimeZoneRule)rit.next());
\r
807 * Create a default TZNAME from TZID
\r
809 private static String getDefaultTZName(String tzid, boolean isDST) {
\r
811 return tzid + "(DST)";
\r
813 return tzid + "(STD)";
\r
817 * Create a TimeZoneRule by the RRULE definition
\r
819 private static TimeZoneRule createRuleByRRULE(String tzname,
\r
820 int rawOffset, int dstSavings, long start, List dates, int fromOffset) {
\r
821 if (dates == null || dates.size() == 0) {
\r
824 // Parse the first rule
\r
825 String rrule = (String)dates.get(0);
\r
827 long until[] = new long[1];
\r
828 int[] ruleFields = parseRRULE(rrule, until);
\r
829 if (ruleFields == null) {
\r
834 int month = ruleFields[0];
\r
835 int dayOfWeek = ruleFields[1];
\r
836 int nthDayOfWeek = ruleFields[2];
\r
837 int dayOfMonth = ruleFields[3];
\r
839 if (dates.size() == 1) {
\r
841 if (ruleFields.length > 4) {
\r
842 // Multiple BYMONTHDAY values
\r
844 if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) {
\r
845 // Only support the rule using 7 continuous days
\r
846 // BYMONTH and BYDAY must be set at the same time
\r
849 int firstDay = 31; // max possible number of dates in a month
\r
850 int days[] = new int[7];
\r
851 for (int i = 0; i < 7; i++) {
\r
852 days[i] = ruleFields[3 + i];
\r
853 // Resolve negative day numbers. A negative day number should
\r
854 // not be used in February, but if we see such case, we use 28
\r
856 days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1;
\r
857 firstDay = days[i] < firstDay ? days[i] : firstDay;
\r
859 // Make sure days are continuous
\r
860 for (int i = 1; i < 7; i++) {
\r
861 boolean found = false;
\r
862 for (int j = 0; j < 7; j++) {
\r
863 if (days[j] == firstDay + i) {
\r
869 // days are not continuous
\r
873 // Use DOW_GEQ_DOM rule with firstDay as the start date
\r
874 dayOfMonth = firstDay;
\r
877 // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines.
\r
878 // Otherwise, not supported.
\r
879 if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) {
\r
880 // This is not the case
\r
883 // Parse the rest of rules if number of rules is not exceeding 7.
\r
884 // We can only support 7 continuous days starting from a day of month.
\r
885 if (dates.size() > 7) {
\r
889 // Note: To check valid date range across multiple rule is a little
\r
890 // bit complicated. For now, this code is not doing strict range
\r
891 // checking across month boundary
\r
893 int earliestMonth = month;
\r
894 int daysCount = ruleFields.length - 3;
\r
895 int earliestDay = 31;
\r
896 for (int i = 0; i < daysCount; i++) {
\r
897 int dom = ruleFields[3 + i];
\r
898 dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1;
\r
899 earliestDay = dom < earliestDay ? dom : earliestDay;
\r
902 int anotherMonth = -1;
\r
903 for (int i = 1; i < dates.size(); i++) {
\r
904 rrule = (String)dates.get(i);
\r
905 long[] unt = new long[1];
\r
906 int[] fields = parseRRULE(rrule, unt);
\r
908 // If UNTIL is newer than previous one, use the one
\r
909 if (unt[0] > until[0]) {
\r
913 // Check if BYMONTH + BYMONTHDAY + BYDAY rule
\r
914 if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) {
\r
917 // Count number of BYMONTHDAY
\r
918 int count = fields.length - 3;
\r
919 if (daysCount + count > 7) {
\r
920 // We cannot support BYMONTHDAY more than 7
\r
923 // Check if the same BYDAY is used. Otherwise, we cannot
\r
924 // support the rule
\r
925 if (fields[1] != dayOfWeek) {
\r
928 // Check if the month is same or right next to the primary month
\r
929 if (fields[0] != month) {
\r
930 if (anotherMonth == -1) {
\r
931 int diff = fields[0] - month;
\r
932 if (diff == -11 || diff == -1) {
\r
934 anotherMonth = fields[0];
\r
935 earliestMonth = anotherMonth;
\r
936 // Reset earliest day
\r
938 } else if (diff == 11 || diff == 1) {
\r
940 anotherMonth = fields[0];
\r
942 // The day range cannot exceed more than 2 months
\r
945 } else if (fields[0] != month && fields[0] != anotherMonth) {
\r
946 // The day range cannot exceed more than 2 months
\r
950 // If ealier month, go through days to find the earliest day
\r
951 if (fields[0] == earliestMonth) {
\r
952 for (int j = 0; j < count; j++) {
\r
953 int dom = fields[3 + j];
\r
954 dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1;
\r
955 earliestDay = dom < earliestDay ? dom : earliestDay;
\r
958 daysCount += count;
\r
960 if (daysCount != 7) {
\r
961 // Number of BYMONTHDAY entries must be 7
\r
964 month = earliestMonth;
\r
965 dayOfMonth = earliestDay;
\r
968 // Calculate start/end year and missing fields
\r
969 int[] dfields = Grego.timeToFields(start + fromOffset, null);
\r
970 int startYear = dfields[0];
\r
972 // If MYMONTH is not set, use the month of DTSTART
\r
973 month = dfields[1];
\r
975 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) {
\r
976 // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY
\r
977 dayOfMonth = dfields[2];
\r
979 int timeInDay = dfields[5];
\r
981 int endYear = AnnualTimeZoneRule.MAX_YEAR;
\r
982 if (until[0] != MIN_TIME) {
\r
983 Grego.timeToFields(until[0], dfields);
\r
984 endYear = dfields[0];
\r
987 // Create the AnnualDateTimeRule
\r
988 DateTimeRule adtr = null;
\r
989 if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
\r
990 // Day in month rule, for example, 15th day in the month
\r
991 adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME);
\r
992 } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) {
\r
993 // Nth day of week rule, for example, last Sunday
\r
994 adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME);
\r
995 } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
\r
996 // First day of week after day of month rule, for example,
\r
997 // first Sunday after 15th day in the month
\r
998 adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME);
\r
1000 // RRULE attributes are insufficient
\r
1004 return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear);
\r
1008 * Parse individual RRULE
\r
1012 * int[0] month calculated by BYMONTH - 1, or -1 when not found
\r
1013 * int[1] day of week in BYDAY, or 0 when not found
\r
1014 * int[2] day of week ordinal number in BYDAY, or 0 when not found
\r
1015 * int[i >= 3] day of month, which could be multiple values, or 0 when not found
\r
1019 * null on any error cases, for exmaple, FREQ=YEARLY is not available
\r
1021 * When UNTIL attribute is available, the time will be set to until[0],
\r
1022 * otherwise, MIN_TIME
\r
1024 private static int[] parseRRULE(String rrule, long[] until) {
\r
1026 int dayOfWeek = 0;
\r
1027 int nthDayOfWeek = 0;
\r
1028 int[] dayOfMonth = null;
\r
1030 long untilTime = MIN_TIME;
\r
1031 boolean yearly = false;
\r
1032 boolean parseError = false;
\r
1033 StringTokenizer st= new StringTokenizer(rrule, SEMICOLON);
\r
1035 while (st.hasMoreTokens()) {
\r
1036 String attr, value;
\r
1037 String prop = st.nextToken();
\r
1038 int sep = prop.indexOf(EQUALS_SIGN);
\r
1040 attr = prop.substring(0, sep);
\r
1041 value = prop.substring(sep + 1);
\r
1043 parseError = true;
\r
1047 if (attr.equals(ICAL_FREQ)) {
\r
1048 // only support YEARLY frequency type
\r
1049 if (value.equals(ICAL_YEARLY)) {
\r
1052 parseError = true;
\r
1055 } else if (attr.equals(ICAL_UNTIL)) {
\r
1056 // ISO8601 UTC format, for example, "20060315T020000Z"
\r
1058 untilTime = parseDateTimeString(value, 0);
\r
1059 } catch (IllegalArgumentException iae) {
\r
1060 parseError = true;
\r
1063 } else if (attr.equals(ICAL_BYMONTH)) {
\r
1064 // Note: BYMONTH may contain multiple months, but only single month make sense for
\r
1065 // VTIMEZONE property.
\r
1066 if (value.length() > 2) {
\r
1067 parseError = true;
\r
1071 month = Integer.parseInt(value) - 1;
\r
1072 if (month < 0 || month >= 12) {
\r
1073 parseError = true;
\r
1076 } catch (NumberFormatException nfe) {
\r
1077 parseError = true;
\r
1080 } else if (attr.equals(ICAL_BYDAY)) {
\r
1081 // Note: BYDAY may contain multiple day of week separated by comma. It is unlikely used for
\r
1082 // VTIMEZONE property. We do not support the case.
\r
1084 // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday
\r
1085 // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday
\r
1086 int length = value.length();
\r
1087 if (length < 2 || length > 4) {
\r
1088 parseError = true;
\r
1092 // Nth day of week
\r
1094 if (value.charAt(0) == '+') {
\r
1096 } else if (value.charAt(0) == '-') {
\r
1098 } else if (length == 4) {
\r
1099 parseError = true;
\r
1103 int n = Integer.parseInt(value.substring(length - 3, length - 2));
\r
1104 if (n == 0 || n > 4) {
\r
1105 parseError = true;
\r
1108 nthDayOfWeek = n * sign;
\r
1109 } catch(NumberFormatException nfe) {
\r
1110 parseError = true;
\r
1113 value = value.substring(length - 2);
\r
1116 for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) {
\r
1117 if (value.equals(ICAL_DOW_NAMES[wday])) {
\r
1121 if (wday < ICAL_DOW_NAMES.length) {
\r
1122 // Sunday(1) - Saturday(7)
\r
1123 dayOfWeek = wday + 1;
\r
1125 parseError = true;
\r
1128 } else if (attr.equals(ICAL_BYMONTHDAY)) {
\r
1129 // Note: BYMONTHDAY may contain multiple days delimited by comma
\r
1131 // A value of BYMONTHDAY could be negative, for example, -1 means
\r
1132 // the last day in a month
\r
1133 StringTokenizer days = new StringTokenizer(value, COMMA);
\r
1134 int count = days.countTokens();
\r
1135 dayOfMonth = new int[count];
\r
1137 while(days.hasMoreTokens()) {
\r
1139 dayOfMonth[index++] = Integer.parseInt(days.nextToken());
\r
1140 } catch (NumberFormatException nfe) {
\r
1141 parseError = true;
\r
1152 // FREQ=YEARLY must be set
\r
1156 until[0] = untilTime;
\r
1159 if (dayOfMonth == null) {
\r
1160 results = new int[4];
\r
1163 results = new int[3 + dayOfMonth.length];
\r
1164 for (int i = 0; i < dayOfMonth.length; i++) {
\r
1165 results[3 + i] = dayOfMonth[i];
\r
1168 results[0] = month;
\r
1169 results[1] = dayOfWeek;
\r
1170 results[2] = nthDayOfWeek;
\r
1175 * Create a TimeZoneRule by the RDATE definition
\r
1177 private static TimeZoneRule createRuleByRDATE(String tzname,
\r
1178 int rawOffset, int dstSavings, long start, List dates, int fromOffset) {
\r
1179 // Create an array of transition times
\r
1181 if (dates == null || dates.size() == 0) {
\r
1182 // When no RDATE line is provided, use start (DTSTART)
\r
1183 // as the transition time
\r
1184 times = new long[1];
\r
1187 times = new long[dates.size()];
\r
1188 Iterator it = dates.iterator();
\r
1191 while(it.hasNext()) {
\r
1192 times[idx++] = parseDateTimeString((String)it.next(), fromOffset);
\r
1194 } catch (IllegalArgumentException iae) {
\r
1198 return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME);
\r
1202 * Write the time zone rules in RFC2445 VTIMEZONE format
\r
1204 private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException {
\r
1205 // Write the header
\r
1208 if (customProperties != null && customProperties.length > 0) {
\r
1209 for (int i = 0; i < customProperties.length; i++) {
\r
1210 if (customProperties[i] != null) {
\r
1211 w.write(customProperties[i]);
\r
1217 long t = MIN_TIME;
\r
1218 String dstName = null;
\r
1219 int dstFromOffset = 0;
\r
1220 int dstFromDSTSavings = 0;
\r
1221 int dstToOffset = 0;
\r
1222 int dstStartYear = 0;
\r
1224 int dstDayOfWeek = 0;
\r
1225 int dstWeekInMonth = 0;
\r
1226 int dstMillisInDay = 0;
\r
1227 long dstStartTime = 0;
\r
1228 long dstUntilTime = 0;
\r
1230 AnnualTimeZoneRule finalDstRule = null;
\r
1232 String stdName = null;
\r
1233 int stdFromOffset = 0;
\r
1234 int stdFromDSTSavings = 0;
\r
1235 int stdToOffset = 0;
\r
1236 int stdStartYear = 0;
\r
1238 int stdDayOfWeek = 0;
\r
1239 int stdWeekInMonth = 0;
\r
1240 int stdMillisInDay = 0;
\r
1241 long stdStartTime = 0;
\r
1242 long stdUntilTime = 0;
\r
1244 AnnualTimeZoneRule finalStdRule = null;
\r
1246 int[] dtfields = new int[6];
\r
1247 boolean hasTransitions = false;
\r
1249 // Going through all transitions
\r
1251 TimeZoneTransition tzt = basictz.getNextTransition(t, false);
\r
1252 if (tzt == null) {
\r
1255 hasTransitions = true;
\r
1256 t = tzt.getTime();
\r
1257 String name = tzt.getTo().getName();
\r
1258 boolean isDst = (tzt.getTo().getDSTSavings() != 0);
\r
1259 int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
\r
1260 int fromDSTSavings = tzt.getFrom().getDSTSavings();
\r
1261 int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
\r
1262 Grego.timeToFields(tzt.getTime() + fromOffset, dtfields);
\r
1263 int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]);
\r
1264 int year = dtfields[0];
\r
1265 boolean sameRule = false;
\r
1267 if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
\r
1268 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
1269 finalDstRule = (AnnualTimeZoneRule)tzt.getTo();
\r
1272 if (dstCount > 0) {
\r
1273 if (year == dstStartYear + dstCount
\r
1274 && name.equals(dstName)
\r
1275 && dstFromOffset == fromOffset
\r
1276 && dstToOffset == toOffset
\r
1277 && dstMonth == dtfields[1]
\r
1278 && dstDayOfWeek == dtfields[3]
\r
1279 && dstWeekInMonth == weekInMonth
\r
1280 && dstMillisInDay == dtfields[5]) {
\r
1281 // Update until time
\r
1287 if (dstCount == 1) {
\r
1288 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
\r
1289 dstStartTime, true);
\r
1291 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1292 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1297 // Reset this DST information
\r
1299 dstFromOffset = fromOffset;
\r
1300 dstFromDSTSavings = fromDSTSavings;
\r
1301 dstToOffset = toOffset;
\r
1302 dstStartYear = year;
\r
1303 dstMonth = dtfields[1];
\r
1304 dstDayOfWeek = dtfields[3];
\r
1305 dstWeekInMonth = weekInMonth;
\r
1306 dstMillisInDay = dtfields[5];
\r
1307 dstStartTime = dstUntilTime = t;
\r
1310 if (finalStdRule != null && finalDstRule != null) {
\r
1314 if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
\r
1315 if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
\r
1316 finalStdRule = (AnnualTimeZoneRule)tzt.getTo();
\r
1319 if (stdCount > 0) {
\r
1320 if (year == stdStartYear + stdCount
\r
1321 && name.equals(stdName)
\r
1322 && stdFromOffset == fromOffset
\r
1323 && stdToOffset == toOffset
\r
1324 && stdMonth == dtfields[1]
\r
1325 && stdDayOfWeek == dtfields[3]
\r
1326 && stdWeekInMonth == weekInMonth
\r
1327 && stdMillisInDay == dtfields[5]) {
\r
1328 // Update until time
\r
1334 if (stdCount == 1) {
\r
1335 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
\r
1336 stdStartTime, true);
\r
1338 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1339 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1344 // Reset this STD information
\r
1346 stdFromOffset = fromOffset;
\r
1347 stdFromDSTSavings = fromDSTSavings;
\r
1348 stdToOffset = toOffset;
\r
1349 stdStartYear = year;
\r
1350 stdMonth = dtfields[1];
\r
1351 stdDayOfWeek = dtfields[3];
\r
1352 stdWeekInMonth = weekInMonth;
\r
1353 stdMillisInDay = dtfields[5];
\r
1354 stdStartTime = stdUntilTime = t;
\r
1357 if (finalStdRule != null && finalDstRule != null) {
\r
1362 if (!hasTransitions) {
\r
1363 // No transition - put a single non transition RDATE
\r
1364 int offset = basictz.getOffset(0 /* any time */);
\r
1365 boolean isDst = (offset != basictz.getRawOffset());
\r
1366 writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst),
\r
1367 offset, offset, DEF_TZSTARTTIME - offset, false);
\r
1369 if (dstCount > 0) {
\r
1370 if (finalDstRule == null) {
\r
1371 if (dstCount == 1) {
\r
1372 writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
\r
1373 dstStartTime, true);
\r
1375 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1376 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1379 if (dstCount == 1) {
\r
1380 writeFinalRule(w, true, finalDstRule,
\r
1381 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
\r
1383 // Use a single rule if possible
\r
1384 if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) {
\r
1385 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1386 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME);
\r
1388 // Not equivalent rule - write out two different rules
\r
1389 writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
\r
1390 dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
\r
1391 writeFinalRule(w, true, finalDstRule,
\r
1392 dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
\r
1397 if (stdCount > 0) {
\r
1398 if (finalStdRule == null) {
\r
1399 if (stdCount == 1) {
\r
1400 writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
\r
1401 stdStartTime, true);
\r
1403 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1404 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1407 if (stdCount == 1) {
\r
1408 writeFinalRule(w, false, finalStdRule,
\r
1409 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
\r
1411 // Use a single rule if possible
\r
1412 if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) {
\r
1413 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1414 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME);
\r
1416 // Not equivalent rule - write out two different rules
\r
1417 writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
\r
1418 stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
\r
1419 writeFinalRule(w, false, finalStdRule,
\r
1420 stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
\r
1430 * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent
\r
1431 * to the DateTimerule.
\r
1433 private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) {
\r
1434 if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) {
\r
1437 if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) {
\r
1438 // Do not try to do more intelligent comparison for now.
\r
1441 if (dtrule.getDateRuleType() == DateTimeRule.DOW
\r
1442 && dtrule.getRuleWeekInMonth() == weekInMonth) {
\r
1445 int ruleDOM = dtrule.getRuleDayOfMonth();
\r
1446 if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) {
\r
1447 if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) {
\r
1450 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6
\r
1451 && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) {
\r
1455 if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) {
\r
1456 if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) {
\r
1459 if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0
\r
1460 && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) {
\r
1468 * Write a single start time
\r
1470 private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname,
\r
1471 int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException {
\r
1472 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time);
\r
1474 writer.write(ICAL_RDATE);
\r
1475 writer.write(COLON);
\r
1476 writer.write(getDateTimeString(time + fromOffset));
\r
1477 writer.write(NEWLINE);
\r
1479 endZoneProps(writer, isDst);
\r
1483 * Write start times defined by a DOM rule using VTIMEZONE RRULE
\r
1485 private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1486 int month, int dayOfMonth, long startTime, long untilTime) throws IOException {
\r
1487 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1489 beginRRULE(writer, month);
\r
1490 writer.write(ICAL_BYMONTHDAY);
\r
1491 writer.write(EQUALS_SIGN);
\r
1492 writer.write(Integer.toString(dayOfMonth));
\r
1494 if (untilTime != MAX_TIME) {
\r
1495 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1497 writer.write(NEWLINE);
\r
1499 endZoneProps(writer, isDst);
\r
1503 * Write start times defined by a DOW rule using VTIMEZONE RRULE
\r
1505 private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1506 int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1507 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1509 beginRRULE(writer, month);
\r
1510 writer.write(ICAL_BYDAY);
\r
1511 writer.write(EQUALS_SIGN);
\r
1512 writer.write(Integer.toString(weekInMonth)); // -4, -3, -2, -1, 1, 2, 3, 4
\r
1513 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU...
\r
1515 if (untilTime != MAX_TIME) {
\r
1516 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1518 writer.write(NEWLINE);
\r
1520 endZoneProps(writer, isDst);
\r
1524 * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE
\r
1526 private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1527 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1528 // Check if this rule can be converted to DOW rule
\r
1529 if (dayOfMonth%7 == 1) {
\r
1530 // Can be represented by DOW rule
\r
1531 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1532 month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime);
\r
1533 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) {
\r
1534 // Can be represented by DOW rule with negative week number
\r
1535 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1536 month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime);
\r
1538 // Otherwise, use BYMONTHDAY to include all possible dates
\r
1539 beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);
\r
1541 // Check if all days are in the same month
\r
1542 int startDay = dayOfMonth;
\r
1543 int currentMonthDays = 7;
\r
1545 if (dayOfMonth <= 0) {
\r
1546 // The start day is in previous month
\r
1547 int prevMonthDays = 1 - dayOfMonth;
\r
1548 currentMonthDays -= prevMonthDays;
\r
1550 int prevMonth = (month - 1) < 0 ? 11 : month - 1;
\r
1552 // Note: When a rule is separated into two, UNTIL attribute needs to be
\r
1553 // calculated for each of them. For now, we skip this, because we basically use this method
\r
1554 // only for final rules, which does not have the UNTIL attribute
\r
1555 writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
\r
1557 // Start from 1 for the rest
\r
1559 } else if (dayOfMonth + 6 > MONTHLENGTH[month]) {
\r
1560 // Note: This code does not actually work well in February. For now, days in month in
\r
1562 int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month];
\r
1563 currentMonthDays -= nextMonthDays;
\r
1565 int nextMonth = (month + 1) > 11 ? 0 : month + 1;
\r
1567 writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
\r
1569 writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset);
\r
1570 endZoneProps(writer, isDst);
\r
1575 * Called from writeZonePropsByDOW_GEQ_DOM
\r
1577 private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month,
\r
1578 int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException {
\r
1580 int startDayNum = dayOfMonth;
\r
1581 boolean isFeb = (month == Calendar.FEBRUARY);
\r
1582 if (dayOfMonth < 0 && !isFeb) {
\r
1583 // Use positive number if possible
\r
1584 startDayNum = MONTHLENGTH[month] + dayOfMonth + 1;
\r
1586 beginRRULE(writer, month);
\r
1587 writer.write(ICAL_BYDAY);
\r
1588 writer.write(EQUALS_SIGN);
\r
1589 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU...
\r
1590 writer.write(SEMICOLON);
\r
1591 writer.write(ICAL_BYMONTHDAY);
\r
1592 writer.write(EQUALS_SIGN);
\r
1594 writer.write(Integer.toString(startDayNum));
\r
1595 for (int i = 1; i < numDays; i++) {
\r
1596 writer.write(COMMA);
\r
1597 writer.write(Integer.toString(startDayNum + i));
\r
1600 if (untilTime != MAX_TIME) {
\r
1601 appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
\r
1603 writer.write(NEWLINE);
\r
1607 * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE
\r
1609 private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
\r
1610 int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
\r
1611 // Check if this rule can be converted to DOW rule
\r
1612 if (dayOfMonth%7 == 0) {
\r
1613 // Can be represented by DOW rule
\r
1614 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1615 month, dayOfMonth/7, dayOfWeek, startTime, untilTime);
\r
1616 } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){
\r
1617 // Can be represented by DOW rule with negative week number
\r
1618 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1619 month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime);
\r
1620 } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) {
\r
1621 // Specical case for February
\r
1622 writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
\r
1623 Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime);
\r
1625 // Otherwise, convert this to DOW_GEQ_DOM rule
\r
1626 writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset,
\r
1627 month, dayOfMonth - 6, dayOfWeek, startTime, untilTime);
\r
1632 * Write the final time zone rule using RRULE, with no UNTIL attribute
\r
1634 private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule,
\r
1635 int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{
\r
1636 DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings);
\r
1637 int toOffset = rule.getRawOffset() + rule.getDSTSavings();
\r
1638 switch (dtrule.getDateRuleType()) {
\r
1639 case DateTimeRule.DOM:
\r
1640 writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1641 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME);
\r
1643 case DateTimeRule.DOW:
\r
1644 writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1645 dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1647 case DateTimeRule.DOW_GEQ_DOM:
\r
1648 writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1649 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1651 case DateTimeRule.DOW_LEQ_DOM:
\r
1652 writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
\r
1653 dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
\r
1659 * Convert the rule to its equivalent rule using WALL_TIME mode
\r
1661 private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) {
\r
1662 if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
\r
1665 int wallt = rule.getRuleMillisInDay();
\r
1666 if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) {
\r
1667 wallt += (rawOffset + dstSavings);
\r
1668 } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) {
\r
1669 wallt += dstSavings;
\r
1672 int month = -1, dom = 0, dow = 0, dtype = -1;
\r
1676 wallt += Grego.MILLIS_PER_DAY;
\r
1677 } else if (wallt >= Grego.MILLIS_PER_DAY) {
\r
1679 wallt -= Grego.MILLIS_PER_DAY;
\r
1682 month = rule.getRuleMonth();
\r
1683 dom = rule.getRuleDayOfMonth();
\r
1684 dow = rule.getRuleDayOfWeek();
\r
1685 dtype = rule.getDateRuleType();
\r
1687 if (dshift != 0) {
\r
1688 if (dtype == DateTimeRule.DOW) {
\r
1689 // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first
\r
1690 int wim = rule.getRuleWeekInMonth();
\r
1692 dtype = DateTimeRule.DOW_GEQ_DOM;
\r
1693 dom = 7 * (wim - 1) + 1;
\r
1695 dtype = DateTimeRule.DOW_LEQ_DOM;
\r
1696 dom = MONTHLENGTH[month] + 7 * (wim + 1);
\r
1700 // Shift one day before or after
\r
1704 month = month < Calendar.JANUARY ? Calendar.DECEMBER : month;
\r
1705 dom = MONTHLENGTH[month];
\r
1706 } else if (dom > MONTHLENGTH[month]) {
\r
1708 month = month > Calendar.DECEMBER ? Calendar.JANUARY : month;
\r
1711 if (dtype != DateTimeRule.DOM) {
\r
1712 // Adjust day of week
\r
1714 if (dow < Calendar.SUNDAY) {
\r
1715 dow = Calendar.SATURDAY;
\r
1716 } else if (dow > Calendar.SATURDAY) {
\r
1717 dow = Calendar.SUNDAY;
\r
1721 // Create a new rule
\r
1722 DateTimeRule modifiedRule;
\r
1723 if (dtype == DateTimeRule.DOM) {
\r
1724 modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME);
\r
1726 modifiedRule = new DateTimeRule(month, dom, dow,
\r
1727 (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME);
\r
1729 return modifiedRule;
\r
1733 * Write the opening section of zone properties
\r
1735 private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException {
\r
1736 writer.write(ICAL_BEGIN);
\r
1737 writer.write(COLON);
\r
1739 writer.write(ICAL_DAYLIGHT);
\r
1741 writer.write(ICAL_STANDARD);
\r
1743 writer.write(NEWLINE);
\r
1746 writer.write(ICAL_TZOFFSETTO);
\r
1747 writer.write(COLON);
\r
1748 writer.write(millisToOffset(toOffset));
\r
1749 writer.write(NEWLINE);
\r
1752 writer.write(ICAL_TZOFFSETFROM);
\r
1753 writer.write(COLON);
\r
1754 writer.write(millisToOffset(fromOffset));
\r
1755 writer.write(NEWLINE);
\r
1758 writer.write(ICAL_TZNAME);
\r
1759 writer.write(COLON);
\r
1760 writer.write(tzname);
\r
1761 writer.write(NEWLINE);
\r
1764 writer.write(ICAL_DTSTART);
\r
1765 writer.write(COLON);
\r
1766 writer.write(getDateTimeString(startTime + fromOffset));
\r
1767 writer.write(NEWLINE);
\r
1771 * Writes the closing section of zone properties
\r
1773 private static void endZoneProps(Writer writer, boolean isDst) throws IOException{
\r
1774 // END:STANDARD or END:DAYLIGHT
\r
1775 writer.write(ICAL_END);
\r
1776 writer.write(COLON);
\r
1778 writer.write(ICAL_DAYLIGHT);
\r
1780 writer.write(ICAL_STANDARD);
\r
1782 writer.write(NEWLINE);
\r
1786 * Write the beginning part of RRULE line
\r
1788 private static void beginRRULE(Writer writer, int month) throws IOException {
\r
1789 writer.write(ICAL_RRULE);
\r
1790 writer.write(COLON);
\r
1791 writer.write(ICAL_FREQ);
\r
1792 writer.write(EQUALS_SIGN);
\r
1793 writer.write(ICAL_YEARLY);
\r
1794 writer.write(SEMICOLON);
\r
1795 writer.write(ICAL_BYMONTH);
\r
1796 writer.write(EQUALS_SIGN);
\r
1797 writer.write(Integer.toString(month + 1));
\r
1798 writer.write(SEMICOLON);
\r
1802 * Append the UNTIL attribute after RRULE line
\r
1804 private static void appendUNTIL(Writer writer, String until) throws IOException {
\r
1805 if (until != null) {
\r
1806 writer.write(SEMICOLON);
\r
1807 writer.write(ICAL_UNTIL);
\r
1808 writer.write(EQUALS_SIGN);
\r
1809 writer.write(until);
\r
1814 * Write the opening section of the VTIMEZONE block
\r
1816 private void writeHeader(Writer writer)throws IOException {
\r
1817 writer.write(ICAL_BEGIN);
\r
1818 writer.write(COLON);
\r
1819 writer.write(ICAL_VTIMEZONE);
\r
1820 writer.write(NEWLINE);
\r
1821 writer.write(ICAL_TZID);
\r
1822 writer.write(COLON);
\r
1823 writer.write(tz.getID());
\r
1824 writer.write(NEWLINE);
\r
1825 if (tzurl != null) {
\r
1826 writer.write(ICAL_TZURL);
\r
1827 writer.write(COLON);
\r
1828 writer.write(tzurl);
\r
1829 writer.write(NEWLINE);
\r
1831 if (lastmod != null) {
\r
1832 writer.write(ICAL_LASTMOD);
\r
1833 writer.write(COLON);
\r
1834 writer.write(getUTCDateTimeString(lastmod.getTime()));
\r
1835 writer.write(NEWLINE);
\r
1840 * Write the closing section of the VTIMEZONE definition block
\r
1842 private static void writeFooter(Writer writer) throws IOException {
\r
1843 writer.write(ICAL_END);
\r
1844 writer.write(COLON);
\r
1845 writer.write(ICAL_VTIMEZONE);
\r
1846 writer.write(NEWLINE);
\r
1850 * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME
\r
1852 private static String getDateTimeString(long time) {
\r
1853 int[] fields = Grego.timeToFields(time, null);
\r
1854 StringBuffer sb = new StringBuffer(15);
\r
1855 sb.append(numToString(fields[0], 4));
\r
1856 sb.append(numToString(fields[1] + 1, 2));
\r
1857 sb.append(numToString(fields[2], 2));
\r
1860 int t = fields[5];
\r
1861 int hour = t / Grego.MILLIS_PER_HOUR;
\r
1862 t %= Grego.MILLIS_PER_HOUR;
\r
1863 int min = t / Grego.MILLIS_PER_MINUTE;
\r
1864 t %= Grego.MILLIS_PER_MINUTE;
\r
1865 int sec = t / Grego.MILLIS_PER_SECOND;
\r
1867 sb.append(numToString(hour, 2));
\r
1868 sb.append(numToString(min, 2));
\r
1869 sb.append(numToString(sec, 2));
\r
1870 return sb.toString();
\r
1874 * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME
\r
1876 private static String getUTCDateTimeString(long time) {
\r
1877 return getDateTimeString(time) + "Z";
\r
1881 * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and
\r
1882 * #2 DATE WITH UTC TIME
\r
1884 private static long parseDateTimeString(String str, int offset) {
\r
1885 int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
\r
1886 boolean isUTC = false;
\r
1887 boolean isValid = false;
\r
1889 if (str == null) {
\r
1893 int length = str.length();
\r
1894 if (length != 15 && length != 16) {
\r
1895 // FORM#1 15 characters, such as "20060317T142115"
\r
1896 // FORM#2 16 characters, such as "20060317T142115Z"
\r
1899 if (str.charAt(8) != 'T') {
\r
1900 // charcter "T" must be used for separating date and time
\r
1903 if (length == 16) {
\r
1904 if (str.charAt(15) != 'Z') {
\r
1912 year = Integer.parseInt(str.substring(0, 4));
\r
1913 month = Integer.parseInt(str.substring(4, 6)) - 1; // 0-based
\r
1914 day = Integer.parseInt(str.substring(6, 8));
\r
1915 hour = Integer.parseInt(str.substring(9, 11));
\r
1916 min = Integer.parseInt(str.substring(11, 13));
\r
1917 sec = Integer.parseInt(str.substring(13, 15));
\r
1918 } catch (NumberFormatException nfe) {
\r
1922 // check valid range
\r
1923 int maxDayOfMonth = Grego.monthLength(year, month);
\r
1924 if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth ||
\r
1925 hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) {
\r
1933 throw new IllegalArgumentException("Invalid date time string format");
\r
1935 // Calculate the time
\r
1936 long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY;
\r
1937 time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND);
\r
1945 * Convert RFC2445 utc-offset string to milliseconds
\r
1947 private static int offsetStrToMillis(String str) {
\r
1948 boolean isValid = false;
\r
1949 int sign = 0, hour = 0, min = 0, sec = 0;
\r
1952 if (str == null) {
\r
1955 int length = str.length();
\r
1956 if (length != 5 && length != 7) {
\r
1957 // utf-offset must be 5 or 7 characters
\r
1961 char s = str.charAt(0);
\r
1964 } else if (s == '-') {
\r
1967 // utf-offset must start with "+" or "-"
\r
1972 hour = Integer.parseInt(str.substring(1, 3));
\r
1973 min = Integer.parseInt(str.substring(3, 5));
\r
1974 if (length == 7) {
\r
1975 sec = Integer.parseInt(str.substring(5, 7));
\r
1977 } catch (NumberFormatException nfe) {
\r
1984 throw new IllegalArgumentException("Bad offset string");
\r
1986 int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000;
\r
1991 * Convert milliseconds to RFC2445 utc-offset string
\r
1993 private static String millisToOffset(int millis) {
\r
1994 StringBuffer sb = new StringBuffer(7);
\r
1995 if (millis >= 0) {
\r
2001 int hour, min, sec;
\r
2002 int t = millis / 1000;
\r
2005 t = (t - sec) / 60;
\r
2009 sb.append(numToString(hour, 2));
\r
2010 sb.append(numToString(min, 2));
\r
2011 sb.append(numToString(sec, 2));
\r
2013 return sb.toString();
\r
2017 * Format integer number
\r
2019 private static String numToString(int num, int width) {
\r
2020 String str = Integer.toString(num);
\r
2021 int len = str.length();
\r
2022 if (len >= width) {
\r
2023 return str.substring(len - width, len);
\r
2025 StringBuffer sb = new StringBuffer(width);
\r
2026 for (int i = len; i < width; i++) {
\r
2030 return sb.toString();
\r