Skip to content

Commit

Permalink
ICU-22407 Park Chinese half way
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankYFTang committed Jun 29, 2023
1 parent 238dd4f commit 8de21c2
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 11 deletions.
57 changes: 47 additions & 10 deletions icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,14 @@ public class ChineseCalendar extends Calendar {
private transient CalendarCache newYearCache = new CalendarCache();

/**
* True if the current year is a leap year. Updated with each time to
* fields resolution.
* True if there is a leap month between the Winter Solstice before and after the
* current date.This is different from leap year because in some year, such as
* 1813 and 2033, the leap month is after the Winter Solstice of that year. So
* this value could be false for a date prior to the Winter Solstice of that
* year but that year still has a leap month and therefor is a leap year.
* @see #computeChineseFields
*/
private transient boolean isLeapYear;
private transient boolean hasLeapMonthBetweenWinterSolstices;

//------------------------------------------------------------------
// Constructors
Expand Down Expand Up @@ -558,6 +561,7 @@ private void offsetMonth(int newMoon, int dom, int delta) {
public void add(int field, int amount) {
switch (field) {
case MONTH:
case ORDINAL_MONTH:
if (amount != 0) {
int dom = get(DAY_OF_MONTH);
int day = get(JULIAN_DAY) - EPOCH_JULIAN_DAY; // Get local day
Expand All @@ -578,6 +582,7 @@ public void add(int field, int amount) {
public void roll(int field, int amount) {
switch (field) {
case MONTH:
case ORDINAL_MONTH:
if (amount != 0) {
int dom = get(DAY_OF_MONTH);
int day = get(JULIAN_DAY) - EPOCH_JULIAN_DAY; // Get local day
Expand All @@ -590,7 +595,7 @@ public void roll(int field, int amount) {
// value from 0..11 in a non-leap year, and from 0..12 in a
// leap year.
int m = get(MONTH); // 0-based month
if (isLeapYear) { // (member variable)
if (hasLeapMonthBetweenWinterSolstices) { // (member variable)
if (get(IS_LEAP_MONTH) == 1) {
++m;
} else {
Expand All @@ -612,7 +617,7 @@ public void roll(int field, int amount) {

// Now do the standard roll computation on m, with the
// allowed range of 0..n-1, where n is 12 or 13.
int n = isLeapYear ? 13 : 12; // Months in this year
int n = hasLeapMonthBetweenWinterSolstices ? 13 : 12; // Months in this year
int newM = (m + amount) % n;
if (newM < 0) {
newM += n;
Expand Down Expand Up @@ -838,7 +843,7 @@ protected void handleComputeFields(int julianDay) {
* IS_LEAP_MONTH fields, as required by
* <code>handleComputeMonthStart()</code>.
*
* <p>As a side effect, this method sets {@link #isLeapYear}.
* <p>As a side effect, this method sets {@link #hasLeapMonthBetweenWinterSolstices}.
* @param days days after January 1, 1970 0:00 astronomical base zone of the
* date to compute fields for
* @param gyear the Gregorian year of the given date
Expand Down Expand Up @@ -869,22 +874,31 @@ private void computeChineseFields(int days, int gyear, int gmonth,
int firstMoon = newMoonNear(solsticeBefore + 1, true);
int lastMoon = newMoonNear(solsticeAfter + 1, false);
int thisMoon = newMoonNear(days + 1, false); // Start of this month
// Note: isLeapYear is a member variable
isLeapYear = synodicMonthsBetween(firstMoon, lastMoon) == 12;
// Note: hasLeapMonthBetweenWinterSolstices is a member variable
hasLeapMonthBetweenWinterSolstices = synodicMonthsBetween(firstMoon, lastMoon) == 12;

int month = synodicMonthsBetween(firstMoon, thisMoon);
if (isLeapYear && isLeapMonthBetween(firstMoon, thisMoon)) {
int theNewYear = newYear(gyear);
if (days < theNewYear) {
theNewYear = newYear(gyear-1);
}
if (hasLeapMonthBetweenWinterSolstices && isLeapMonthBetween(firstMoon, thisMoon)) {
month--;
}
if (month < 1) {
month += 12;
}
int ordinalMonth = synodicMonthsBetween(theNewYear, thisMoon);
if (ordinalMonth < 0) {
ordinalMonth += 12;
}

boolean isLeapMonth = isLeapYear &&
boolean isLeapMonth = hasLeapMonthBetweenWinterSolstices &&
hasNoMajorSolarTerm(thisMoon) &&
!isLeapMonthBetween(firstMoon, newMoonNear(thisMoon - SYNODIC_GAP, false));

internalSet(MONTH, month-1); // Convert from 1-based to 0-based
internalSet(ORDINAL_MONTH, ordinalMonth);
internalSet(IS_LEAP_MONTH, isLeapMonth?1:0);

if (setAllFields) {
Expand Down Expand Up @@ -986,6 +1000,7 @@ protected int handleComputeMonthStart(int eyear, int month, boolean useMonth) {

// Save fields for later restoration
int saveMonth = internalGet(MONTH);
int saveOrdinalMonth = internalGet(ORDINAL_MONTH);
int saveIsLeapMonth = internalGet(IS_LEAP_MONTH);

// Ignore IS_LEAP_MONTH field if useMonth is false
Expand All @@ -1004,6 +1019,7 @@ protected int handleComputeMonthStart(int eyear, int month, boolean useMonth) {
}

internalSet(MONTH, saveMonth);
internalSet(ORDINAL_MONTH, saveOrdinalMonth);
internalSet(IS_LEAP_MONTH, saveIsLeapMonth);

return julianDay - 1;
Expand Down Expand Up @@ -1121,6 +1137,27 @@ public void setTemporalMonthCode( String temporalMonth ) {
// End of Temporal Calendar API
//-------------------------------------------------------------------------

/**
* {@inheritDoc}
* @internal
*/
protected int internalGetMonth()
{
if (resolveFields(MONTH_PRECEDENCE) == MONTH) {
return internalGet(MONTH);
}
Calendar temp = (Calendar) clone();
temp.set(Calendar.MONTH, 0);
temp.set(Calendar.IS_LEAP_MONTH, 0);
temp.set(Calendar.DATE, 1);
// Calculate the MONTH and IS_LEAP_MONTH by adding number of months.
temp.roll(Calendar.MONTH, internalGet(Calendar.ORDINAL_MONTH));
internalSet(Calendar.IS_LEAP_MONTH, temp.get(Calendar.IS_LEAP_MONTH));
int month = temp.get(Calendar.MONTH);
internalSet(Calendar.MONTH, month);
return month;
}

/*
private static CalendarFactory factory;
public static CalendarFactory factory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,8 @@ protected void handleComputeFields(int julianDay){
internalSet(ERA, 0);
internalSet(EXTENDED_YEAR, IndianYear);
internalSet(YEAR, IndianYear);
internalSet(MONTH, IndianMonth);
internalSet(ORDINAL_MONTH, IndianMonth);
internalSet(MONTH, IndianMonth);
internalSet(DAY_OF_MONTH, IndianDayOfMonth );
internalSet(DAY_OF_YEAR, yday + 1); // yday is 0-based
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// © 2023 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.calendar;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.HebrewCalendar;
import com.ibm.icu.util.IslamicCalendar;
import com.ibm.icu.util.ULocale;

@RunWith(JUnit4.class)
public class InTemporalLeapYearTest extends com.ibm.icu.dev.test.TestFmwk {
@Test
public void TestGregorian() {
// test from year 1800 to 2500
GregorianCalendar gc = new GregorianCalendar();
for (int year = 1900; year < 2400; ++year) {
gc.set(year, Calendar.MARCH, 7);
assertEquals("Calendar::inTemporalLeapYear",
gc.isLeapYear(year), gc.inTemporalLeapYear() == true);
}
}

private void RunChinese(Calendar cal) {
GregorianCalendar gc = new GregorianCalendar();
Calendar leapTest = (Calendar)cal.clone();
// Start our test from 1900, Jan 1.
// Check every 29 days in exhausted mode.
int incrementDays = 29;
int startYear = 1900;
int stopYear = 2400;

boolean quick = true;
if (quick) {
incrementDays = 317;
stopYear = 2100;
}
int yearForHasLeapMonth = -1;
boolean hasLeapMonth = false;
for (gc.set(startYear, Calendar.JANUARY, 1);
gc.get(Calendar.YEAR) <= stopYear;
gc.add(Calendar.DATE, incrementDays)) {
cal.setTime(gc.getTime());
int cal_year = cal.get(Calendar.EXTENDED_YEAR);
if (yearForHasLeapMonth != cal_year) {
leapTest.set(Calendar.EXTENDED_YEAR, cal_year);
leapTest.set(Calendar.MONTH, 0);
leapTest.set(Calendar.DATE, 1);
// seek any leap month
// check any leap month in the next 12 months.
for (hasLeapMonth = false;
(!hasLeapMonth) && cal_year == leapTest.get(Calendar.EXTENDED_YEAR);
leapTest.add(Calendar.MONTH, 1)) {
hasLeapMonth = leapTest.get(Calendar.IS_LEAP_MONTH) != 0;
}
yearForHasLeapMonth = cal_year;
}

boolean actualInLeap = cal.inTemporalLeapYear();
if (hasLeapMonth != actualInLeap) {
logln("Gregorian y=" + gc.get(Calendar.YEAR) +
" m=" + gc.get(Calendar.MONTH) +
" d=" + gc.get(Calendar.DATE) +
" => cal y=" + cal.get(Calendar.EXTENDED_YEAR) +
" m=" + (cal.get(Calendar.IS_LEAP_MONTH) == 1 ? "L" : "") +
cal.get(Calendar.MONTH) +
" d=" + cal.get(Calendar.DAY_OF_MONTH) +
" expected:" + (hasLeapMonth ? "true" : "false") +
" actual:" + (actualInLeap ? "true" : "false"));
}
assertEquals("inTemporalLeapYear", hasLeapMonth, actualInLeap);
}
}

@Test
public void TestChinese() {
RunChinese(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "chinese")));
}

@Test
public void TestDangi() {
RunChinese(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "dangi")));
}

@Test
public void TestHebrew() {
Calendar cal = Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "hebrew"));

GregorianCalendar gc = new GregorianCalendar();
Calendar leapTest = (Calendar)cal.clone();
// Start our test from 1900, Jan 1.
// Check every 29 days in exhausted mode.
int incrementDays = 29;
int startYear = 1900;
int stopYear = 2400;

boolean quick = true;
if (quick) {
incrementDays = 317;
stopYear = 2100;
}
int yearForHasLeapMonth = -1;
boolean hasLeapMonth = false;
for (gc.set(startYear, Calendar.JANUARY, 1);
gc.get(Calendar.YEAR) <= stopYear;
gc.add(Calendar.DATE, incrementDays)) {
cal.setTime(gc.getTime());
int cal_year = cal.get(Calendar.EXTENDED_YEAR);
if (yearForHasLeapMonth != cal_year) {
leapTest.set(Calendar.EXTENDED_YEAR, cal_year);
leapTest.set(Calendar.MONTH, 0);
leapTest.set(Calendar.DATE, 1);
hasLeapMonth = leapTest.get(Calendar.MONTH) == HebrewCalendar.TAMUZ;
yearForHasLeapMonth = cal_year;
}
boolean actualInLeap = cal.inTemporalLeapYear();
if (hasLeapMonth != actualInLeap) {
logln("Gregorian y=" + gc.get(Calendar.YEAR) +
" m=" + gc.get(Calendar.MONTH) +
" d=" + gc.get(Calendar.DATE) +
" => cal y=" + cal.get(Calendar.EXTENDED_YEAR) +
" m=" + (cal.get(Calendar.IS_LEAP_MONTH) == 1 ? "L" : "") +
cal.get(Calendar.MONTH) +
" d=" + cal.get(Calendar.DAY_OF_MONTH) +
" expected:" + (hasLeapMonth ? "true" : "false") +
" actual:" + (actualInLeap ? "true" : "false"));
}
assertEquals("inTemporalLeapYear", hasLeapMonth, actualInLeap);
}
}

private void RunIslamic(Calendar cal) {
RunXDaysIsLeap(cal, 355);
}

@Test
public void TestIslamic() {
RunIslamic(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "islamic")));
}

@Test
public void TestIslamicCivil() {
RunIslamic(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "islamic-civil")));
}

@Test
public void TestIslamicUmalqura() {
RunIslamic(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "islamic-umalqura")));
}

@Test
public void TestIslamicRGSA() {
RunIslamic(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "islamic-rgsa")));
}

@Test
public void TestIslamicTBLA() {
RunIslamic(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "islamic-tbla")));
}

private void RunXDaysIsLeap(Calendar cal, int x) {
GregorianCalendar gc = new GregorianCalendar();
Calendar leapTest = (Calendar)cal.clone();
// Start our test from 1900, Jan 1.
// Check every 29 days in exhausted mode.
int incrementDays = 29;
int startYear = 1900;
int stopYear = 2400;

boolean quick = true;
if (quick) {
incrementDays = 317;
stopYear = 2100;
}
int yearForHasLeapMonth = -1;
boolean hasLeapMonth = false;
for (gc.set(startYear, Calendar.JANUARY, 1);
gc.get(Calendar.YEAR) <= stopYear;
gc.add(Calendar.DATE, incrementDays)) {
cal.setTime(gc.getTime());
int cal_year = cal.get(Calendar.EXTENDED_YEAR);
if (yearForHasLeapMonth != cal_year) {
// If that year has exactly x days, it is a leap year.
hasLeapMonth = cal.getActualMaximum(Calendar.DAY_OF_YEAR) == x;
yearForHasLeapMonth = cal_year;
}

boolean actualInLeap = cal.inTemporalLeapYear();
if (hasLeapMonth != actualInLeap) {
logln("Gregorian y=" + gc.get(Calendar.YEAR) +
" m=" + gc.get(Calendar.MONTH) +
" d=" + gc.get(Calendar.DATE) +
" => cal y=" + cal.get(Calendar.EXTENDED_YEAR) +
" m=" + (cal.get(Calendar.IS_LEAP_MONTH) == 1 ? "L" : "") +
cal.get(Calendar.MONTH) +
" d=" + cal.get(Calendar.DAY_OF_MONTH) +
" expected:" + (hasLeapMonth ? "true" : "false") +
" actual:" + (actualInLeap ? "true" : "false"));
}
assertEquals("inTemporalLeapYear", hasLeapMonth, actualInLeap);
}
}

@Test
public void TestTaiwan() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "roc")), 366);
}

@Test
public void TestJapanese() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "japanese")), 366);
}

@Test
public void TestBuddhist() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "buddhist")), 366);
}

@Test
public void TestPersian() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "persian")), 366);
}

@Test
public void TestIndian() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "indian")), 366);
}

@Test
public void TestCoptic() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "coptic")), 366);
}

@Test
public void TestEthiopic() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "ethiopic")), 366);
}

@Test
public void TestEthiopicAmeteAlem() {
RunXDaysIsLeap(Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "ethiopic-amete-alem")), 366);
}
}

0 comments on commit 8de21c2

Please sign in to comment.