com.ibm.icu.util.VTimeZone Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
/*
*******************************************************************************
* Copyright (C) 2007-2014, 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) {
VTimeZone vtz = new VTimeZone(tzid);
vtz.tz = (BasicTimeZone)TimeZone.getTimeZone(tzid, TimeZone.TIMEZONE_ICU);
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}
* @internal
* @deprecated This API is ICU internal only.
*/
@Deprecated
@Override
public void getOffsetFromLocal(long date,
int nonExistingTimeOpt, int 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 transtion
// 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 ealier 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 exmaple, 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) {
// Specical 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') {
// charcter "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
*/
public boolean isFrozen() {
return isFrozen;
}
/**
* {@inheritDoc}
* @stable ICU 49
*/
public TimeZone freeze() {
isFrozen = true;
return this;
}
/**
* {@inheritDoc}
* @stable ICU 49
*/
public TimeZone cloneAsThawed() {
VTimeZone vtz = (VTimeZone)super.cloneAsThawed();
vtz.tz = (BasicTimeZone)tz.cloneAsThawed();
vtz.isFrozen = false;
return vtz;
}
}