From 1e8424956ae3f583f1b13c57a7df37991a1d7162 Mon Sep 17 00:00:00 2001 From: Frank Tang Date: Mon, 10 Jul 2023 16:28:10 -0700 Subject: [PATCH] ICU-22407 Implement Java Temporal Calendar API --- icu4c/source/i18n/calendar.cpp | 2 - .../core/src/com/ibm/icu/impl/CalType.java | 4 +- .../core/src/com/ibm/icu/util/CECalendar.java | 47 ++ .../core/src/com/ibm/icu/util/Calendar.java | 189 +++++++- .../src/com/ibm/icu/util/ChineseCalendar.java | 149 +++++- .../src/com/ibm/icu/util/CopticCalendar.java | 1 + .../com/ibm/icu/util/EthiopicCalendar.java | 1 + .../com/ibm/icu/util/GregorianCalendar.java | 4 +- .../src/com/ibm/icu/util/HebrewCalendar.java | 88 +++- .../src/com/ibm/icu/util/IndianCalendar.java | 3 + .../src/com/ibm/icu/util/IslamicCalendar.java | 26 + .../src/com/ibm/icu/util/PersianCalendar.java | 4 + .../test/calendar/CalendarRegressionTest.java | 3 +- .../test/calendar/InTemporalLeapYearTest.java | 248 ++++++++++ .../dev/test/calendar/OrdinalMonthTest.java | 409 ++++++++++++++++ .../test/calendar/TemporalMonthCodeTest.java | 446 ++++++++++++++++++ 16 files changed, 1594 insertions(+), 30 deletions(-) create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/InTemporalLeapYearTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/OrdinalMonthTest.java create mode 100644 icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/TemporalMonthCodeTest.java diff --git a/icu4c/source/i18n/calendar.cpp b/icu4c/source/i18n/calendar.cpp index b9972086c651..6075aa131d13 100644 --- a/icu4c/source/i18n/calendar.cpp +++ b/icu4c/source/i18n/calendar.cpp @@ -1827,7 +1827,6 @@ void Calendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode& statu } set(field, newYear); pinField(UCAL_MONTH,status); - pinField(UCAL_ORDINAL_MONTH,status); pinField(UCAL_DAY_OF_MONTH,status); return; } @@ -1836,7 +1835,6 @@ void Calendar::roll(UCalendarDateFields field, int32_t amount, UErrorCode& statu // Rolling the year can involve pinning the DAY_OF_MONTH. set(field, internalGet(field) + amount); pinField(UCAL_MONTH,status); - pinField(UCAL_ORDINAL_MONTH,status); pinField(UCAL_DAY_OF_MONTH,status); return; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/impl/CalType.java b/icu4j/main/classes/core/src/com/ibm/icu/impl/CalType.java index 11ea3505d17a..5a7205adaf68 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/impl/CalType.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/impl/CalType.java @@ -26,9 +26,7 @@ public enum CalType { ISLAMIC_UMALQURA("islamic-umalqura"), JAPANESE("japanese"), PERSIAN("persian"), - ROC("roc"), - - UNKNOWN("unknown"); + ROC("roc"); String id; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/CECalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/CECalendar.java index ce3bd888be40..d85c49d82b7d 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/CECalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/CECalendar.java @@ -45,6 +45,8 @@ abstract class CECalendar extends Calendar { { -5000000, -5000000, 5000000, 5000000 }, // EXTENDED_YEAR {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY + {/* */}, // IS_LEAP_YEAR + { 0, 0, 12, 12 }, // ORDINAL_MONTH }; //------------------------------------------------------------------------- @@ -273,4 +275,49 @@ public static void jdToCE(int julianDay, int jdEpochOffset, int[] fields) { // day fields[2] = (doy % 30) + 1; // 1-based days in a month } + + + //------------------------------------------------------------------------- + // Temporal Calendar API. + //------------------------------------------------------------------------- + private static String [] gTemporalMonthCodes = { + "M01", "M02", "M03", "M04", "M05", "M06", "M07", "M08", "M09", "M10", "M11", "M12" + }; + + /** + * Gets The Temporal monthCode value corresponding to the month for the date. + * The value is a string identifier that starts with the literal grapheme + * "M" followed by two graphemes representing the zero-padded month number + * of the current month in a normal (non-leap) year. For the short thirteen + * month in each year in the CECalendar, the value is "M13". + * + * @return One of 13 possible strings in {"M01".. "M12", "M13"}. + * @draft ICU 74 + */ + public String getTemporalMonthCode() { + if (get(MONTH) == 12) return "M13"; + return super.getTemporalMonthCode(); + } + + /** + * Sets The Temporal monthCode which is a string identifier that starts + * with the literal grapheme "M" followed by two graphemes representing + * the zero-padded month number of the current month in a normal + * (non-leap) year. For CECalendar calendar, the values are "M01" .. "M13" + * while the "M13" is represent the short thirteen month in each year. + * @param temporalMonth One of 13 possible strings in {"M01".. "M12", "M13"}. + * @draft ICU 74 + */ + public void setTemporalMonthCode( String temporalMonth ) { + if (temporalMonth.equals("M13")) { + set(MONTH, 12); + set(IS_LEAP_MONTH, 0); + return; + } + super.setTemporalMonthCode(temporalMonth); + } + + //------------------------------------------------------------------------- + // End of Temporal Calendar API + //------------------------------------------------------------------------- } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java index 3893a9796ddf..d317057b3480 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java @@ -957,13 +957,37 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable UNSET) ? fields[field] : defaultValue; } + /* + * @internal + * @deprecated This API is ICU internal only. + * Use this function instead of internalGet(MONTH). The implementation + * check the timestamp of MONTH and ORDINAL_MONTH and use the + * one set later. The subclass should override it to conver the value of ORDINAL_MONTH + * to MONTH correctly if ORDINAL_MONTH has higher priority. + * @return the value for the given time field. + */ + protected int internalGetMonth() + { + if (resolveFields(MONTH_PRECEDENCE) == MONTH) { + return internalGet(MONTH); + } + return internalGet(ORDINAL_MONTH); + } + /** + * @internal + * @deprecated This API is ICU internal only. + * Use this function instead of internalGet(MONTH, defaultValue). The implementation + * check the timestamp of MONTH and ORDINAL_MONTH and use the + * one set later. The subclass should override it to conver the value of ORDINAL_MONTH + * to MONTH correctly if ORDINAL_MONTH has higher priority. + * @param defaultValue a default value used if the MONTH and + * ORDINAL_MONTH are both unset. + * @return the value for the MONTH. + */ + protected int internalGetMonth(int defaultValue) { + if (resolveFields(MONTH_PRECEDENCE) == MONTH) { + return internalGet(MONTH, defaultValue); + } + return internalGet(ORDINAL_MONTH, defaultValue); + } + + /** * Sets the time field with the given value. * @param field the given time field. * @param value the value to be set for the given time field. @@ -2327,6 +2470,14 @@ public final void clear(int field) } fields[field] = 0; stamp[field] = UNSET; + if (field == MONTH) { + fields[ORDINAL_MONTH] = 0; + stamp[ORDINAL_MONTH] = UNSET; + } + if (field == ORDINAL_MONTH) { + fields[MONTH] = 0; + stamp[MONTH] = UNSET; + } isTimeSet = areFieldsSet = areAllFieldsSet = areFieldsVirtuallySet = false; } @@ -2525,6 +2676,10 @@ public int getActualMaximum(int field) { result = getMaximum(field); break; + case ORDINAL_MONTH: + result = inTemporalLeapYear() ? getMaximum(ORDINAL_MONTH) : getLeastMaximum(ORDINAL_MONTH); + break; + default: // For all other fields, do it the hard way.... result = getActualHelper(field, getLeastMaximum(field), getMaximum(field)); @@ -2887,13 +3042,14 @@ public void roll(int field, int amount) { } case MONTH: + case ORDINAL_MONTH: // Rolling the month involves both pinning the final value // and adjusting the DAY_OF_MONTH if necessary. We only adjust the // DAY_OF_MONTH if, after updating the MONTH field, it is illegal. // E.g., .roll(MONTH, 1) -> or . { int max = getActualMaximum(MONTH); - int mon = (internalGet(MONTH) + amount) % (max+1); + int mon = (internalGetMonth() + amount) % (max+1); if (mon < 0) { mon += (max + 1); @@ -3090,6 +3246,7 @@ public void roll(int field, int amount) { // have to be updated as well. set(DAY_OF_YEAR, day_of_year); clear(MONTH); + clear(ORDINAL_MONTH); return; } case DAY_OF_YEAR: @@ -3265,6 +3422,7 @@ public void add(int field, int amount) { // Fall through into standard handling case EXTENDED_YEAR: case MONTH: + case ORDINAL_MONTH: { boolean oldLenient = isLenient(); setLenient(true); @@ -4416,6 +4574,7 @@ public int getMinimalDaysInFirstWeek() { -0x7F000000, -0x7F000000, 0x7F000000, 0x7F000000 }, // JULIAN_DAY { 0, 0, 24*ONE_HOUR-1, 24*ONE_HOUR-1 }, // MILLISECONDS_IN_DAY { 0, 0, 1, 1 }, // IS_LEAP_MONTH + { 0, 0, 12, 12 }, // ORDINAL_MONTH }; /** @@ -5302,6 +5461,13 @@ private final void computeWeekFields() { }, }; + static final int[][][] MONTH_PRECEDENCE = { + { + { MONTH }, + { ORDINAL_MONTH }, + }, + }; + /** * Given a precedence table, return the newest field combination in * the table, or -1 if none is found. @@ -5434,7 +5600,7 @@ protected void validateField(int field) { switch (field) { case DAY_OF_MONTH: y = handleGetExtendedYear(); - validateField(field, 1, handleGetMonthLength(y, internalGet(MONTH))); + validateField(field, 1, handleGetMonthLength(y, internalGetMonth())); break; case DAY_OF_YEAR: y = handleGetExtendedYear(); @@ -5905,6 +6071,7 @@ protected int computeJulianDay() { if (stamp[JULIAN_DAY] >= MINIMUM_USER_STAMP) { int bestStamp = newestStamp(ERA, DAY_OF_WEEK_IN_MONTH, UNSET); bestStamp = newestStamp(YEAR_WOY, EXTENDED_YEAR, bestStamp); + bestStamp = newestStamp(ORDINAL_MONTH, ORDINAL_MONTH, bestStamp); if (bestStamp <= stamp[JULIAN_DAY]) { return internalGet(JULIAN_DAY); } @@ -6050,7 +6217,7 @@ protected int handleComputeJulianDay(int bestField) { internalSet(EXTENDED_YEAR, year); - int month = useMonth ? internalGet(MONTH, getDefaultMonthInYear(year)) : 0; + int month = useMonth ? internalGetMonth(getDefaultMonthInYear(year)) : 0; // Get the Julian day of the day BEFORE the start of this year. // If useMonth is true, get the day before the start of the month. @@ -6128,7 +6295,7 @@ protected int handleComputeJulianDay(int bestField) { // past the first of the given day-of-week in this month. // Note that we handle -2, -3, etc. correctly, even though // values < -1 are technically disallowed. - int m = internalGet(MONTH, JANUARY); + int m = internalGetMonth(JANUARY); int monthLength = handleGetMonthLength(year, m); date += ((monthLength - date) / 7 + dim + 1) * 7; } @@ -6219,7 +6386,9 @@ protected int computeGregorianMonthStart(int year, int month) { * @stable ICU 2.0 */ protected void handleComputeFields(int julianDay) { - internalSet(MONTH, getGregorianMonth()); + int gmonth = getGregorianMonth(); + internalSet(MONTH, gmonth); + internalSet(ORDINAL_MONTH, gmonth); internalSet(DAY_OF_MONTH, getGregorianDayOfMonth()); internalSet(DAY_OF_YEAR, getGregorianDayOfYear()); int eyear = getGregorianYear(); @@ -6455,7 +6624,7 @@ protected static final int floorDivide(long numerator, int denominator, int[] re "DAY_OF_WEEK_IN_MONTH", "AM_PM", "HOUR", "HOUR_OF_DAY", "MINUTE", "SECOND", "MILLISECOND", "ZONE_OFFSET", "DST_OFFSET", "YEAR_WOY", "DOW_LOCAL", "EXTENDED_YEAR", - "JULIAN_DAY", "MILLISECONDS_IN_DAY", + "JULIAN_DAY", "MILLISECONDS_IN_DAY", "IS_LEAP_MONTH", "ORDINAL_MONTH" }; /** diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java index 23d400e5a025..069b6271e569 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java @@ -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 @@ -420,6 +423,7 @@ protected ChineseCalendar(TimeZone zone, ULocale locale, int epochYear, TimeZone {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY { 0, 0, 1, 1 }, // IS_LEAP_MONTH + { 0, 0, 11, 12 }, // ORDINAL_MONTH }; /** @@ -557,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 @@ -577,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 @@ -589,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 { @@ -611,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; @@ -837,7 +843,7 @@ protected void handleComputeFields(int julianDay) { * IS_LEAP_MONTH fields, as required by * handleComputeMonthStart(). * - *

As a side effect, this method sets {@link #isLeapYear}. + *

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 @@ -868,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) { @@ -985,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 @@ -1003,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; @@ -1042,7 +1059,117 @@ private void readObject(ObjectInputStream stream) winterSolsticeCache = new CalendarCache(); newYearCache = new CalendarCache(); } - + + //------------------------------------------------------------------------- + // Temporal Calendar API. + //------------------------------------------------------------------------- + /** + * {@icu} Returns true if the date is in a leap year. Recalculate the current time + * field values if the time value has been changed by a call to setTime(). + * This method is semantically const, but may alter the object in memory. + * A "leap year" is a year that contains more days than other years (for + * solar or lunar calendars) or more months than other years (for lunisolar + * calendars like Hebrew or Chinese), as defined in the ECMAScript Temporal + * proposal. + * @return true if the date in the fields is in a Temporal proposal + * defined leap year. False otherwise. + * @draft ICU 74 + */ + public boolean inTemporalLeapYear() { + return getActualMaximum(DAY_OF_YEAR) > 360; + } + + private static String [] gTemporalLeapMonthCodes = { + "M01L", "M02L", "M03L", "M04L", "M05L", "M06L", "M07L", "M08L", "M09L", "M10L", "M11L", "M12L" + }; + + /** + * Gets The Temporal monthCode value corresponding to the month for the date. + * The value is a string identifier that starts with the literal grapheme + * "M" followed by two graphemes representing the zero-padded month number + * of the current month in a normal (non-leap) year and suffixed by an + * optional literal grapheme "L" if this is a leap month in a lunisolar + * calendar. For the Chinese calendar, the values are "M01" .. "M12" for + * non-leap year and * in leap year with another monthCode in "M01L" .. "M12L". + * + * @return One of 24 possible strings in {"M01".."M12", "M01L".."M12L"}. + * @draft ICU 74 + */ + public String getTemporalMonthCode() { + // We need to call get, not internalGet, to force the calculation + // from ORDINAL_MONTH. + int is_leap = get(IS_LEAP_MONTH); + if (is_leap != 0) { + return gTemporalLeapMonthCodes[get(MONTH)]; + } + return super.getTemporalMonthCode(); + } + + /** + * Sets The Temporal monthCode which is a string identifier that starts + * with the literal grapheme "M" followed by two graphemes representing + * the zero-padded month number of the current month in a normal + * (non-leap) year and suffixed by an optional literal grapheme "L" if this + * is a leap month in a lunisolar calendar. + * For the Chinese calendar, the values are "M01" .. "M12" for non-leap year and + * in leap year with another monthCode in "M01L" .. "M12L". + * @param temporalMonth One of 25 possible strings in {"M01".. "M12", "M13", "M01L", + * "M12L"}. + * @draft ICU 74 + */ + public void setTemporalMonthCode( String temporalMonth ) { + if (temporalMonth.length() != 4 || temporalMonth.charAt(0) != 'M' || temporalMonth.charAt(3) != 'L') { + set(IS_LEAP_MONTH, 0); + super.setTemporalMonthCode(temporalMonth); + return; + } + for (int m = 0; m < gTemporalLeapMonthCodes.length; m++) { + if (temporalMonth.equals(gTemporalLeapMonthCodes[m])) { + set(MONTH, m); + set(IS_LEAP_MONTH, 1); + return; + } + } + throw new IllegalArgumentException("Incorrect temporal Month code: " + 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; + } + + /** + * {@inheritDoc} + * @internal + */ + protected int internalGetMonth(int defaultValue) + { + if (resolveFields(MONTH_PRECEDENCE) == MONTH) { + return internalGet(MONTH, defaultValue); + } + return internalGetMonth(); + } + /* private static CalendarFactory factory; public static CalendarFactory factory() { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/CopticCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/CopticCalendar.java index 9f38324755df..9dbcd6b42e69 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/CopticCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/CopticCalendar.java @@ -293,6 +293,7 @@ protected void handleComputeFields(int julianDay) { internalSet(ERA, era); internalSet(YEAR, year); internalSet(MONTH, fields[1]); + internalSet(ORDINAL_MONTH, fields[1]); internalSet(DAY_OF_MONTH, fields[2]); internalSet(DAY_OF_YEAR, (30 * fields[1]) + fields[2]); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/EthiopicCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/EthiopicCalendar.java index 356365b5fe63..9f6bdba1b80e 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/EthiopicCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/EthiopicCalendar.java @@ -353,6 +353,7 @@ protected void handleComputeFields(int julianDay) { internalSet(ERA, era); internalSet(YEAR, year); internalSet(MONTH, fields[1]); + internalSet(ORDINAL_MONTH, fields[1]); internalSet(DAY_OF_MONTH, fields[2]); internalSet(DAY_OF_YEAR, (30 * fields[1]) + fields[2]); } diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/GregorianCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/GregorianCalendar.java index aa76c0279d07..70e36a868777 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/GregorianCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/GregorianCalendar.java @@ -280,6 +280,7 @@ public class GregorianCalendar extends Calendar { {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY {/* */}, // IS_LEAP_MONTH + { 0, 0, 11, 11 }, // ORDINAL_MONTH }; /** @@ -572,7 +573,7 @@ public void roll(int field, int amount) { // may be one year before or after the calendar year. int isoYear = get(YEAR_WOY); int isoDoy = internalGet(DAY_OF_YEAR); - if (internalGet(MONTH) == Calendar.JANUARY) { + if (internalGetMonth() == Calendar.JANUARY) { if (woy >= 52) { isoDoy += handleGetYearLength(isoYear); } @@ -792,6 +793,7 @@ protected void handleComputeFields(int julianDay) { ++dayOfYear; } internalSet(MONTH, month); + internalSet(ORDINAL_MONTH, month); internalSet(DAY_OF_MONTH, dayOfMonth); internalSet(DAY_OF_YEAR, dayOfYear); internalSet(EXTENDED_YEAR, eyear); diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/HebrewCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/HebrewCalendar.java index 73322d112d8b..0330872871fc 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/HebrewCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/HebrewCalendar.java @@ -195,6 +195,8 @@ public class HebrewCalendar extends Calendar { { -5000000, -5000000, 5000000, 5000000 }, // EXTENDED_YEAR {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY + {/* */}, // IS_LEAP_MONTH + { 0, 0, 11, 12 }, // ORDINAL_MONTH }; /** @@ -450,6 +452,7 @@ public void add(int field, int amount) { switch (field) { case MONTH: + case ORDINAL_MONTH: { // We can't just do a set(MONTH, get(MONTH) + amount). The // reason is ADAR_1. Suppose amount is +2 and we land in @@ -537,6 +540,7 @@ public void roll(int field, int amount) { switch (field) { case MONTH: + case ORDINAL_MONTH: { int month = get(MONTH); int year = get(YEAR); @@ -766,7 +770,8 @@ protected int handleGetYearLength(int eyear) { @Override @Deprecated protected void validateField(int field) { - if (field == MONTH && !isLeapYear(handleGetExtendedYear()) && internalGet(MONTH) == ADAR_1) { + if ((field == MONTH || field == ORDINAL_MONTH) && + !isLeapYear(handleGetExtendedYear()) && internalGetMonth() == ADAR_1) { throw new IllegalArgumentException("MONTH cannot be ADAR_1(5) except leap years"); } @@ -815,7 +820,8 @@ protected void handleComputeFields(int julianDay) { // Now figure out which month we're in, and the date within that month int yearType = yearType(year); - int monthStart[][] = isLeapYear(year) ? LEAP_MONTH_START : MONTH_START; + boolean isLeap = isLeapYear(year); + int monthStart[][] = isLeap ? LEAP_MONTH_START : MONTH_START; int month = 0; while (dayOfYear > monthStart[month][yearType]) { @@ -827,6 +833,11 @@ protected void handleComputeFields(int julianDay) { internalSet(ERA, 0); internalSet(YEAR, year); internalSet(EXTENDED_YEAR, year); + int ordinal_month = month; + if (!isLeap && ordinal_month > ADAR_1) { + ordinal_month--; + } + internalSet(ORDINAL_MONTH, ordinal_month); internalSet(MONTH, month); internalSet(DAY_OF_MONTH, dayOfMonth); internalSet(DAY_OF_YEAR, dayOfYear); @@ -893,6 +904,79 @@ public String getType() { return "hebrew"; } + //------------------------------------------------------------------------- + // Temporal Calendar API. + //------------------------------------------------------------------------- + /** + * {@inheritDoc} + * @draft ICU 74 + */ + public boolean inTemporalLeapYear() { + return isLeapYear(get(EXTENDED_YEAR)); + } + + private static String [] gTemporalMonthCodesForHebrew = { + "M01", "M02", "M03", "M04", "M05", "M05L", + "M06", "M07", "M08", "M09", "M10", "M11", "M12" + }; + + /** + * Gets The Temporal monthCode value corresponding to the month for the date. + * The value is a string identifier that starts with the literal grapheme + * "M" followed by two graphemes representing the zero-padded month number + * of the current month in a normal (non-leap) year and suffixed by an + * optional literal grapheme "L" if this is a leap month in a lunisolar + * calendar. For the Hebrew calendar, the values are "M01" .. "M12" for + * non-leap year, and "M01" .. "M05", "M05L", "M06" .. "M12" for leap year. + * + * @return One of 13 possible strings in {"M01".. "M05", "M05L", "M06" .. "M12"}. + * @draft ICU 74 + */ + public String getTemporalMonthCode() { + return gTemporalMonthCodesForHebrew[get(MONTH)]; + } + + /** + * Sets The Temporal monthCode which is a string identifier that starts + * with the literal grapheme "M" followed by two graphemes representing + * the zero-padded month number of the current month in a normal + * (non-leap) year and suffixed by an optional literal grapheme "L" if this + * is a leap month in a lunisolar calendar. For Hebrew calendar, the values + * are "M01" .. "M12" for non-leap years, and "M01" .. "M05", "M05L", "M06" + * .. "M12" for leap year. + * @param temporalMonth The value to be set for temporal monthCode. + * @draft ICU 74 + */ + public void setTemporalMonthCode( String temporalMonth ) { + if (temporalMonth.length() == 3 || temporalMonth.length() == 4) { + for (int m = 0; m < gTemporalMonthCodesForHebrew.length; m++) { + if (temporalMonth.equals(gTemporalMonthCodesForHebrew[m])) { + set(MONTH, m); + return; + } + } + } + throw new IllegalArgumentException("Incorrect temporal Month code: " + temporalMonth); + } + + //------------------------------------------------------------------------- + // End of Temporal Calendar API + //------------------------------------------------------------------------- + + /** + * {@inheritDoc} + * @internal + */ + protected int internalGetMonth() + { + if (resolveFields(MONTH_PRECEDENCE) == ORDINAL_MONTH) { + int ordinalMonth = internalGet(ORDINAL_MONTH); + int year = handleGetExtendedYear(); + return ordinalMonth + ((isLeapYear(year) && (ordinalMonth > ADAR_1)) ? 1: 0); + } + return super.internalGetMonth(); + } + /* private static CalendarFactory factory; public static CalendarFactory factory() { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/IndianCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/IndianCalendar.java index 23e5be97c1c8..cb284c8785d1 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/IndianCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/IndianCalendar.java @@ -395,6 +395,7 @@ protected void handleComputeFields(int julianDay){ internalSet(EXTENDED_YEAR, IndianYear); internalSet(YEAR, IndianYear); internalSet(MONTH, IndianMonth); + internalSet(ORDINAL_MONTH, IndianMonth); internalSet(DAY_OF_MONTH, IndianDayOfMonth ); internalSet(DAY_OF_YEAR, yday + 1); // yday is 0-based } @@ -424,6 +425,8 @@ protected void handleComputeFields(int julianDay){ { -5000000, -5000000, 5000000, 5000000}, // EXTENDED_YEAR {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY + {/* */}, // IS_LEAP_MONTH + { 0, 0, 11, 11 }, // ORDINAL_MONTH }; diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/IslamicCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/IslamicCalendar.java index b8ae53c792c1..14a507ecbfdb 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/IslamicCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/IslamicCalendar.java @@ -395,6 +395,8 @@ public boolean isCivil() { { 1, 1, 5000000, 5000000}, // EXTENDED_YEAR {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY + {/* */}, // IS_LEAP_MONTH + { 0, 0, 11, 11 }, // ORDINAL_MONTH }; /* @@ -917,6 +919,7 @@ protected void handleComputeFields(int julianDay) { internalSet(YEAR, year); internalSet(EXTENDED_YEAR, year); internalSet(MONTH, month); + internalSet(ORDINAL_MONTH, month); internalSet(DAY_OF_MONTH, dayOfMonth); internalSet(DAY_OF_YEAR, dayOfYear); } @@ -1031,6 +1034,29 @@ private void readObject(ObjectInputStream in) throws IOException,ClassNotFoundEx } } + //------------------------------------------------------------------------- + // Temporal Calendar API. + //------------------------------------------------------------------------- + /** + * {@icu} Returns true if the date is in a leap year. Recalculate the current time + * field values if the time value has been changed by a call to setTime(). + * This method is semantically const, but may alter the object in memory. + * A "leap year" is a year that contains more days than other years (for + * solar or lunar calendars) or more months than other years (for lunisolar + * calendars like Hebrew or Chinese), as defined in the ECMAScript Temporal + * proposal. + * @return true if the date in the fields is in a Temporal proposal + * defined leap year. False otherwise. + * @draft ICU 74 + */ + public boolean inTemporalLeapYear() { + return getActualMaximum(DAY_OF_YEAR) == 355; + } + + //------------------------------------------------------------------------- + // End of Temporal Calendar API + //------------------------------------------------------------------------- + /* private static CalendarFactory factory; public static CalendarFactory factory() { diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/PersianCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/PersianCalendar.java index 68919b580eb3..b43121997cd0 100644 --- a/icu4j/main/classes/core/src/com/ibm/icu/util/PersianCalendar.java +++ b/icu4j/main/classes/core/src/com/ibm/icu/util/PersianCalendar.java @@ -289,6 +289,9 @@ public PersianCalendar(int year, int month, int date, int hour, { -5000000, -5000000, 5000000, 5000000}, // EXTENDED_YEAR {/* */}, // JULIAN_DAY {/* */}, // MILLISECONDS_IN_DAY + {/* */}, // IS_LEAP_MONTH + { 0, 0, 11, 11 }, // ORDINAL_MONTH + }; /** @@ -436,6 +439,7 @@ protected void handleComputeFields(int julianDay) { internalSet(YEAR, year); internalSet(EXTENDED_YEAR, year); internalSet(MONTH, month); + internalSet(ORDINAL_MONTH, month); internalSet(DAY_OF_MONTH, dayOfMonth); internalSet(DAY_OF_YEAR, dayOfYear); } diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/CalendarRegressionTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/CalendarRegressionTest.java index 5a00df2d2395..bd07c862fba3 100644 --- a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/CalendarRegressionTest.java +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/CalendarRegressionTest.java @@ -53,7 +53,7 @@ public class CalendarRegressionTest extends com.ibm.icu.dev.test.TestFmwk { "DAY_OF_WEEK_IN_MONTH", "AM_PM", "HOUR", "HOUR_OF_DAY", "MINUTE", "SECOND", "MILLISECOND", "ZONE_OFFSET", "DST_OFFSET", "YEAR_WOY", "DOW_LOCAL", "EXTENDED_YEAR", - "JULIAN_DAY", "MILLISECONDS_IN_DAY" + "JULIAN_DAY", "MILLISECONDS_IN_DAY", "IS_LEAP_YEAR", "ORDINAL_MONTH" }; @@ -1427,6 +1427,7 @@ public void Test4173516() { cal.get(Calendar.SECOND) != fields[5] || cal.get(Calendar.MILLISECOND) != fields[6]) { errln("Field " + field + + " " + (op==0 ? "add" : "roll") + " (" + FIELD_NAME[field] + ") FAIL, expected " + fields[0] + diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/InTemporalLeapYearTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/InTemporalLeapYearTest.java new file mode 100644 index 000000000000..a860b89666cb --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/InTemporalLeapYearTest.java @@ -0,0 +1,248 @@ +// © 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); + leapTest.add(Calendar.MONTH, 10); + 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); + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/OrdinalMonthTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/OrdinalMonthTest.java new file mode 100644 index 000000000000..95c567f22128 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/OrdinalMonthTest.java @@ -0,0 +1,409 @@ +// © 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.CopticCalendar; +import com.ibm.icu.util.EthiopicCalendar; +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 OrdinalMonthTest extends com.ibm.icu.dev.test.TestFmwk { + + private void VerifyMonth(String message, Calendar cc, int expectedMonth, + int expectedOrdinalMonth, boolean expectedLeapMonth, + String expectedMonthCode) { + assertEquals(message + " get(MONTH)", + expectedMonth, cc.get(Calendar.MONTH)); + assertEquals(message + " get(ORDINAL_MONTH)", + expectedOrdinalMonth, cc.get(Calendar.ORDINAL_MONTH)); + assertEquals(message + " get(IS_LEAP_MONTH)", + expectedLeapMonth ? 1 : 0, cc.get(Calendar.IS_LEAP_MONTH)); + assertEquals(message + " getTemporalMonthCode()", + expectedMonthCode, cc.getTemporalMonthCode()); + } + + @Test + public void TestMostCalendarsSet() { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + String [] calendars = Calendar.getKeywordValuesForLocale( + "calendar", ULocale.ROOT, false); + for (String calendar : calendars) { + // Test these three calendars differently. + if (calendar == "chinese") continue; // work around ICU-22444 + if (calendar == "dangi") continue; // work around ICU-22444 + if (calendar == "hebrew") continue; // work around ICU-22444 + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", calendar)); + Calendar cc2 = (Calendar)cc1.clone(); + Calendar cc3 = (Calendar)cc1.clone(); + + cc1.set(Calendar.EXTENDED_YEAR, 2134); + cc2.set(Calendar.EXTENDED_YEAR, 2134); + cc3.set(Calendar.EXTENDED_YEAR, 2134); + cc1.set(Calendar.MONTH, 5); + cc2.set(Calendar.ORDINAL_MONTH, 5); + cc3.setTemporalMonthCode("M06"); + cc1.set(Calendar.DATE, 23); + cc2.set(Calendar.DATE, 23); + cc3.set(Calendar.DATE, 23); + assertEquals("M06 cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("M06 cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + VerifyMonth("cc1", cc1, 5, 5, false, "M06"); + VerifyMonth("cc2", cc2, 5, 5, false, "M06"); + VerifyMonth("cc3", cc3, 5, 5, false, "M06"); + + cc1.set(Calendar.ORDINAL_MONTH, 6); + cc2.setTemporalMonthCode("M07"); + cc3.set(Calendar.MONTH, 6); + assertEquals("M07 cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("M07 cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + VerifyMonth("cc1", cc1, 6, 6, false, "M07"); + VerifyMonth("cc2", cc2, 6, 6, false, "M07"); + VerifyMonth("cc3", cc3, 6, 6, false, "M07"); + + cc1.setTemporalMonthCode("M08"); + cc2.set(Calendar.MONTH, 7); + cc3.set(Calendar.ORDINAL_MONTH, 7); + assertEquals("M08 cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("M08 cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + VerifyMonth("cc1", cc1, 7, 7, false, "M08"); + VerifyMonth("cc2", cc2, 7, 7, false, "M08"); + VerifyMonth("cc3", cc3, 7, 7, false, "M08"); + + cc1.set(Calendar.DATE, 3); + // For "M13", do not return error for these three calendars. + if (calendar == "coptic" || calendar == "ethiopic" || + calendar == "ethiopic-amete-alem") { + cc1.setTemporalMonthCode("M13"); + assertEquals("get(UCAL_MONTH) after setTemporalMonthCode(\"M13\")", + 12, cc1.get(Calendar.MONTH)); + assertEquals("get(UCAL_ORDINAL_MONTH) after setTemporalMonthCode(\"M13\")", + 12, cc1.get(Calendar.ORDINAL_MONTH)); + } else { + try { + cc1.setTemporalMonthCode("M13"); + fail("setTemporalMonthCode(\"M13\") should get IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expect to catch IllegalArgumentException + } + } + + // Out of bound monthCodes should return error. + // These are not valid for calendar do not have a leap month + String [] kInvalidMonthCodes = { + "M00", "M14", "M01L", "M02L", "M03L", "M04L", "M05L", "M06L", "M07L", + "M08L", "M09L", "M10L", "M11L", "M12L" + }; + for (String code : kInvalidMonthCodes) { + try { + cc1.setTemporalMonthCode(code); + fail("setTemporalMonthCode(\"" + code + "\") should get IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expect to catch IllegalArgumentException + } + } + } + } + + private void RunTestChineseCalendarSet(String calendar, int notLeapYear, int leapMarchYear) { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + Calendar cc1 = Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", calendar)); + Calendar cc2 = (Calendar)cc1.clone(); + Calendar cc3 = (Calendar)cc1.clone(); + cc1.set(Calendar.EXTENDED_YEAR, leapMarchYear); + cc2.set(Calendar.EXTENDED_YEAR, leapMarchYear); + cc3.set(Calendar.EXTENDED_YEAR, leapMarchYear); + + cc1.set(Calendar.MONTH, Calendar.MARCH); + cc1.set(Calendar.IS_LEAP_MONTH, 1); + cc2.set(Calendar.ORDINAL_MONTH, 3); + cc3.setTemporalMonthCode("M03L"); + cc1.set(Calendar.DATE, 1); + cc2.set(Calendar.DATE, 1); + cc3.set(Calendar.DATE, 1); + assertEquals("" + leapMarchYear + " M03L cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("" + leapMarchYear + " M03L cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + VerifyMonth("" + leapMarchYear + " M03L cc1", cc1, Calendar.MARCH, 3, true, "M03L"); + VerifyMonth("" + leapMarchYear + " M03L cc2", cc2, Calendar.MARCH, 3, true, "M03L"); + VerifyMonth("" + leapMarchYear + " M03L cc3", cc3, Calendar.MARCH, 3, true, "M03L"); + + cc1.set(Calendar.EXTENDED_YEAR, notLeapYear); + cc2.set(Calendar.EXTENDED_YEAR, notLeapYear); + cc3.set(Calendar.EXTENDED_YEAR, notLeapYear); + cc1.set(Calendar.ORDINAL_MONTH, 5); + cc2.setTemporalMonthCode("M06"); + cc3.set(Calendar.MONTH, Calendar.JUNE); + cc3.set(Calendar.IS_LEAP_MONTH, 0); + assertEquals("" + notLeapYear + " M06 cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("" + notLeapYear + " M06 cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + VerifyMonth("" + notLeapYear + " M06 cc1", cc1, Calendar.JUNE, 5, false, "M06"); + VerifyMonth("" + notLeapYear + " M06 cc2", cc2, Calendar.JUNE, 5, false, "M06"); + VerifyMonth("" + notLeapYear + " M06 cc3", cc3, Calendar.JUNE, 5, false, "M06"); + + cc1.set(Calendar.EXTENDED_YEAR, leapMarchYear); + cc2.set(Calendar.EXTENDED_YEAR, leapMarchYear); + cc3.set(Calendar.EXTENDED_YEAR, leapMarchYear); + cc1.setTemporalMonthCode("M04"); + cc2.set(Calendar.MONTH, Calendar.APRIL); + cc2.set(Calendar.IS_LEAP_MONTH, 0); + cc3.set(Calendar.ORDINAL_MONTH, 4); + assertEquals("" + leapMarchYear + " M04 cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("" + leapMarchYear + " M04 cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + // 4592 has leap March so April is the 5th month in that year. + VerifyMonth("" + leapMarchYear + " M04 cc1", cc1, Calendar.APRIL, 4, false, "M04"); + VerifyMonth("" + leapMarchYear + " M04 cc2", cc2, Calendar.APRIL, 4, false, "M04"); + VerifyMonth("" + leapMarchYear + " M04 cc3", cc3, Calendar.APRIL, 4, false, "M04"); + + cc1.set(Calendar.EXTENDED_YEAR, notLeapYear); + cc2.set(Calendar.EXTENDED_YEAR, notLeapYear); + cc3.set(Calendar.EXTENDED_YEAR, notLeapYear); + assertEquals("" + leapMarchYear + " M04 no leap month before cc2==cc1 set month by UCAL_MONTH and UCAL_UCAL_ORDINAL_MONTH", + cc1, cc2); + assertEquals("" + leapMarchYear + " M04 no leap month before cc2==cc3 set month by UCAL_MONTH and setTemporalMonthCode", + cc2, cc3); + // 4592 has no leap month before April so April is the 4th month in that year. + VerifyMonth("" + leapMarchYear + " M04 cc1", cc1, Calendar.APRIL, 3, false, "M04"); + VerifyMonth("" + leapMarchYear + " M04 cc2", cc2, Calendar.APRIL, 3, false, "M04"); + VerifyMonth("" + leapMarchYear + " M04 cc3", cc3, Calendar.APRIL, 3, false, "M04"); + + // Out of bound monthCodes should return error. + // These are not valid for calendar do not have a leap month + String [] kInvalidMonthCodes = { "M00", "M13", "M14" }; + for (String code : kInvalidMonthCodes) { + try { + cc1.setTemporalMonthCode(code); + fail("setTemporalMonthCode(\"" + code + "\") should get IllegalArgumentException"); + } catch (IllegalArgumentException expected) { + // expect to catch IllegalArgumentException + } + } + } + + @Test + public void TestChineseCalendarSet() { + RunTestChineseCalendarSet("chinese", 4591, 4592); + } + + @Test + public void TestDangiCalendarSet() { + RunTestChineseCalendarSet("dangi", 4287, 4288); + } + + @Test + public void TestHebrewCalendarSet() { + } + + @Test + public void TestAdd() { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + String [] calendars = Calendar.getKeywordValuesForLocale( + "calendar", ULocale.ROOT, false); + for (String calendar : calendars) { + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", calendar)); + cc1.setTime(gc.getTime()); + Calendar cc2 = (Calendar)cc1.clone(); + for (int i = 0; i < 8; i++) { + for (int j = 1; j < 8; j++) { + cc1.add(Calendar.MONTH, j); + cc2.add(Calendar.ORDINAL_MONTH, j); + assertEquals("two add produce the same result", cc1, cc2); + } + for (int j = 1; j < 8; j++) { + cc1.add(Calendar.MONTH, -j); + cc2.add(Calendar.ORDINAL_MONTH, -j); + assertEquals("two add produce the same result", cc1, cc2); + } + } + } + } + + @Test + public void TestRoll() { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + String [] calendars = Calendar.getKeywordValuesForLocale( + "calendar", ULocale.ROOT, false); + for (String calendar : calendars) { + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", calendar)); + cc1.setTime(gc.getTime()); + Calendar cc2 = (Calendar)cc1.clone(); + for (int i = 0; i < 8; i++) { + for (int j = 1; j < 8; j++) { + cc1.roll(Calendar.MONTH, j); + cc2.roll(Calendar.ORDINAL_MONTH, j); + assertEquals("two roll produce the same result", cc1, cc2); + } + for (int j = 1; j < 8; j++) { + cc1.roll(Calendar.MONTH, -j); + cc2.roll(Calendar.ORDINAL_MONTH, -j); + assertEquals("two roll produce the same result", cc1, cc2); + } + for (int j = 1; j < 3; j++) { + cc1.roll(Calendar.MONTH, true); + cc2.roll(Calendar.ORDINAL_MONTH, true); + assertEquals("two roll produce the same result", cc1, cc2); + } + for (int j = 1; j < 3; j++) { + cc1.roll(Calendar.MONTH, false); + cc2.roll(Calendar.ORDINAL_MONTH, false); + assertEquals("two roll produce the same result", cc1, cc2); + } + } + } + } + + @Test + public void TestLimits() { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + String [] calendars = Calendar.getKeywordValuesForLocale( + "calendar", ULocale.ROOT, false); + Object[][] cases = { + { "gregorian", 0, 11, 0, 11 }, + { "japanese", 0, 11, 0, 11 }, + { "buddhist", 0, 11, 0, 11 }, + { "roc", 0, 11, 0, 11 }, + { "persian", 0, 11, 0, 11 }, + { "islamic-civil", 0, 11, 0, 11 }, + { "islamic", 0, 11, 0, 11 }, + { "hebrew", 0, 12, 0, 11 }, + { "chinese", 0, 12, 0, 11 }, + { "indian", 0, 11, 0, 11 }, + { "coptic", 0, 12, 0, 12 }, + { "ethiopic", 0, 12, 0, 12 }, + { "ethiopic-amete-alem", 0, 12, 0, 12 }, + { "iso8601", 0, 11, 0, 11 }, + { "dangi", 0, 12, 0, 11 }, + { "islamic-umalqura", 0, 11, 0, 11 }, + { "islamic-tbla", 0, 11, 0, 11 }, + { "islamic-rgsa", 0, 11, 0, 11 }, + }; + for (String calendar : calendars) { + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", calendar)); + boolean found = false; + for (Object[] cas : cases) { + if (calendar.equals((String) cas[0])) { + int min = (Integer) cas[1]; + int max = (Integer) cas[2]; + int greatestMin = (Integer) cas[3]; + int leastMax = (Integer) cas[4]; + assertEquals("getMinimum(Calendar.ORDINAL_MONTH)", + min, cc1.getMinimum(Calendar.ORDINAL_MONTH)); + assertEquals("getMaximum(Calendar.ORDINAL_MONTH)", + max, cc1.getMaximum(Calendar.ORDINAL_MONTH)); + assertEquals("getMinimum(Calendar.ORDINAL_MONTH)", + greatestMin, cc1.getGreatestMinimum(Calendar.ORDINAL_MONTH)); + assertEquals("getMinimum(Calendar.ORDINAL_MONTH)", + leastMax, cc1.getLeastMaximum(Calendar.ORDINAL_MONTH)); + found = true; + break; + } + } + if (!found) { + errln("Cannot find expectation" + calendar); + } + } + } + + @Test + public void TestActaulLimits() { + GregorianCalendar gc = new GregorianCalendar(); + gc.set(2022, Calendar.DECEMBER, 16); + Object[][] cases = { + { "gregorian", 2021, 0, 11 }, + { "gregorian", 2022, 0, 11 }, + { "gregorian", 2023, 0, 11 }, + { "japanese", 2021, 0, 11 }, + { "japanese", 2022, 0, 11 }, + { "japanese", 2023, 0, 11 }, + { "buddhist", 2021, 0, 11 }, + { "buddhist", 2022, 0, 11 }, + { "buddhist", 2023, 0, 11 }, + { "roc", 2021, 0, 11 }, + { "roc", 2022, 0, 11 }, + { "roc", 2023, 0, 11 }, + { "persian", 1400, 0, 11 }, + { "persian", 1401, 0, 11 }, + { "persian", 1402, 0, 11 }, + { "hebrew", 5782, 0, 12 }, + { "hebrew", 5783, 0, 11 }, + { "hebrew", 5789, 0, 11 }, + { "hebrew", 5790, 0, 12 }, + { "chinese", 4645, 0, 11 }, + { "chinese", 4646, 0, 12 }, + { "chinese", 4647, 0, 11 }, + { "dangi", 4645 + 304, 0, 11 }, + { "dangi", 4646 + 304, 0, 12 }, + { "dangi", 4647 + 304, 0, 11 }, + { "indian", 1944, 0, 11 }, + { "indian", 1945, 0, 11 }, + { "indian", 1946, 0, 11 }, + { "coptic", 1737, 0, 12 }, + { "coptic", 1738, 0, 12 }, + { "coptic", 1739, 0, 12 }, + { "ethiopic", 2013, 0, 12 }, + { "ethiopic", 2014, 0, 12 }, + { "ethiopic", 2015, 0, 12 }, + { "ethiopic-amete-alem", 2014, 0, 12 }, + { "ethiopic-amete-alem", 2015, 0, 12 }, + { "ethiopic-amete-alem", 2016, 0, 12 }, + { "iso8601", 2022, 0, 11 }, + { "islamic-civil", 1443, 0, 11 }, + { "islamic-civil", 1444, 0, 11 }, + { "islamic-civil", 1445, 0, 11 }, + { "islamic", 1443, 0, 11 }, + { "islamic", 1444, 0, 11 }, + { "islamic", 1445, 0, 11 }, + { "islamic-umalqura", 1443, 0, 11 }, + { "islamic-umalqura", 1444, 0, 11 }, + { "islamic-umalqura", 1445, 0, 11 }, + { "islamic-tbla", 1443, 0, 11 }, + { "islamic-tbla", 1444, 0, 11 }, + { "islamic-tbla", 1445, 0, 11 }, + { "islamic-rgsa", 1443, 0, 11 }, + { "islamic-rgsa", 1444, 0, 11 }, + { "islamic-rgsa", 1445, 0, 11 }, + }; + for (Object[] cas : cases) { + String calendar = (String) cas[0]; + int extended_year = (Integer) cas[1]; + int actualMinOrdinalMonth = (Integer) cas[2]; + int actualMaxOrdinalMonth = (Integer) cas[3]; + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", calendar)); + cc1.set(Calendar.EXTENDED_YEAR, extended_year); + cc1.set(Calendar.ORDINAL_MONTH, 0); + cc1.set(Calendar.DATE, 1); + assertEquals("getActualMinimum(UCAL_ORDINAL_MONTH)", + actualMinOrdinalMonth, cc1.getActualMinimum(Calendar.ORDINAL_MONTH)); + assertEquals("getActualMaximum(UCAL_ORDINAL_MONTH)", + actualMaxOrdinalMonth, cc1.getActualMaximum(Calendar.ORDINAL_MONTH)); + } + } +} diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/TemporalMonthCodeTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/TemporalMonthCodeTest.java new file mode 100644 index 000000000000..4b87f3216402 --- /dev/null +++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/TemporalMonthCodeTest.java @@ -0,0 +1,446 @@ +// © 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.CopticCalendar; +import com.ibm.icu.util.EthiopicCalendar; +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 TemporalMonthCodeTest extends com.ibm.icu.dev.test.TestFmwk { + @Test + public void TestChineseCalendarGetTemporalMonthCode() { + RunChineseGetTemporalMonthCode( + Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "chinese"))); + } + + @Test + public void TestDangiCalendarGetTemporalMonthCode() { + RunChineseGetTemporalMonthCode( + Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "dangi"))); + } + + private String MonthCode(int month, boolean leap) { + return String.format("M%02d%s", month, leap ? "L" : ""); + } + + private String HebrewMonthCode(int month) { + if (month == HebrewCalendar.ADAR_1) { + return MonthCode(month, true); + } + return MonthCode(month < HebrewCalendar.ADAR_1 ? month+1 : month, false); + } + + private void RunChineseGetTemporalMonthCode(Calendar cal) { + GregorianCalendar gc = new GregorianCalendar(); + // 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; + startYear = 1950; + stopYear = 2050; + } + for (gc.set(startYear, Calendar.JANUARY, 1); + gc.get(Calendar.YEAR) <= stopYear; + gc.add(Calendar.DATE, incrementDays)) { + cal.setTime(gc.getTime()); + int cal_month = cal.get(Calendar.MONTH); + String expected = MonthCode(cal_month+1, cal.get(Calendar.IS_LEAP_MONTH) != 0); + assertEquals("getTemporalMonthCode", expected, cal.getTemporalMonthCode()); + } + } + + @Test + public void TestHebrewCalendarGetTemporalMonthCode() { + Calendar cal = Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "hebrew")); + GregorianCalendar gc = new GregorianCalendar(); + // 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; + } + for (gc.set(startYear, Calendar.JANUARY, 1); + gc.get(Calendar.YEAR) <= stopYear; + gc.add(Calendar.DATE, incrementDays)) { + cal.setTime(gc.getTime()); + int cal_month = cal.get(Calendar.MONTH); + String expected = HebrewMonthCode(cal_month); + assertEquals("getTemporalMonthCode", expected, cal.getTemporalMonthCode()); + } + } + + private void RunCEGetTemporalMonthCode(Calendar cal) { + GregorianCalendar gc = new GregorianCalendar(); + // Start our test from 1900, Jan 1. + // // Start testing from 1900 + gc.set(1900, Calendar.JANUARY, 1); + cal.setTime(gc.getTime()); + int year = cal.get(Calendar.YEAR); + for (int m = 0; m < 13; m++) { + String expected = MonthCode(m+1, false); + for (int y = year; y < year + 500 ; y++) { + cal.set(y, m, 1); + assertEquals("getTemporalMonthCode", expected, cal.getTemporalMonthCode()); + } + } + } + + @Test + public void TestCopticCalendarGetTemporalMonthCode() { + RunCEGetTemporalMonthCode( + Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "coptic"))); + } + @Test + public void TestEthiopicCalendarGetTemporalMonthCode() { + RunCEGetTemporalMonthCode( + Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "ethiopic"))); + } + @Test + public void TestEthiopicAmeteAlemCalendarGetTemporalMonthCode() { + RunCEGetTemporalMonthCode( + Calendar.getInstance(ULocale.ROOT.setKeywordValue("calendar", "ethiopic-amete-alem"))); + } + + @Test + public void TestGregorianCalendarSetTemporalMonthCode() { + Object[][] cases = { + { 1911, Calendar.JANUARY, 31, "M01", 0 }, + { 1970, Calendar.FEBRUARY, 22, "M02", 1 }, + { 543, Calendar.MARCH, 3, "M03", 2 }, + { 2340, Calendar.APRIL, 21, "M04", 3 }, + { 1234, Calendar.MAY, 21, "M05", 4 }, + { 1931, Calendar.JUNE, 17, "M06", 5 }, + { 2000, Calendar.JULY, 1, "M07", 6 }, + { 2033, Calendar.AUGUST, 3, "M08", 7 }, + { 2013, Calendar.SEPTEMBER, 9, "M09", 8 }, + { 1849, Calendar.OCTOBER, 31, "M10", 9 }, + { 1433, Calendar.NOVEMBER, 30, "M11", 10 }, + { 2022, Calendar.DECEMBER, 25, "M12", 11 }, + }; + GregorianCalendar gc1 = new GregorianCalendar(); + GregorianCalendar gc2 = new GregorianCalendar(); + for (Object[] cas : cases) { + int year = (Integer) cas[0]; + int month = (Integer) cas[1]; + int date = (Integer) cas[2]; + String monthCode = (String) cas[3]; + int ordinalMonth = (Integer) cas[4]; + gc1.clear(); + gc2.clear(); + gc1.set(year, month, date); + gc2.set(Calendar.YEAR, year); + gc2.setTemporalMonthCode(monthCode); + gc2.set(Calendar.DATE, date); + assertEquals("by set and setTemporalMonthCode()", gc1, gc2); + String actualMonthCode1 = gc1.getTemporalMonthCode(); + String actualMonthCode2 = gc2.getTemporalMonthCode(); + assertEquals("getTemporalMonthCode()", actualMonthCode1, actualMonthCode2); + assertEquals("getTemporalMonthCode()", monthCode, actualMonthCode2); + assertEquals("ordinalMonth", ordinalMonth, gc2.get(Calendar.ORDINAL_MONTH)); + assertEquals("ordinalMonth", + gc1.get(Calendar.ORDINAL_MONTH), gc2.get(Calendar.ORDINAL_MONTH)); + } + } + + @Test + public void TestChineseCalendarSetTemporalMonthCode() { + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", "chinese")); + Calendar cc2 = (Calendar)cc1.clone(); + GregorianCalendar gc1 = new GregorianCalendar(); + Object[][] cases = { + // https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2022.pdf + { 2022, Calendar.DECEMBER, 15, 4659, Calendar.NOVEMBER, 22, "M11", false, 10}, + // M01L is very hard to find. Cannot find a year has M01L in these several + // centuries. + // M02L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2004.pdf + { 2004, Calendar.MARCH, 20, 4641, Calendar.FEBRUARY, 30, "M02", false, 1}, + { 2004, Calendar.MARCH, 21, 4641, Calendar.FEBRUARY, 1, "M02L", true, 2}, + { 2004, Calendar.APRIL, 18, 4641, Calendar.FEBRUARY, 29, "M02L", true, 2}, + { 2004, Calendar.APRIL, 19, 4641, Calendar.MARCH, 1, "M03", false, 3}, + // M03L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/1995.pdf + { 1955, Calendar.APRIL, 21, 4592, Calendar.MARCH, 29, "M03", false, 2}, + { 1955, Calendar.APRIL, 22, 4592, Calendar.MARCH, 1, "M03L", true, 3}, + { 1955, Calendar.MAY, 21, 4592, Calendar.MARCH, 30, "M03L", true, 3}, + { 1955, Calendar.MAY, 22, 4592, Calendar.APRIL, 1, "M04", false, 4}, + // M12 https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/1996.pdf + { 1956, Calendar.FEBRUARY, 11, 4592, Calendar.DECEMBER, 30, "M12", false, 12}, + // M04L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2001.pdf + { 2001, Calendar.MAY, 22, 4638, Calendar.APRIL, 30, "M04", false, 3}, + { 2001, Calendar.MAY, 23, 4638, Calendar.APRIL, 1, "M04L", true, 4}, + { 2001, Calendar.JUNE, 20, 4638, Calendar.APRIL, 29, "M04L", true, 4}, + { 2001, Calendar.JUNE, 21, 4638, Calendar.MAY, 1, "M05", false, 5}, + // M05L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2009.pdf + { 2009, Calendar.JUNE, 22, 4646, Calendar.MAY, 30, "M05", false, 4}, + { 2009, Calendar.JUNE, 23, 4646, Calendar.MAY, 1, "M05L", true, 5}, + { 2009, Calendar.JULY, 21, 4646, Calendar.MAY, 29, "M05L", true, 5}, + { 2009, Calendar.JULY, 22, 4646, Calendar.JUNE, 1, "M06", false, 6}, + // M06L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2017.pdf + { 2017, Calendar.JULY, 22, 4654, Calendar.JUNE, 29, "M06", false, 5}, + { 2017, Calendar.JULY, 23, 4654, Calendar.JUNE, 1, "M06L", true, 6}, + { 2017, Calendar.AUGUST, 21, 4654, Calendar.JUNE, 30, "M06L", true, 6}, + { 2017, Calendar.AUGUST, 22, 4654, Calendar.JULY, 1, "M07", false, 7}, + // M07L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2006.pdf + { 2006, Calendar.AUGUST, 23, 4643, Calendar.JULY, 30, "M07", false, 6}, + { 2006, Calendar.AUGUST, 24, 4643, Calendar.JULY, 1, "M07L", true, 7}, + { 2006, Calendar.SEPTEMBER, 21, 4643, Calendar.JULY, 29, "M07L", true, 7}, + { 2006, Calendar.SEPTEMBER, 22, 4643, Calendar.AUGUST, 1, "M08", false, 8}, + // M08L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/1995.pdf + { 1995, Calendar.SEPTEMBER, 24, 4632, Calendar.AUGUST, 30, "M08", false, 7}, + { 1995, Calendar.SEPTEMBER, 25, 4632, Calendar.AUGUST, 1, "M08L", true, 8}, + { 1995, Calendar.OCTOBER, 23, 4632, Calendar.AUGUST, 29, "M08L", true, 8}, + { 1995, Calendar.OCTOBER, 24, 4632, Calendar.SEPTEMBER, 1, "M09", false, 9}, + // M09L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2014.pdf + { 2014, Calendar.OCTOBER, 23, 4651, Calendar.SEPTEMBER, 30, "M09", false, 8}, + { 2014, Calendar.OCTOBER, 24, 4651, Calendar.SEPTEMBER, 1, "M09L", true, 9}, + { 2014, Calendar.NOVEMBER, 21, 4651, Calendar.SEPTEMBER, 29, "M09L", true, 9}, + { 2014, Calendar.NOVEMBER, 22, 4651, Calendar.OCTOBER, 1, "M10", false, 10}, + // M10L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/1984.pdf + { 1984, Calendar.NOVEMBER, 22, 4621, Calendar.OCTOBER, 30, "M10", false, 9}, + { 1984, Calendar.NOVEMBER, 23, 4621, Calendar.OCTOBER, 1, "M10L", true, 10}, + { 1984, Calendar.DECEMBER, 21, 4621, Calendar.OCTOBER, 29, "M10L", true, 10}, + { 1984, Calendar.DECEMBER, 22, 4621, Calendar.NOVEMBER, 1, "M11", false, 11}, + // M11L https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2033.pdf + // https://www.hko.gov.hk/tc/gts/time/calendar/pdf/files/2034.pdf + { 2033, Calendar.DECEMBER, 21, 4670, Calendar.NOVEMBER, 30, "M11", false, 10}, + { 2033, Calendar.DECEMBER, 22, 4670, Calendar.NOVEMBER, 1, "M11L", true, 11}, + { 2034, Calendar.JANUARY, 19, 4670, Calendar.NOVEMBER, 29, "M11L", true, 11}, + { 2034, Calendar.JANUARY, 20, 4670, Calendar.DECEMBER, 1, "M12", false, 12}, + // M12L is very hard to find. Cannot find a year has M01L in these several + // centuries. + }; + for (Object[] cas : cases) { + int gYear = (Integer) cas[0]; + int gMonth = (Integer) cas[1]; + int gDate = (Integer) cas[2]; + int cYear = (Integer) cas[3]; + int cMonth = (Integer) cas[4]; + int cDate = (Integer) cas[5]; + String cMonthCode = (String) cas[6]; + boolean cLeapMonth = (Boolean) cas[7]; + int cOrdinalMonth = (Integer) cas[8]; + gc1.clear(); + cc1.clear(); + cc2.clear(); + gc1.set(gYear, gMonth, gDate); + cc1.setTime(gc1.getTime()); + cc2.set(Calendar.EXTENDED_YEAR, cYear); + cc2.setTemporalMonthCode(cMonthCode); + cc2.set(Calendar.DATE, cDate); + assertEquals("year", cYear, cc1.get(Calendar.EXTENDED_YEAR)); + assertEquals("month", cMonth, cc1.get(Calendar.MONTH)); + assertEquals("date", cDate, cc1.get(Calendar.DATE)); + assertEquals("is_leap_month", cLeapMonth ? 1 : 0, + cc1.get(Calendar.IS_LEAP_MONTH)); + assertEquals("getTemporalMonthCode()", cMonthCode, + cc1.getTemporalMonthCode()); + assertEquals("ordinalMonth", cOrdinalMonth, cc1.get(Calendar.ORDINAL_MONTH)); + assertEquals("by set() and setTemporalMonthCode()", cc1, cc2); + } + } + + @Test + public void TestHebrewCalendarSetTemporalMonthCode() { + Calendar hc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", "hebrew")); + Calendar hc2 = (Calendar)hc1.clone(); + GregorianCalendar gc1 = new GregorianCalendar(); + Object[][] cases = { + { 2022, Calendar.JANUARY, 11, 5782, HebrewCalendar.SHEVAT, 9, "M05", 4}, + { 2022, Calendar.FEBRUARY, 12, 5782, HebrewCalendar.ADAR_1, 11, "M05L", 5}, + { 2022, Calendar.MARCH, 13, 5782, HebrewCalendar.ADAR, 10, "M06", 6}, + { 2022, Calendar.APRIL, 14, 5782, HebrewCalendar.NISAN, 13, "M07", 7}, + { 2022, Calendar.MAY, 15, 5782, HebrewCalendar.IYAR, 14, "M08", 8}, + { 2022, Calendar.JUNE, 16, 5782, HebrewCalendar.SIVAN, 17, "M09", 9}, + { 2022, Calendar.JULY, 17, 5782, HebrewCalendar.TAMUZ, 18, "M10", 10}, + { 2022, Calendar.AUGUST, 18, 5782, HebrewCalendar.AV, 21, "M11", 11}, + { 2022, Calendar.SEPTEMBER, 19, 5782, HebrewCalendar.ELUL, 23, "M12", 12}, + { 2022, Calendar.OCTOBER, 20, 5783, HebrewCalendar.TISHRI, 25, "M01", 0}, + { 2022, Calendar.NOVEMBER, 21, 5783, HebrewCalendar.HESHVAN, 27, "M02", 1}, + { 2022, Calendar.DECEMBER, 22, 5783, HebrewCalendar.KISLEV, 28, "M03", 2}, + { 2023, Calendar.JANUARY, 20, 5783, HebrewCalendar.TEVET, 27, "M04", 3}, + }; + for (Object[] cas : cases) { + int gYear = (Integer) cas[0]; + int gMonth = (Integer) cas[1]; + int gDate = (Integer) cas[2]; + int hYear = (Integer) cas[3]; + int hMonth = (Integer) cas[4]; + int hDate = (Integer) cas[5]; + String hMonthCode = (String) cas[6]; + int hOrdinalMonth = (Integer) cas[7]; + gc1.clear(); + hc1.clear(); + hc2.clear(); + gc1.set(gYear, gMonth, gDate); + hc1.setTime(gc1.getTime()); + hc2.set(Calendar.EXTENDED_YEAR, hYear); + hc2.setTemporalMonthCode(hMonthCode); + hc2.set(Calendar.DATE, hDate); + assertEquals("year", hYear, hc1.get(Calendar.EXTENDED_YEAR)); + assertEquals("month", hMonth, hc1.get(Calendar.MONTH)); + assertEquals("date", hDate, hc1.get(Calendar.DATE)); + assertEquals("getTemporalMonthCode()", hMonthCode, + hc1.getTemporalMonthCode()); + assertEquals("by set() and setTemporalMonthCode()", hc1, hc2); + assertEquals("ordinalMonth", hOrdinalMonth, hc1.get(Calendar.ORDINAL_MONTH)); + assertEquals("ordinalMonth", hOrdinalMonth, hc2.get(Calendar.ORDINAL_MONTH)); + } + } + + @Test + public void TestCopticCalendarSetTemporalMonthCode() { + Calendar cc1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", "coptic")); + Calendar cc2 = (Calendar)cc1.clone(); + GregorianCalendar gc1 = new GregorianCalendar(); + Object[][] cases = { + { 1900, Calendar.JANUARY, 1, 1616, CopticCalendar.KIAHK, 23, "M04", 3}, + { 1900, Calendar.SEPTEMBER, 6, 1616, CopticCalendar.NASIE, 1, "M13", 12}, + { 1900, Calendar.SEPTEMBER, 10, 1616, CopticCalendar.NASIE, 5, "M13", 12}, + { 1900, Calendar.SEPTEMBER, 11, 1617, CopticCalendar.TOUT, 1, "M01", 0}, + + { 2022, Calendar.JANUARY, 11, 1738, CopticCalendar.TOBA, 3, "M05", 4}, + { 2022, Calendar.FEBRUARY, 12, 1738, CopticCalendar.AMSHIR, 5, "M06", 5}, + { 2022, Calendar.MARCH, 13, 1738, CopticCalendar.BARAMHAT, 4, "M07", 6}, + { 2022, Calendar.APRIL, 14, 1738, CopticCalendar.BARAMOUDA, 6, "M08", 7}, + { 2022, Calendar.MAY, 15, 1738, CopticCalendar.BASHANS, 7, "M09", 8}, + { 2022, Calendar.JUNE, 16, 1738, CopticCalendar.PAONA, 9, "M10", 9}, + { 2022, Calendar.JULY, 17, 1738, CopticCalendar.EPEP, 10, "M11", 10}, + { 2022, Calendar.AUGUST, 18, 1738, CopticCalendar.MESRA, 12, "M12", 11}, + { 2022, Calendar.SEPTEMBER, 6, 1738, CopticCalendar.NASIE, 1, "M13", 12}, + { 2022, Calendar.SEPTEMBER, 10, 1738, CopticCalendar.NASIE, 5, "M13", 12}, + { 2022, Calendar.SEPTEMBER, 11, 1739, CopticCalendar.TOUT, 1, "M01", 0}, + { 2022, Calendar.SEPTEMBER, 19, 1739, CopticCalendar.TOUT, 9, "M01", 0}, + { 2022, Calendar.OCTOBER, 20, 1739, CopticCalendar.BABA, 10, "M02", 1}, + { 2022, Calendar.NOVEMBER, 21, 1739, CopticCalendar.HATOR, 12, "M03", 2}, + { 2022, Calendar.DECEMBER, 22, 1739, CopticCalendar.KIAHK, 13, "M04", 3}, + + { 2023, Calendar.JANUARY, 1, 1739, CopticCalendar.KIAHK, 23, "M04", 3}, + { 2023, Calendar.SEPTEMBER, 6, 1739, CopticCalendar.NASIE, 1, "M13", 12}, + { 2023, Calendar.SEPTEMBER, 11, 1739, CopticCalendar.NASIE, 6, "M13", 12}, + { 2023, Calendar.SEPTEMBER, 12, 1740, CopticCalendar.TOUT, 1, "M01", 0}, + + { 2030, Calendar.JANUARY, 1, 1746, CopticCalendar.KIAHK, 23, "M04", 3}, + { 2030, Calendar.SEPTEMBER, 6, 1746, CopticCalendar.NASIE, 1, "M13", 12}, + { 2030, Calendar.SEPTEMBER, 10, 1746, CopticCalendar.NASIE, 5, "M13", 12}, + { 2030, Calendar.SEPTEMBER, 11, 1747, CopticCalendar.TOUT, 1, "M01", 0}, + }; + for (Object[] cas : cases) { + int gYear = (Integer) cas[0]; + int gMonth = (Integer) cas[1]; + int gDate = (Integer) cas[2]; + int cYear = (Integer) cas[3]; + int cMonth = (Integer) cas[4]; + int cDate = (Integer) cas[5]; + String cMonthCode = (String) cas[6]; + int cOrdinalMonth = (Integer) cas[7]; + gc1.clear(); + cc1.clear(); + cc2.clear(); + gc1.set(gYear, gMonth, gDate); + cc1.setTime(gc1.getTime()); + cc2.set(Calendar.EXTENDED_YEAR, cYear); + cc2.setTemporalMonthCode(cMonthCode); + cc2.set(Calendar.DATE, cDate); + assertEquals("year", cYear, cc1.get(Calendar.EXTENDED_YEAR)); + assertEquals("month", cMonth, cc1.get(Calendar.MONTH)); + assertEquals("date", cDate, cc1.get(Calendar.DATE)); + assertEquals("getTemporalMonthCode()", cMonthCode, + cc1.getTemporalMonthCode()); + + assertEquals("getTimeInMillis()", cc1.getTimeInMillis(), cc2.getTimeInMillis()); + assertEquals("by set() and setTemporalMonthCode()", cc1, cc2); + assertEquals("ordinalMonth", cOrdinalMonth, cc1.get(Calendar.ORDINAL_MONTH)); + assertEquals("ordinalMonth", cOrdinalMonth, cc2.get(Calendar.ORDINAL_MONTH)); + } + } + + @Test + public void TestEthiopicCalendarSetTemporalMonthCode() { + Calendar ec1 = Calendar.getInstance( + ULocale.ROOT.setKeywordValue("calendar", "ethiopic")); + Calendar ec2 = (Calendar)ec1.clone(); + GregorianCalendar gc1 = new GregorianCalendar(); + Object[][] cases = { + { 1900, Calendar.JANUARY, 1, 1892, EthiopicCalendar.TAHSAS, 23, "M04", 3}, + { 1900, Calendar.SEPTEMBER, 6, 1892, EthiopicCalendar.PAGUMEN, 1, "M13", 12}, + { 1900, Calendar.SEPTEMBER, 10, 1892, EthiopicCalendar.PAGUMEN, 5, "M13", 12}, + { 1900, Calendar.SEPTEMBER, 11, 1893, EthiopicCalendar.MESKEREM, 1, "M01", 0}, + + { 2022, Calendar.JANUARY, 11, 2014, EthiopicCalendar.TER, 3, "M05", 4}, + { 2022, Calendar.FEBRUARY, 12, 2014, EthiopicCalendar.YEKATIT, 5, "M06", 5}, + { 2022, Calendar.MARCH, 13, 2014, EthiopicCalendar.MEGABIT, 4, "M07", 6}, + { 2022, Calendar.APRIL, 14, 2014, EthiopicCalendar.MIAZIA, 6, "M08", 7}, + { 2022, Calendar.MAY, 15, 2014, EthiopicCalendar.GENBOT, 7, "M09", 8}, + { 2022, Calendar.JUNE, 16, 2014, EthiopicCalendar.SENE, 9, "M10", 9}, + { 2022, Calendar.JULY, 17, 2014, EthiopicCalendar.HAMLE, 10, "M11", 10}, + { 2022, Calendar.AUGUST, 18, 2014, EthiopicCalendar.NEHASSE, 12, "M12", 11}, + { 2022, Calendar.SEPTEMBER, 6, 2014, EthiopicCalendar.PAGUMEN, 1, "M13", 12}, + { 2022, Calendar.SEPTEMBER, 10, 2014, EthiopicCalendar.PAGUMEN, 5, "M13", 12}, + { 2022, Calendar.SEPTEMBER, 11, 2015, EthiopicCalendar.MESKEREM, 1, "M01", 0}, + { 2022, Calendar.SEPTEMBER, 19, 2015, EthiopicCalendar.MESKEREM, 9, "M01", 0}, + { 2022, Calendar.OCTOBER, 20, 2015, EthiopicCalendar.TEKEMT, 10, "M02", 1}, + { 2022, Calendar.NOVEMBER, 21, 2015, EthiopicCalendar.HEDAR, 12, "M03", 2}, + { 2022, Calendar.DECEMBER, 22, 2015, EthiopicCalendar.TAHSAS, 13, "M04", 3}, + + { 2023, Calendar.JANUARY, 1, 2015, EthiopicCalendar.TAHSAS, 23, "M04", 3}, + { 2023, Calendar.SEPTEMBER, 6, 2015, EthiopicCalendar.PAGUMEN, 1, "M13", 12}, + { 2023, Calendar.SEPTEMBER, 11, 2015, EthiopicCalendar.PAGUMEN, 6, "M13", 12}, + { 2023, Calendar.SEPTEMBER, 12, 2016, EthiopicCalendar.MESKEREM, 1, "M01", 0}, + + { 2030, Calendar.JANUARY, 1, 2022, EthiopicCalendar.TAHSAS, 23, "M04", 3}, + { 2030, Calendar.SEPTEMBER, 6, 2022, EthiopicCalendar.PAGUMEN, 1, "M13", 12}, + { 2030, Calendar.SEPTEMBER, 10, 2022, EthiopicCalendar.PAGUMEN, 5, "M13", 12}, + { 2030, Calendar.SEPTEMBER, 11, 2023, EthiopicCalendar.MESKEREM, 1, "M01", 0}, + }; + for (Object[] cas : cases) { + int gYear = (Integer) cas[0]; + int gMonth = (Integer) cas[1]; + int gDate = (Integer) cas[2]; + int eYear = (Integer) cas[3]; + int eMonth = (Integer) cas[4]; + int eDate = (Integer) cas[5]; + String eMonthCode = (String) cas[6]; + int eOrdinalMonth = (Integer) cas[7]; + gc1.clear(); + ec1.clear(); + ec2.clear(); + gc1.set(gYear, gMonth, gDate); + ec1.setTime(gc1.getTime()); + + ec2.set(Calendar.EXTENDED_YEAR, eYear); + ec2.setTemporalMonthCode(eMonthCode); + ec2.set(Calendar.DATE, eDate); + + assertEquals("year", eYear, ec1.get(Calendar.EXTENDED_YEAR)); + assertEquals("month", eMonth, ec1.get(Calendar.MONTH)); + assertEquals("date", eDate, ec1.get(Calendar.DATE)); + assertEquals("getTemporalMonthCode()", eMonthCode, + ec1.getTemporalMonthCode()); + assertEquals("by set() and setTemporalMonthCode()", + ec1, ec2); + assertEquals("ordinalMonth", eOrdinalMonth, ec1.get(Calendar.ORDINAL_MONTH)); + assertEquals("ordinalMonth", eOrdinalMonth, ec2.get(Calendar.ORDINAL_MONTH)); + } + } +}