// the object, not class level, under the assumption that typical
// usage will be to have one instance of ChineseCalendar at a time.
+ /**
+ * The start year of this Chinese calendar instance.
+ */
+ private int epochYear;
+
+ /**
+ * The zone used for the astronomical calculation of this Chinese
+ * calendar instance.
+ */
+ private TimeZone zoneAstro;
+
/**
* We have one instance per object, and we don't synchronize it because
* Calendar doesn't support multithreaded execution in the first place.
* @stable ICU 2.8
*/
public ChineseCalendar() {
- super();
- setTimeInMillis(System.currentTimeMillis());
+ this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
}
/**
* @stable ICU 4.0
*/
public ChineseCalendar(Date date) {
- super();
+ this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
setTime(date);
}
* @stable ICU 4.0
*/
public ChineseCalendar(int year, int month, int isLeapMonth, int date) {
- super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
-
- // We need to set the current time once to initialize the ChineseCalendar's
- // ERA field to be the current era.
- setTimeInMillis(System.currentTimeMillis());
- // Then we need to clean up time fields
- this.set(MILLISECONDS_IN_DAY, 0);
-
- // Then set the given field values.
- this.set(YEAR, year);
- this.set(MONTH, month);
- this.set(IS_LEAP_MONTH, isLeapMonth);
- this.set(DATE, date);
+ this(year, month, isLeapMonth, date, 0, 0, 0);
}
/**
public ChineseCalendar(int year, int month, int isLeapMonth, int date, int hour,
int minute, int second)
{
- super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
+ this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
- // We need to set the current time once to initialize the ChineseCalendar's
- // ERA field to be the current era.
- setTimeInMillis(System.currentTimeMillis());
- // Then set 0 to millisecond field
+ // The current time is set at this point, so ERA field is already
+ // set to the current era.
+
+ // Then we need to clean up time fields
this.set(MILLISECOND, 0);
// Then, set the given field values.
*/
public ChineseCalendar(int era, int year, int month, int isLeapMonth, int date)
{
- super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
-
- // We need to set the current time once to initialize the ChineseCalendar's
- // ERA field to be the current era.
- setTimeInMillis(System.currentTimeMillis());
-
- // Then we need to clean up time fields
- this.set(MILLISECONDS_IN_DAY, 0);
-
- // Then set the given field values.
- this.set(ERA, era);
- this.set(YEAR, year);
- this.set(MONTH, month);
- this.set(IS_LEAP_MONTH, isLeapMonth);
- this.set(DATE, date);
+ this(era, year, month, isLeapMonth, 0, 0, 0);
}
/**
public ChineseCalendar(int era, int year, int month, int isLeapMonth, int date, int hour,
int minute, int second)
{
- super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
-
- // We need to set the current time once to initialize the ChineseCalendar's
- // ERA field to be the current era.
- setTimeInMillis(System.currentTimeMillis());
+ this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
- // Then set 0 to millisecond field
+ // Set 0 to millisecond field
this.set(MILLISECOND, 0);
// Then, set the given field values.
* @stable ICU 4.0
*/
public ChineseCalendar(Locale aLocale) {
- this(TimeZone.getDefault(), aLocale);
- setTimeInMillis(System.currentTimeMillis());
+ this(TimeZone.getDefault(), ULocale.forLocale(aLocale), CHINESE_EPOCH_YEAR, CHINA_ZONE);
}
/**
* @stable ICU 4.0
*/
public ChineseCalendar(TimeZone zone) {
- super(zone, ULocale.getDefault(Category.FORMAT));
- setTimeInMillis(System.currentTimeMillis());
+ this(zone, ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
}
/**
* @stable ICU 2.8
*/
public ChineseCalendar(TimeZone zone, Locale aLocale) {
- super(zone, aLocale);
- setTimeInMillis(System.currentTimeMillis());
+ this(zone, ULocale.forLocale(aLocale), CHINESE_EPOCH_YEAR, CHINA_ZONE);
}
/**
* @stable ICU 4.0
*/
public ChineseCalendar(ULocale locale) {
- this(TimeZone.getDefault(), locale);
- setTimeInMillis(System.currentTimeMillis());
+ this(TimeZone.getDefault(), locale, CHINESE_EPOCH_YEAR, CHINA_ZONE);
}
/**
* @stable ICU 3.2
*/
public ChineseCalendar(TimeZone zone, ULocale locale) {
+ this(zone, locale, CHINESE_EPOCH_YEAR, CHINA_ZONE);
+ }
+
+ /**
+ * Construct a <code>ChineseCalenar</code> based on the current time
+ * with the given time zone, the locale, the epoch year and the time zone
+ * used for astronomical calculation.
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+ protected ChineseCalendar(TimeZone zone, ULocale locale, int epochYear, TimeZone zoneAstroCalc) {
super(zone, locale);
+ this.epochYear = epochYear;
+ this.zoneAstro = zoneAstroCalc;
setTimeInMillis(System.currentTimeMillis());
}
year = internalGet(EXTENDED_YEAR, 1); // Default to year 1
} else {
int cycle = internalGet(ERA, 1) - 1; // 0-based cycle
- year = cycle * 60 + internalGet(YEAR, 1);
+ // adjust to the instance specific epoch
+ year = cycle * 60 + internalGet(YEAR, 1) - (epochYear - CHINESE_EPOCH_YEAR);
}
return year;
}
private static final int CHINESE_EPOCH_YEAR = -2636; // Gregorian year
/**
- * The offset from GMT in milliseconds at which we perform astronomical
- * computations. Some sources use a different historically accurate
+ * The time zone used for performing astronomical computations.
+ * Some sources use a different historically accurate
* offset of GMT+7:45:40 for years before 1929; we do not do this.
*/
- private static final long CHINA_OFFSET = 8*ONE_HOUR;
+ private static final TimeZone CHINA_ZONE = new SimpleTimeZone(8 * ONE_HOUR, "CHINA_ZONE").freeze();
/**
* Value to be added or subtracted from the local days of a new moon to
/**
* Convert local days to UTC epoch milliseconds.
- * @param days days after January 1, 1970 0:00 Asia/Shanghai
+ * This is not an accurate conversion in terms that getTimezoneOffset
+ * takes the milliseconds in GMT (not local time). In theory, more
+ * accurate algorithm can be implemented but practically we do not need
+ * to go through that complication as long as the historically timezone
+ * changes did not happen around the 'tricky' new moon (new moon around
+ * the midnight).
+ *
+ * @param days days after January 1, 1970 0:00 in the astronomical base zone
* @return milliseconds after January 1, 1970 0:00 GMT
*/
- private static final long daysToMillis(int days) {
- return (days * ONE_DAY) - CHINA_OFFSET;
+ private final long daysToMillis(int days) {
+ long millis = days * ONE_DAY;
+ return millis - zoneAstro.getOffset(millis);
}
/**
* Convert UTC epoch milliseconds to local days.
* @param millis milliseconds after January 1, 1970 0:00 GMT
- * @return days after January 1, 1970 0:00 Asia/Shanghai
+ * @return days days after January 1, 1970 0:00 in the astronomical base zone
*/
- private static final int millisToDays(long millis) {
- return (int) floorDivide(millis + CHINA_OFFSET, ONE_DAY);
+ private final int millisToDays(long millis) {
+ return (int) floorDivide(millis + zoneAstro.getOffset(millis), ONE_DAY);
}
//------------------------------------------------------------------
/**
* Return true if there is a leap month on or after month newMoon1 and
* at or before month newMoon2.
- * @param newMoon1 days after January 1, 1970 0:00 Asia/Shanghai of a
+ * @param newMoon1 days after January 1, 1970 0:00 astronomical base zone of a
* new moon
- * @param newMoon2 days after January 1, 1970 0:00 Asia/Shanghai of a
+ * @param newMoon2 days after January 1, 1970 0:00 astronomical base zone of a
* new moon
*/
private boolean isLeapMonthBetween(int newMoon1, int newMoon2) {
* <code>handleComputeMonthStart()</code>.
*
* <p>As a side effect, this method sets {@link #isLeapYear}.
- * @param days days after January 1, 1970 0:00 Asia/Shanghai of the
+ * @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
* @param gmonth the Gregorian month of the given date
if (setAllFields) {
- int year = gyear - CHINESE_EPOCH_YEAR;
+ // Extended year and cycle year is based on the epoch year
+ int extended_year = gyear - epochYear;
+ int cycle_year = gyear - CHINESE_EPOCH_YEAR;
if (month < 11 ||
gmonth >= JULY) {
- year++;
+ extended_year++;
+ cycle_year++;
}
int dayOfMonth = days - thisMoon + 1;
- internalSet(EXTENDED_YEAR, year);
+ internalSet(EXTENDED_YEAR, extended_year);
// 0->0,60 1->1,1 60->1,60 61->2,1 etc.
int[] yearOfCycle = new int[1];
- int cycle = floorDivide(year-1, 60, yearOfCycle);
+ int cycle = floorDivide(cycle_year-1, 60, yearOfCycle);
internalSet(ERA, cycle+1);
internalSet(YEAR, yearOfCycle[0]+1);
/**
* Return the Chinese new year of the given Gregorian year.
* @param gyear a Gregorian year
- * @return days after January 1, 1970 0:00 Asia/Shanghai of the
+ * @return days after January 1, 1970 0:00 astronomical base zone of the
* Chinese new year of the given year (this will be a new moon)
*/
private int newYear(int gyear) {
month = rem[0];
}
- int gyear = eyear + CHINESE_EPOCH_YEAR - 1; // Gregorian year
+ int gyear = eyear + epochYear - 1; // Gregorian year
int newYear = newYear(gyear);
int newMoon = newMoonNear(newYear + month * 29, true);
private void readObject(ObjectInputStream stream)
throws IOException, ClassNotFoundException
{
+ epochYear = CHINESE_EPOCH_YEAR;
+ zoneAstro = CHINA_ZONE;
+
stream.defaultReadObject();
-
+
/* set up the transient caches... */
astro = new CalendarAstronomer();
winterSolsticeCache = new CalendarCache();
--- /dev/null
+/*
+ *******************************************************************************
+ * Copyright (C) 2012, International Business Machines Corporation and *
+ * others. All Rights Reserved. *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.calendar;
+import java.util.Date;
+
+import com.ibm.icu.text.DateFormat;
+import com.ibm.icu.util.Calendar;
+import com.ibm.icu.util.DangiCalendar;
+import com.ibm.icu.util.GregorianCalendar;
+import com.ibm.icu.util.TimeZone;
+import com.ibm.icu.util.ULocale;
+
+public class DangiTest extends CalendarTest {
+
+ public static void main(String args[]) throws Exception {
+ new DangiTest().run(args);
+ }
+
+ /**
+ * Test basic mapping to and from Gregorian.
+ */
+ public void TestMapping() {
+ final int[] DATA = {
+ // (Note: months are 1-based)
+ // Gregorian Korean (Dan-gi)
+ 1964, 9, 4, 4297, 7,0, 28,
+ 1964, 9, 5, 4297, 7,0, 29,
+ 1964, 9, 6, 4297, 8,0, 1,
+ 1964, 9, 7, 4297, 8,0, 2,
+ 1961, 12, 25, 4294, 11,0, 18,
+ 1999, 6, 4, 4332, 4,0, 21,
+
+ 1990, 5, 23, 4323, 4,0, 29,
+ 1990, 5, 24, 4323, 5,0, 1,
+ 1990, 6, 22, 4323, 5,0, 30,
+ 1990, 6, 23, 4323, 5,1, 1,
+ 1990, 7, 20, 4323, 5,1, 28,
+ 1990, 7, 21, 4323, 5,1, 29,
+ 1990, 7, 22, 4323, 6,0, 1,
+
+ // Some tricky dates (where GMT+8 doesn't agree with GMT+9)
+ //
+ // The list is from http://www.math.snu.ac.kr/~kye/others/lunar.html ('kye ref').
+ // However, for some dates disagree with the above reference so KASI's
+ // calculation was cross-referenced:
+ // http://astro.kasi.re.kr/Life/ConvertSolarLunarForm.aspx?MenuID=115
+ 1880, 11, 3, 4213, 10,0, 1, // astronomer's GMT+8 / KASI disagrees with the kye ref
+ 1882, 12, 10, 4215, 11,0, 1,
+ 1883, 7, 4, 4216, 6,0, 1,
+ 1884, 4, 25, 4217, 4,0, 1,
+ 1885, 5, 14, 4218, 4,0, 1,
+ 1891, 1, 10, 4223, 12,0, 1,
+ 1893, 4, 16, 4226, 3,0, 1,
+ 1894, 5, 5, 4227, 4,0, 1,
+ 1897, 7, 29, 4230, 7,0, 1, // astronomer's GMT+8 disagrees with all other ref (looks like our astronomer's error, see ad hoc fix at ChineseCalendar::getTimezoneOffset)
+ 1903, 10, 20, 4236, 9,0, 1,
+ 1904, 1, 17, 4236, 12,0, 1,
+ 1904, 11, 7, 4237, 10,0, 1,
+ 1905, 5, 4, 4238, 4,0, 1,
+ 1907, 7, 10, 4240, 6,0, 1,
+ 1908, 4, 30, 4241, 4,0, 1,
+ 1908, 9, 25, 4241, 9,0, 1,
+ 1909, 9, 14, 4242, 8,0, 1,
+ 1911, 12, 20, 4244, 11,0, 1,
+ 1976, 11, 22, 4309, 10,0, 1,
+ };
+
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ StringBuilder buf = new StringBuilder();
+
+ logln("Gregorian -> Korean Lunar (Dangi)");
+
+ Calendar grego = Calendar.getInstance();
+ grego.clear();
+ for (int i = 0; i < DATA.length;) {
+ grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
+ Date date = grego.getTime();
+ cal.setTime(date);
+ int y = cal.get(Calendar.EXTENDED_YEAR);
+ int m = cal.get(Calendar.MONTH) + 1; // 0-based -> 1-based
+ int L = cal.get(Calendar.IS_LEAP_MONTH);
+ int d = cal.get(Calendar.DAY_OF_MONTH);
+ int yE = DATA[i++]; // Expected y, m, isLeapMonth, d
+ int mE = DATA[i++]; // 1-based
+ int LE = DATA[i++];
+ int dE = DATA[i++];
+ buf.setLength(0);
+ buf.append(date + " -> ");
+ buf.append(y + "/" + m + (L == 1 ? "(leap)" : "") + "/" + d);
+ if (y == yE && m == mE && L == LE && d == dE) {
+ logln("OK: " + buf.toString());
+ } else {
+ errln("Fail: " + buf.toString() + ", expected " + yE + "/" + mE + (LE == 1 ? "(leap)" : "") + "/" + dE);
+ }
+ }
+
+ logln("Korean Lunar (Dangi) -> Gregorian");
+ for (int i = 0; i < DATA.length;) {
+ grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
+ Date dexp = grego.getTime();
+ int cyear = DATA[i++];
+ int cmonth = DATA[i++];
+ int cisleapmonth = DATA[i++];
+ int cdayofmonth = DATA[i++];
+ cal.clear();
+ cal.set(Calendar.EXTENDED_YEAR, cyear);
+ cal.set(Calendar.MONTH, cmonth - 1);
+ cal.set(Calendar.IS_LEAP_MONTH, cisleapmonth);
+ cal.set(Calendar.DAY_OF_MONTH, cdayofmonth);
+ Date date = cal.getTime();
+ buf.setLength(0);
+ buf.append(cyear + "/" + cmonth + (cisleapmonth == 1 ? "(leap)" : "") + "/" + cdayofmonth);
+ buf.append(" -> " + date);
+ if (date.equals(dexp)) {
+ logln("OK: " + buf.toString());
+ } else {
+ errln("Fail: " + buf.toString() + ", expected " + dexp);
+ }
+ }
+ }
+
+ /**
+ * Make sure no Gregorian dates map to Chinese 1-based day of
+ * month zero. This was a problem with some of the astronomical
+ * new moon determinations.
+ */
+ public void TestZeroDOM() {
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ GregorianCalendar greg = new GregorianCalendar(1989, Calendar.SEPTEMBER, 1);
+ logln("Start: " + greg.getTime());
+ for (int i=0; i<1000; ++i) {
+ cal.setTimeInMillis(greg.getTimeInMillis());
+ if (cal.get(Calendar.DAY_OF_MONTH) == 0) {
+ errln("Fail: " + greg.getTime() + " -> " +
+ cal.get(Calendar.YEAR) + "/" +
+ cal.get(Calendar.MONTH) +
+ (cal.get(Calendar.IS_LEAP_MONTH)==1?"(leap)":"") +
+ "/" + cal.get(Calendar.DAY_OF_MONTH));
+ }
+ greg.add(Calendar.DAY_OF_YEAR, 1);
+ }
+ logln("End: " + greg.getTime());
+ }
+
+ /**
+ * Test minimum and maximum functions.
+ */
+ public void TestLimits() {
+ // The number of days and the start date can be adjusted
+ // arbitrarily to either speed up the test or make it more
+ // thorough, but try to test at least a full year, preferably a
+ // full non-leap and a full leap year.
+
+ // Final parameter is either number of days, if > 0, or test
+ // duration in seconds, if < 0.
+ Calendar tempcal = Calendar.getInstance();
+ tempcal.clear();
+ tempcal.set(1989, Calendar.NOVEMBER, 1);
+ Calendar dangi = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ doLimitsTest(dangi, null, tempcal.getTime());
+ doTheoreticalLimitsTest(dangi, true);
+ }
+
+ /**
+ * Make sure IS_LEAP_MONTH participates in field resolution.
+ */
+ public void TestResolution() {
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ DateFormat fmt = DateFormat.getDateInstance(cal, DateFormat.DEFAULT);
+
+ // May 22 4334 = y4334 m4 d30 doy119
+ // May 23 4334 = y4334 m4* d1 doy120
+
+ final int THE_YEAR = 4334;
+ final int END = -1;
+
+ int[] DATA = {
+ // Format:
+ // (field, value)+, END, exp.month, exp.isLeapMonth, exp.DOM
+ // Note: exp.month is ONE-BASED
+
+ // If we set DAY_OF_YEAR only, that should be used
+ Calendar.DAY_OF_YEAR, 1,
+ END,
+ 1,0,1, // Expect 1-1
+
+ // If we set MONTH only, that should be used
+ Calendar.IS_LEAP_MONTH, 1,
+ Calendar.DAY_OF_MONTH, 1,
+ Calendar.MONTH, 3,
+ END,
+ 4,1,1, // Expect 4*-1
+
+ // If we set the DOY last, that should take precedence
+ Calendar.MONTH, 1, // Should ignore
+ Calendar.IS_LEAP_MONTH, 1, // Should ignore
+ Calendar.DAY_OF_MONTH, 1, // Should ignore
+ Calendar.DAY_OF_YEAR, 121,
+ END,
+ 4,1,2, // Expect 4*-2
+
+ // If we set IS_LEAP_MONTH last, that should take precedence
+ Calendar.MONTH, 3,
+ Calendar.DAY_OF_MONTH, 1,
+ Calendar.DAY_OF_YEAR, 5, // Should ignore
+ Calendar.IS_LEAP_MONTH, 1,
+ END,
+ 4,1,1, // Expect 4*-1
+ };
+
+ StringBuilder buf = new StringBuilder();
+ for (int i=0; i<DATA.length; ) {
+ cal.clear();
+ cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
+ buf.setLength(0);
+ buf.append("EXTENDED_YEAR=" + THE_YEAR);
+ while (DATA[i] != END) {
+ cal.set(DATA[i++], DATA[i++]);
+ buf.append(" " + fieldName(DATA[i-2]) + "=" + DATA[i-1]);
+ }
+ ++i; // Skip over END mark
+ int expMonth = DATA[i++]-1;
+ int expIsLeapMonth = DATA[i++];
+ int expDOM = DATA[i++];
+ int month = cal.get(Calendar.MONTH);
+ int isLeapMonth = cal.get(Calendar.IS_LEAP_MONTH);
+ int dom = cal.get(Calendar.DAY_OF_MONTH);
+ if (expMonth == month && expIsLeapMonth == isLeapMonth &&
+ dom == expDOM) {
+ logln("OK: " + buf + " => " + fmt.format(cal.getTime()));
+ } else {
+ String s = fmt.format(cal.getTime());
+ cal.clear();
+ cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
+ cal.set(Calendar.MONTH, expMonth);
+ cal.set(Calendar.IS_LEAP_MONTH, expIsLeapMonth);
+ cal.set(Calendar.DAY_OF_MONTH, expDOM);
+ errln("Fail: " + buf + " => " + s +
+ "=" + (month+1) + "," + isLeapMonth + "," + dom +
+ ", expected " + fmt.format(cal.getTime()) +
+ "=" + (expMonth+1) + "," + expIsLeapMonth + "," + expDOM);
+ }
+ }
+ }
+
+ /**
+ * Test the behavior of fields that are out of range.
+ */
+ public void TestOutOfRange() {
+ int[] DATA = new int[] {
+ // Input Output
+ 4334, 13, 1, 4335, 1, 1,
+ 4334, 18, 1, 4335, 6, 1,
+ 4335, 0, 1, 4334, 12, 1,
+ 4335, -6, 1, 4334, 6, 1,
+ 4334, 1, 32, 4334, 2, 2, // 1-4334 has 30 days
+ 4334, 2, -1, 4334, 1, 29,
+ };
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ for (int i = 0; i < DATA.length;) {
+ int y1 = DATA[i++];
+ int m1 = DATA[i++] - 1;
+ int d1 = DATA[i++];
+ int y2 = DATA[i++];
+ int m2 = DATA[i++] - 1;
+ int d2 = DATA[i++];
+ cal.clear();
+ cal.set(Calendar.EXTENDED_YEAR, y1);
+ cal.set(MONTH, m1);
+ cal.set(DATE, d1);
+ int y = cal.get(Calendar.EXTENDED_YEAR);
+ int m = cal.get(MONTH);
+ int d = cal.get(DATE);
+ if (y != y2 || m != m2 || d != d2) {
+ errln("Fail: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d
+ + ", expected " + y2 + "/" + (m2 + 1) + "/" + d2);
+ } else if (isVerbose()) {
+ logln("OK: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d);
+ }
+ }
+ }
+
+ /**
+ * Test the behavior of KoreanLunarCalendar.add(). The only real
+ * nastiness with roll is the MONTH field around leap months.
+ */
+ public void TestAdd() {
+ int[][] tests = new int[][] {
+ // MONTHS ARE 1-BASED HERE
+ // input add output
+ // year mon day field amount year mon day
+ { 4338, 3,0, 15, MONTH, 3, 4338, 6,0, 15 }, // normal
+ { 4335, 12,0, 15, MONTH, 1, 4336, 1,0, 15 }, // across year
+ { 4336, 1,0, 15, MONTH, -1, 4335, 12,0, 15 }, // across year
+ { 4334, 3,0, 15, MONTH, 3, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 3,0, 15, MONTH, 2, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 4,0, 15, MONTH, 1, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 4,1, 15, MONTH, 1, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 3,0, 30, MONTH, 2, 4334, 4,1, 29 }, // dom should pin
+ { 4334, 3,0, 30, MONTH, 3, 4334, 5,0, 30 }, // no dom pin
+ { 4334, 3,0, 30, MONTH, 4, 4334, 6,0, 29 }, // dom should pin
+ };
+
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ doRollAddDangi(ADD, cal, tests);
+ }
+
+ /**
+ * Test the behavior of KoreanLunarCalendar.roll(). The only real
+ * nastiness with roll is the MONTH field around leap months.
+ */
+ public void TestRoll() {
+ int[][] tests = new int[][] {
+ // MONTHS ARE 1-BASED HERE
+ // input add output
+ // year mon day field amount year mon day
+ { 4338, 3,0, 15, MONTH, 3, 4338, 6,0, 15 }, // normal
+ { 4338, 3,0, 15, MONTH, 11, 4338, 2,0, 15 }, // normal
+ { 4335, 12,0, 15, MONTH, 1, 4335, 1,0, 15 }, // across year
+ { 4336, 1,0, 15, MONTH, -1, 4336, 12,0, 15 }, // across year
+ { 4334, 3,0, 15, MONTH, 3, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 3,0, 15, MONTH, 16, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 3,0, 15, MONTH, 2, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 3,0, 15, MONTH, 28, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 4,0, 15, MONTH, 1, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 4,0, 15, MONTH, -12, 4334, 4,1, 15 }, // 4=leap
+ { 4334, 4,1, 15, MONTH, 1, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 4,1, 15, MONTH, -25, 4334, 5,0, 15 }, // 4=leap
+ { 4334, 3,0, 30, MONTH, 2, 4334, 4,1, 29 }, // dom should pin
+ { 4334, 3,0, 30, MONTH, 15, 4334, 4,1, 29 }, // dom should pin
+ { 4334, 3,0, 30, MONTH, 16, 4334, 5,0, 30 }, // no dom pin
+ { 4334, 3,0, 30, MONTH, -9, 4334, 6,0, 29 }, // dom should pin
+ };
+
+ Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ doRollAddDangi(ROLL, cal, tests);
+ }
+
+ void doRollAddDangi(boolean roll, Calendar cal, int[][] tests) {
+ String name = roll ? "rolling" : "adding";
+
+ for (int i = 0; i < tests.length; i++) {
+ int[] test = tests[i];
+
+ cal.clear();
+ cal.set(Calendar.EXTENDED_YEAR, test[0]);
+ cal.set(Calendar.MONTH, test[1] - 1);
+ cal.set(Calendar.IS_LEAP_MONTH, test[2]);
+ cal.set(Calendar.DAY_OF_MONTH, test[3]);
+ if (roll) {
+ cal.roll(test[4], test[5]);
+ } else {
+ cal.add(test[4], test[5]);
+ }
+ if (cal.get(Calendar.EXTENDED_YEAR) != test[6] || cal.get(MONTH) != (test[7] - 1)
+ || cal.get(Calendar.IS_LEAP_MONTH) != test[8] || cal.get(DATE) != test[9]) {
+ errln("Fail: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
+ + fieldName(test[4]) + " by " + test[5] + ": expected "
+ + ymdToString(test[6], test[7] - 1, test[8], test[9]) + ", got " + ymdToString(cal));
+ } else if (isVerbose()) {
+ logln("OK: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
+ + fieldName(test[4]) + " by " + test[5] + ": got " + ymdToString(cal));
+ }
+ }
+ }
+
+ /**
+ * Convert year,month,day values to the form "year/month/day".
+ * On input the month value is zero-based, but in the result string it is one-based.
+ */
+ static public String ymdToString(int year, int month, int isLeapMonth, int day) {
+ return "" + year + "/" + (month + 1) + ((isLeapMonth != 0) ? "(leap)" : "") + "/" + day;
+ }
+
+ public void TestCoverage() {
+ // DangiCalendar()
+ // DangiCalendar(Date)
+ // DangiCalendar(TimeZone, ULocale)
+ Date d = new Date();
+
+ DangiCalendar cal1 = new DangiCalendar();
+ cal1.setTime(d);
+
+ DangiCalendar cal2 = new DangiCalendar(d);
+
+ DangiCalendar cal3 = new DangiCalendar(TimeZone.getDefault(), ULocale.getDefault());
+ cal3.setTime(d);
+
+ assertEquals("DangiCalendar() and DangiCalendar(Date)", cal1, cal2);
+ assertEquals("DangiCalendar() and DangiCalendar(TimeZone,ULocale)", cal1, cal3);
+
+ // String getType()
+ String type = cal1.getType();
+ assertEquals("getType()", "dangi", type);
+ }
+
+ public void TestInitWithCurrentTime() {
+ // If the chinese calendar current millis isn't called, the default year is wrong.
+ // this test is assuming the 'year' is the current cycle
+ // so when we cross a cycle boundary, the target will need to change
+ // that shouldn't be for awhile yet...
+
+ Calendar cc = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+ cc.set(Calendar.EXTENDED_YEAR, 4338);
+ cc.set(Calendar.MONTH, 0);
+ // need to set leap month flag off, otherwise, the test case always fails when
+ // current time is in a leap month
+ cc.set(Calendar.IS_LEAP_MONTH, 0);
+ cc.set(Calendar.DATE, 19);
+ cc.set(Calendar.HOUR_OF_DAY, 0);
+ cc.set(Calendar.MINUTE, 0);
+ cc.set(Calendar.SECOND, 0);
+ cc.set(Calendar.MILLISECOND, 0);
+
+ cc.add(Calendar.DATE, 1);
+
+ Calendar cal = new GregorianCalendar(2005, Calendar.FEBRUARY, 28);
+ Date target = cal.getTime();
+ Date result = cc.getTime();
+
+ assertEquals("chinese and gregorian date should match", target, result);
+ }
+}