From 868c08ebc41886de0337c7038de1ae8c7f9e5118 Mon Sep 17 00:00:00 2001 From: Markus Scherer Date: Fri, 3 Nov 2006 23:43:40 +0000 Subject: [PATCH] ICU-5470 fix time zone calculations for late February, taking leap years into account X-SVN-Rev: 20630 --- .../icu/dev/test/timezone/TimeZoneTest.java | 84 +++++++++++++++++++ icu4j/src/com/ibm/icu/impl/Grego.java | 60 +++++++++++++ icu4j/src/com/ibm/icu/impl/OlsonTimeZone.java | 14 ++-- .../src/com/ibm/icu/util/SimpleTimeZone.java | 40 +++++---- 4 files changed, 174 insertions(+), 24 deletions(-) create mode 100644 icu4j/src/com/ibm/icu/impl/Grego.java diff --git a/icu4j/src/com/ibm/icu/dev/test/timezone/TimeZoneTest.java b/icu4j/src/com/ibm/icu/dev/test/timezone/TimeZoneTest.java index 27fe378ed04..281cf8ef577 100755 --- a/icu4j/src/com/ibm/icu/dev/test/timezone/TimeZoneTest.java +++ b/icu4j/src/com/ibm/icu/dev/test/timezone/TimeZoneTest.java @@ -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 diff --git a/icu4j/src/com/ibm/icu/impl/Grego.java b/icu4j/src/com/ibm/icu/impl/Grego.java new file mode 100644 index 00000000000..eec9ed4b881 --- /dev/null +++ b/icu4j/src/com/ibm/icu/impl/Grego.java @@ -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; + } +} diff --git a/icu4j/src/com/ibm/icu/impl/OlsonTimeZone.java b/icu4j/src/com/ibm/icu/impl/OlsonTimeZone.java index fb8f500253e..dc242bcf565 100644 --- a/icu4j/src/com/ibm/icu/impl/OlsonTimeZone.java +++ b/icu4j/src/com/ibm/icu/impl/OlsonTimeZone.java @@ -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); diff --git a/icu4j/src/com/ibm/icu/util/SimpleTimeZone.java b/icu4j/src/com/ibm/icu/util/SimpleTimeZone.java index 177f012ba87..e592ac5a7f9 100755 --- a/icu4j/src/com/ibm/icu/util/SimpleTimeZone.java +++ b/icu4j/src/com/ibm/icu/util/SimpleTimeZone.java @@ -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;