Skip to content

Commit

Permalink
ICU-21283 Fix Java for calendar bugs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
FrankYFTang committed Sep 17, 2020
1 parent 4583c1e commit 5769803
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 79 deletions.
90 changes: 13 additions & 77 deletions icu4j/main/classes/core/src/com/ibm/icu/util/IndianCalendar.java
Expand Up @@ -341,7 +341,7 @@ protected int handleGetMonthLength(int extendedYear, int month) {
month = remainder[0];
}

if(isGregorianLeap(extendedYear + INDIAN_ERA_START) && month == 0) {
if(isGregorianLeapYear(extendedYear + INDIAN_ERA_START) && month == 0) {
return 31;
}

Expand All @@ -359,20 +359,20 @@ protected int handleGetMonthLength(int extendedYear, int month) {
protected void handleComputeFields(int julianDay){
double jdAtStartOfGregYear;
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
IndianYear = gregorianDay[0] - INDIAN_ERA_START; // Year in Saka era
jdAtStartOfGregYear = gregorianToJD(gregorianDay[0], 1, 1); // JD at start of Gregorian year
jdAtStartOfGregYear = gregorianToJD(gregorianYear, 0 /* first month in 0 base */, 1); // JD at start of Gregorian year
yday = (int)(julianDay - jdAtStartOfGregYear); // Day number in Gregorian year (starting from 0)

if (yday < INDIAN_YEAR_START) {
// Day is at the end of the preceding Saka year
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;
} 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;
}

Expand Down Expand Up @@ -465,19 +465,19 @@ protected int handleComputeMonthStart(int year, int month, boolean useMonth) {
* @param month The month according to Indian calendar (between 1 to 12)
* @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;
double start, jd;

gyear = year + INDIAN_ERA_START;


if(isGregorianLeap(gyear)) {
if(isGregorianLeapYear(gyear)) {
leapMonth = 31;
start = gregorianToJD(gyear, 3, 21);
start = gregorianToJD(gyear, 2 /* third month in 0 based */, 21);
} else {
leapMonth = 30;
start = gregorianToJD(gyear, 3, 22);
start = gregorianToJD(gyear, 2 /* third month in 0 based */, 22);
}

if (month == 1) {
Expand All @@ -504,74 +504,10 @@ private static double IndianToJD(int year, int month, int date) {
* @param month The month according to Gregorian calendar (between 0 to 11)
* @param date The date in month
*/
private static double gregorianToJD(int year, int month, int date) {
double JULIAN_EPOCH = 1721425.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)));
private double gregorianToJD(int year, int month, int date) {
return computeGregorianMonthStart(year, month) + date - 0.5;
}


/**
* {@inheritDoc}
* @stable ICU 3.8
Expand Down
Expand Up @@ -872,8 +872,8 @@ protected void handleComputeFields(int julianDay) {
months--;
}

year = months / 12 + 1;
month = months % 12;
year = months >= 0 ? ((months / 12) + 1) : ((months + 1 ) / 12);
month = ((months % 12) + 12 ) % 12;
} else if (cType == CalculationType.ISLAMIC_UMALQURA) {
long umalquraStartdays = yearStart(UMALQURA_YEAR_START);
if( days < umalquraStartdays) {
Expand Down
Expand Up @@ -28,6 +28,9 @@
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ChineseCalendar;
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.TaiwanCalendar;
import com.ibm.icu.util.TimeZone;
Expand Down Expand Up @@ -2013,4 +2016,221 @@ public void run(){
StubSimpleDateFormat stub = new StubSimpleDateFormat("EEE MMM dd yyyy G HH:mm:ss.SSS", Locale.US);
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.");
}
}
}

0 comments on commit 5769803

Please sign in to comment.