ICU-5470 fix time zone calculations for late February, taking leap years into account

X-SVN-Rev: 20630
This commit is contained in:
Markus Scherer 2006-11-03 23:43:40 +00:00
parent fcd1805872
commit 868c08ebc4
4 changed files with 174 additions and 24 deletions

View file

@ -1215,6 +1215,90 @@ public class TimeZoneTest extends TestFmwk
//reset
java.util.TimeZone.setDefault(save);
}
// Copied from the protected constant in TimeZone.
private static final int MILLIS_PER_HOUR = 60*60*1000;
// Test that a transition at the end of February is handled correctly.
public void TestFebruary() {
// Time zone with daylight savings time from the first Sunday in November
// to the last Sunday in February.
// Similar to the new rule for Brazil (Sao Paulo) in tzdata2006n.
SimpleTimeZone tz1 = new SimpleTimeZone(
-3 * MILLIS_PER_HOUR, // raw offset: 3h before (west of) GMT
"nov-feb",
Calendar.NOVEMBER, 1, Calendar.SUNDAY, // start: November, first, Sunday
0, // midnight wall time
Calendar.FEBRUARY, -1, Calendar.SUNDAY, // end: February, last, Sunday
0); // midnight wall time
// Time zone for Brazil, with effectively the same rules as above,
// but expressed with DOW_GE_DOM_MODE and DOW_LE_DOM_MODE rules.
TimeZone tz2 = TimeZone.getTimeZone("America/Sao_Paulo");
// Now hardcode the same rules as for Brazil, so that we cover the intended code
// even when in the future zoneinfo hardcodes these transition dates.
SimpleTimeZone tz3= new SimpleTimeZone(
-3 * MILLIS_PER_HOUR, // raw offset: 3h before (west of) GMT
"nov-feb2",
Calendar.NOVEMBER, 1, -Calendar.SUNDAY, // start: November, 1 or after, Sunday
0, // midnight wall time
Calendar.FEBRUARY, -29, -Calendar.SUNDAY,// end: February, 29 or before, Sunday
0); // midnight wall time
// Gregorian calendar with the UTC time zone for getting sample test date/times.
GregorianCalendar gc = new GregorianCalendar(TimeZone.getTimeZone("Etc/GMT"));
// "Unable to create the UTC calendar: %s"
int[] data = {
// UTC time (6 fields) followed by
// expected time zone offset in hours after GMT (negative=before GMT).
// int year, month, day, hour, minute, second, offsetHours
2006, Calendar.NOVEMBER, 5, 02, 59, 59, -3,
2006, Calendar.NOVEMBER, 5, 03, 00, 00, -2,
2007, Calendar.FEBRUARY, 25, 01, 59, 59, -2,
2007, Calendar.FEBRUARY, 25, 02, 00, 00, -3,
2007, Calendar.NOVEMBER, 4, 02, 59, 59, -3,
2007, Calendar.NOVEMBER, 4, 03, 00, 00, -2,
2008, Calendar.FEBRUARY, 24, 01, 59, 59, -2,
2008, Calendar.FEBRUARY, 24, 02, 00, 00, -3,
2008, Calendar.NOVEMBER, 2, 02, 59, 59, -3,
2008, Calendar.NOVEMBER, 2, 03, 00, 00, -2,
2009, Calendar.FEBRUARY, 22, 01, 59, 59, -2,
2009, Calendar.FEBRUARY, 22, 02, 00, 00, -3,
2009, Calendar.NOVEMBER, 1, 02, 59, 59, -3,
2009, Calendar.NOVEMBER, 1, 03, 00, 00, -2,
2010, Calendar.FEBRUARY, 28, 01, 59, 59, -2,
2010, Calendar.FEBRUARY, 28, 02, 00, 00, -3
};
TimeZone timezones[] = { tz1, tz2, tz3 };
TimeZone tz;
Date dt;
int t, i, raw, dst;
int[] offsets = new int[2]; // raw = offsets[0], dst = offsets[1]
for (t = 0; t < timezones.length; ++t) {
tz = timezones[t];
for (i = 0; i < data.length; i+=7) {
gc.set(data[i], data[i+1], data[i+2],
data[i+3], data[i+4], data[i+5]);
dt = gc.getTime();
tz.getOffset(dt.getTime(), false, offsets);
raw = offsets[0];
dst = offsets[1];
if ((raw + dst) != data[i+6] * MILLIS_PER_HOUR) {
errln("test case " + t + "." + (i/7) + ": " +
"tz.getOffset(" + data[i] + "-" + (data[i+1] + 1) + "-" + data[i+2] + " " +
data[i+3] + ":" + data[i+4] + ":" + data[i+5] +
") returns " + raw + "+" + dst + " != " + data[i+6] * MILLIS_PER_HOUR);
}
}
}
}
}
//eof

View file

@ -0,0 +1,60 @@
/**
*******************************************************************************
* Copyright (C) 2003-2006, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
* Partial port from ICU4C's Grego class in i18n/gregoimp.h.
*
* Methods ported, or moved here from OlsonTimeZone, initially
* for work on Jitterbug 5470:
* tzdata2006n Brazil incorrect fall-back date 2009-mar-01
* Only the methods necessary for that work are provided - this is not a full
* port of ICU4C's Grego class (yet).
*
* These utilities are used by both OlsonTimeZone and SimpleTimeZone.
*/
package com.ibm.icu.impl;
/**
* A utility class providing proleptic Gregorian calendar functions
* used by time zone and calendar code. Do not instantiate.
*
* Note: Unlike GregorianCalendar, all computations performed by this
* class occur in the pure proleptic GregorianCalendar.
*/
public class Grego {
/**
* Return true if the given year is a leap year.
* @param year Gregorian year, with 0 == 1 BCE, -1 == 2 BCE, etc.
* @return true if the year is a leap year
*/
public static final boolean isLeapYear(int year) {
// year&0x3 == year%4
return ((year&0x3) == 0) && ((year%100 != 0) || (year%400 == 0));
}
private static final int[] MONTH_LENGTH = new int[] {
31,28,31,30,31,30,31,31,30,31,30,31,
31,29,31,30,31,30,31,31,30,31,30,31
};
/**
* Return the number of days in the given month.
* @param year Gregorian year, with 0 == 1 BCE, -1 == 2 BCE, etc.
* @param month 0-based month, with 0==Jan
* @return the number of days in the given month
*/
public static final int monthLength(int year, int month) {
return MONTH_LENGTH[month + (isLeapYear(year) ? 12 : 0)];
}
/**
* Return the length of a previous month of the Gregorian calendar.
* @param y the extended year
* @param m the 0-based month number
* @return the number of days in the month previous to the given month
*/
public static final int previousMonthLength(int y, int m) {
return (m > 0) ? monthLength(y, m-1) : 31;
}
}

View file

@ -8,6 +8,8 @@ package com.ibm.icu.impl;
import java.util.Date;
import com.ibm.icu.impl.Grego;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.SimpleTimeZone;
@ -117,7 +119,7 @@ public class OlsonTimeZone extends TimeZone {
if (month < Calendar.JANUARY || month > Calendar.DECEMBER) {
throw new IllegalArgumentException("Month is not in the legal range: " +month);
} else {
return getOffset(era, year, month, day, dayOfWeek, milliseconds,MONTH_LENGTH[month + (isLeapYear(year)?12:0)]);
return getOffset(era, year, month, day, dayOfWeek, milliseconds, Grego.monthLength(year, month));
}
}
@ -625,8 +627,6 @@ public class OlsonTimeZone extends TimeZone {
private static final int[] DAYS_BEFORE = new int[] {0,31,59,90,120,151,181,212,243,273,304,334,
0,31,60,91,121,152,182,213,244,274,305,335};
private static final int[] MONTH_LENGTH = new int[]{31,28,31,30,31,30,31,31,30,31,30,31,
31,29,31,30,31,30,31,31,30,31,30,31};
private static final int JULIAN_1_CE = 1721426; // January 1, 1 CE Gregorian
private static final int JULIAN_1970_CE = 2440588; // January 1, 1970 CE Gregorian
private static final int MILLIS_PER_SECOND = 1000;
@ -636,14 +636,10 @@ public class OlsonTimeZone extends TimeZone {
int y = year - 1;
double julian = 365 * y + myFloorDivide(y, 4) + (JULIAN_1_CE - 3) + // Julian cal
myFloorDivide(y, 400) - myFloorDivide(y, 100) + 2 + // => Gregorian cal
DAYS_BEFORE[month + (isLeapYear(year) ? 12 : 0)] + dom; // => month/dom
DAYS_BEFORE[month + (Grego.isLeapYear(year) ? 12 : 0)] + dom; // => month/dom
return julian - JULIAN_1970_CE; // JD => epoch day
}
private static final boolean isLeapYear(int year) {
// year&0x3 == year%4
return ((year&0x3) == 0) && ((year%100 != 0) || (year%400 == 0));
}
private static ICUResourceBundle loadRule(ICUResourceBundle top, String ruleid) {
ICUResourceBundle r = top.get("Rules");
@ -699,7 +695,7 @@ public class OlsonTimeZone extends TimeZone {
++year;
}
boolean isLeap = isLeapYear(year);
boolean isLeap = Grego.isLeapYear(year);
// Gregorian day zero is a Monday.
dow = (int) ((day + 1) % 7);

View file

@ -4,6 +4,8 @@
*/
package com.ibm.icu.util;
import com.ibm.icu.impl.Grego;
import com.ibm.icu.impl.JDKTimeZone;
import java.io.IOException;
@ -506,14 +508,18 @@ public class SimpleTimeZone extends JDKTimeZone {
}
return xinfo;
}
// WARNING: assumes that no rule is measured from the end of February,
// since we don't handle leap years. Could handle assuming always
// Gregorian, since we know they didn't have daylight time when
// Gregorian calendar started.
// private static final int[] STATICMONTHLENGTH = new int[]{31,29,31,30,31,30,31,31,30,31,30,31};
// private final byte monthLength[] = staticMonthLength;
// Use only for decodeStartRule() and decodeEndRule() where the year is not
// available. Set February to 29 days to accomodate rules with that date
// and day-of-week-on-or-before-that-date mode (DOW_LE_DOM_MODE).
// The compareToRule() method adjusts to February 28 in non-leap years.
//
// For actual getOffset() calculations, use TimeZone::monthLength() and
// TimeZone::previousMonthLength() which take leap years into account.
// We handle leap years assuming always
// Gregorian, since we know they didn't have daylight time when
// Gregorian calendar started.
private final static byte staticMonthLength[] = {31,29,31,30,31,30,31,31,30,31,30,31};
// private final static byte staticLeapMonthLength[] = {31,29,31,30,31,30,31,31,30,31,30,31};
// -------------------------------------
@ -524,7 +530,7 @@ public class SimpleTimeZone extends JDKTimeZone {
public int getOffset(int era, int year, int month, int day,
int dayOfWeek, int millis)
{
// Check the month before indexing into STATICMONTHLENGTH. This
// Check the month before calling Grego.monthLength(). This
// duplicates the test that occurs in the 7-argument getOffset(),
// however, this is unavoidable. We don't mind because this method, in
// fact, should not be called; internal code should always call the
@ -535,7 +541,7 @@ public class SimpleTimeZone extends JDKTimeZone {
throw new IllegalArgumentException();
}
return getOffset(era, year, month, day, dayOfWeek, millis, staticMonthLength[month]);
return getOffset(era, year, month, day, dayOfWeek, millis, Grego.monthLength(year, month));
}
/**
@ -545,7 +551,7 @@ public class SimpleTimeZone extends JDKTimeZone {
public int getOffset(int era, int year, int month, int day,
int dayOfWeek, int millis,
int monthLength) {
// Check the month before indexing into STATICMONTHLENGTH. This
// Check the month before calling Grego.monthLength(). This
// duplicates a test that occurs in the 9-argument getOffset(),
// however, this is unavoidable. We don't mind because this method, in
// fact, should not be called; internal code should always call the
@ -556,11 +562,8 @@ public class SimpleTimeZone extends JDKTimeZone {
throw new IllegalArgumentException();
}
// TODO FIX We don't handle leap years yet!
int prevMonthLength = (month >= 1) ? staticMonthLength[month - 1] : 31;
return getOffset(era, year, month, day, dayOfWeek, millis,
monthLength, prevMonthLength);
Grego.monthLength(year, month), Grego.previousMonthLength(year, month));
}
int getOffset(int era, int year, int month, int day,
@ -713,7 +716,7 @@ public class SimpleTimeZone extends JDKTimeZone {
dayOfWeek = 1 + (dayOfWeek % 7); // dayOfWeek is one-based
if (dayOfMonth > monthLen) {
dayOfMonth = 1;
/* When incrementing the month, it is desirible to overflow
/* When incrementing the month, it is desirable to overflow
* from DECEMBER to DECEMBER+1, since we use the result to
* compare against a real month. Wraparound of the value
* leads to bug 4173604. */
@ -734,6 +737,12 @@ public class SimpleTimeZone extends JDKTimeZone {
else if (month > ruleMonth) return 1;
int ruleDayOfMonth = 0;
// Adjust the ruleDay to the monthLen, for non-leap year February 29 rule days.
if (ruleDay > monthLen) {
ruleDay = monthLen;
}
switch (ruleMode)
{
case DOM_MODE:
@ -773,6 +782,7 @@ public class SimpleTimeZone extends JDKTimeZone {
return 0;
}
}
// data needed for streaming mutated SimpleTimeZones in JDK14
private int raw;// the TimeZone's raw GMT offset
private int dst = 3600000;