ICU-21283 Fix Java for calendar bugs

This is the java port of ICU-21043 (for C++)
This PR fixes
ICU-21043 Erroneous date display in indian calendar of all dates prior to 0001-01-01.
ICU-21044 Hebrew Calendar calculation is incorrect when the year < 1
ICU-21045 Erroneous date display in islamic and islamic-rgsa calendars of all dates prior to 0622-07-18.
ICU-21046 Erroneous date display in islamic-umalqura calendar of all dates prior to -195366-07-23.

The problem in the IndianCalendarl is
ICU-21043 the gregorian/julain convesion is wrong. Swith to use the
calculation function in the Calendar class.

The problem in the HebrewCalendar is
ICU-21044 the use of bulit in / is wrong when the year or month could be < 1.

The problem in the IslamicCalendar is

ICU-21045: The math of % negative number for year and month is wrong.
Also add tests to exhaust test 8000 years for all calendar. In quick
mode, only test 2.5 years.

reduce the number of date in quick mode
This commit is contained in:
Frank Tang 2020-09-16 20:22:51 -07:00 committed by Frank Yung-Fong Tang
parent 4583c1e77c
commit 5769803253
3 changed files with 235 additions and 79 deletions

View File

@ -341,7 +341,7 @@ public class IndianCalendar extends Calendar {
month = remainder[0]; month = remainder[0];
} }
if(isGregorianLeap(extendedYear + INDIAN_ERA_START) && month == 0) { if(isGregorianLeapYear(extendedYear + INDIAN_ERA_START) && month == 0) {
return 31; return 31;
} }
@ -359,20 +359,20 @@ public class IndianCalendar extends Calendar {
protected void handleComputeFields(int julianDay){ protected void handleComputeFields(int julianDay){
double jdAtStartOfGregYear; double jdAtStartOfGregYear;
int leapMonth, IndianYear, yday, IndianMonth, IndianDayOfMonth, mday; int leapMonth, IndianYear, yday, IndianMonth, IndianDayOfMonth, mday;
int[] gregorianDay; // Stores gregorian date corresponding to Julian day; computeGregorianFields(julianDay);
int gregorianYear = getGregorianYear(); // Stores gregorian date corresponding to Julian day;
IndianYear = gregorianYear - INDIAN_ERA_START; // Year in Saka era
gregorianDay = jdToGregorian(julianDay); // Gregorian date for Julian day jdAtStartOfGregYear = gregorianToJD(gregorianYear, 0 /* first month in 0 base */, 1); // JD at start of Gregorian year
IndianYear = gregorianDay[0] - INDIAN_ERA_START; // Year in Saka era
jdAtStartOfGregYear = gregorianToJD(gregorianDay[0], 1, 1); // JD at start of Gregorian year
yday = (int)(julianDay - jdAtStartOfGregYear); // Day number in Gregorian year (starting from 0) yday = (int)(julianDay - jdAtStartOfGregYear); // Day number in Gregorian year (starting from 0)
if (yday < INDIAN_YEAR_START) { if (yday < INDIAN_YEAR_START) {
// Day is at the end of the preceding Saka year // Day is at the end of the preceding Saka year
IndianYear -= 1; IndianYear -= 1;
leapMonth = isGregorianLeap(gregorianDay[0] - 1) ? 31 : 30; // Days in leapMonth this year, previous Gregorian year leapMonth = isGregorianLeapYear(gregorianYear - 1) ? 31 : 30; // Days in leapMonth this year, previous Gregorian year
yday += leapMonth + (31 * 5) + (30 * 3) + 10; yday += leapMonth + (31 * 5) + (30 * 3) + 10;
} else { } else {
leapMonth = isGregorianLeap(gregorianDay[0]) ? 31 : 30; // Days in leapMonth this year leapMonth = isGregorianLeapYear(gregorianYear) ? 31 : 30; // Days in leapMonth this year
yday -= INDIAN_YEAR_START; yday -= INDIAN_YEAR_START;
} }
@ -465,19 +465,19 @@ public class IndianCalendar extends Calendar {
* @param month The month according to Indian calendar (between 1 to 12) * @param month The month according to Indian calendar (between 1 to 12)
* @param date The date in month * @param date The date in month
*/ */
private static double IndianToJD(int year, int month, int date) { private double IndianToJD(int year, int month, int date) {
int leapMonth, gyear, m; int leapMonth, gyear, m;
double start, jd; double start, jd;
gyear = year + INDIAN_ERA_START; gyear = year + INDIAN_ERA_START;
if(isGregorianLeap(gyear)) { if(isGregorianLeapYear(gyear)) {
leapMonth = 31; leapMonth = 31;
start = gregorianToJD(gyear, 3, 21); start = gregorianToJD(gyear, 2 /* third month in 0 based */, 21);
} else { } else {
leapMonth = 30; leapMonth = 30;
start = gregorianToJD(gyear, 3, 22); start = gregorianToJD(gyear, 2 /* third month in 0 based */, 22);
} }
if (month == 1) { if (month == 1) {
@ -504,74 +504,10 @@ public class IndianCalendar extends Calendar {
* @param month The month according to Gregorian calendar (between 0 to 11) * @param month The month according to Gregorian calendar (between 0 to 11)
* @param date The date in month * @param date The date in month
*/ */
private static double gregorianToJD(int year, int month, int date) { private double gregorianToJD(int year, int month, int date) {
double JULIAN_EPOCH = 1721425.5; return computeGregorianMonthStart(year, month) + date - 0.5;
int y = year - 1;
int result = (365 * y)
+ (y / 4)
- (y / 100)
+ (y / 400)
+ (((367 * month) - 362) / 12)
+ ((month <= 2) ? 0 : (isGregorianLeap(year) ? -1 : -2))
+ date;
return result - 1 + JULIAN_EPOCH;
}
/*
* The following function is not needed for basic calendar functioning.
* This routine converts a julian day (jd) to the corresponding date in Gregorian calendar"
* @param jd The Julian date in Julian Calendar which is to be converted to Indian date"
*/
private static int[] jdToGregorian(double jd) {
double JULIAN_EPOCH = 1721425.5;
double wjd, depoch, quadricent, dqc, cent, dcent, quad, dquad, yindex, yearday, leapadj;
int year, month, day;
wjd = Math.floor(jd - 0.5) + 0.5;
depoch = wjd - JULIAN_EPOCH;
quadricent = Math.floor(depoch / 146097);
dqc = depoch % 146097;
cent = Math.floor(dqc / 36524);
dcent = dqc % 36524;
quad = Math.floor(dcent / 1461);
dquad = dcent % 1461;
yindex = Math.floor(dquad / 365);
year = (int)((quadricent * 400) + (cent * 100) + (quad * 4) + yindex);
if (!((cent == 4) || (yindex == 4))) {
year++;
}
yearday = wjd - gregorianToJD(year, 1, 1);
leapadj = ((wjd < gregorianToJD(year, 3, 1)) ? 0
:
(isGregorianLeap(year) ? 1 : 2)
);
month = (int)Math.floor((((yearday + leapadj) * 12) + 373) / 367);
day = (int)(wjd - gregorianToJD(year, month, 1)) + 1;
int[] julianDate = new int[3];
julianDate[0] = year;
julianDate[1] = month;
julianDate[2] = day;
return julianDate;
}
/*
* The following function is not needed for basic calendar functioning.
* This routine checks if the Gregorian year is a leap year"
* @param year The year in Gregorian Calendar
*/
private static boolean isGregorianLeap(int year)
{
return ((year % 4) == 0) &&
(!(((year % 100) == 0) && ((year % 400) != 0)));
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* @stable ICU 3.8 * @stable ICU 3.8

View File

@ -872,8 +872,8 @@ public class IslamicCalendar extends Calendar {
months--; months--;
} }
year = months / 12 + 1; year = months >= 0 ? ((months / 12) + 1) : ((months + 1 ) / 12);
month = months % 12; month = ((months % 12) + 12 ) % 12;
} else if (cType == CalculationType.ISLAMIC_UMALQURA) { } else if (cType == CalculationType.ISLAMIC_UMALQURA) {
long umalquraStartdays = yearStart(UMALQURA_YEAR_START); long umalquraStartdays = yearStart(UMALQURA_YEAR_START);
if( days < umalquraStartdays) { if( days < umalquraStartdays) {

View File

@ -28,6 +28,9 @@ import com.ibm.icu.util.BuddhistCalendar;
import com.ibm.icu.util.Calendar; import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ChineseCalendar; import com.ibm.icu.util.ChineseCalendar;
import com.ibm.icu.util.GregorianCalendar; import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.HebrewCalendar;
import com.ibm.icu.util.IndianCalendar;
import com.ibm.icu.util.IslamicCalendar;
import com.ibm.icu.util.JapaneseCalendar; import com.ibm.icu.util.JapaneseCalendar;
import com.ibm.icu.util.TaiwanCalendar; import com.ibm.icu.util.TaiwanCalendar;
import com.ibm.icu.util.TimeZone; import com.ibm.icu.util.TimeZone;
@ -2013,4 +2016,221 @@ public class IBMCalendarTest extends CalendarTestFmwk {
StubSimpleDateFormat stub = new StubSimpleDateFormat("EEE MMM dd yyyy G HH:mm:ss.SSS", Locale.US); StubSimpleDateFormat stub = new StubSimpleDateFormat("EEE MMM dd yyyy G HH:mm:ss.SSS", Locale.US);
stub.run(); stub.run();
} }
@Test
public void TestConsistencyGregorian() {
checkConsistency("en@calendar=gregorian");
}
@Test
public void TestConsistencyIndian() {
checkConsistency("en@calendar=indian");
}
@Test
public void TestConsistencyHebrew() {
checkConsistency("en@calendar=hebrew");
}
@Test
public void TestConsistencyIslamic() {
checkConsistency("en@calendar=islamic");
}
@Test
public void TestConsistencyIslamicRGSA() {
checkConsistency("en@calendar=islamic-rgsa");
}
@Test
public void TestConsistencyIslamicTBLA() {
checkConsistency("en@calendar=islamic-tbla");
}
@Test
public void TestConsistencyIslamicUmalqura() {
checkConsistency("en@calendar=islamic-umalqura");
}
@Test
public void TestConsistencyIslamicCivil() {
checkConsistency("en@calendar=islamic-civil");
}
@Test
public void TestConsistencyCoptic() {
checkConsistency("en@calendar=coptic");
}
@Test
public void TestConsistencyEthiopic() {
checkConsistency("en@calendar=ethiopic");
}
@Test
public void TestConsistencyROC() {
checkConsistency("en@calendar=roc");
}
@Test
public void TestConsistencyChinese() {
checkConsistency("en@calendar=chinese");
}
@Test
public void TestConsistencyDangi() {
checkConsistency("en@calendar=dangi");
}
@Test
public void TestConsistencyPersian() {
checkConsistency("en@calendar=persian");
}
@Test
public void TestConsistencyBuddhist() {
checkConsistency("en@calendar=buddhist");
}
@Test
public void TestConsistencyJapanese() {
checkConsistency("en@calendar=japanese");
}
@Test
public void TestConsistencyEthiopicAmeteAlem() {
checkConsistency("en@calendar=ethiopic-amete-alem");
}
public void checkConsistency(String locale) {
boolean quick = getExhaustiveness() <= 5;
// Check 3 years in quick mode and 8000 years in exhaustive mode.
int numOfDaysToTest = (quick ? 3 * 365 : 8000 * 365);
int msInADay = 1000*60*60*24;
// g is just for debugging messages.
Calendar g = new GregorianCalendar(TimeZone.GMT_ZONE, ULocale.ENGLISH);
Calendar base = Calendar.getInstance(TimeZone.GMT_ZONE, new ULocale(locale));
Date test = Calendar.getInstance().getTime();
Calendar r = (Calendar)base.clone();
int lastDay = 1;
for (int j = 0; j < numOfDaysToTest; j++, test.setTime(test.getTime() - msInADay)) {
g.setTime(test);
base.clear();
base.setTime(test);
// First, we verify the date from base is decrease one day from the
// last day unless the last day is 1.
int cday = base.get(Calendar.DAY_OF_MONTH);
if (lastDay == 1) {
lastDay = cday;
} else {
if (cday != lastDay-1) {
// Ignore if it is the last day before Gregorian Calendar switch on
// 1582 Oct 4
if ( g.get(Calendar.YEAR) == 1582 && (g.get(Calendar.MONTH) + 1) == 10 &&
g.get(Calendar.DAY_OF_MONTH) == 4) {
lastDay = 5;
} else {
errln("Day is not one less from previous date for Gregorian(e=" +
g.get(Calendar.ERA) + " " + g.get(Calendar.YEAR) + "/" +
(g.get(Calendar.MONTH) + 1) + "/" + g.get(Calendar.DAY_OF_MONTH) +
") " + locale + "(" +
base.get(Calendar.ERA) + " " + base.get(Calendar.YEAR) + "/" +
(base.get(Calendar.MONTH) + 1 ) + "/" + base.get(Calendar.DAY_OF_MONTH) +
")");
}
}
lastDay--;
}
// Second, we verify the month is in reasonale range.
int cmonth = base.get(Calendar.MONTH);
if (cmonth < 0 || cmonth > 13) {
errln("Month is out of range Gregorian(e=" + g.get(Calendar.ERA) + " " +
g.get(Calendar.YEAR) + "/" + (g.get(Calendar.MONTH) + 1) + "/" +
g.get(Calendar.DAY_OF_MONTH) + ") " + locale + "(" + base.get(Calendar.ERA) +
" " + base.get(Calendar.YEAR) + "/" + (base.get(Calendar.MONTH) + 1 ) + "/" +
base.get(Calendar.DAY_OF_MONTH) + ")");
}
// Third, we verify the set function can round trip the time back.
r.clear();
for (int f = 0; f < base.getFieldCount(); f++) {
r.set(f, base.get(f));
}
Date result = r.getTime();
if (!test.equals(result)) {
errln("Round trip conversion produces different time from " + test + " to " +
result + " delta: " + (result.getTime() - test.getTime()) +
" Gregorian(e=" + g.get(Calendar.ERA) + " " + g.get(Calendar.YEAR) + "/" +
(g.get(Calendar.MONTH) + 1) + "/" + g.get(Calendar.DAY_OF_MONTH) + ") ");
}
}
}
@Test
public void TestBug21043Indian() {
Calendar cal = new IndianCalendar(ULocale.ENGLISH);
Calendar g = new GregorianCalendar(ULocale.ENGLISH);
// set to 10 BC
g.set(Calendar.ERA, GregorianCalendar.BC);
g.set(10, 1, 1);
cal.setTime(g.getTime());
int m = cal.get(Calendar.MONTH);
if (m < 0 || m > 11) {
errln("Month (" + m + ") should be between 0 and 11 in India calendar");
}
}
@Test
public void TestBug21044Hebrew() {
Calendar cal = new HebrewCalendar(ULocale.ENGLISH);
Calendar g = new GregorianCalendar(ULocale.ENGLISH);
// set to 3771/10/27 BC which is before 3760 BC.
g.set(Calendar.ERA, GregorianCalendar.BC);
g.set(3771, 9, 27);
cal.setTime(g.getTime());
int y = cal.get(Calendar.YEAR);
int m = cal.get(Calendar.MONTH);
int d = cal.get(Calendar.DATE);
if (y > 0 || m < 0 || m > 12 || d < 0 || d > 32) {
errln("Out of rage!\nYear " + y + " should be " +
"negative number before 1AD.\nMonth " + m + " should " +
"be between 0 and 12 in Hebrew calendar.\nDate " + d +
" should be between 0 and 32 in Islamic calendar.");
}
}
@Test
public void TestBug21045Islamic() {
Calendar cal = new IslamicCalendar(ULocale.ENGLISH);
Calendar g = new GregorianCalendar(ULocale.ENGLISH);
// set to 500 AD before 622 AD.
g.set(Calendar.ERA, GregorianCalendar.AD);
g.set(500, 1, 1);
cal.setTime(g.getTime());
int m = cal.get(Calendar.MONTH);
if (m < 0 || m > 11) {
errln("Month (" + m + ") should be between 0 and 11 in Islamic calendar");
}
}
@Test
public void TestBug21046IslamicUmalqura() {
IslamicCalendar cal = new IslamicCalendar(ULocale.ENGLISH);
cal.setCalculationType(IslamicCalendar.CalculationType.ISLAMIC_UMALQURA);
Calendar g = new GregorianCalendar(ULocale.ENGLISH);
// set to 195366 BC
g.set(Calendar.ERA, GregorianCalendar.BC);
g.set(195366, 1, 1);
cal.setTime(g.getTime());
int y = cal.get(Calendar.YEAR);
int m = cal.get(Calendar.MONTH);
int d = cal.get(Calendar.DATE);
if (y > 0 || m < 0 || m > 11 || d < 0 || d > 32) {
errln("Out of rage!\nYear " + y + " should be " +
"negative number before 1AD.\nMonth " + m + " should " +
"be between 0 and 11 in Islamic calendar.\nDate " + d +
" should be between 0 and 32 in Islamic calendar.");
}
}
} }