]> granicus.if.org Git - icu/commitdiff
ICU-9255 Adding the traditional Korean calendar (Dangi) support to ICU4J.
authorYoshito Umaoka <y.umaoka@gmail.com>
Tue, 25 Sep 2012 21:45:16 +0000 (21:45 +0000)
committerYoshito Umaoka <y.umaoka@gmail.com>
Tue, 25 Sep 2012 21:45:16 +0000 (21:45 +0000)
X-SVN-Rev: 32426

icu4j/main/classes/core/src/com/ibm/icu/impl/CalendarData.java
icu4j/main/classes/core/src/com/ibm/icu/text/DateTimePatternGenerator.java
icu4j/main/classes/core/src/com/ibm/icu/util/Calendar.java
icu4j/main/classes/core/src/com/ibm/icu/util/ChineseCalendar.java
icu4j/main/classes/core/src/com/ibm/icu/util/DangiCalendar.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/DangiTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/DateFormatTest.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/CalendarTests.java
icu4j/main/tests/core/src/com/ibm/icu/dev/test/serializable/SerializableTest.java

index 3e59dd9377ef66c825b9a55b2872637917b41593..ee5b16af9d7b5eaca1191d3523606027b6a6297a 100644 (file)
@@ -34,6 +34,10 @@ public class CalendarData {
             fMainType = "gregorian";
             fFallbackType = null;
         } else {
+            // TODO: Until CLDR supports "dangi" calendar type
+            if (type.equalsIgnoreCase("dangi")) {
+                type = "chinese";
+            }
             fMainType = type;
             fFallbackType = "gregorian";
         }
index b2a834d8ff9185d4418363feb3027a39be47687c..d7a42e8fc69dad63c8ab67c5bdc38c152f7aa585 100644 (file)
@@ -184,6 +184,12 @@ public class DateTimePatternGenerator implements Freezable<DateTimePatternGenera
         if ( calendarTypeToUse == null ) {
             calendarTypeToUse = "gregorian"; // fallback
         }
+
+        // TODO: Until CLDR supports "dangi" calendar type
+        if (calendarTypeToUse.equalsIgnoreCase("dangi")) {
+            calendarTypeToUse = "chinese";
+        }
+
         // Get data for that calendar
         ICUResourceBundle calBundle = rb.getWithFallback("calendar");
         ICUResourceBundle calTypeBundle = calBundle.getWithFallback(calendarTypeToUse);
index a370d4a593d639a393ef27d915306b0f0281886c..01be745b803a4800379114bffbd70b942485c4c0 100644 (file)
@@ -1739,6 +1739,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
         "ethiopic",
         "ethiopic-amete-alem",
         "iso8601",
+        "dangi",
     };
 
     // must be in the order of calTypes above
@@ -1756,6 +1757,7 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
     private static final int CALTYPE_ETHIOPIC = 11;
     private static final int CALTYPE_ETHIOPIC_AMETE_ALEM = 12;
     private static final int CALTYPE_ISO8601 = 13;
+    private static final int CALTYPE_DANGI = 14;
     private static final int CALTYPE_UNKNOWN = -1;
 
     private static int getCalendarTypeForLocale(ULocale l) {
@@ -1901,6 +1903,9 @@ public abstract class Calendar implements Serializable, Cloneable, Comparable<Ca
             cal = new EthiopicCalendar(zone, locale);
             ((EthiopicCalendar)cal).setAmeteAlemEra(true);
             break;
+        case CALTYPE_DANGI:
+            cal = new DangiCalendar(zone, locale);
+            break;
         case CALTYPE_ISO8601:
             // Only differs week numbering rule from Gregorian
             cal = new GregorianCalendar(zone, locale);
index 4a785739c5ba12710645dbfb8d2909e57862bd9b..1cdfe2f79d18e8ddfb45046be4996d7292d522bf 100644 (file)
@@ -101,6 +101,17 @@ public class ChineseCalendar extends Calendar {
     // the object, not class level, under the assumption that typical
     // usage will be to have one instance of ChineseCalendar at a time.
  
+    /**
+     * The start year of this Chinese calendar instance. 
+     */
+    private int epochYear;
+
+    /**
+     * The zone used for the astronomical calculation of this Chinese
+     * calendar instance.
+     */
+    private TimeZone zoneAstro;
+
     /**
      * We have one instance per object, and we don't synchronize it because
      * Calendar doesn't support multithreaded execution in the first place.
@@ -135,8 +146,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 2.8
      */
     public ChineseCalendar() {
-        super();
-        setTimeInMillis(System.currentTimeMillis());
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
     }
 
     /**
@@ -146,7 +156,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 4.0
      */
     public ChineseCalendar(Date date) {
-        super();
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
         setTime(date);
     }
 
@@ -164,19 +174,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 4.0
      */
     public ChineseCalendar(int year, int month, int isLeapMonth, int date) {
-        super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
-
-        // We need to set the current time once to initialize the ChineseCalendar's
-        // ERA field to be the current era.
-        setTimeInMillis(System.currentTimeMillis());
-        // Then we need to clean up time fields
-        this.set(MILLISECONDS_IN_DAY, 0);
-
-        // Then set the given field values.
-        this.set(YEAR, year);
-        this.set(MONTH, month);
-        this.set(IS_LEAP_MONTH, isLeapMonth);
-        this.set(DATE, date);
+        this(year, month, isLeapMonth, date, 0, 0, 0);
     }
 
     /**
@@ -201,12 +199,12 @@ public class ChineseCalendar extends Calendar {
     public ChineseCalendar(int year, int month, int isLeapMonth, int date, int hour,
                              int minute, int second)
     {
-        super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
 
-        // We need to set the current time once to initialize the ChineseCalendar's
-        // ERA field to be the current era.
-        setTimeInMillis(System.currentTimeMillis());
-        // Then set 0 to millisecond field
+        // The current time is set at this point, so ERA field is already
+        // set to the current era.
+
+        // Then we need to clean up time fields
         this.set(MILLISECOND, 0);
 
         // Then, set the given field values.
@@ -235,21 +233,7 @@ public class ChineseCalendar extends Calendar {
      */ 
     public ChineseCalendar(int era, int year, int month, int isLeapMonth, int date) 
     { 
-        super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT)); 
-
-        // We need to set the current time once to initialize the ChineseCalendar's 
-        // ERA field to be the current era. 
-        setTimeInMillis(System.currentTimeMillis()); 
-
-        // Then we need to clean up time fields 
-        this.set(MILLISECONDS_IN_DAY, 0); 
-
-        // Then set the given field values. 
-        this.set(ERA, era); 
-        this.set(YEAR, year); 
-        this.set(MONTH, month); 
-        this.set(IS_LEAP_MONTH, isLeapMonth); 
-        this.set(DATE, date); 
+        this(era, year, month, isLeapMonth, 0, 0, 0);
     } 
   
     /** 
@@ -275,13 +259,9 @@ public class ChineseCalendar extends Calendar {
     public ChineseCalendar(int era, int year, int month, int isLeapMonth, int date, int hour, 
                            int minute, int second) 
     { 
-        super(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT)); 
-
-        // We need to set the current time once to initialize the ChineseCalendar's 
-        // ERA field to be the current era. 
-        setTimeInMillis(System.currentTimeMillis()); 
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
 
-        // Then set 0 to millisecond field 
+        // Set 0 to millisecond field 
         this.set(MILLISECOND, 0); 
 
         // Then, set the given field values. 
@@ -302,8 +282,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 4.0
      */
     public ChineseCalendar(Locale aLocale) {
-        this(TimeZone.getDefault(), aLocale);
-        setTimeInMillis(System.currentTimeMillis());
+        this(TimeZone.getDefault(), ULocale.forLocale(aLocale), CHINESE_EPOCH_YEAR, CHINA_ZONE);
     }
 
     /**
@@ -314,8 +293,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 4.0
      */
     public ChineseCalendar(TimeZone zone) {
-        super(zone, ULocale.getDefault(Category.FORMAT));
-        setTimeInMillis(System.currentTimeMillis());
+        this(zone, ULocale.getDefault(Category.FORMAT), CHINESE_EPOCH_YEAR, CHINA_ZONE);
     }
 
     /**
@@ -326,8 +304,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 2.8
      */
     public ChineseCalendar(TimeZone zone, Locale aLocale) {
-        super(zone, aLocale);
-        setTimeInMillis(System.currentTimeMillis());
+        this(zone, ULocale.forLocale(aLocale), CHINESE_EPOCH_YEAR, CHINA_ZONE);
     }
 
     /**
@@ -338,8 +315,7 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 4.0
      */
     public ChineseCalendar(ULocale locale) {
-        this(TimeZone.getDefault(), locale);
-        setTimeInMillis(System.currentTimeMillis());
+        this(TimeZone.getDefault(), locale, CHINESE_EPOCH_YEAR, CHINA_ZONE);
     }
 
     /**
@@ -350,7 +326,20 @@ public class ChineseCalendar extends Calendar {
      * @stable ICU 3.2
      */
     public ChineseCalendar(TimeZone zone, ULocale locale) {
+        this(zone, locale, CHINESE_EPOCH_YEAR, CHINA_ZONE);
+    }
+
+    /**
+     * Construct a <code>ChineseCalenar</code> based on the current time
+     * with the given time zone, the locale, the epoch year and the time zone
+     * used for astronomical calculation.
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    protected ChineseCalendar(TimeZone zone, ULocale locale, int epochYear, TimeZone zoneAstroCalc) {
         super(zone, locale);
+        this.epochYear = epochYear;
+        this.zoneAstro = zoneAstroCalc;
         setTimeInMillis(System.currentTimeMillis());
     }
 
@@ -455,7 +444,8 @@ public class ChineseCalendar extends Calendar {
             year = internalGet(EXTENDED_YEAR, 1); // Default to year 1
         } else {
             int cycle = internalGet(ERA, 1) - 1; // 0-based cycle
-            year = cycle * 60 + internalGet(YEAR, 1);
+            // adjust to the instance specific epoch
+            year = cycle * 60 + internalGet(YEAR, 1) - (epochYear - CHINESE_EPOCH_YEAR);
         }
         return year;
     }
@@ -652,11 +642,11 @@ public class ChineseCalendar extends Calendar {
     private static final int CHINESE_EPOCH_YEAR = -2636; // Gregorian year
 
     /**
-     * The offset from GMT in milliseconds at which we perform astronomical
-     * computations.  Some sources use a different historically accurate
+     * The time zone used for performing astronomical computations.
+     * Some sources use a different historically accurate
      * offset of GMT+7:45:40 for years before 1929; we do not do this.
      */
-    private static final long CHINA_OFFSET = 8*ONE_HOUR;
+    private static final TimeZone CHINA_ZONE = new SimpleTimeZone(8 * ONE_HOUR, "CHINA_ZONE").freeze();
 
     /**
      * Value to be added or subtracted from the local days of a new moon to
@@ -667,20 +657,28 @@ public class ChineseCalendar extends Calendar {
 
     /**
      * Convert local days to UTC epoch milliseconds.
-     * @param days days after January 1, 1970 0:00 Asia/Shanghai
+     * This is not an accurate conversion in terms that getTimezoneOffset 
+     * takes the milliseconds in GMT (not local time).  In theory, more 
+     * accurate algorithm can be implemented but practically we do not need 
+     * to go through that complication as long as the historically timezone 
+     * changes did not happen around the 'tricky' new moon (new moon around 
+     * the midnight). 
+     *  
+     * @param days days after January 1, 1970 0:00 in the astronomical base zone
      * @return milliseconds after January 1, 1970 0:00 GMT
      */
-    private static final long daysToMillis(int days) {
-        return (days * ONE_DAY) - CHINA_OFFSET;
+    private final long daysToMillis(int days) {
+        long millis = days * ONE_DAY;
+        return millis - zoneAstro.getOffset(millis);
     }
 
     /**
      * Convert UTC epoch milliseconds to local days.
      * @param millis milliseconds after January 1, 1970 0:00 GMT
-     * @return days after January 1, 1970 0:00 Asia/Shanghai
+     * @return days days after January 1, 1970 0:00 in the astronomical base zone
      */
-    private static final int millisToDays(long millis) {
-        return (int) floorDivide(millis + CHINA_OFFSET, ONE_DAY);
+    private final int millisToDays(long millis) {
+        return (int) floorDivide(millis + zoneAstro.getOffset(millis), ONE_DAY);
     }
 
     //------------------------------------------------------------------
@@ -787,9 +785,9 @@ public class ChineseCalendar extends Calendar {
     /**
      * Return true if there is a leap month on or after month newMoon1 and
      * at or before month newMoon2.
-     * @param newMoon1 days after January 1, 1970 0:00 Asia/Shanghai of a
+     * @param newMoon1 days after January 1, 1970 0:00 astronomical base zone of a
      * new moon
-     * @param newMoon2 days after January 1, 1970 0:00 Asia/Shanghai of a
+     * @param newMoon2 days after January 1, 1970 0:00 astronomical base zone of a
      * new moon
      */
     private boolean isLeapMonthBetween(int newMoon1, int newMoon2) {
@@ -841,7 +839,7 @@ public class ChineseCalendar extends Calendar {
      * <code>handleComputeMonthStart()</code>.
      *
      * <p>As a side effect, this method sets {@link #isLeapYear}.
-     * @param days days after January 1, 1970 0:00 Asia/Shanghai of the
+     * @param days days after January 1, 1970 0:00 astronomical base zone of the
      * date to compute fields for
      * @param gyear the Gregorian year of the given date
      * @param gmonth the Gregorian month of the given date
@@ -891,18 +889,21 @@ public class ChineseCalendar extends Calendar {
 
         if (setAllFields) {
 
-            int year = gyear - CHINESE_EPOCH_YEAR;
+            // Extended year and cycle year is based on the epoch year
+            int extended_year = gyear - epochYear;
+            int cycle_year = gyear - CHINESE_EPOCH_YEAR;
             if (month < 11 ||
                 gmonth >= JULY) {
-                year++;
+                extended_year++;
+                cycle_year++;
             }
             int dayOfMonth = days - thisMoon + 1;
 
-            internalSet(EXTENDED_YEAR, year);
+            internalSet(EXTENDED_YEAR, extended_year);
 
             // 0->0,60  1->1,1  60->1,60  61->2,1  etc.
             int[] yearOfCycle = new int[1];
-            int cycle = floorDivide(year-1, 60, yearOfCycle);
+            int cycle = floorDivide(cycle_year-1, 60, yearOfCycle);
             internalSet(ERA, cycle+1);
             internalSet(YEAR, yearOfCycle[0]+1);
 
@@ -927,7 +928,7 @@ public class ChineseCalendar extends Calendar {
     /**
      * Return the Chinese new year of the given Gregorian year.
      * @param gyear a Gregorian year
-     * @return days after January 1, 1970 0:00 Asia/Shanghai of the
+     * @return days after January 1, 1970 0:00 astronomical base zone of the
      * Chinese new year of the given year (this will be a new moon)
      */
     private int newYear(int gyear) {
@@ -977,7 +978,7 @@ public class ChineseCalendar extends Calendar {
             month = rem[0];
         }
 
-        int gyear = eyear + CHINESE_EPOCH_YEAR - 1; // Gregorian year
+        int gyear = eyear + epochYear - 1; // Gregorian year
         int newYear = newYear(gyear);
         int newMoon = newMoonNear(newYear + month * 29, true);
         
@@ -1022,8 +1023,11 @@ public class ChineseCalendar extends Calendar {
     private void readObject(ObjectInputStream stream)
         throws IOException, ClassNotFoundException
     {
+        epochYear = CHINESE_EPOCH_YEAR;
+        zoneAstro = CHINA_ZONE;
+
         stream.defaultReadObject();
-        
+
         /* set up the transient caches... */
         astro = new CalendarAstronomer();
         winterSolsticeCache = new CalendarCache();
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/util/DangiCalendar.java b/icu4j/main/classes/core/src/com/ibm/icu/util/DangiCalendar.java
new file mode 100644 (file)
index 0000000..556b302
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2012, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.util;
+
+import java.util.Date;
+
+import com.ibm.icu.util.ULocale.Category;
+
+/**
+ * <code>DangiCalendar</code> is a concrete subclass of {@link Calendar}
+ * that implements a traditional Korean calendar.
+ * 
+ * @internal
+ * @deprecated This API is ICU internal only.
+ */
+public class DangiCalendar extends ChineseCalendar {
+
+    private static final long serialVersionUID = 8156297445349501985L;
+
+    /**
+     * The start year of the Korean traditional calendar (Dan-gi) is the inaugural
+     * year of Dan-gun (BC 2333).
+     */
+    private static final int DANGI_EPOCH_YEAR = -2332;
+
+    /**
+     * The time zone used for performing astronomical computations for
+     * Dangi calendar. In Korea various timezones have been used historically 
+     * (cf. http://www.math.snu.ac.kr/~kye/others/lunar.html): 
+     *  
+     *            - 1908/04/01: GMT+8 
+     * 1908/04/01 - 1911/12/31: GMT+8.5 
+     * 1912/01/01 - 1954/03/20: GMT+9 
+     * 1954/03/21 - 1961/08/09: GMT+8.5 
+     * 1961/08/10 -           : GMT+9 
+     *  
+     * Note that, in 1908-1911, the government did not apply the timezone change 
+     * but used GMT+8. In addition, 1954-1961's timezone change does not affect 
+     * the lunar date calculation. Therefore, the following simpler rule works: 
+     *   
+     * -1911: GMT+8 
+     * 1912-: GMT+9 
+     *  
+     * Unfortunately, our astronomer's approximation doesn't agree with the 
+     * references (http://www.math.snu.ac.kr/~kye/others/lunar.html and 
+     * http://astro.kasi.re.kr/Life/ConvertSolarLunarForm.aspx?MenuID=115) 
+     * in 1897/7/30. So the following ad hoc fix is used here: 
+     *  
+     *     -1896: GMT+8 
+     *      1897: GMT+7 
+     * 1898-1911: GMT+8 
+     * 1912-    : GMT+9 
+     */
+    private static final TimeZone KOREA_ZONE;
+
+    static {
+        InitialTimeZoneRule initialTimeZone = new InitialTimeZoneRule("GMT+8", 8 * ONE_HOUR, 0);
+        long[] millis1897 = { (1897 - 1970) * 365L * ONE_DAY }; // some days of error is not a problem here
+        long[] millis1898 = { (1898 - 1970) * 365L * ONE_DAY }; // some days of error is not a problem here
+        long[] millis1912 = { (1912 - 1970) * 365L * ONE_DAY }; // this doesn't create an issue for 1911/12/20
+        TimeZoneRule rule1897 = new TimeArrayTimeZoneRule("Korean 1897", 7 * ONE_HOUR, 0, millis1897,
+                DateTimeRule.STANDARD_TIME);
+        TimeZoneRule rule1898to1911 = new TimeArrayTimeZoneRule("Korean 1898-1911", 8 * ONE_HOUR, 0, millis1898,
+                DateTimeRule.STANDARD_TIME);
+        TimeZoneRule ruleFrom1912 = new TimeArrayTimeZoneRule("Korean 1912-", 9 * ONE_HOUR, 0, millis1912,
+                DateTimeRule.STANDARD_TIME);
+
+        RuleBasedTimeZone tz = new RuleBasedTimeZone("KOREA_ZONE", initialTimeZone);
+        tz.addTransitionRule(rule1897);
+        tz.addTransitionRule(rule1898to1911);
+        tz.addTransitionRule(ruleFrom1912);
+        tz.freeze();
+        KOREA_ZONE = tz;
+    };
+
+    /**
+     * Construct a <code>DangiCalendar</code> with the default time zone and locale.
+     * 
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public DangiCalendar() {
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
+    }
+
+    /**
+     * Construct a <code>DangiCalendar</code> with the give date set in the default time zone
+     * with the default locale.
+     * @param date The date to which the new calendar is set.
+     * 
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public DangiCalendar(Date date) {
+        this(TimeZone.getDefault(), ULocale.getDefault(Category.FORMAT));
+        setTime(date);
+    }
+
+    /**
+     * Construct a <code>DangiCalendar</code>  based on the current time
+     * with the given time zone with the given locale.
+     * @param zone the given time zone
+     * @param locale the given locale
+     * 
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public DangiCalendar(TimeZone zone, ULocale locale) {
+        super(zone, locale, DANGI_EPOCH_YEAR, KOREA_ZONE);
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * @internal
+     * @deprecated This API is ICU internal only.
+     */
+    public String getType() {
+        return "dangi";
+    }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/DangiTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/calendar/DangiTest.java
new file mode 100644 (file)
index 0000000..201e59a
--- /dev/null
@@ -0,0 +1,427 @@
+/*
+ *******************************************************************************
+ * Copyright (C) 2012, International Business Machines Corporation and         *
+ * others. All Rights Reserved.                                                *
+ *******************************************************************************
+ */
+package com.ibm.icu.dev.test.calendar;
+import java.util.Date;
+
+import com.ibm.icu.text.DateFormat;
+import com.ibm.icu.util.Calendar;
+import com.ibm.icu.util.DangiCalendar;
+import com.ibm.icu.util.GregorianCalendar;
+import com.ibm.icu.util.TimeZone;
+import com.ibm.icu.util.ULocale;
+
+public class DangiTest extends CalendarTest {
+
+    public static void main(String args[]) throws Exception {
+        new DangiTest().run(args);
+    }
+
+    /**
+     * Test basic mapping to and from Gregorian.
+     */
+    public void TestMapping() {
+        final int[] DATA = {
+            // (Note: months are 1-based)
+            // Gregorian    Korean (Dan-gi)
+            1964,  9,  4,   4297,  7,0, 28,
+            1964,  9,  5,   4297,  7,0, 29,
+            1964,  9,  6,   4297,  8,0,  1,
+            1964,  9,  7,   4297,  8,0,  2,
+            1961, 12, 25,   4294, 11,0, 18,
+            1999,  6,  4,   4332,  4,0, 21,
+            
+            1990,  5, 23,   4323,  4,0, 29,
+            1990,  5, 24,   4323,  5,0,  1,
+            1990,  6, 22,   4323,  5,0, 30,
+            1990,  6, 23,   4323,  5,1,  1,
+            1990,  7, 20,   4323,  5,1, 28,
+            1990,  7, 21,   4323,  5,1, 29,
+            1990,  7, 22,   4323,  6,0,  1,
+
+            // Some tricky dates (where GMT+8 doesn't agree with GMT+9)
+            //
+            // The list is from http://www.math.snu.ac.kr/~kye/others/lunar.html ('kye ref').
+            // However, for some dates disagree with the above reference so KASI's
+            // calculation was cross-referenced:
+            //  http://astro.kasi.re.kr/Life/ConvertSolarLunarForm.aspx?MenuID=115
+            1880, 11,  3,   4213, 10,0,  1, // astronomer's GMT+8 / KASI disagrees with the kye ref
+            1882, 12, 10,   4215, 11,0,  1,
+            1883,  7, 4,    4216,  6,0,  1,
+            1884,  4, 25,   4217,  4,0,  1,
+            1885,  5, 14,   4218,  4,0,  1,
+            1891,  1, 10,   4223, 12,0,  1,
+            1893,  4, 16,   4226,  3,0,  1,
+            1894,  5,  5,   4227,  4,0,  1,
+            1897,  7, 29,   4230,  7,0,  1, // astronomer's GMT+8 disagrees with all other ref (looks like our astronomer's error, see ad hoc fix at ChineseCalendar::getTimezoneOffset)
+            1903, 10, 20,   4236,  9,0,  1,
+            1904,  1, 17,   4236, 12,0,  1,
+            1904, 11,  7,   4237, 10,0,  1,
+            1905,  5,  4,   4238,  4,0,  1,
+            1907,  7, 10,   4240,  6,0,  1,
+            1908,  4, 30,   4241,  4,0,  1,
+            1908,  9, 25,   4241,  9,0,  1,
+            1909,  9, 14,   4242,  8,0,  1,
+            1911, 12, 20,   4244, 11,0,  1,
+            1976, 11, 22,   4309, 10,0,  1,
+        };
+
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        StringBuilder buf = new StringBuilder();
+
+        logln("Gregorian -> Korean Lunar (Dangi)");
+
+        Calendar grego = Calendar.getInstance();
+        grego.clear();
+        for (int i = 0; i < DATA.length;) {
+            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
+            Date date = grego.getTime();
+            cal.setTime(date);
+            int y = cal.get(Calendar.EXTENDED_YEAR);
+            int m = cal.get(Calendar.MONTH) + 1; // 0-based -> 1-based
+            int L = cal.get(Calendar.IS_LEAP_MONTH);
+            int d = cal.get(Calendar.DAY_OF_MONTH);
+            int yE = DATA[i++]; // Expected y, m, isLeapMonth, d
+            int mE = DATA[i++]; // 1-based
+            int LE = DATA[i++];
+            int dE = DATA[i++];
+            buf.setLength(0);
+            buf.append(date + " -> ");
+            buf.append(y + "/" + m + (L == 1 ? "(leap)" : "") + "/" + d);
+            if (y == yE && m == mE && L == LE && d == dE) {
+                logln("OK: " + buf.toString());
+            } else {
+                errln("Fail: " + buf.toString() + ", expected " + yE + "/" + mE + (LE == 1 ? "(leap)" : "") + "/" + dE);
+            }
+        }
+
+        logln("Korean Lunar (Dangi) -> Gregorian");
+        for (int i = 0; i < DATA.length;) {
+            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
+            Date dexp = grego.getTime();
+            int cyear = DATA[i++];
+            int cmonth = DATA[i++];
+            int cisleapmonth = DATA[i++];
+            int cdayofmonth = DATA[i++];
+            cal.clear();
+            cal.set(Calendar.EXTENDED_YEAR, cyear);
+            cal.set(Calendar.MONTH, cmonth - 1);
+            cal.set(Calendar.IS_LEAP_MONTH, cisleapmonth);
+            cal.set(Calendar.DAY_OF_MONTH, cdayofmonth);
+            Date date = cal.getTime();
+            buf.setLength(0);
+            buf.append(cyear + "/" + cmonth + (cisleapmonth == 1 ? "(leap)" : "") + "/" + cdayofmonth);
+            buf.append(" -> " + date);
+            if (date.equals(dexp)) {
+                logln("OK: " + buf.toString());
+            } else {
+                errln("Fail: " + buf.toString() + ", expected " + dexp);
+            }
+        }
+    }
+
+    /**
+     * Make sure no Gregorian dates map to Chinese 1-based day of
+     * month zero.  This was a problem with some of the astronomical
+     * new moon determinations.
+     */
+    public void TestZeroDOM() {
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        GregorianCalendar greg = new GregorianCalendar(1989, Calendar.SEPTEMBER, 1);
+        logln("Start: " + greg.getTime());
+        for (int i=0; i<1000; ++i) {
+            cal.setTimeInMillis(greg.getTimeInMillis());
+            if (cal.get(Calendar.DAY_OF_MONTH) == 0) {
+                errln("Fail: " + greg.getTime() + " -> " +
+                      cal.get(Calendar.YEAR) + "/" +
+                      cal.get(Calendar.MONTH) +
+                      (cal.get(Calendar.IS_LEAP_MONTH)==1?"(leap)":"") +
+                      "/" + cal.get(Calendar.DAY_OF_MONTH));
+            }
+            greg.add(Calendar.DAY_OF_YEAR, 1);
+        }
+        logln("End: " + greg.getTime());
+    }
+
+    /**
+     * Test minimum and maximum functions.
+     */
+    public void TestLimits() {
+        // The number of days and the start date can be adjusted
+        // arbitrarily to either speed up the test or make it more
+        // thorough, but try to test at least a full year, preferably a
+        // full non-leap and a full leap year.
+
+        // Final parameter is either number of days, if > 0, or test
+        // duration in seconds, if < 0.
+        Calendar tempcal = Calendar.getInstance();
+        tempcal.clear();
+        tempcal.set(1989, Calendar.NOVEMBER, 1);
+        Calendar dangi = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        doLimitsTest(dangi, null, tempcal.getTime());
+        doTheoreticalLimitsTest(dangi, true);
+    }
+
+    /**
+     * Make sure IS_LEAP_MONTH participates in field resolution.
+     */
+    public void TestResolution() {
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        DateFormat fmt = DateFormat.getDateInstance(cal, DateFormat.DEFAULT);
+
+        // May 22 4334 = y4334 m4 d30 doy119
+        // May 23 4334 = y4334 m4* d1 doy120
+
+        final int THE_YEAR = 4334;
+        final int END = -1;
+
+        int[] DATA = {
+            // Format:
+            // (field, value)+, END, exp.month, exp.isLeapMonth, exp.DOM
+            // Note: exp.month is ONE-BASED
+
+            // If we set DAY_OF_YEAR only, that should be used
+            Calendar.DAY_OF_YEAR, 1,
+            END,
+            1,0,1, // Expect 1-1
+            
+            // If we set MONTH only, that should be used
+            Calendar.IS_LEAP_MONTH, 1,
+            Calendar.DAY_OF_MONTH, 1,
+            Calendar.MONTH, 3,
+            END,
+            4,1,1, // Expect 4*-1
+            
+            // If we set the DOY last, that should take precedence
+            Calendar.MONTH, 1, // Should ignore
+            Calendar.IS_LEAP_MONTH, 1, // Should ignore
+            Calendar.DAY_OF_MONTH, 1, // Should ignore
+            Calendar.DAY_OF_YEAR, 121,
+            END,
+            4,1,2, // Expect 4*-2
+            
+            // If we set IS_LEAP_MONTH last, that should take precedence
+            Calendar.MONTH, 3,
+            Calendar.DAY_OF_MONTH, 1,
+            Calendar.DAY_OF_YEAR, 5, // Should ignore
+            Calendar.IS_LEAP_MONTH, 1,
+            END,
+            4,1,1, // Expect 4*-1
+        };
+
+        StringBuilder buf = new StringBuilder();
+        for (int i=0; i<DATA.length; ) {
+            cal.clear();
+            cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
+            buf.setLength(0);
+            buf.append("EXTENDED_YEAR=" + THE_YEAR);
+            while (DATA[i] != END) {
+                cal.set(DATA[i++], DATA[i++]);
+                buf.append(" " + fieldName(DATA[i-2]) + "=" + DATA[i-1]);
+            }
+            ++i; // Skip over END mark
+            int expMonth = DATA[i++]-1;
+            int expIsLeapMonth = DATA[i++];
+            int expDOM = DATA[i++];
+            int month = cal.get(Calendar.MONTH);
+            int isLeapMonth = cal.get(Calendar.IS_LEAP_MONTH);
+            int dom = cal.get(Calendar.DAY_OF_MONTH);
+            if (expMonth == month && expIsLeapMonth == isLeapMonth &&
+                dom == expDOM) {
+                logln("OK: " + buf + " => " + fmt.format(cal.getTime()));
+            } else {
+                String s = fmt.format(cal.getTime());
+                cal.clear();
+                cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
+                cal.set(Calendar.MONTH, expMonth);
+                cal.set(Calendar.IS_LEAP_MONTH, expIsLeapMonth);
+                cal.set(Calendar.DAY_OF_MONTH, expDOM);
+                errln("Fail: " + buf + " => " + s +
+                      "=" + (month+1) + "," + isLeapMonth + "," + dom +
+                      ", expected " + fmt.format(cal.getTime()) +
+                      "=" + (expMonth+1) + "," + expIsLeapMonth + "," + expDOM);
+            }
+        }
+    }
+
+    /**
+     * Test the behavior of fields that are out of range.
+     */
+    public void TestOutOfRange() {
+        int[] DATA = new int[] {
+            // Input       Output
+            4334, 13,  1,   4335,  1,  1,
+            4334, 18,  1,   4335,  6,  1,
+            4335,  0,  1,   4334, 12,  1,
+            4335, -6,  1,   4334,  6,  1,
+            4334,  1, 32,   4334,  2,  2, // 1-4334 has 30 days
+            4334,  2, -1,   4334,  1, 29,
+        };
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        for (int i = 0; i < DATA.length;) {
+            int y1 = DATA[i++];
+            int m1 = DATA[i++] - 1;
+            int d1 = DATA[i++];
+            int y2 = DATA[i++];
+            int m2 = DATA[i++] - 1;
+            int d2 = DATA[i++];
+            cal.clear();
+            cal.set(Calendar.EXTENDED_YEAR, y1);
+            cal.set(MONTH, m1);
+            cal.set(DATE, d1);
+            int y = cal.get(Calendar.EXTENDED_YEAR);
+            int m = cal.get(MONTH);
+            int d = cal.get(DATE);
+            if (y != y2 || m != m2 || d != d2) {
+                errln("Fail: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d
+                        + ", expected " + y2 + "/" + (m2 + 1) + "/" + d2);
+            } else if (isVerbose()) {
+                logln("OK: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d);
+            }
+        }
+    }
+
+    /**
+     * Test the behavior of KoreanLunarCalendar.add().  The only real
+     * nastiness with roll is the MONTH field around leap months.
+     */
+    public void TestAdd() {
+        int[][] tests = new int[][] {
+            // MONTHS ARE 1-BASED HERE
+            // input               add           output
+            // year  mon    day    field amount  year  mon    day
+            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
+            {  4335,  12,0,  15,   MONTH,   1,   4336,   1,0,  15 }, // across year
+            {  4336,   1,0,  15,   MONTH,  -1,   4335,  12,0,  15 }, // across year
+            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
+            {  4334,   3,0,  30,   MONTH,   3,   4334,   5,0,  30 }, // no dom pin
+            {  4334,   3,0,  30,   MONTH,   4,   4334,   6,0,  29 }, // dom should pin
+        };
+       
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        doRollAddDangi(ADD, cal, tests);
+    }
+
+    /**
+     * Test the behavior of KoreanLunarCalendar.roll().  The only real
+     * nastiness with roll is the MONTH field around leap months.
+     */
+    public void TestRoll() {
+        int[][] tests = new int[][] {
+            // MONTHS ARE 1-BASED HERE
+            // input               add           output
+            // year  mon    day    field amount  year  mon    day
+            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
+            {  4338,   3,0,  15,   MONTH,  11,   4338,   2,0,  15 }, // normal
+            {  4335,  12,0,  15,   MONTH,   1,   4335,   1,0,  15 }, // across year
+            {  4336,   1,0,  15,   MONTH,  -1,   4336,  12,0,  15 }, // across year
+            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   3,0,  15,   MONTH,  16,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   3,0,  15,   MONTH,  28,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   4,0,  15,   MONTH, -12,   4334,   4,1,  15 }, // 4=leap
+            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   4,1,  15,   MONTH, -25,   4334,   5,0,  15 }, // 4=leap
+            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
+            {  4334,   3,0,  30,   MONTH,  15,   4334,   4,1,  29 }, // dom should pin
+            {  4334,   3,0,  30,   MONTH,  16,   4334,   5,0,  30 }, // no dom pin
+            {  4334,   3,0,  30,   MONTH,  -9,   4334,   6,0,  29 }, // dom should pin
+        };
+       
+        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        doRollAddDangi(ROLL, cal, tests);
+    }
+    
+    void doRollAddDangi(boolean roll, Calendar cal, int[][] tests) {
+        String name = roll ? "rolling" : "adding";
+
+        for (int i = 0; i < tests.length; i++) {
+            int[] test = tests[i];
+
+            cal.clear();
+            cal.set(Calendar.EXTENDED_YEAR, test[0]);
+            cal.set(Calendar.MONTH, test[1] - 1);
+            cal.set(Calendar.IS_LEAP_MONTH, test[2]);
+            cal.set(Calendar.DAY_OF_MONTH, test[3]);
+            if (roll) {
+                cal.roll(test[4], test[5]);
+            } else {
+                cal.add(test[4], test[5]);
+            }
+            if (cal.get(Calendar.EXTENDED_YEAR) != test[6] || cal.get(MONTH) != (test[7] - 1)
+                    || cal.get(Calendar.IS_LEAP_MONTH) != test[8] || cal.get(DATE) != test[9]) {
+                errln("Fail: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
+                        + fieldName(test[4]) + " by " + test[5] + ": expected "
+                        + ymdToString(test[6], test[7] - 1, test[8], test[9]) + ", got " + ymdToString(cal));
+            } else if (isVerbose()) {
+                logln("OK: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
+                        + fieldName(test[4]) + " by " + test[5] + ": got " + ymdToString(cal));
+            }
+        }
+    }
+
+    /**
+     * Convert year,month,day values to the form "year/month/day".
+     * On input the month value is zero-based, but in the result string it is one-based.
+     */
+    static public String ymdToString(int year, int month, int isLeapMonth, int day) {
+        return "" + year + "/" + (month + 1) + ((isLeapMonth != 0) ? "(leap)" : "") + "/" + day;
+    }
+
+    public void TestCoverage() {
+        // DangiCalendar()
+        // DangiCalendar(Date)
+        // DangiCalendar(TimeZone, ULocale)
+        Date d = new Date();
+
+        DangiCalendar cal1 = new DangiCalendar();
+        cal1.setTime(d);
+
+        DangiCalendar cal2 = new DangiCalendar(d);
+
+        DangiCalendar cal3 = new DangiCalendar(TimeZone.getDefault(), ULocale.getDefault());
+        cal3.setTime(d);
+
+        assertEquals("DangiCalendar() and DangiCalendar(Date)", cal1, cal2);
+        assertEquals("DangiCalendar() and DangiCalendar(TimeZone,ULocale)", cal1, cal3);
+
+        // String getType()
+        String type = cal1.getType();
+        assertEquals("getType()", "dangi", type);
+    }
+
+    public void TestInitWithCurrentTime() {
+        // If the chinese calendar current millis isn't called, the default year is wrong.
+        // this test is assuming the 'year' is the current cycle
+        // so when we cross a cycle boundary, the target will need to change
+        // that shouldn't be for awhile yet... 
+
+        Calendar cc = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
+        cc.set(Calendar.EXTENDED_YEAR, 4338);
+        cc.set(Calendar.MONTH, 0);
+         // need to set leap month flag off, otherwise, the test case always fails when
+         // current time is in a leap month
+        cc.set(Calendar.IS_LEAP_MONTH, 0);
+        cc.set(Calendar.DATE, 19);
+        cc.set(Calendar.HOUR_OF_DAY, 0);
+        cc.set(Calendar.MINUTE, 0);
+        cc.set(Calendar.SECOND, 0);
+        cc.set(Calendar.MILLISECOND, 0);
+
+        cc.add(Calendar.DATE, 1);
+        Calendar cal = new GregorianCalendar(2005, Calendar.FEBRUARY, 28);
+        Date target = cal.getTime();
+        Date result = cc.getTime();
+
+        assertEquals("chinese and gregorian date should match", target, result);
+    }
+}
index dad2d0a413fb2962dee714dfb942c4913be372c7..04a07a1f77e2dde11af2fc0e3a0ceb510d794897 100644 (file)
@@ -3391,6 +3391,7 @@ public class DateFormatTest extends com.ibm.icu.dev.test.TestFmwk {
                 new ULocale("en@calendar=islamic"),
                 new ULocale("ja_JP@calendar=japanese"),
                 new ULocale("zh_Hans_CN@calendar=bogus"),
+                new ULocale("ko_KR@calendar=dangi"),
         };
 
         SimpleDateFormat[] formatters = new SimpleDateFormat[5];
index de7406f308703fd6047323291452edbf72bab076..588096978fe22d5e447853101fa73524c7e3ecb9 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (C) 1996-2007, International Business Machines Corporation and    *
+ * Copyright (C) 1996-2012, International Business Machines Corporation and    *
  * others. All Rights Reserved.                                                *
  *******************************************************************************
  *
@@ -14,6 +14,7 @@ import com.ibm.icu.util.BuddhistCalendar;
 import com.ibm.icu.util.Calendar;
 import com.ibm.icu.util.ChineseCalendar;
 import com.ibm.icu.util.CopticCalendar;
+import com.ibm.icu.util.DangiCalendar;
 import com.ibm.icu.util.EthiopicCalendar;
 import com.ibm.icu.util.GregorianCalendar;
 import com.ibm.icu.util.HebrewCalendar;
@@ -22,6 +23,7 @@ import com.ibm.icu.util.IslamicCalendar;
 import com.ibm.icu.util.JapaneseCalendar;
 import com.ibm.icu.util.TaiwanCalendar;
 import com.ibm.icu.util.TimeZone;
+import com.ibm.icu.util.ULocale;
 
 /**
  * @author emader
@@ -110,6 +112,22 @@ public class CalendarTests
         }
     }
 
+    static class DangiCalendarHandler extends CalendarHandler
+    {
+        public Object[] getTestObjects()
+        {
+            Locale locales[] = SerializableTest.getLocales();
+            TimeZone kst = TimeZone.getTimeZone("Asia/Seoul");
+            DangiCalendar calendars[] = new DangiCalendar[locales.length];
+            
+            for (int i = 0; i < locales.length; i += 1) {
+                calendars[i] = new DangiCalendar(kst, ULocale.forLocale(locales[i]));
+            }
+            
+            return calendars; 
+        }
+    }
+
     static class EthiopicCalendarHandler extends CalendarHandler
     {
         public Object[] getTestObjects()
index bcb9789acafbda3eb15599b42897ab94f17d09b8..bd69d6f423008f25cad3156b83ab4e2d1d59c80f 100644 (file)
@@ -682,6 +682,7 @@ public class SerializableTest extends TestFmwk.TestGroup
         map.put("com.ibm.icu.util.BuddhistCalendar", new CalendarTests.BuddhistCalendarHandler());
         map.put("com.ibm.icu.util.ChineseCalendar", new CalendarTests.ChineseCalendarHandler());
         map.put("com.ibm.icu.util.CopticCalendar", new CalendarTests.CopticCalendarHandler());
+        map.put("com.ibm.icu.util.DangiCalendar", new CalendarTests.DangiCalendarHandler());
         map.put("com.ibm.icu.util.EthiopicCalendar", new CalendarTests.EthiopicCalendarHandler());
         map.put("com.ibm.icu.util.GregorianCalendar", new CalendarTests.GregorianCalendarHandler());
         map.put("com.ibm.icu.util.HebrewCalendar", new CalendarTests.HebrewCalendarHandler());