All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.util.VTimeZone Maven / Gradle / Ivy

Go to download

International Component for Unicode for Java (ICU4J) is a mature, widely used Java library providing Unicode and Globalization support

There is a newer version: 76.1
Show newest version
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2007-2015, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.util;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.StringTokenizer;

import com.ibm.icu.impl.Grego;

/**
 * VTimeZone is a class implementing RFC2445 VTIMEZONE.  You can create a
 * VTimeZone instance from a time zone ID supported by TimeZone.
 * With the VTimeZone instance created from the ID, you can write out the rule
 * in RFC2445 VTIMEZONE format.  Also, you can create a VTimeZone instance
 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time
 * zone offset by the rules defined by the data.

* * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to * decode or encode Non-ASCII text. Methods reading/writing VTIMEZONE data in this class * do nothing with MIME encoding. * * @stable ICU 3.8 */ public class VTimeZone extends BasicTimeZone { private static final long serialVersionUID = -6851467294127795902L; /** * Create a VTimeZone instance by the time zone ID. * * @param tzid The time zone ID, such as America/New_York * @return A VTimeZone initialized by the time zone ID, or null * when the ID is unknown. * * @stable ICU 3.8 */ public static VTimeZone create(String tzid) { BasicTimeZone basicTimeZone = TimeZone.getFrozenICUTimeZone(tzid, true); if (basicTimeZone == null) { return null; } VTimeZone vtz = new VTimeZone(tzid); vtz.tz = (BasicTimeZone) basicTimeZone.cloneAsThawed(); vtz.olsonzid = vtz.tz.getID(); return vtz; } /** * Create a VTimeZone instance by RFC2445 VTIMEZONE data. * * @param reader The Reader for VTIMEZONE data input stream * @return A VTimeZone initialized by the VTIMEZONE data or * null if failed to load the rule from the VTIMEZONE data. * * @stable ICU 3.8 */ public static VTimeZone create(Reader reader) { VTimeZone vtz = new VTimeZone(); if (vtz.load(reader)) { return vtz; } return null; } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) { return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public void getOffset(long date, boolean local, int[] offsets) { tz.getOffset(date, local, offsets); } /** * {@inheritDoc} * @stable ICU 69 */ @Override public void getOffsetFromLocal(long date, LocalOption nonExistingTimeOpt, LocalOption duplicatedTimeOpt, int[] offsets) { tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public int getRawOffset() { return tz.getRawOffset(); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public boolean inDaylightTime(Date date) { return tz.inDaylightTime(date); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public void setRawOffset(int offsetMillis) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); } tz.setRawOffset(offsetMillis); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public boolean useDaylightTime() { return tz.useDaylightTime(); } /** * {@inheritDoc} * @stable ICU 49 */ @Override public boolean observesDaylightTime() { return tz.observesDaylightTime(); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public boolean hasSameRules(TimeZone other) { if (this == other) { return true; } if (other instanceof VTimeZone) { return tz.hasSameRules(((VTimeZone)other).tz); } return tz.hasSameRules(other); } /** * Gets the RFC2445 TZURL property value. When a VTimeZone instance was created from * VTIMEZONE data, the value is set by the TZURL property value in the data. Otherwise, * the initial value is null. * * @return The RFC2445 TZURL property value * * @stable ICU 3.8 */ public String getTZURL() { return tzurl; } /** * Sets the RFC2445 TZURL property value. * * @param url The TZURL property value. * * @stable ICU 3.8 */ public void setTZURL(String url) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); } tzurl = url; } /** * Gets the RFC2445 LAST-MODIFIED property value. When a VTimeZone instance was created * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data. * Otherwise, the initial value is null. * * @return The Date represents the RFC2445 LAST-MODIFIED date. * * @stable ICU 3.8 */ public Date getLastModified() { return lastmod; } /** * Sets the date used for RFC2445 LAST-MODIFIED property value. * * @param date The Date object represents the date for RFC2445 LAST-MODIFIED property value. * * @stable ICU 3.8 */ public void setLastModified(Date date) { if (isFrozen()) { throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance."); } lastmod = date; } /** * Writes RFC2445 VTIMEZONE data for this time zone * * @param writer A Writer used for the output * @throws IOException If there were problems creating a buffered writer or writing to it. * * @stable ICU 3.8 */ public void write(Writer writer) throws IOException { BufferedWriter bw = new BufferedWriter(writer); if (vtzlines != null) { for (String line : vtzlines) { if (line.startsWith(ICAL_TZURL + COLON)) { if (tzurl != null) { bw.write(ICAL_TZURL); bw.write(COLON); bw.write(tzurl); bw.write(NEWLINE); } } else if (line.startsWith(ICAL_LASTMOD + COLON)) { if (lastmod != null) { bw.write(ICAL_LASTMOD); bw.write(COLON); bw.write(getUTCDateTimeString(lastmod.getTime())); bw.write(NEWLINE); } } else { bw.write(line); bw.write(NEWLINE); } } bw.flush(); } else { String[] customProperties = null; if (olsonzid != null && ICU_TZVERSION != null) { customProperties = new String[1]; customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]"; } writeZone(writer, tz, customProperties); } } /** * Writes RFC2445 VTIMEZONE data applicable for dates after * the specified start time. * * @param writer The Writer used for the output * @param start The start time * * @throws IOException If there were problems reading and writing to the writer. * * @stable ICU 3.8 */ public void write(Writer writer, long start) throws IOException { // Extract rules applicable to dates after the start time TimeZoneRule[] rules = tz.getTimeZoneRules(start); // Create a RuleBasedTimeZone with the subset rule RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]); for (int i = 1; i < rules.length; i++) { rbtz.addTransitionRule(rules[i]); } String[] customProperties = null; if (olsonzid != null && ICU_TZVERSION != null) { customProperties = new String[1]; customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "/Partial@" + start + "]"; } writeZone(writer, rbtz, customProperties); } /** * Writes RFC2445 VTIMEZONE data applicable near the specified date. * Some common iCalendar implementations can only handle a single time * zone property or a pair of standard and daylight time properties using * BYDAY rule with day of week (such as BYDAY=1SUN). This method produce * the VTIMEZONE data which can be handled these implementations. The rules * produced by this method can be used only for calculating time zone offset * around the specified date. * * @param writer The Writer used for the output * @param time The date * * @throws IOException If there were problems reading or writing to the writer. * * @stable ICU 3.8 */ public void writeSimple(Writer writer, long time) throws IOException { // Extract simple rules TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time); // Create a RuleBasedTimeZone with the subset rule RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]); for (int i = 1; i < rules.length; i++) { rbtz.addTransitionRule(rules[i]); } String[] customProperties = null; if (olsonzid != null && ICU_TZVERSION != null) { customProperties = new String[1]; customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "/Simple@" + time + "]"; } writeZone(writer, rbtz, customProperties); } // BasicTimeZone methods /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public TimeZoneTransition getNextTransition(long base, boolean inclusive) { return tz.getNextTransition(base, inclusive); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) { return tz.getPreviousTransition(base, inclusive); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public boolean hasEquivalentTransitions(TimeZone other, long start, long end) { if (this == other) { return true; } return tz.hasEquivalentTransitions(other, start, end); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public TimeZoneRule[] getTimeZoneRules() { return tz.getTimeZoneRules(); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public TimeZoneRule[] getTimeZoneRules(long start) { return tz.getTimeZoneRules(start); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public Object clone() { if (isFrozen()) { return this; } return cloneAsThawed(); } // private stuff ------------------------------------------------------ private BasicTimeZone tz; private List vtzlines; private String olsonzid = null; private String tzurl = null; private Date lastmod = null; private static String ICU_TZVERSION; private static final String ICU_TZINFO_PROP = "X-TZINFO"; // Default DST savings private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour // Default time start private static final long DEF_TZSTARTTIME = 0; // minimum/max private static final long MIN_TIME = Long.MIN_VALUE; private static final long MAX_TIME = Long.MAX_VALUE; // Symbol characters used by RFC2445 VTIMEZONE private static final String COLON = ":"; private static final String SEMICOLON = ";"; private static final String EQUALS_SIGN = "="; private static final String COMMA = ","; private static final String NEWLINE = "\r\n"; // CRLF // RFC2445 VTIMEZONE tokens private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE"; private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE"; private static final String ICAL_BEGIN = "BEGIN"; private static final String ICAL_END = "END"; private static final String ICAL_VTIMEZONE = "VTIMEZONE"; private static final String ICAL_TZID = "TZID"; private static final String ICAL_STANDARD = "STANDARD"; private static final String ICAL_DAYLIGHT = "DAYLIGHT"; private static final String ICAL_DTSTART = "DTSTART"; private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM"; private static final String ICAL_TZOFFSETTO = "TZOFFSETTO"; private static final String ICAL_RDATE = "RDATE"; private static final String ICAL_RRULE = "RRULE"; private static final String ICAL_TZNAME = "TZNAME"; private static final String ICAL_TZURL = "TZURL"; private static final String ICAL_LASTMOD = "LAST-MODIFIED"; private static final String ICAL_FREQ = "FREQ"; private static final String ICAL_UNTIL = "UNTIL"; private static final String ICAL_YEARLY = "YEARLY"; private static final String ICAL_BYMONTH = "BYMONTH"; private static final String ICAL_BYDAY = "BYDAY"; private static final String ICAL_BYMONTHDAY = "BYMONTHDAY"; private static final String[] ICAL_DOW_NAMES = {"SU", "MO", "TU", "WE", "TH", "FR", "SA"}; // Month length in regular year private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; static { // Initialize ICU_TZVERSION try { ICU_TZVERSION = TimeZone.getTZDataVersion(); } catch (MissingResourceException e) { ///CLOVER:OFF ICU_TZVERSION = null; ///CLOVER:ON } } /* Hide the constructor */ private VTimeZone() { } private VTimeZone(String tzid) { super(tzid); } /* * Read the input stream to locate the VTIMEZONE block and * parse the contents to initialize this VTimeZone object. * The reader skips other RFC2445 message headers. After * the parse is completed, the reader points at the beginning * of the header field just after the end of VTIMEZONE block. * When VTIMEZONE block is found and this object is successfully * initialized by the rules described in the data, this method * returns true. Otherwise, returns false. */ private boolean load(Reader reader) { // Read VTIMEZONE block into string array try { vtzlines = new LinkedList<>(); boolean eol = false; boolean start = false; boolean success = false; StringBuilder line = new StringBuilder(); while (true) { int ch = reader.read(); if (ch == -1) { // end of file if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) { vtzlines.add(line.toString()); success = true; } break; } if (ch == 0x0D) { // CR, must be followed by LF by the definition in RFC2445 continue; } if (eol) { if (ch != 0x09 && ch != 0x20) { // NOT followed by TAB/SP -> new line if (start) { if (line.length() > 0) { vtzlines.add(line.toString()); } } line.setLength(0); if (ch != 0x0A) { line.append((char)ch); } } eol = false; } else { if (ch == 0x0A) { // LF eol = true; if (start) { if (line.toString().startsWith(ICAL_END_VTIMEZONE)) { vtzlines.add(line.toString()); success = true; break; } } else { if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) { vtzlines.add(line.toString()); line.setLength(0); start = true; eol = false; } } } else { line.append((char)ch); } } } if (!success) { return false; } } catch (IOException ioe) { ///CLOVER:OFF return false; ///CLOVER:ON } return parse(); } // parser state private static final int INI = 0; // Initial state private static final int VTZ = 1; // In VTIMEZONE private static final int TZI = 2; // In STANDARD or DAYLIGHT private static final int ERR = 3; // Error state /* * Parse VTIMEZONE data and create a RuleBasedTimeZone */ private boolean parse() { ///CLOVER:OFF if (vtzlines == null || vtzlines.size() == 0) { return false; } ///CLOVER:ON // timezone ID String tzid = null; int state = INI; boolean dst = false; // current zone type String from = null; // current zone from offset String to = null; // current zone offset String tzname = null; // current zone name String dtstart = null; // current zone starts boolean isRRULE = false; // true if the rule is described by RRULE List dates = null; // list of RDATE or RRULE strings List rules = new ArrayList<>(); // rule list int initialRawOffset = 0; // initial offset int initialDSTSavings = 0; // initial offset long firstStart = MAX_TIME; // the earliest rule start time for (String line : vtzlines) { int valueSep = line.indexOf(COLON); if (valueSep < 0) { continue; } String name = line.substring(0, valueSep); String value = line.substring(valueSep + 1); switch (state) { case INI: if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) { state = VTZ; } break; case VTZ: if (name.equals(ICAL_TZID)) { tzid = value; } else if (name.equals(ICAL_TZURL)) { tzurl = value; } else if (name.equals(ICAL_LASTMOD)) { // Always in 'Z' format, so the offset argument for the parse method // can be any value. lastmod = new Date(parseDateTimeString(value, 0)); } else if (name.equals(ICAL_BEGIN)) { boolean isDST = value.equals(ICAL_DAYLIGHT); if (value.equals(ICAL_STANDARD) || isDST) { // tzid must be ready at this point if (tzid == null) { state = ERR; break; } // initialize current zone properties dates = null; isRRULE = false; from = null; to = null; tzname = null; dst = isDST; state = TZI; } else { // BEGIN property other than STANDARD/DAYLIGHT // must not be there. state = ERR; break; } } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) { break; } break; case TZI: if (name.equals(ICAL_DTSTART)) { dtstart = value; } else if (name.equals(ICAL_TZNAME)) { tzname = value; } else if (name.equals(ICAL_TZOFFSETFROM)) { from = value; } else if (name.equals(ICAL_TZOFFSETTO)) { to = value; } else if (name.equals(ICAL_RDATE)) { // RDATE mixed with RRULE is not supported if (isRRULE) { state = ERR; break; } if (dates == null) { dates = new LinkedList<>(); } // RDATE value may contain multiple date delimited // by comma StringTokenizer st = new StringTokenizer(value, COMMA); while (st.hasMoreTokens()) { String date = st.nextToken(); dates.add(date); } } else if (name.equals(ICAL_RRULE)) { // RRULE mixed with RDATE is not supported if (!isRRULE && dates != null) { state = ERR; break; } else if (dates == null) { dates = new LinkedList<>(); } isRRULE = true; dates.add(value); } else if (name.equals(ICAL_END)) { // Mandatory properties if (dtstart == null || from == null || to == null) { state = ERR; break; } // if tzname is not available, create one from tzid if (tzname == null) { tzname = getDefaultTZName(tzid, dst); } // create a time zone rule TimeZoneRule rule = null; int fromOffset = 0; int toOffset = 0; int rawOffset = 0; int dstSavings = 0; long start = 0; try { // Parse TZOFFSETFROM/TZOFFSETTO fromOffset = offsetStrToMillis(from); toOffset = offsetStrToMillis(to); if (dst) { // If daylight, use the previous offset as rawoffset if positive if (toOffset - fromOffset > 0) { rawOffset = fromOffset; dstSavings = toOffset - fromOffset; } else { // This is rare case.. just use 1 hour DST savings rawOffset = toOffset - DEF_DSTSAVINGS; dstSavings = DEF_DSTSAVINGS; } } else { rawOffset = toOffset; dstSavings = 0; } // start time start = parseDateTimeString(dtstart, fromOffset); // Create the rule Date actualStart = null; if (isRRULE) { rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset); } else { rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset); } if (rule != null) { actualStart = rule.getFirstStart(fromOffset, 0); if (actualStart.getTime() < firstStart) { // save from offset information for the earliest rule firstStart = actualStart.getTime(); // If this is STD, assume the time before this transition // is DST when the difference is 1 hour. This might not be // accurate, but VTIMEZONE data does not have such info. if (dstSavings > 0) { initialRawOffset = fromOffset; initialDSTSavings = 0; } else { if (fromOffset - toOffset == DEF_DSTSAVINGS) { initialRawOffset = fromOffset - DEF_DSTSAVINGS; initialDSTSavings = DEF_DSTSAVINGS; } else { initialRawOffset = fromOffset; initialDSTSavings = 0; } } } } } catch (IllegalArgumentException iae) { // bad format - rule == null.. } if (rule == null) { state = ERR; break; } rules.add(rule); state = VTZ; } break; } if (state == ERR) { vtzlines = null; return false; } } // Must have at least one rule if (rules.size() == 0) { return false; } // Create a initial rule InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false), initialRawOffset, initialDSTSavings); // Finally, create the RuleBasedTimeZone RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule); int finalRuleIdx = -1; int finalRuleCount = 0; for (int i = 0; i < rules.size(); i++) { TimeZoneRule r = rules.get(i); if (r instanceof AnnualTimeZoneRule) { if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { finalRuleCount++; finalRuleIdx = i; } } } if (finalRuleCount > 2) { // Too many final rules return false; } if (finalRuleCount == 1) { if (rules.size() == 1) { // Only one final rule, only governs the initial rule, // which is already initialized, thus, we do not need to // add this transition rule rules.clear(); } else { // Normalize the final rule AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx); int tmpRaw = finalRule.getRawOffset(); int tmpDST = finalRule.getDSTSavings(); // Find the last non-final rule Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings); Date start = finalStart; for (int i = 0; i < rules.size(); i++) { if (finalRuleIdx == i) { continue; } TimeZoneRule r = rules.get(i); Date lastStart = r.getFinalStart(tmpRaw, tmpDST); if (lastStart.after(start)) { start = finalRule.getNextStart(lastStart.getTime(), r.getRawOffset(), r.getDSTSavings(), false); } } TimeZoneRule newRule; if (start == finalStart) { // Transform this into a single transition newRule = new TimeArrayTimeZoneRule( finalRule.getName(), finalRule.getRawOffset(), finalRule.getDSTSavings(), new long[] {finalStart.getTime()}, DateTimeRule.UTC_TIME); } else { // Update the end year int fields[] = Grego.timeToFields(start.getTime(), null); newRule = new AnnualTimeZoneRule( finalRule.getName(), finalRule.getRawOffset(), finalRule.getDSTSavings(), finalRule.getRule(), finalRule.getStartYear(), fields[0]); } rules.set(finalRuleIdx, newRule); } } for (TimeZoneRule r : rules) { rbtz.addTransitionRule(r); } tz = rbtz; setID(tzid); return true; } /* * Create a default TZNAME from TZID */ private static String getDefaultTZName(String tzid, boolean isDST) { if (isDST) { return tzid + "(DST)"; } return tzid + "(STD)"; } /* * Create a TimeZoneRule by the RRULE definition */ private static TimeZoneRule createRuleByRRULE(String tzname, int rawOffset, int dstSavings, long start, List dates, int fromOffset) { if (dates == null || dates.size() == 0) { return null; } // Parse the first rule String rrule = dates.get(0); long until[] = new long[1]; int[] ruleFields = parseRRULE(rrule, until); if (ruleFields == null) { // Invalid RRULE return null; } int month = ruleFields[0]; int dayOfWeek = ruleFields[1]; int nthDayOfWeek = ruleFields[2]; int dayOfMonth = ruleFields[3]; if (dates.size() == 1) { // No more rules if (ruleFields.length > 4) { // Multiple BYMONTHDAY values if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) { // Only support the rule using 7 continuous days // BYMONTH and BYDAY must be set at the same time return null; } int firstDay = 31; // max possible number of dates in a month int days[] = new int[7]; for (int i = 0; i < 7; i++) { days[i] = ruleFields[3 + i]; // Resolve negative day numbers. A negative day number should // not be used in February, but if we see such case, we use 28 // as the base. days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1; firstDay = days[i] < firstDay ? days[i] : firstDay; } // Make sure days are continuous for (int i = 1; i < 7; i++) { boolean found = false; for (int j = 0; j < 7; j++) { if (days[j] == firstDay + i) { found = true; break; } } if (!found) { // days are not continuous return null; } } // Use DOW_GEQ_DOM rule with firstDay as the start date dayOfMonth = firstDay; } } else { // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines. // Otherwise, not supported. if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) { // This is not the case return null; } // Parse the rest of rules if number of rules is not exceeding 7. // We can only support 7 continuous days starting from a day of month. if (dates.size() > 7) { return null; } // Note: To check valid date range across multiple rule is a little // bit complicated. For now, this code is not doing strict range // checking across month boundary int earliestMonth = month; int daysCount = ruleFields.length - 3; int earliestDay = 31; for (int i = 0; i < daysCount; i++) { int dom = ruleFields[3 + i]; dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1; earliestDay = dom < earliestDay ? dom : earliestDay; } int anotherMonth = -1; for (int i = 1; i < dates.size(); i++) { rrule = dates.get(i); long[] unt = new long[1]; int[] fields = parseRRULE(rrule, unt); // If UNTIL is newer than previous one, use the one if (unt[0] > until[0]) { until = unt; } // Check if BYMONTH + BYMONTHDAY + BYDAY rule if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) { return null; } // Count number of BYMONTHDAY int count = fields.length - 3; if (daysCount + count > 7) { // We cannot support BYMONTHDAY more than 7 return null; } // Check if the same BYDAY is used. Otherwise, we cannot // support the rule if (fields[1] != dayOfWeek) { return null; } // Check if the month is same or right next to the primary month if (fields[0] != month) { if (anotherMonth == -1) { int diff = fields[0] - month; if (diff == -11 || diff == -1) { // Previous month anotherMonth = fields[0]; earliestMonth = anotherMonth; // Reset earliest day earliestDay = 31; } else if (diff == 11 || diff == 1) { // Next month anotherMonth = fields[0]; } else { // The day range cannot exceed more than 2 months return null; } } else if (fields[0] != month && fields[0] != anotherMonth) { // The day range cannot exceed more than 2 months return null; } } // If earlier month, go through days to find the earliest day if (fields[0] == earliestMonth) { for (int j = 0; j < count; j++) { int dom = fields[3 + j]; dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1; earliestDay = dom < earliestDay ? dom : earliestDay; } } daysCount += count; } if (daysCount != 7) { // Number of BYMONTHDAY entries must be 7 return null; } month = earliestMonth; dayOfMonth = earliestDay; } // Calculate start/end year and missing fields int[] dfields = Grego.timeToFields(start + fromOffset, null); int startYear = dfields[0]; if (month == -1) { // If MYMONTH is not set, use the month of DTSTART month = dfields[1]; } if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) { // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY dayOfMonth = dfields[2]; } int timeInDay = dfields[5]; int endYear = AnnualTimeZoneRule.MAX_YEAR; if (until[0] != MIN_TIME) { Grego.timeToFields(until[0], dfields); endYear = dfields[0]; } // Create the AnnualDateTimeRule DateTimeRule adtr = null; if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) { // Day in month rule, for example, 15th day in the month adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME); } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) { // Nth day of week rule, for example, last Sunday adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME); } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) { // First day of week after day of month rule, for example, // first Sunday after 15th day in the month adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME); } else { // RRULE attributes are insufficient return null; } return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear); } /* * Parse individual RRULE * * On return - * * int[0] month calculated by BYMONTH - 1, or -1 when not found * int[1] day of week in BYDAY, or 0 when not found * int[2] day of week ordinal number in BYDAY, or 0 when not found * int[i >= 3] day of month, which could be multiple values, or 0 when not found * * or * * null on any error cases, for example, FREQ=YEARLY is not available * * When UNTIL attribute is available, the time will be set to until[0], * otherwise, MIN_TIME */ private static int[] parseRRULE(String rrule, long[] until) { int month = -1; int dayOfWeek = 0; int nthDayOfWeek = 0; int[] dayOfMonth = null; long untilTime = MIN_TIME; boolean yearly = false; boolean parseError = false; StringTokenizer st= new StringTokenizer(rrule, SEMICOLON); while (st.hasMoreTokens()) { String attr, value; String prop = st.nextToken(); int sep = prop.indexOf(EQUALS_SIGN); if (sep != -1) { attr = prop.substring(0, sep); value = prop.substring(sep + 1); } else { parseError = true; break; } if (attr.equals(ICAL_FREQ)) { // only support YEARLY frequency type if (value.equals(ICAL_YEARLY)) { yearly = true; } else { parseError = true; break; } } else if (attr.equals(ICAL_UNTIL)) { // ISO8601 UTC format, for example, "20060315T020000Z" try { untilTime = parseDateTimeString(value, 0); } catch (IllegalArgumentException iae) { parseError = true; break; } } else if (attr.equals(ICAL_BYMONTH)) { // Note: BYMONTH may contain multiple months, but only single month make sense for // VTIMEZONE property. if (value.length() > 2) { parseError = true; break; } try { month = Integer.parseInt(value) - 1; if (month < 0 || month >= 12) { parseError = true; break; } } catch (NumberFormatException nfe) { parseError = true; break; } } else if (attr.equals(ICAL_BYDAY)) { // Note: BYDAY may contain multiple day of week separated by comma. It is unlikely used for // VTIMEZONE property. We do not support the case. // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday int length = value.length(); if (length < 2 || length > 4) { parseError = true; break; } if (length > 2) { // Nth day of week int sign = 1; if (value.charAt(0) == '+') { sign = 1; } else if (value.charAt(0) == '-') { sign = -1; } else if (length == 4) { parseError = true; break; } try { int n = Integer.parseInt(value.substring(length - 3, length - 2)); if (n == 0 || n > 4) { parseError = true; break; } nthDayOfWeek = n * sign; } catch(NumberFormatException nfe) { parseError = true; break; } value = value.substring(length - 2); } int wday; for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) { if (value.equals(ICAL_DOW_NAMES[wday])) { break; } } if (wday < ICAL_DOW_NAMES.length) { // Sunday(1) - Saturday(7) dayOfWeek = wday + 1; } else { parseError = true; break; } } else if (attr.equals(ICAL_BYMONTHDAY)) { // Note: BYMONTHDAY may contain multiple days delimited by comma // // A value of BYMONTHDAY could be negative, for example, -1 means // the last day in a month StringTokenizer days = new StringTokenizer(value, COMMA); int count = days.countTokens(); dayOfMonth = new int[count]; int index = 0; while(days.hasMoreTokens()) { try { dayOfMonth[index++] = Integer.parseInt(days.nextToken()); } catch (NumberFormatException nfe) { parseError = true; break; } } } } if (parseError) { return null; } if (!yearly) { // FREQ=YEARLY must be set return null; } until[0] = untilTime; int[] results; if (dayOfMonth == null) { results = new int[4]; results[3] = 0; } else { results = new int[3 + dayOfMonth.length]; for (int i = 0; i < dayOfMonth.length; i++) { results[3 + i] = dayOfMonth[i]; } } results[0] = month; results[1] = dayOfWeek; results[2] = nthDayOfWeek; return results; } /* * Create a TimeZoneRule by the RDATE definition */ private static TimeZoneRule createRuleByRDATE(String tzname, int rawOffset, int dstSavings, long start, List dates, int fromOffset) { // Create an array of transition times long[] times; if (dates == null || dates.size() == 0) { // When no RDATE line is provided, use start (DTSTART) // as the transition time times = new long[1]; times[0] = start; } else { times = new long[dates.size()]; int idx = 0; try { for (String date : dates) { times[idx++] = parseDateTimeString(date, fromOffset); } } catch (IllegalArgumentException iae) { return null; } } return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME); } /* * Write the time zone rules in RFC2445 VTIMEZONE format */ private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException { // Write the header writeHeader(w); if (customProperties != null && customProperties.length > 0) { for (int i = 0; i < customProperties.length; i++) { if (customProperties[i] != null) { w.write(customProperties[i]); w.write(NEWLINE); } } } long t = MIN_TIME; String dstName = null; int dstFromOffset = 0; int dstFromDSTSavings = 0; int dstToOffset = 0; int dstStartYear = 0; int dstMonth = 0; int dstDayOfWeek = 0; int dstWeekInMonth = 0; int dstMillisInDay = 0; long dstStartTime = 0; long dstUntilTime = 0; int dstCount = 0; AnnualTimeZoneRule finalDstRule = null; String stdName = null; int stdFromOffset = 0; int stdFromDSTSavings = 0; int stdToOffset = 0; int stdStartYear = 0; int stdMonth = 0; int stdDayOfWeek = 0; int stdWeekInMonth = 0; int stdMillisInDay = 0; long stdStartTime = 0; long stdUntilTime = 0; int stdCount = 0; AnnualTimeZoneRule finalStdRule = null; int[] dtfields = new int[6]; boolean hasTransitions = false; // Going through all transitions while(true) { TimeZoneTransition tzt = basictz.getNextTransition(t, false); if (tzt == null) { break; } hasTransitions = true; t = tzt.getTime(); String name = tzt.getTo().getName(); boolean isDst = (tzt.getTo().getDSTSavings() != 0); int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings(); int fromDSTSavings = tzt.getFrom().getDSTSavings(); int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings(); Grego.timeToFields(tzt.getTime() + fromOffset, dtfields); int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]); int year = dtfields[0]; boolean sameRule = false; if (isDst) { if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) { if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { finalDstRule = (AnnualTimeZoneRule)tzt.getTo(); } } if (dstCount > 0) { if (year == dstStartYear + dstCount && name.equals(dstName) && dstFromOffset == fromOffset && dstToOffset == toOffset && dstMonth == dtfields[1] && dstDayOfWeek == dtfields[3] && dstWeekInMonth == weekInMonth && dstMillisInDay == dtfields[5]) { // Update until time dstUntilTime = t; dstCount++; sameRule = true; } if (!sameRule) { if (dstCount == 1) { writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset, dstStartTime, true); } else { writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); } } } if (!sameRule) { // Reset this DST information dstName = name; dstFromOffset = fromOffset; dstFromDSTSavings = fromDSTSavings; dstToOffset = toOffset; dstStartYear = year; dstMonth = dtfields[1]; dstDayOfWeek = dtfields[3]; dstWeekInMonth = weekInMonth; dstMillisInDay = dtfields[5]; dstStartTime = dstUntilTime = t; dstCount = 1; } if (finalStdRule != null && finalDstRule != null) { break; } } else { if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) { if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) { finalStdRule = (AnnualTimeZoneRule)tzt.getTo(); } } if (stdCount > 0) { if (year == stdStartYear + stdCount && name.equals(stdName) && stdFromOffset == fromOffset && stdToOffset == toOffset && stdMonth == dtfields[1] && stdDayOfWeek == dtfields[3] && stdWeekInMonth == weekInMonth && stdMillisInDay == dtfields[5]) { // Update until time stdUntilTime = t; stdCount++; sameRule = true; } if (!sameRule) { if (stdCount == 1) { writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset, stdStartTime, true); } else { writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); } } } if (!sameRule) { // Reset this STD information stdName = name; stdFromOffset = fromOffset; stdFromDSTSavings = fromDSTSavings; stdToOffset = toOffset; stdStartYear = year; stdMonth = dtfields[1]; stdDayOfWeek = dtfields[3]; stdWeekInMonth = weekInMonth; stdMillisInDay = dtfields[5]; stdStartTime = stdUntilTime = t; stdCount = 1; } if (finalStdRule != null && finalDstRule != null) { break; } } } if (!hasTransitions) { // No transition - put a single non transition RDATE int offset = basictz.getOffset(0 /* any time */); boolean isDst = (offset != basictz.getRawOffset()); writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst), offset, offset, DEF_TZSTARTTIME - offset, false); } else { if (dstCount > 0) { if (finalDstRule == null) { if (dstCount == 1) { writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset, dstStartTime, true); } else { writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); } } else { if (dstCount == 1) { writeFinalRule(w, true, finalDstRule, dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime); } else { // Use a single rule if possible if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) { writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME); } else { // Not equivalent rule - write out two different rules writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset, dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime); Date nextStart = finalDstRule.getNextStart(dstUntilTime, dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, false); assert nextStart != null; if (nextStart != null) { writeFinalRule(w, true, finalDstRule, dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, nextStart.getTime()); } } } } } if (stdCount > 0) { if (finalStdRule == null) { if (stdCount == 1) { writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset, stdStartTime, true); } else { writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); } } else { if (stdCount == 1) { writeFinalRule(w, false, finalStdRule, stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime); } else { // Use a single rule if possible if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) { writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME); } else { // Not equivalent rule - write out two different rules writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset, stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime); Date nextStart = finalStdRule.getNextStart(stdUntilTime, stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, false); assert nextStart != null; if (nextStart != null) { writeFinalRule(w, false, finalStdRule, stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, nextStart.getTime()); } } } } } } writeFooter(w); } /* * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent * to the DateTimerule. */ private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) { if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) { return false; } if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) { // Do not try to do more intelligent comparison for now. return false; } if (dtrule.getDateRuleType() == DateTimeRule.DOW && dtrule.getRuleWeekInMonth() == weekInMonth) { return true; } int ruleDOM = dtrule.getRuleDayOfMonth(); if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) { if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) { return true; } if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6 && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) { return true; } } if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) { if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) { return true; } if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0 && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) { return true; } } return false; } /* * Write a single start time */ private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException { beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time); if (withRDATE) { writer.write(ICAL_RDATE); writer.write(COLON); writer.write(getDateTimeString(time + fromOffset)); writer.write(NEWLINE); } endZoneProps(writer, isDst); } /* * Write start times defined by a DOM rule using VTIMEZONE RRULE */ private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, long startTime, long untilTime) throws IOException { beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); beginRRULE(writer, month); writer.write(ICAL_BYMONTHDAY); writer.write(EQUALS_SIGN); writer.write(Integer.toString(dayOfMonth)); if (untilTime != MAX_TIME) { appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); } writer.write(NEWLINE); endZoneProps(writer, isDst); } /* * Write start times defined by a DOW rule using VTIMEZONE RRULE */ private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); beginRRULE(writer, month); writer.write(ICAL_BYDAY); writer.write(EQUALS_SIGN); writer.write(Integer.toString(weekInMonth)); // -4, -3, -2, -1, 1, 2, 3, 4 writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU... if (untilTime != MAX_TIME) { appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); } writer.write(NEWLINE); endZoneProps(writer, isDst); } /* * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE */ private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { // Check if this rule can be converted to DOW rule if (dayOfMonth%7 == 1) { // Can be represented by DOW rule writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime); } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) { // Can be represented by DOW rule with negative week number writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime); } else { // Otherwise, use BYMONTHDAY to include all possible dates beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime); // Check if all days are in the same month int startDay = dayOfMonth; int currentMonthDays = 7; if (dayOfMonth <= 0) { // The start day is in previous month int prevMonthDays = 1 - dayOfMonth; currentMonthDays -= prevMonthDays; int prevMonth = (month - 1) < 0 ? 11 : month - 1; // Note: When a rule is separated into two, UNTIL attribute needs to be // calculated for each of them. For now, we skip this, because we basically use this method // only for final rules, which does not have the UNTIL attribute writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset); // Start from 1 for the rest startDay = 1; } else if (dayOfMonth + 6 > MONTHLENGTH[month]) { // Note: This code does not actually work well in February. For now, days in month in // non-leap year. int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month]; currentMonthDays -= nextMonthDays; int nextMonth = (month + 1) > 11 ? 0 : month + 1; writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset); } writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset); endZoneProps(writer, isDst); } } /* * Called from writeZonePropsByDOW_GEQ_DOM */ private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month, int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException { int startDayNum = dayOfMonth; boolean isFeb = (month == Calendar.FEBRUARY); if (dayOfMonth < 0 && !isFeb) { // Use positive number if possible startDayNum = MONTHLENGTH[month] + dayOfMonth + 1; } beginRRULE(writer, month); writer.write(ICAL_BYDAY); writer.write(EQUALS_SIGN); writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]); // SU, MO, TU... writer.write(SEMICOLON); writer.write(ICAL_BYMONTHDAY); writer.write(EQUALS_SIGN); writer.write(Integer.toString(startDayNum)); for (int i = 1; i < numDays; i++) { writer.write(COMMA); writer.write(Integer.toString(startDayNum + i)); } if (untilTime != MAX_TIME) { appendUNTIL(writer, getDateTimeString(untilTime + fromOffset)); } writer.write(NEWLINE); } /* * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE */ private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException { // Check if this rule can be converted to DOW rule if (dayOfMonth%7 == 0) { // Can be represented by DOW rule writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, month, dayOfMonth/7, dayOfWeek, startTime, untilTime); } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){ // Can be represented by DOW rule with negative week number writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime); } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) { // Special case for February writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset, Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime); } else { // Otherwise, convert this to DOW_GEQ_DOM rule writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset, month, dayOfMonth - 6, dayOfWeek, startTime, untilTime); } } /* * Write the final time zone rule using RRULE, with no UNTIL attribute */ private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule, int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{ DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings); // If the rule's mills in a day is out of range, adjust start time. // Olson tzdata supports 24:00 of a day, but VTIMEZONE does not. // See ticket#7008/#7518 int timeInDay = dtrule.getRuleMillisInDay(); if (timeInDay < 0) { startTime = startTime + (0 - timeInDay); } else if (timeInDay >= Grego.MILLIS_PER_DAY) { startTime = startTime - (timeInDay - (Grego.MILLIS_PER_DAY - 1)); } int toOffset = rule.getRawOffset() + rule.getDSTSavings(); switch (dtrule.getDateRuleType()) { case DateTimeRule.DOM: writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME); break; case DateTimeRule.DOW: writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); break; case DateTimeRule.DOW_GEQ_DOM: writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); break; case DateTimeRule.DOW_LEQ_DOM: writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset, dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME); break; } } /* * Convert the rule to its equivalent rule using WALL_TIME mode */ private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) { if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) { return rule; } int wallt = rule.getRuleMillisInDay(); if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) { wallt += (rawOffset + dstSavings); } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) { wallt += dstSavings; } int month = -1, dom = 0, dow = 0, dtype = -1; int dshift = 0; if (wallt < 0) { dshift = -1; wallt += Grego.MILLIS_PER_DAY; } else if (wallt >= Grego.MILLIS_PER_DAY) { dshift = 1; wallt -= Grego.MILLIS_PER_DAY; } month = rule.getRuleMonth(); dom = rule.getRuleDayOfMonth(); dow = rule.getRuleDayOfWeek(); dtype = rule.getDateRuleType(); if (dshift != 0) { if (dtype == DateTimeRule.DOW) { // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first int wim = rule.getRuleWeekInMonth(); if (wim > 0) { dtype = DateTimeRule.DOW_GEQ_DOM; dom = 7 * (wim - 1) + 1; } else { dtype = DateTimeRule.DOW_LEQ_DOM; dom = MONTHLENGTH[month] + 7 * (wim + 1); } } // Shift one day before or after dom += dshift; if (dom == 0) { month--; month = month < Calendar.JANUARY ? Calendar.DECEMBER : month; dom = MONTHLENGTH[month]; } else if (dom > MONTHLENGTH[month]) { month++; month = month > Calendar.DECEMBER ? Calendar.JANUARY : month; dom = 1; } if (dtype != DateTimeRule.DOM) { // Adjust day of week dow += dshift; if (dow < Calendar.SUNDAY) { dow = Calendar.SATURDAY; } else if (dow > Calendar.SATURDAY) { dow = Calendar.SUNDAY; } } } // Create a new rule DateTimeRule modifiedRule; if (dtype == DateTimeRule.DOM) { modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME); } else { modifiedRule = new DateTimeRule(month, dom, dow, (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME); } return modifiedRule; } /* * Write the opening section of zone properties */ private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException { writer.write(ICAL_BEGIN); writer.write(COLON); if (isDst) { writer.write(ICAL_DAYLIGHT); } else { writer.write(ICAL_STANDARD); } writer.write(NEWLINE); // TZOFFSETTO writer.write(ICAL_TZOFFSETTO); writer.write(COLON); writer.write(millisToOffset(toOffset)); writer.write(NEWLINE); // TZOFFSETFROM writer.write(ICAL_TZOFFSETFROM); writer.write(COLON); writer.write(millisToOffset(fromOffset)); writer.write(NEWLINE); // TZNAME writer.write(ICAL_TZNAME); writer.write(COLON); writer.write(tzname); writer.write(NEWLINE); // DTSTART writer.write(ICAL_DTSTART); writer.write(COLON); writer.write(getDateTimeString(startTime + fromOffset)); writer.write(NEWLINE); } /* * Writes the closing section of zone properties */ private static void endZoneProps(Writer writer, boolean isDst) throws IOException{ // END:STANDARD or END:DAYLIGHT writer.write(ICAL_END); writer.write(COLON); if (isDst) { writer.write(ICAL_DAYLIGHT); } else { writer.write(ICAL_STANDARD); } writer.write(NEWLINE); } /* * Write the beginning part of RRULE line */ private static void beginRRULE(Writer writer, int month) throws IOException { writer.write(ICAL_RRULE); writer.write(COLON); writer.write(ICAL_FREQ); writer.write(EQUALS_SIGN); writer.write(ICAL_YEARLY); writer.write(SEMICOLON); writer.write(ICAL_BYMONTH); writer.write(EQUALS_SIGN); writer.write(Integer.toString(month + 1)); writer.write(SEMICOLON); } /* * Append the UNTIL attribute after RRULE line */ private static void appendUNTIL(Writer writer, String until) throws IOException { if (until != null) { writer.write(SEMICOLON); writer.write(ICAL_UNTIL); writer.write(EQUALS_SIGN); writer.write(until); } } /* * Write the opening section of the VTIMEZONE block */ private void writeHeader(Writer writer)throws IOException { writer.write(ICAL_BEGIN); writer.write(COLON); writer.write(ICAL_VTIMEZONE); writer.write(NEWLINE); writer.write(ICAL_TZID); writer.write(COLON); writer.write(tz.getID()); writer.write(NEWLINE); if (tzurl != null) { writer.write(ICAL_TZURL); writer.write(COLON); writer.write(tzurl); writer.write(NEWLINE); } if (lastmod != null) { writer.write(ICAL_LASTMOD); writer.write(COLON); writer.write(getUTCDateTimeString(lastmod.getTime())); writer.write(NEWLINE); } } /* * Write the closing section of the VTIMEZONE definition block */ private static void writeFooter(Writer writer) throws IOException { writer.write(ICAL_END); writer.write(COLON); writer.write(ICAL_VTIMEZONE); writer.write(NEWLINE); } /* * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME */ private static String getDateTimeString(long time) { int[] fields = Grego.timeToFields(time, null); StringBuilder sb = new StringBuilder(15); sb.append(numToString(fields[0], 4)); sb.append(numToString(fields[1] + 1, 2)); sb.append(numToString(fields[2], 2)); sb.append('T'); int t = fields[5]; int hour = t / Grego.MILLIS_PER_HOUR; t %= Grego.MILLIS_PER_HOUR; int min = t / Grego.MILLIS_PER_MINUTE; t %= Grego.MILLIS_PER_MINUTE; int sec = t / Grego.MILLIS_PER_SECOND; sb.append(numToString(hour, 2)); sb.append(numToString(min, 2)); sb.append(numToString(sec, 2)); return sb.toString(); } /* * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME */ private static String getUTCDateTimeString(long time) { return getDateTimeString(time) + "Z"; } /* * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and * #2 DATE WITH UTC TIME */ private static long parseDateTimeString(String str, int offset) { int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0; boolean isUTC = false; boolean isValid = false; do { if (str == null) { break; } int length = str.length(); if (length != 15 && length != 16) { // FORM#1 15 characters, such as "20060317T142115" // FORM#2 16 characters, such as "20060317T142115Z" break; } if (str.charAt(8) != 'T') { // character "T" must be used for separating date and time break; } if (length == 16) { if (str.charAt(15) != 'Z') { // invalid format break; } isUTC = true; } try { year = Integer.parseInt(str.substring(0, 4)); month = Integer.parseInt(str.substring(4, 6)) - 1; // 0-based day = Integer.parseInt(str.substring(6, 8)); hour = Integer.parseInt(str.substring(9, 11)); min = Integer.parseInt(str.substring(11, 13)); sec = Integer.parseInt(str.substring(13, 15)); } catch (NumberFormatException nfe) { break; } // check valid range int maxDayOfMonth = Grego.monthLength(year, month); if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth || hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) { break; } isValid = true; } while(false); if (!isValid) { throw new IllegalArgumentException("Invalid date time string format"); } // Calculate the time long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY; time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND); if (!isUTC) { time -= offset; } return time; } /* * Convert RFC2445 utc-offset string to milliseconds */ private static int offsetStrToMillis(String str) { boolean isValid = false; int sign = 0, hour = 0, min = 0, sec = 0; do { if (str == null) { break; } int length = str.length(); if (length != 5 && length != 7) { // utf-offset must be 5 or 7 characters break; } // sign char s = str.charAt(0); if (s == '+') { sign = 1; } else if (s == '-') { sign = -1; } else { // utf-offset must start with "+" or "-" break; } try { hour = Integer.parseInt(str.substring(1, 3)); min = Integer.parseInt(str.substring(3, 5)); if (length == 7) { sec = Integer.parseInt(str.substring(5, 7)); } } catch (NumberFormatException nfe) { break; } isValid = true; } while(false); if (!isValid) { throw new IllegalArgumentException("Bad offset string"); } int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000; return millis; } /* * Convert milliseconds to RFC2445 utc-offset string */ private static String millisToOffset(int millis) { StringBuilder sb = new StringBuilder(7); if (millis >= 0) { sb.append('+'); } else { sb.append('-'); millis = -millis; } int hour, min, sec; int t = millis / 1000; sec = t % 60; t = (t - sec) / 60; min = t % 60; hour = t / 60; sb.append(numToString(hour, 2)); sb.append(numToString(min, 2)); sb.append(numToString(sec, 2)); return sb.toString(); } /* * Format integer number */ private static String numToString(int num, int width) { String str = Integer.toString(num); int len = str.length(); if (len >= width) { return str.substring(len - width, len); } StringBuilder sb = new StringBuilder(width); for (int i = len; i < width; i++) { sb.append('0'); } sb.append(str); return sb.toString(); } // Freezable stuffs private volatile transient boolean isFrozen = false; /** * {@inheritDoc} * @stable ICU 49 */ @Override public boolean isFrozen() { return isFrozen; } /** * {@inheritDoc} * @stable ICU 49 */ @Override public TimeZone freeze() { isFrozen = true; return this; } /** * {@inheritDoc} * @stable ICU 49 */ @Override public TimeZone cloneAsThawed() { VTimeZone vtz = (VTimeZone)super.cloneAsThawed(); vtz.tz = (BasicTimeZone)tz.cloneAsThawed(); vtz.isFrozen = false; return vtz; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy