]> granicus.if.org Git - icu/commitdiff
ICU-9226 (J) Calendar add +year should always move forward in time; roll year should...
authorPeter Edberg <pedberg@unicode.org>
Tue, 19 Jun 2012 09:51:24 +0000 (09:51 +0000)
committerPeter Edberg <pedberg@unicode.org>
Tue, 19 Jun 2012 09:51:24 +0000 (09:51 +0000)
X-SVN-Rev: 31968

icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/IBMCalendarTest.java

index a1f471e7a52036ca35b17979357b61e243d93485..4ba0414b695e1f61f4bfa6e98c12e11f117e91bb 100644 (file)
@@ -2637,6 +2637,18 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
      * 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
@@ -2685,6 +2697,18 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
      * 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
@@ -2806,6 +2830,44 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
 
         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);
@@ -3024,6 +3086,10 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
      * 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
@@ -3099,8 +3165,25 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
             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();
index 5f8159559ea773ef430ee230729a56fc3ecdfea3..31669f99c9e307427d9d82a6086503c5ca3df6ec 100644 (file)
@@ -1365,4 +1365,164 @@ public class IBMCalendarTest extends CalendarTest {
         }
     }
 
+    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 + ")");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
 }