* must be adjusted so that the result is 2/29/96 rather than the invalid
* 2/31/96.
* <p>
+ * Rolling up always means rolling forward in time (unless
+ * the limit of the field is reached, in which case it may pin or wrap), so for the
+ * Gregorian calendar, starting with 100 BC and rolling the year up results in 99 BC.
+ * When eras have a definite beginning and end (as in the Chinese calendar, or as in
+ * most eras in the Japanese calendar) then rolling the year past either limit of the
+ * era will cause the year to wrap around. When eras only have a limit at one end,
+ * then attempting to roll the year past that limit will result in pinning the year
+ * at that limit. Note that for most calendars in which era 0 years move forward in
+ * time (such as Buddhist, Hebrew, or Islamic), it is possible for add or roll to
+ * result in negative years for era 0 (that is the only way to represent years before
+ * the calendar epoch in such calendars).
+ * <p>
* <b>Note:</b> Calling <tt>roll(field, true)</tt> N times is <em>not</em>
* necessarily equivalent to calling <tt>roll(field, N)</tt>. For example,
* imagine that you start with the date Gregorian date January 31, 1995. If you call
* must be adjusted so that the result is 2/29/96 rather than the invalid
* 2/31/96.
* <p>
+ * Rolling by a positive value always means rolling forward in time (unless
+ * the limit of the field is reached, in which case it may pin or wrap), so for the
+ * Gregorian calendar, starting with 100 BC and rolling the year by + 1 results in 99 BC.
+ * When eras have a definite beginning and end (as in the Chinese calendar, or as in
+ * most eras in the Japanese calendar) then rolling the year past either limit of the
+ * era will cause the year to wrap around. When eras only have a limit at one end,
+ * then attempting to roll the year past that limit will result in pinning the year
+ * at that limit. Note that for most calendars in which era 0 years move forward in
+ * time (such as Buddhist, Hebrew, or Islamic), it is possible for add or roll to
+ * result in negative years for era 0 (that is the only way to represent years before
+ * the calendar epoch in such calendars).
+ * <p>
* {@icunote} the ICU implementation of this method is able to roll
* all fields except for {@link #ERA ERA}, {@link #DST_OFFSET DST_OFFSET},
* and {@link #ZONE_OFFSET ZONE_OFFSET}. Subclasses may, of course, add support for
case YEAR:
case YEAR_WOY:
+ // * If era==0 and years go backwards in time, change sign of amount.
+ // * Until we have new API per #9393, we temporarily hardcode knowledge of
+ // which calendars have era 0 years that go backwards.
+ {
+ boolean era0WithYearsThatGoBackwards = false;
+ int era = get(ERA);
+ if (era == 0) {
+ String calType = getType();
+ if (calType.equals("gregorian") || calType.equals("roc") || calType.equals("coptic")) {
+ amount = -amount;
+ era0WithYearsThatGoBackwards = true;
+ }
+ }
+ int newYear = internalGet(field) + amount;
+ if (era > 0 || newYear >= 1) {
+ int maxYear = getActualMaximum(field);
+ if (maxYear < 32768) {
+ // this era has real bounds, roll should wrap years
+ if (newYear < 1) {
+ newYear = maxYear - ((-newYear) % maxYear);
+ } else if (newYear > maxYear) {
+ newYear = ((newYear - 1) % maxYear) + 1;
+ }
+ // else era is unbounded, just pin low year instead of wrapping
+ } else if (newYear < 1) {
+ newYear = 1;
+ }
+ // else we are in era 0 with newYear < 1;
+ // calendars with years that go backwards must pin the year value at 0,
+ // other calendars can have years < 0 in era 0
+ } else if (era0WithYearsThatGoBackwards) {
+ newYear = 1;
+ }
+ set(field, newYear);
+ pinField(MONTH);
+ pinField(DAY_OF_MONTH);
+ return;
+ }
case EXTENDED_YEAR:
// Rolling the year can involve pinning the DAY_OF_MONTH.
set(field, internalGet(field) + amount);
* must be adjusted so that the result is 2/29/96 rather than the invalid
* 2/31/96.
* <p>
+ * Adding a positive value always means moving forward in time, so for the Gregorian
+ * calendar, starting with 100 BC and adding +1 to year results in 99 BC (even though
+ * this actually reduces the numeric value of the field itself).
+ * <p>
* {@icunote} The ICU implementation of this method is able to add to
* all fields except for {@link #ERA ERA}, {@link #DST_OFFSET DST_OFFSET},
* and {@link #ZONE_OFFSET ZONE_OFFSET}. Subclasses may, of course, add support for
return;
case YEAR:
- case EXTENDED_YEAR:
case YEAR_WOY:
+ // * If era=0 and years go backwards in time, change sign of amount.
+ // * Until we have new API per #9393, we temporarily hardcode knowledge of
+ // which calendars have era 0 years that go backwards.
+ // * Note that for YEAR (but not YEAR_WOY) we could instead handle
+ // this by applying the amount to the EXTENDED_YEAR field; but since
+ // we would still need to handle YEAR_WOY as below, might as well
+ // also handle YEAR the same way.
+ {
+ int era = get(ERA);
+ if (era == 0) {
+ String calType = getType();
+ if (calType.equals("gregorian") || calType.equals("roc") || calType.equals("coptic")) {
+ amount = -amount;
+ }
+ }
+ }
+ // Fall through into standard handling
+ case EXTENDED_YEAR:
case MONTH:
{
boolean oldLenient = isLenient();
}
}
+ public void TestAddRollEra0AndEraBounds() {
+ final String[] localeIDs = {
+ // calendars with non-modern era 0 that goes backwards, max era == 1
+ "en@calendar=gregorian",
+ "en@calendar=roc",
+ "en@calendar=coptic",
+ // calendars with non-modern era 0 that goes forwards, max era > 1
+ "en@calendar=japanese",
+ "en@calendar=chinese",
+ // calendars with non-modern era 0 that goes forwards, max era == 1
+ "en@calendar=ethiopic",
+ // calendars with only one era = 0, forwards
+ "en@calendar=buddhist",
+ "en@calendar=hebrew",
+ "en@calendar=islamic",
+ "en@calendar=indian",
+ //"en@calendar=persian", // no persian calendar in ICU4J yet
+ "en@calendar=ethiopic-amete-alem",
+ };
+ TimeZone zoneGMT = TimeZone.getFrozenTimeZone("GMT");
+ for (String localeID : localeIDs) {
+ Calendar ucalTest = Calendar.getInstance(zoneGMT, new ULocale(localeID));
+ String calType = ucalTest.getType();
+ boolean era0YearsGoBackwards = (calType.equals("gregorian") || calType.equals("roc") || calType.equals("coptic"));
+ int yrBefore, yrAfter, yrMax, eraAfter, eraMax, eraNow;
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 2);
+ ucalTest.set(Calendar.ERA, 0);
+ yrBefore = ucalTest.get(Calendar.YEAR);
+ ucalTest.add(Calendar.YEAR, 1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ if ( (era0YearsGoBackwards && yrAfter>yrBefore) || (!era0YearsGoBackwards && yrAfter<yrBefore) ) {
+ errln("Fail: era 0 add 1 year does not move forward in time for " + localeID);
+ }
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 2);
+ ucalTest.set(Calendar.ERA, 0);
+ yrBefore = ucalTest.get(Calendar.YEAR);
+ ucalTest.roll(Calendar.YEAR, 1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ if ( (era0YearsGoBackwards && yrAfter>yrBefore) || (!era0YearsGoBackwards && yrAfter<yrBefore) ) {
+ errln("Fail: era 0 roll 1 year does not move forward in time for " + localeID);
+ }
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 1);
+ ucalTest.set(Calendar.ERA, 0);
+ if (era0YearsGoBackwards) {
+ ucalTest.roll(Calendar.YEAR, 1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ if (eraAfter != 0 || yrAfter != 1) {
+ errln("Fail: era 0 roll 1 year from year 1 does not stay within era or pin to year 1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ } else {
+ // roll backward in time to where era 0 years go negative, except for the Chinese
+ // calendar, which uses negative eras instead of having years outside the range 1-60
+ ucalTest.roll(Calendar.YEAR, -2);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ if ( !calType.equals("chinese") && (eraAfter != 0 || yrAfter != -1) ) {
+ errln("Fail: era 0 roll -2 years from year 1 does not stay within era or produce year -1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ }
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 1);
+ ucalTest.set(Calendar.ERA, 0);
+ eraMax = ucalTest.getMaximum(Calendar.ERA);
+ if (eraMax > 0) {
+ // try similar tests for era 1 (if calendar has it), in which years always go forward
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 2);
+ ucalTest.set(Calendar.ERA, 1);
+ yrBefore = ucalTest.get(Calendar.YEAR);
+ ucalTest.add(Calendar.YEAR, 1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ if ( yrAfter<yrBefore ) {
+ errln("Fail: era 1 add 1 year does not move forward in time for " + localeID);
+ }
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 2);
+ ucalTest.set(Calendar.ERA, 1);
+ yrBefore = ucalTest.get(Calendar.YEAR);
+ ucalTest.roll(Calendar.YEAR, 1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ if ( yrAfter<yrBefore ) {
+ errln("Fail: era 1 roll 1 year does not move forward in time for " + localeID);
+ }
+
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 1);
+ ucalTest.set(Calendar.ERA, 1);
+ yrMax = ucalTest.getActualMaximum(Calendar.YEAR);
+ ucalTest.roll(Calendar.YEAR, -1); // roll down which should pin or wrap to end
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ // if yrMax is reasonable we should wrap to that, else we should pin at yr 1
+ if (yrMax >= 32768) {
+ if (eraAfter != 1 || yrAfter != 1) {
+ errln("Fail: era 1 roll -1 year from year 1 does not stay within era or pin to year 1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ } else if (eraAfter != 1 || yrAfter != yrMax) {
+ errln("Fail: era 1 roll -1 year from year 1 does not stay within era or wrap to year "
+ + yrMax + " for " + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ } else {
+ ucalTest.roll(Calendar.YEAR, 1); // now roll up which should wrap to beginning
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ if (eraAfter != 1 || yrAfter != 1) {
+ errln("Fail: era 1 roll 1 year from year " + yrMax +
+ " does not stay within era or wrap to year 1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ }
+
+ // if current era > 1, try the same roll tests for current era
+ ucalTest.setTime(new Date());
+ eraNow = ucalTest.get(Calendar.ERA);
+ if (eraNow > 1) {
+ ucalTest.clear();
+ ucalTest.set(Calendar.YEAR, 1);
+ ucalTest.set(Calendar.ERA, eraNow);
+ yrMax = ucalTest.getActualMaximum(Calendar.YEAR); // max year value for this era
+ ucalTest.roll(Calendar.YEAR, -1);
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ // if yrMax is reasonable we should wrap to that, else we should pin at yr 1
+ if (yrMax >= 32768) {
+ if (eraAfter != eraNow || yrAfter != 1) {
+ errln("Fail: era " + eraNow +
+ " roll -1 year from year 1 does not stay within era or pin to year 1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ } else if (eraAfter != eraNow || yrAfter != yrMax) {
+ errln("Fail: era " + eraNow +
+ " roll -1 year from year 1 does not stay within era or wrap to year " + yrMax
+ + " for " + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ } else {
+ ucalTest.roll(Calendar.YEAR, 1); // now roll up which should wrap to beginning
+ yrAfter = ucalTest.get(Calendar.YEAR);
+ eraAfter = ucalTest.get(Calendar.ERA);
+ if (eraAfter != eraNow || yrAfter != 1) {
+ errln("Fail: era " + eraNow + " roll 1 year from year " + yrMax +
+ " does not stay within era or wrap to year 1 for "
+ + localeID + " (get era " + eraAfter + " year " + yrAfter + ")");
+ }
+ }
+ }
+ }
+ }
+ }
+
}