Skip to content

Commit

Permalink
ICU-22407 Implement Java Temporal Calendar API
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankYFTang committed Jul 10, 2023
1 parent cd6ff4a commit cc33596
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 20 deletions.
47 changes: 47 additions & 0 deletions icu4j/main/classes/core/src/com/ibm/icu/util/CECalendar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

//-------------------------------------------------------------------------
Expand Down Expand Up @@ -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
//-------------------------------------------------------------------------
}
185 changes: 178 additions & 7 deletions icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java
Original file line number Diff line number Diff line change
Expand Up @@ -955,13 +955,37 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
*/
public static final int IS_LEAP_MONTH = 22;

/**
* {@icu} Field indicating the month. This is a calendar-specific value.
* Differ from MONTH, this value is continuous and unique within a
* year and range from 0 to 11 or 0 to 12 depending on how many months in a
* year, the calendar system has leap month or not, and in leap year or not.
* It is the ordinal position of that month in the corresponding year of
* the calendar. For Chinese, Dangi, and Hebrew calendar, the range is
* 0 to 11 in non-leap years and 0 to 12 in leap years. For Coptic and Ethiopian
* calendar, the range is always 0 to 12. For other calendars supported by
* ICU now, the range is 0 to 11. When the number of months in a year of the
* identified calendar is variable, a different ORDINAL_MONTH value can
* be used for dates that are part of the same named month in different years.
* For example, in the Hebrew calendar, "1 Nisan 5781" is associated with
* ORDINAL_MONTH value 6 while "1 Nisan 5782" is associated with
* ORDINAL_MONTH value 7 because 5782 is a leap year and Nisan follows
* the insertion of Adar I. In Chinese calendar, "Year 4664 Month 6 Day 2"
* is associated with ORDINAL_MONTH value 5 while "Year 4665 Month 6 Day 2"
* is associated with ORDINAL_MONTH value 6 because 4665 is a leap year
* and there is an extra "Leap Month 5" which associated with ORDINAL_MONTH
* value 5 before "Month 6" of year 4664.
* @draft ICU 74
*/
public static final int ORDINAL_MONTH = 23;

/**
* The number of fields defined by this class. Subclasses may define
* addition fields starting with this number.
* @deprecated ICU 58 The numeric value may change over time, see ICU ticket #12420.
*/
@Deprecated
protected static final int BASE_FIELD_COUNT = 23;
protected static final int BASE_FIELD_COUNT = 24;

/**
* The maximum number of fields possible. Subclasses must not define
Expand Down Expand Up @@ -1704,7 +1728,8 @@ private void initInternal()
(1 << DAY_OF_MONTH) |
(1 << DAY_OF_YEAR) |
(1 << EXTENDED_YEAR) |
(1 << IS_LEAP_MONTH);
(1 << IS_LEAP_MONTH) |
(1 << ORDINAL_MONTH) ;
for (int i=BASE_FIELD_COUNT; i<fields.length; ++i) {
mask |= (1 << i);
}
Expand Down Expand Up @@ -2027,6 +2052,89 @@ public void setTimeInMillis( long millis ) {

}


//-------------------------------------------------------------------------
// 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() {
// Default to Gregorian based leap year rule.
return getActualMaximum(DAY_OF_YEAR) == 366;
}

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 and suffixed by an
* optional literal grapheme "L" if this is a leap month in a lunisolar
* calendar. The 25 possible values are "M01" .. "M13" and "M01L" .. "M12L".
* For the Hebrew calendar, the values are "M01" .. "M12" for non-leap year, and
* "M01" .. "M05", "M05L", "M06" .. "M12" for leap year.
* For the Chinese calendar, the values are "M01" .. "M12" for non-leap year and
* in leap year with another monthCode in "M01L" .. "M12L".
* For Coptic and Ethiopian calendar, the Temporal monthCode values for any
* years are "M01" to "M13".
*
* @return One of 25 possible strings in {"M01".."M13", "M01L".."M12L"}.
* @draft ICU 74
*/
public String getTemporalMonthCode() {
int month = get(MONTH);
assert(month < 12);
assert(internalGet(IS_LEAP_MONTH) == 0);
return gTemporalMonthCodes[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. The 25 possible values are
* "M01" .. "M13" and "M01L" .. "M12L". For Hebrew calendar, the values are
* "M01" .. "M12" for non-leap years, and "M01" .. "M05", "M05L", "M06"
* .. "M12" for leap year.
* For the Chinese calendar, the values are "M01" .. "M12" for non-leap year and
* in leap year with another monthCode in "M01L" .. "M12L".
* For Coptic and Ethiopian calendar, the Temporal monthCode values for any
* years are "M01" to "M13".
* @param temporalMonth One of 25 possible strings in {"M01".. "M12", "M13", "M01L",
* "M12L"}.
* @draft ICU 74
*/
public void setTemporalMonthCode( String temporalMonth ) {
if (temporalMonth.length() == 3 && temporalMonth.charAt(0) == 'M') {
for (int m = 0; m < gTemporalMonthCodes.length; m++) {
if (temporalMonth.equals(gTemporalMonthCodes[m])) {
set(MONTH, m);
set(IS_LEAP_MONTH, 0);
return;
}
}
}
throw new IllegalArgumentException("Incorrect temporal Month code: " + temporalMonth);
}

//-------------------------------------------------------------------------
// End of Temporal Calendar API
//-------------------------------------------------------------------------

/**
* Returns the value for a given time field.
* @param field the given time field.
Expand Down Expand Up @@ -2065,7 +2173,42 @@ protected final int internalGet(int field, int defaultValue) {
return (stamp[field] > 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.
Expand Down Expand Up @@ -2320,6 +2463,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;
}

Expand Down Expand Up @@ -2518,6 +2669,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));
Expand Down Expand Up @@ -2880,13 +3035,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., <jan31>.roll(MONTH, 1) -> <feb28> or <feb29>.
{
int max = getActualMaximum(MONTH);
int mon = (internalGet(MONTH) + amount) % (max+1);
int mon = (internalGetMonth() + amount) % (max+1);

if (mon < 0) {
mon += (max + 1);
Expand Down Expand Up @@ -2937,13 +3093,15 @@ public void roll(int field, int amount) {
}
set(field, newYear);
pinField(MONTH);
pinField(ORDINAL_MONTH);
pinField(DAY_OF_MONTH);
return;
}
case EXTENDED_YEAR:
// Rolling the year can involve pinning the DAY_OF_MONTH.
set(field, internalGet(field) + amount);
pinField(MONTH);
pinField(ORDINAL_MONTH);
pinField(DAY_OF_MONTH);
return;

Expand Down Expand Up @@ -3083,6 +3241,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:
Expand Down Expand Up @@ -3258,6 +3417,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);
Expand Down Expand Up @@ -4409,6 +4569,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
};

/**
Expand Down Expand Up @@ -5295,6 +5456,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.
Expand Down Expand Up @@ -5427,7 +5595,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();
Expand Down Expand Up @@ -5898,6 +6066,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);
}
Expand Down Expand Up @@ -6043,7 +6212,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.
Expand Down Expand Up @@ -6121,7 +6290,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;
}
Expand Down Expand Up @@ -6212,7 +6381,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();
Expand Down
Loading

0 comments on commit cc33596

Please sign in to comment.