| /* |
| ******************************************************************************* |
| * Copyright (C) 2007-2013, 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; |
| |
| /** |
| * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE. You can create a |
| * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>. |
| * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule |
| * in RFC2445 VTIMEZONE format. Also, you can create a <code>VTimeZone</code> instance |
| * from RFC2445 VTIMEZONE data stream, which allows you to calculate time |
| * zone offset by the rules defined by the data.<br><br> |
| * |
| * 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 <code>VTimeZone</code> instance by the time zone ID. |
| * |
| * @param tzid The time zone ID, such as America/New_York |
| * @return A <code>VTimeZone</code> 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 <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data. |
| * |
| * @param reader The Reader for VTIMEZONE data input stream |
| * @return A <code>VTimeZone</code> 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. |
| */ |
| @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 <code>VTimeZone</code> 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 <code>VTimeZone</code> 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 <code>Date</code> 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 <code>Writer</code> 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 <code>Writer</code> 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 <code>Writer</code> 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<String> 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<String>(); |
| 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<String> dates = null; // list of RDATE or RRULE strings |
| List<TimeZoneRule> rules = new ArrayList<TimeZoneRule>(); // 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<String>(); |
| } |
| // 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<String>(); |
| } |
| 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<String> 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<String> 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 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; |
| } |
| } |