mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-08 06:53:45 +00:00
ICU-5470 fix time zone calculations for late February, taking leap years into account
X-SVN-Rev: 20630
This commit is contained in:
parent
fcd1805872
commit
868c08ebc4
4 changed files with 174 additions and 24 deletions
|
@ -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
|
||||
|
|
60
icu4j/src/com/ibm/icu/impl/Grego.java
Normal file
60
icu4j/src/com/ibm/icu/impl/Grego.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue