/*************************************************************************** * Copyright © 2005-2009 by Gabriele Svelto * * gabriele.svelto@gmail.com * * * * This file is part of Jelatine. * * * * Jelatine is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * Jelatine is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with Jelatine. If not, see . * ***************************************************************************/ package jelatine; import java.util.Calendar; import java.util.TimeZone; /** * Internal VM implementation of the standard TimeZone */ public final class VMTimeZone extends TimeZone { /** * The raw time zone offset in milliseconds to GMT, ignoring * daylight savings. */ private int rawOffset; /** * True, if this timezone uses daylight savings, false otherwise. */ private boolean useDaylight; /** * The daylight savings offset. This is a positive offset in * milliseconds with respect to standard time. Typically this * is one hour, but for some time zones this may be half an hour. */ private int dstSavings = 60 * 60 * 1000; /** * The first year, in which daylight savings rules applies. */ private int startYear; private static final int DOM_MODE = 1; private static final int DOW_IN_MONTH_MODE = 2; private static final int DOW_GE_DOM_MODE = 3; private static final int DOW_LE_DOM_MODE = 4; /** * The mode of the start rule. This takes one of the following values: *
*
DOM_MODE (1)
*
startDay contains the day in month of the start date, * startDayOfWeek is unused.
*
DOW_IN_MONTH_MODE (2)
*
The startDay gives the day of week in month, and * startDayOfWeek the day of week. For example startDay=2 and * startDayOfWeek=Calender.SUNDAY specifies that the change is on * the second sunday in that month. You must make sure, that this * day always exists (ie. don't specify the 5th sunday). *
*
DOW_GE_DOM_MODE (3)
*
The start is on the first startDayOfWeek on or after * startDay. For example startDay=13 and * startDayOfWeek=Calendar.FRIDAY specifies that the daylight * savings start on the first FRIDAY on or after the 13th of that * Month. Make sure that the change is always in the given month, or * the result is undefined. *
*
DOW_LE_DOM_MONTH (4)
*
The start is on the first startDayOfWeek on or before the * startDay. Make sure that the change is always in the given * month, or the result is undefined.
*
*/ private int startMode; /** * The month in which daylight savings start. This is one of the * constants Calendar.JANUARY, ..., Calendar.DECEMBER. */ private int startMonth; /** * This variable can have different meanings. See startMode for details */ private int startDay; /** * This variable specifies the day of week the change takes place. If * startMode == DOM_MODE, this is undefined. */ private int startDayOfWeek; /** * This variable specifies the time of change to daylight savings. * This time is given in milliseconds after midnight local * standard time. */ private int startTime; /** * This variable specifies the mode that startTime is specified in. By * default it is WALL_TIME, but can also be STANDARD_TIME or UTC_TIME. For * startTime, STANDARD_TIME and WALL_TIME are equivalent. */ private int startTimeMode = WALL_TIME; /** * The month in which daylight savings ends. This is one of the * constants Calendar.JANUARY, ..., Calendar.DECEMBER. */ private int endMonth; /** * This variable gives the mode for the end of daylight savings rule. * It can take the same values as startMode. */ private int endMode; /** * This variable can have different meanings. See startMode for details */ private int endDay; /** * This variable specifies the day of week the change takes place. If * endMode == DOM_MODE, this is undefined. */ private int endDayOfWeek; /** * This variable specifies the time of change back to standard time. * This time is given in milliseconds after midnight local * standard time. */ private int endTime; /** * This variable specifies the mode that endTime is specified in. By * default it is WALL_TIME, but can also be STANDARD_TIME or UTC_TIME. */ private int endTimeMode = WALL_TIME; /** * This variable points to a deprecated array from JDK 1.1. It is * ignored in JDK 1.2 but streamed out for compatibility with JDK 1.1. * The array contains the lengths of the months in the year and is * assigned from a private static final field to avoid allocating * the array for every instance of the object. * Note that static final fields are not serialized. */ // private byte[] monthLength = monthArr; private static final byte[] monthArr = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; /** * Constant to indicate that start and end times are specified in standard * time, without adjusting for daylight savings. */ public static final int STANDARD_TIME = 1; /** * Constant to indicate that start and end times are specified in wall * time, adjusting for daylight savings. This is the default. */ public static final int WALL_TIME = 0; /** * Constant to indicate that start and end times are specified in UTC. */ public static final int UTC_TIME = 2; /** * Create a SimpleTimeZone with the given time offset * from GMT and without daylight savings. * @param rawOffset the time offset from GMT in milliseconds. * @param id The identifier of this time zone. */ public VMTimeZone(int rawOffset, String id) { this.rawOffset = rawOffset; useDaylight = false; startYear = 0; } /** * Sets the first year, where daylight savings applies. The daylight * savings rule never apply for years in the BC era. Note that this * is gregorian calendar specific. * @param year the start year. */ public void setStartYear(int year) { startYear = year; useDaylight = true; } /** * Checks if the month, day, dayOfWeek arguments are in range and * returns the mode of the rule. * @param month the month parameter as in the constructor * @param day the day parameter as in the constructor * @param dayOfWeek the day of week parameter as in the constructor * @return the mode of this rule see startMode. * @throws IllegalArgumentException if parameters are out of range. */ private int checkRule(int month, int day, int dayOfWeek) { if (month < 0 || month > 11) throw new IllegalArgumentException(); int daysInMonth = getDaysInMonth(month, 1); if (dayOfWeek == 0) { if (day <= 0 || day > daysInMonth) throw new IllegalArgumentException(); return DOM_MODE; } else if (dayOfWeek > 0) { if (Math.abs(day) > (daysInMonth + 6) / 7) throw new IllegalArgumentException(); if (dayOfWeek > Calendar.SATURDAY) throw new IllegalArgumentException(); return DOW_IN_MONTH_MODE; } else { if (day == 0 || Math.abs(day) > daysInMonth) throw new IllegalArgumentException(); if (dayOfWeek < -Calendar.SATURDAY) throw new IllegalArgumentException(); if (day < 0) return DOW_LE_DOM_MODE; else return DOW_GE_DOM_MODE; } } /** * Sets the daylight savings start rule. You must also set the * end rule with setEndRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * @param month The month where daylight savings start, zero * based. You should use the constants in Calendar. * @param day A day of month or day of week in month. * @param dayOfWeek The day of week where daylight savings start. * @param time The time in milliseconds standard time where daylight * savings start. * @throws IllegalArgumentException if parameters are out of range. */ public void setStartRule(int month, int day, int dayOfWeek, int time) { this.startMode = checkRule(month, day, dayOfWeek); this.startMonth = month; this.startDay = day; this.startDayOfWeek = Math.abs(dayOfWeek); if (this.startTimeMode == WALL_TIME || this.startTimeMode == STANDARD_TIME) { this.startTime = time; } else { // Convert from UTC to STANDARD this.startTime = time + this.rawOffset; } useDaylight = true; } /** * Sets the daylight savings start rule. You must also set the * end rule with setEndRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * Note that this API isn't incredibly well specified. It appears that the * after flag must override the parameters, since normally, the day and * dayofweek can select this. I.e., if day < 0 and dayOfWeek < 0, on or * before mode is chosen. But if after == true, this implementation * overrides the signs of the other arguments. And if dayOfWeek == 0, it * falls back to the behavior in the other APIs. I guess this should be * checked against Sun's implementation. * * @param month The month where daylight savings start, zero * based. You should use the constants in Calendar. * @param day A day of month or day of week in month. * @param dayOfWeek The day of week where daylight savings start. * @param time The time in milliseconds standard time where daylight * savings start. * @param after If true, day and dayOfWeek specify first day of week on or * after day, else first day of week on or before. */ public void setStartRule(int month, int day, int dayOfWeek, int time, boolean after) { // FIXME: XXX: Validate that checkRule and offset processing work with // on or before mode. this.startDay = after ? Math.abs(day) : -Math.abs(day); this.startDayOfWeek = after ? Math.abs(dayOfWeek) : -Math.abs(dayOfWeek); this.startMode = (dayOfWeek != 0) ? (after ? DOW_GE_DOM_MODE : DOW_LE_DOM_MODE) : checkRule(month, day, dayOfWeek); this.startDay = Math.abs(this.startDay); this.startDayOfWeek = Math.abs(this.startDayOfWeek); this.startMonth = month; if (this.startTimeMode == WALL_TIME || this.startTimeMode == STANDARD_TIME) { this.startTime = time; } else { // Convert from UTC to STANDARD this.startTime = time + this.rawOffset; } useDaylight = true; } /** * Sets the daylight savings start rule. You must also set the * end rule with setEndRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * @param month The month where daylight savings start, zero * based. You should use the constants in Calendar. * @param day A day of month or day of week in month. * @param time The time in milliseconds standard time where daylight * savings start. */ public void setStartRule(int month, int day, int time) { setStartRule(month, day, 0, time); } /** * Sets the daylight savings end rule. You must also set the * start rule with setStartRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * @param month The end month of daylight savings. * @param day A day in month, or a day of week in month. * @param dayOfWeek A day of week, when daylight savings ends. * @param time A time in millis in standard time. */ public void setEndRule(int month, int day, int dayOfWeek, int time) { this.endMode = checkRule(month, day, dayOfWeek); this.endMonth = month; this.endDay = day; this.endDayOfWeek = Math.abs(dayOfWeek); if (this.endTimeMode == WALL_TIME) this.endTime = time; else if (this.endTimeMode == STANDARD_TIME) { // Convert from STANDARD to DST this.endTime = time + this.dstSavings; } else { // Convert from UTC to DST this.endTime = time + this.rawOffset + this.dstSavings; } useDaylight = true; } /** * Sets the daylight savings end rule. You must also set the * start rule with setStartRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * Note that this API isn't incredibly well specified. It appears that the * after flag must override the parameters, since normally, the day and * dayofweek can select this. I.e., if day < 0 and dayOfWeek < 0, on or * before mode is chosen. But if after == true, this implementation * overrides the signs of the other arguments. And if dayOfWeek == 0, it * falls back to the behavior in the other APIs. I guess this should be * checked against Sun's implementation. * * @param month The end month of daylight savings. * @param day A day in month, or a day of week in month. * @param dayOfWeek A day of week, when daylight savings ends. * @param time A time in millis in standard time. * @param after If true, day and dayOfWeek specify first day of week on or * after day, else first day of week on or before. */ public void setEndRule(int month, int day, int dayOfWeek, int time, boolean after) { // FIXME: XXX: Validate that checkRule and offset processing work with // on or before mode. this.endDay = after ? Math.abs(day) : -Math.abs(day); this.endDayOfWeek = after ? Math.abs(dayOfWeek) : -Math.abs(dayOfWeek); this.endMode = (dayOfWeek != 0) ? (after ? DOW_GE_DOM_MODE : DOW_LE_DOM_MODE) : checkRule(month, day, dayOfWeek); this.endDay = Math.abs(this.endDay); this.endDayOfWeek = Math.abs(endDayOfWeek); this.endMonth = month; if (this.endTimeMode == WALL_TIME) this.endTime = time; else if (this.endTimeMode == STANDARD_TIME) { // Convert from STANDARD to DST this.endTime = time + this.dstSavings; } else { // Convert from UTC to DST this.endTime = time + this.rawOffset + this.dstSavings; } useDaylight = true; } /** * Sets the daylight savings end rule. You must also set the * start rule with setStartRule or the result of * getOffset is undefined. For the parameters see the ten-argument * constructor above. * * @param month The end month of daylight savings. * @param day A day in month, or a day of week in month. * @param time A time in millis in standard time. */ public void setEndRule(int month, int day, int time) { setEndRule(month, day, 0, time); } /** * Gets the time zone offset, for current date, modified in case of * daylight savings. This is the offset to add to UTC to get the local * time. * * In the standard JDK the results given by this method may result in * inaccurate results at the end of February or the beginning of March. * To avoid this, you should use Calendar instead: * offset = cal.get(Calendar.ZONE_OFFSET) * + cal.get(Calendar.DST_OFFSET); * * This version doesn't suffer this inaccuracy. * * The arguments don't follow the approach for setting start and end rules. * The day must be a positive number and dayOfWeek must be a positive value * from Calendar. dayOfWeek is redundant, but must match the other values * or an inaccurate result may be returned. * * @param era the era of the given date * @param year the year of the given date * @param month the month of the given date, 0 for January. * @param day the day of month * @param dayOfWeek the day of week; this must match the other fields. * @param millis the millis in the day (in local standard time) * @return the time zone offset in milliseconds. * @throws IllegalArgumentException if arguments are incorrect. */ public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) { int daysInMonth = getDaysInMonth(month, year); if (day < 1 || day > daysInMonth) throw new IllegalArgumentException(); if (dayOfWeek < Calendar.SUNDAY || dayOfWeek > Calendar.SATURDAY) throw new IllegalArgumentException(); if (month < Calendar.JANUARY || month > Calendar.DECEMBER) throw new IllegalArgumentException(); // This method is called by Calendar, so we mustn't use that class. int daylightSavings = 0; if (useDaylight && era == 1 && year >= startYear) { // This does only work for Gregorian calendars :-( // This is mainly because setStartYear doesn't take an era. boolean afterStart = ! isBefore(year, month, day, dayOfWeek, millis, startMode, startMonth, startDay, startDayOfWeek, startTime); boolean beforeEnd = isBefore(year, month, day, dayOfWeek, millis + dstSavings, endMode, endMonth, endDay, endDayOfWeek, endTime); if (startMonth < endMonth) { // use daylight savings, if the date is after the start of // savings, and before the end of savings. daylightSavings = afterStart && beforeEnd ? dstSavings : 0; } else { // use daylight savings, if the date is before the end of // savings, or after the start of savings. daylightSavings = beforeEnd || afterStart ? dstSavings : 0; } } return rawOffset + daylightSavings; } /** * Returns the time zone offset to GMT in milliseconds, ignoring * day light savings. * @return the time zone offset. */ public int getRawOffset() { return rawOffset; } /** * Sets the standard time zone offset to GMT. * @param rawOffset The time offset from GMT in milliseconds. */ public void setRawOffset(int rawOffset) { this.rawOffset = rawOffset; } /** * Gets the daylight savings offset. This is a positive offset in * milliseconds with respect to standard time. Typically this * is one hour, but for some time zones this may be half an our. * @return the daylight savings offset in milliseconds. */ public int getDSTSavings() { return dstSavings; } /** * Sets the daylight savings offset. This is a positive offset in * milliseconds with respect to standard time. * * @param dstSavings the daylight savings offset in milliseconds. */ public void setDSTSavings(int dstSavings) { if (dstSavings <= 0) throw new IllegalArgumentException(); this.dstSavings = dstSavings; } /** * Returns if this time zone uses daylight savings time. * @return true, if we use daylight savings time, false otherwise. */ public boolean useDaylightTime() { return useDaylight; } /** * Returns the number of days in the given month. * Uses gregorian rules prior to 1582 (The default and earliest cutover) * @param month The month, zero based; use one of the Calendar constants. * @param year The year. */ private int getDaysInMonth(int month, int year) { if (month == Calendar.FEBRUARY) { if ((year & 3) != 0) return 28; // Assume default Gregorian cutover, // all years prior to this must be Julian if (year < 1582) return 29; // Gregorian rules return ((year % 100) != 0 || (year % 400) == 0) ? 29 : 28; } else return monthArr[month]; } /** * Checks if the date given in calXXXX, is before the change between * dst and standard time. * @param calYear the year of the date to check (for leap day checking). * @param calMonth the month of the date to check. * @param calDayOfMonth the day of month of the date to check. * @param calDayOfWeek the day of week of the date to check. * @param calMillis the millis of day of the date to check (standard time). * @param mode the change mode; same semantic as startMode. * @param month the change month; same semantic as startMonth. * @param day the change day; same semantic as startDay. * @param dayOfWeek the change day of week; * @param millis the change time in millis since midnight standard time. * same semantic as startDayOfWeek. * @return true, if cal is before the change, false if cal is on * or after the change. */ private boolean isBefore(int calYear, int calMonth, int calDayOfMonth, int calDayOfWeek, int calMillis, int mode, int month, int day, int dayOfWeek, int millis) { // This method is called by Calendar, so we mustn't use that class. // We have to do all calculations by hand. // check the months: // XXX - this is not correct: // for the DOW_GE_DOM and DOW_LE_DOM modes the change date may // be in a different month. if (calMonth != month) return calMonth < month; // check the day: switch (mode) { case DOM_MODE: if (calDayOfMonth != day) return calDayOfMonth < day; break; case DOW_IN_MONTH_MODE: { // This computes the day of month of the day of type // "dayOfWeek" that lies in the same (sunday based) week as cal. calDayOfMonth += (dayOfWeek - calDayOfWeek); // Now we convert it to 7 based number (to get a one based offset // after dividing by 7). If we count from the end of the // month, we get want a -7 based number counting the days from // the end: if (day < 0) calDayOfMonth -= getDaysInMonth(calMonth, calYear) + 7; else calDayOfMonth += 6; // day > 0 day < 0 // S M T W T F S S M T W T F S // 7 8 9 10 11 12 -36-35-34-33-32-31 // 13 14 15 16 17 18 19 -30-29-28-27-26-25-24 // 20 21 22 23 24 25 26 -23-22-21-20-19-18-17 // 27 28 29 30 31 32 33 -16-15-14-13-12-11-10 // 34 35 36 -9 -8 -7 // Now we calculate the day of week in month: int week = calDayOfMonth / 7; // day > 0 day < 0 // S M T W T F S S M T W T F S // 1 1 1 1 1 1 -5 -5 -4 -4 -4 -4 // 1 2 2 2 2 2 2 -4 -4 -4 -3 -3 -3 -3 // 2 3 3 3 3 3 3 -3 -3 -3 -2 -2 -2 -2 // 3 4 4 4 4 4 4 -2 -2 -2 -1 -1 -1 -1 // 4 5 5 -1 -1 -1 if (week != day) return week < day; if (calDayOfWeek != dayOfWeek) return calDayOfWeek < dayOfWeek; // daylight savings starts/ends on the given day. break; } case DOW_LE_DOM_MODE: // The greatest sunday before or equal December, 12 // is the same as smallest sunday after or equal December, 6. day = Math.abs(day) - 6; case DOW_GE_DOM_MODE: // Calculate the day of month of the day of type // "dayOfWeek" that lies before (or on) the given date. calDayOfMonth -= (calDayOfWeek < dayOfWeek ? 7 : 0) + calDayOfWeek - dayOfWeek; if (calDayOfMonth < day) return true; if (calDayOfWeek != dayOfWeek || calDayOfMonth >= day + 7) return false; // now we have the same day break; } // the millis decides: return (calMillis < millis); } /** * Generates the hashCode for the SimpleDateFormat object. It is * the rawOffset, possibly, if useDaylightSavings is true, xored * with startYear, startMonth, startDayOfWeekInMonth, ..., endTime. */ public synchronized int hashCode() { return rawOffset ^ (useDaylight ? startMonth ^ startDay ^ startDayOfWeek ^ startTime ^ endMonth ^ endDay ^ endDayOfWeek ^ endTime : 0); } /** * Returns a string representation of this SimpleTimeZone object. * @return a string representation of this SimpleTimeZone object. */ public String toString() { // the test for useDaylight is an incompatibility to jdk1.2, but // I think this shouldn't hurt. return getClass().getName() + "[" + "id=" + getID() + ",offset=" + rawOffset + ",dstSavings=" + dstSavings + ",useDaylight=" + useDaylight + (useDaylight ? ",startYear=" + startYear + ",startMode=" + startMode + ",startMonth=" + startMonth + ",startDay=" + startDay + ",startDayOfWeek=" + startDayOfWeek + ",startTime=" + startTime + ",startTimeMode=" + startTimeMode + ",endMode=" + endMode + ",endMonth=" + endMonth + ",endDay=" + endDay + ",endDayOfWeek=" + endDayOfWeek + ",endTime=" + endTime + ",endTimeMode=" + endTimeMode : "") + "]"; } }