1/*
2 *******************************************************************************
3 * Copyright (C) 2012, International Business Machines Corporation and         *
4 * others. All Rights Reserved.                                                *
5 *******************************************************************************
6 */
7package com.ibm.icu.dev.test.calendar;
8import java.util.Date;
9
10import com.ibm.icu.text.DateFormat;
11import com.ibm.icu.util.Calendar;
12import com.ibm.icu.util.DangiCalendar;
13import com.ibm.icu.util.GregorianCalendar;
14import com.ibm.icu.util.TimeZone;
15import com.ibm.icu.util.ULocale;
16
17public class DangiTest extends CalendarTest {
18
19    public static void main(String args[]) throws Exception {
20        new DangiTest().run(args);
21    }
22
23    /**
24     * Test basic mapping to and from Gregorian.
25     */
26    public void TestMapping() {
27        final int[] DATA = {
28            // (Note: months are 1-based)
29            // Gregorian    Korean (Dan-gi)
30            1964,  9,  4,   4297,  7,0, 28,
31            1964,  9,  5,   4297,  7,0, 29,
32            1964,  9,  6,   4297,  8,0,  1,
33            1964,  9,  7,   4297,  8,0,  2,
34            1961, 12, 25,   4294, 11,0, 18,
35            1999,  6,  4,   4332,  4,0, 21,
36
37            1990,  5, 23,   4323,  4,0, 29,
38            1990,  5, 24,   4323,  5,0,  1,
39            1990,  6, 22,   4323,  5,0, 30,
40            1990,  6, 23,   4323,  5,1,  1,
41            1990,  7, 20,   4323,  5,1, 28,
42            1990,  7, 21,   4323,  5,1, 29,
43            1990,  7, 22,   4323,  6,0,  1,
44
45            // Some tricky dates (where GMT+8 doesn't agree with GMT+9)
46            //
47            // The list is from http://www.math.snu.ac.kr/~kye/others/lunar.html ('kye ref').
48            // However, for some dates disagree with the above reference so KASI's
49            // calculation was cross-referenced:
50            //  http://astro.kasi.re.kr/Life/ConvertSolarLunarForm.aspx?MenuID=115
51            1880, 11,  3,   4213, 10,0,  1, // astronomer's GMT+8 / KASI disagrees with the kye ref
52            1882, 12, 10,   4215, 11,0,  1,
53            1883,  7, 4,    4216,  6,0,  1,
54            1884,  4, 25,   4217,  4,0,  1,
55            1885,  5, 14,   4218,  4,0,  1,
56            1891,  1, 10,   4223, 12,0,  1,
57            1893,  4, 16,   4226,  3,0,  1,
58            1894,  5,  5,   4227,  4,0,  1,
59            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)
60            1903, 10, 20,   4236,  9,0,  1,
61            1904,  1, 17,   4236, 12,0,  1,
62            1904, 11,  7,   4237, 10,0,  1,
63            1905,  5,  4,   4238,  4,0,  1,
64            1907,  7, 10,   4240,  6,0,  1,
65            1908,  4, 30,   4241,  4,0,  1,
66            1908,  9, 25,   4241,  9,0,  1,
67            1909,  9, 14,   4242,  8,0,  1,
68            1911, 12, 20,   4244, 11,0,  1,
69            1976, 11, 22,   4309, 10,0,  1,
70        };
71
72        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
73        StringBuilder buf = new StringBuilder();
74
75        logln("Gregorian -> Korean Lunar (Dangi)");
76
77        Calendar grego = Calendar.getInstance();
78        grego.clear();
79        for (int i = 0; i < DATA.length;) {
80            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
81            Date date = grego.getTime();
82            cal.setTime(date);
83            int y = cal.get(Calendar.EXTENDED_YEAR);
84            int m = cal.get(Calendar.MONTH) + 1; // 0-based -> 1-based
85            int L = cal.get(Calendar.IS_LEAP_MONTH);
86            int d = cal.get(Calendar.DAY_OF_MONTH);
87            int yE = DATA[i++]; // Expected y, m, isLeapMonth, d
88            int mE = DATA[i++]; // 1-based
89            int LE = DATA[i++];
90            int dE = DATA[i++];
91            buf.setLength(0);
92            buf.append(date + " -> ");
93            buf.append(y + "/" + m + (L == 1 ? "(leap)" : "") + "/" + d);
94            if (y == yE && m == mE && L == LE && d == dE) {
95                logln("OK: " + buf.toString());
96            } else {
97                errln("Fail: " + buf.toString() + ", expected " + yE + "/" + mE + (LE == 1 ? "(leap)" : "") + "/" + dE);
98            }
99        }
100
101        logln("Korean Lunar (Dangi) -> Gregorian");
102        for (int i = 0; i < DATA.length;) {
103            grego.set(DATA[i++], DATA[i++] - 1, DATA[i++]);
104            Date dexp = grego.getTime();
105            int cyear = DATA[i++];
106            int cmonth = DATA[i++];
107            int cisleapmonth = DATA[i++];
108            int cdayofmonth = DATA[i++];
109            cal.clear();
110            cal.set(Calendar.EXTENDED_YEAR, cyear);
111            cal.set(Calendar.MONTH, cmonth - 1);
112            cal.set(Calendar.IS_LEAP_MONTH, cisleapmonth);
113            cal.set(Calendar.DAY_OF_MONTH, cdayofmonth);
114            Date date = cal.getTime();
115            buf.setLength(0);
116            buf.append(cyear + "/" + cmonth + (cisleapmonth == 1 ? "(leap)" : "") + "/" + cdayofmonth);
117            buf.append(" -> " + date);
118            if (date.equals(dexp)) {
119                logln("OK: " + buf.toString());
120            } else {
121                errln("Fail: " + buf.toString() + ", expected " + dexp);
122            }
123        }
124    }
125
126    /**
127     * Make sure no Gregorian dates map to Chinese 1-based day of
128     * month zero.  This was a problem with some of the astronomical
129     * new moon determinations.
130     */
131    public void TestZeroDOM() {
132        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
133        GregorianCalendar greg = new GregorianCalendar(1989, Calendar.SEPTEMBER, 1);
134        logln("Start: " + greg.getTime());
135        for (int i=0; i<1000; ++i) {
136            cal.setTimeInMillis(greg.getTimeInMillis());
137            if (cal.get(Calendar.DAY_OF_MONTH) == 0) {
138                errln("Fail: " + greg.getTime() + " -> " +
139                      cal.get(Calendar.YEAR) + "/" +
140                      cal.get(Calendar.MONTH) +
141                      (cal.get(Calendar.IS_LEAP_MONTH)==1?"(leap)":"") +
142                      "/" + cal.get(Calendar.DAY_OF_MONTH));
143            }
144            greg.add(Calendar.DAY_OF_YEAR, 1);
145        }
146        logln("End: " + greg.getTime());
147    }
148
149    /**
150     * Test minimum and maximum functions.
151     */
152    public void TestLimits() {
153        // The number of days and the start date can be adjusted
154        // arbitrarily to either speed up the test or make it more
155        // thorough, but try to test at least a full year, preferably a
156        // full non-leap and a full leap year.
157
158        // Final parameter is either number of days, if > 0, or test
159        // duration in seconds, if < 0.
160        Calendar tempcal = Calendar.getInstance();
161        tempcal.clear();
162        tempcal.set(1989, Calendar.NOVEMBER, 1);
163        Calendar dangi = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
164        doLimitsTest(dangi, null, tempcal.getTime());
165        doTheoreticalLimitsTest(dangi, true);
166    }
167
168    /**
169     * Make sure IS_LEAP_MONTH participates in field resolution.
170     */
171    public void TestResolution() {
172        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
173        DateFormat fmt = DateFormat.getDateInstance(cal, DateFormat.DEFAULT);
174
175        // May 22 4334 = y4334 m4 d30 doy119
176        // May 23 4334 = y4334 m4* d1 doy120
177
178        final int THE_YEAR = 4334;
179        final int END = -1;
180
181        int[] DATA = {
182            // Format:
183            // (field, value)+, END, exp.month, exp.isLeapMonth, exp.DOM
184            // Note: exp.month is ONE-BASED
185
186            // If we set DAY_OF_YEAR only, that should be used
187            Calendar.DAY_OF_YEAR, 1,
188            END,
189            1,0,1, // Expect 1-1
190
191            // If we set MONTH only, that should be used
192            Calendar.IS_LEAP_MONTH, 1,
193            Calendar.DAY_OF_MONTH, 1,
194            Calendar.MONTH, 3,
195            END,
196            4,1,1, // Expect 4*-1
197
198            // If we set the DOY last, that should take precedence
199            Calendar.MONTH, 1, // Should ignore
200            Calendar.IS_LEAP_MONTH, 1, // Should ignore
201            Calendar.DAY_OF_MONTH, 1, // Should ignore
202            Calendar.DAY_OF_YEAR, 121,
203            END,
204            4,1,2, // Expect 4*-2
205
206            // If we set IS_LEAP_MONTH last, that should take precedence
207            Calendar.MONTH, 3,
208            Calendar.DAY_OF_MONTH, 1,
209            Calendar.DAY_OF_YEAR, 5, // Should ignore
210            Calendar.IS_LEAP_MONTH, 1,
211            END,
212            4,1,1, // Expect 4*-1
213        };
214
215        StringBuilder buf = new StringBuilder();
216        for (int i=0; i<DATA.length; ) {
217            cal.clear();
218            cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
219            buf.setLength(0);
220            buf.append("EXTENDED_YEAR=" + THE_YEAR);
221            while (DATA[i] != END) {
222                cal.set(DATA[i++], DATA[i++]);
223                buf.append(" " + fieldName(DATA[i-2]) + "=" + DATA[i-1]);
224            }
225            ++i; // Skip over END mark
226            int expMonth = DATA[i++]-1;
227            int expIsLeapMonth = DATA[i++];
228            int expDOM = DATA[i++];
229            int month = cal.get(Calendar.MONTH);
230            int isLeapMonth = cal.get(Calendar.IS_LEAP_MONTH);
231            int dom = cal.get(Calendar.DAY_OF_MONTH);
232            if (expMonth == month && expIsLeapMonth == isLeapMonth &&
233                dom == expDOM) {
234                logln("OK: " + buf + " => " + fmt.format(cal.getTime()));
235            } else {
236                String s = fmt.format(cal.getTime());
237                cal.clear();
238                cal.set(Calendar.EXTENDED_YEAR, THE_YEAR);
239                cal.set(Calendar.MONTH, expMonth);
240                cal.set(Calendar.IS_LEAP_MONTH, expIsLeapMonth);
241                cal.set(Calendar.DAY_OF_MONTH, expDOM);
242                errln("Fail: " + buf + " => " + s +
243                      "=" + (month+1) + "," + isLeapMonth + "," + dom +
244                      ", expected " + fmt.format(cal.getTime()) +
245                      "=" + (expMonth+1) + "," + expIsLeapMonth + "," + expDOM);
246            }
247        }
248    }
249
250    /**
251     * Test the behavior of fields that are out of range.
252     */
253    public void TestOutOfRange() {
254        int[] DATA = new int[] {
255            // Input       Output
256            4334, 13,  1,   4335,  1,  1,
257            4334, 18,  1,   4335,  6,  1,
258            4335,  0,  1,   4334, 12,  1,
259            4335, -6,  1,   4334,  6,  1,
260            4334,  1, 32,   4334,  2,  2, // 1-4334 has 30 days
261            4334,  2, -1,   4334,  1, 29,
262        };
263        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
264        for (int i = 0; i < DATA.length;) {
265            int y1 = DATA[i++];
266            int m1 = DATA[i++] - 1;
267            int d1 = DATA[i++];
268            int y2 = DATA[i++];
269            int m2 = DATA[i++] - 1;
270            int d2 = DATA[i++];
271            cal.clear();
272            cal.set(Calendar.EXTENDED_YEAR, y1);
273            cal.set(MONTH, m1);
274            cal.set(DATE, d1);
275            int y = cal.get(Calendar.EXTENDED_YEAR);
276            int m = cal.get(MONTH);
277            int d = cal.get(DATE);
278            if (y != y2 || m != m2 || d != d2) {
279                errln("Fail: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d
280                        + ", expected " + y2 + "/" + (m2 + 1) + "/" + d2);
281            } else if (isVerbose()) {
282                logln("OK: " + y1 + "/" + (m1 + 1) + "/" + d1 + " resolves to " + y + "/" + (m + 1) + "/" + d);
283            }
284        }
285    }
286
287    /**
288     * Test the behavior of KoreanLunarCalendar.add().  The only real
289     * nastiness with roll is the MONTH field around leap months.
290     */
291    public void TestAdd() {
292        int[][] tests = new int[][] {
293            // MONTHS ARE 1-BASED HERE
294            // input               add           output
295            // year  mon    day    field amount  year  mon    day
296            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
297            {  4335,  12,0,  15,   MONTH,   1,   4336,   1,0,  15 }, // across year
298            {  4336,   1,0,  15,   MONTH,  -1,   4335,  12,0,  15 }, // across year
299            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
300            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
301            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
302            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
303            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
304            {  4334,   3,0,  30,   MONTH,   3,   4334,   5,0,  30 }, // no dom pin
305            {  4334,   3,0,  30,   MONTH,   4,   4334,   6,0,  29 }, // dom should pin
306        };
307
308        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
309        doRollAddDangi(ADD, cal, tests);
310    }
311
312    /**
313     * Test the behavior of KoreanLunarCalendar.roll().  The only real
314     * nastiness with roll is the MONTH field around leap months.
315     */
316    public void TestRoll() {
317        int[][] tests = new int[][] {
318            // MONTHS ARE 1-BASED HERE
319            // input               add           output
320            // year  mon    day    field amount  year  mon    day
321            {  4338,   3,0,  15,   MONTH,   3,   4338,   6,0,  15 }, // normal
322            {  4338,   3,0,  15,   MONTH,  11,   4338,   2,0,  15 }, // normal
323            {  4335,  12,0,  15,   MONTH,   1,   4335,   1,0,  15 }, // across year
324            {  4336,   1,0,  15,   MONTH,  -1,   4336,  12,0,  15 }, // across year
325            {  4334,   3,0,  15,   MONTH,   3,   4334,   5,0,  15 }, // 4=leap
326            {  4334,   3,0,  15,   MONTH,  16,   4334,   5,0,  15 }, // 4=leap
327            {  4334,   3,0,  15,   MONTH,   2,   4334,   4,1,  15 }, // 4=leap
328            {  4334,   3,0,  15,   MONTH,  28,   4334,   4,1,  15 }, // 4=leap
329            {  4334,   4,0,  15,   MONTH,   1,   4334,   4,1,  15 }, // 4=leap
330            {  4334,   4,0,  15,   MONTH, -12,   4334,   4,1,  15 }, // 4=leap
331            {  4334,   4,1,  15,   MONTH,   1,   4334,   5,0,  15 }, // 4=leap
332            {  4334,   4,1,  15,   MONTH, -25,   4334,   5,0,  15 }, // 4=leap
333            {  4334,   3,0,  30,   MONTH,   2,   4334,   4,1,  29 }, // dom should pin
334            {  4334,   3,0,  30,   MONTH,  15,   4334,   4,1,  29 }, // dom should pin
335            {  4334,   3,0,  30,   MONTH,  16,   4334,   5,0,  30 }, // no dom pin
336            {  4334,   3,0,  30,   MONTH,  -9,   4334,   6,0,  29 }, // dom should pin
337        };
338
339        Calendar cal = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
340        doRollAddDangi(ROLL, cal, tests);
341    }
342
343    void doRollAddDangi(boolean roll, Calendar cal, int[][] tests) {
344        String name = roll ? "rolling" : "adding";
345
346        for (int i = 0; i < tests.length; i++) {
347            int[] test = tests[i];
348
349            cal.clear();
350            cal.set(Calendar.EXTENDED_YEAR, test[0]);
351            cal.set(Calendar.MONTH, test[1] - 1);
352            cal.set(Calendar.IS_LEAP_MONTH, test[2]);
353            cal.set(Calendar.DAY_OF_MONTH, test[3]);
354            if (roll) {
355                cal.roll(test[4], test[5]);
356            } else {
357                cal.add(test[4], test[5]);
358            }
359            if (cal.get(Calendar.EXTENDED_YEAR) != test[6] || cal.get(MONTH) != (test[7] - 1)
360                    || cal.get(Calendar.IS_LEAP_MONTH) != test[8] || cal.get(DATE) != test[9]) {
361                errln("Fail: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
362                        + fieldName(test[4]) + " by " + test[5] + ": expected "
363                        + ymdToString(test[6], test[7] - 1, test[8], test[9]) + ", got " + ymdToString(cal));
364            } else if (isVerbose()) {
365                logln("OK: " + name + " " + ymdToString(test[0], test[1] - 1, test[2], test[3]) + " "
366                        + fieldName(test[4]) + " by " + test[5] + ": got " + ymdToString(cal));
367            }
368        }
369    }
370
371    /**
372     * Convert year,month,day values to the form "year/month/day".
373     * On input the month value is zero-based, but in the result string it is one-based.
374     */
375    static public String ymdToString(int year, int month, int isLeapMonth, int day) {
376        return "" + year + "/" + (month + 1) + ((isLeapMonth != 0) ? "(leap)" : "") + "/" + day;
377    }
378
379    public void TestCoverage() {
380        // DangiCalendar()
381        // DangiCalendar(Date)
382        // DangiCalendar(TimeZone, ULocale)
383        Date d = new Date();
384
385        DangiCalendar cal1 = new DangiCalendar();
386        cal1.setTime(d);
387
388        DangiCalendar cal2 = new DangiCalendar(d);
389
390        DangiCalendar cal3 = new DangiCalendar(TimeZone.getDefault(), ULocale.getDefault());
391        cal3.setTime(d);
392
393        assertEquals("DangiCalendar() and DangiCalendar(Date)", cal1, cal2);
394        assertEquals("DangiCalendar() and DangiCalendar(TimeZone,ULocale)", cal1, cal3);
395
396        // String getType()
397        String type = cal1.getType();
398        assertEquals("getType()", "dangi", type);
399    }
400
401    public void TestInitWithCurrentTime() {
402        // If the chinese calendar current millis isn't called, the default year is wrong.
403        // this test is assuming the 'year' is the current cycle
404        // so when we cross a cycle boundary, the target will need to change
405        // that shouldn't be for awhile yet...
406
407        Calendar cc = Calendar.getInstance(new ULocale("ko_KR@calendar=dangi"));
408        cc.set(Calendar.EXTENDED_YEAR, 4338);
409        cc.set(Calendar.MONTH, 0);
410         // need to set leap month flag off, otherwise, the test case always fails when
411         // current time is in a leap month
412        cc.set(Calendar.IS_LEAP_MONTH, 0);
413        cc.set(Calendar.DATE, 19);
414        cc.set(Calendar.HOUR_OF_DAY, 0);
415        cc.set(Calendar.MINUTE, 0);
416        cc.set(Calendar.SECOND, 0);
417        cc.set(Calendar.MILLISECOND, 0);
418
419        cc.add(Calendar.DATE, 1);
420
421        Calendar cal = new GregorianCalendar(2005, Calendar.FEBRUARY, 28);
422        Date target = cal.getTime();
423        Date result = cc.getTime();
424
425        assertEquals("chinese and gregorian date should match", target, result);
426    }
427}
428