1 /*
2  *******************************************************************************
3  * Copyright (C) 1996-2010, International Business Machines Corporation and    *
4  * others. All Rights Reserved.                                                *
5  *******************************************************************************
6  */
7 
8 package com.ibm.icu.dev.test.calendar;
9 
10 import java.util.Date;
11 import java.util.HashMap;
12 import java.util.Locale;
13 import java.util.Map;
14 
15 import com.ibm.icu.dev.test.TestFmwk;
16 import com.ibm.icu.text.DateFormat;
17 import com.ibm.icu.text.SimpleDateFormat;
18 import com.ibm.icu.util.Calendar;
19 import com.ibm.icu.util.ChineseCalendar;
20 import com.ibm.icu.util.GregorianCalendar;
21 import com.ibm.icu.util.SimpleTimeZone;
22 
23 /**
24  * A base class for classes that test individual Calendar subclasses.
25  * Defines various useful utility methods and constants
26  */
27 public class CalendarTest extends TestFmwk {
28 
29     // Constants for use by subclasses, solely to save typing
30     public final static int SUN = Calendar.SUNDAY;
31     public final static int MON = Calendar.MONDAY;
32     public final static int TUE = Calendar.TUESDAY;
33     public final static int WED = Calendar.WEDNESDAY;
34     public final static int THU = Calendar.THURSDAY;
35     public final static int FRI = Calendar.FRIDAY;
36     public final static int SAT = Calendar.SATURDAY;
37 
38     public final static int ERA     = Calendar.ERA;
39     public final static int YEAR    = Calendar.YEAR;
40     public final static int MONTH   = Calendar.MONTH;
41     public final static int DATE    = Calendar.DATE;
42     public final static int HOUR    = Calendar.HOUR;
43     public final static int MINUTE  = Calendar.MINUTE;
44     public final static int SECOND  = Calendar.SECOND;
45     public final static int DOY     = Calendar.DAY_OF_YEAR;
46     public final static int WOY     = Calendar.WEEK_OF_YEAR;
47     public final static int WOM     = Calendar.WEEK_OF_MONTH;
48     public final static int DOW     = Calendar.DAY_OF_WEEK;
49     public final static int DOWM    = Calendar.DAY_OF_WEEK_IN_MONTH;
50 
51     public final static SimpleTimeZone UTC = new SimpleTimeZone(0, "GMT");
52 
53     private static final String[] FIELD_NAME = {
54         "ERA", "YEAR", "MONTH", "WEEK_OF_YEAR", "WEEK_OF_MONTH",
55         "DAY_OF_MONTH", "DAY_OF_YEAR", "DAY_OF_WEEK",
56         "DAY_OF_WEEK_IN_MONTH", "AM_PM", "HOUR", "HOUR_OF_DAY",
57         "MINUTE", "SECOND", "MILLISECOND", "ZONE_OFFSET",
58         "DST_OFFSET", "YEAR_WOY", "DOW_LOCAL", "EXTENDED_YEAR",
59         "JULIAN_DAY", "MILLISECONDS_IN_DAY",
60         "IS_LEAP_MONTH" // (ChineseCalendar only)
61     };
62 
fieldName(int f)63     public static final String fieldName(int f) {
64         return (f>=0 && f<FIELD_NAME.length) ?
65             FIELD_NAME[f] : ("<Field " + f + ">");
66     }
67 
68     /**
69      * Iterates through a list of calendar <code>TestCase</code> objects and
70      * makes sure that the time-to-fields and fields-to-time calculations work
71      * correnctly for the values in each test case.
72      */
doTestCases(TestCase[] cases, Calendar cal)73     public void doTestCases(TestCase[] cases, Calendar cal)
74     {
75         cal.setTimeZone(UTC);
76 
77         // Get a format to use for printing dates in the calendar system we're testing
78         DateFormat format = DateFormat.getDateTimeInstance(cal, DateFormat.SHORT, -1, Locale.getDefault());
79 
80         final String pattern = (cal instanceof ChineseCalendar) ?
81             "E MMl/dd/y G HH:mm:ss.S z" :
82             "E, MM/dd/yyyy G HH:mm:ss.S z";
83 
84         ((SimpleDateFormat)format).applyPattern(pattern);
85 
86         // This format is used for printing Gregorian dates.
87         DateFormat gregFormat = new SimpleDateFormat(pattern);
88         gregFormat.setTimeZone(UTC);
89 
90         GregorianCalendar pureGreg = new GregorianCalendar(UTC);
91         pureGreg.setGregorianChange(new Date(Long.MIN_VALUE));
92         DateFormat pureGregFmt = new SimpleDateFormat("E M/d/yyyy G");
93         pureGregFmt.setCalendar(pureGreg);
94 
95         // Now iterate through the test cases and see what happens
96         for (int i = 0; i < cases.length; i++)
97         {
98             logln("\ntest case: " + i);
99             TestCase test = cases[i];
100 
101             //
102             // First we want to make sure that the millis -> fields calculation works
103             // test.applyTime will call setTime() on the calendar object, and
104             // test.fieldsEqual will retrieve all of the field values and make sure
105             // that they're the same as the ones in the testcase
106             //
107             test.applyTime(cal);
108             if (!test.fieldsEqual(cal, this)) {
109                 errln("Fail: (millis=>fields) " +
110                       gregFormat.format(test.getTime()) + " => " +
111                       format.format(cal.getTime()) +
112                       ", expected " + test);
113             }
114 
115             //
116             // If that was OK, check the fields -> millis calculation
117             // test.applyFields will set all of the calendar's fields to
118             // match those in the test case.
119             //
120             cal.clear();
121             test.applyFields(cal);
122             if (!test.equals(cal)) {
123                 errln("Fail: (fields=>millis) " + test + " => " +
124                       pureGregFmt.format(cal.getTime()) +
125                       ", expected " + pureGregFmt.format(test.getTime()));
126             }
127         }
128     }
129 
130     static public final boolean ROLL = true;
131     static public final boolean ADD = false;
132 
133     /**
134      * Process test cases for <code>add</code> and <code>roll</code> methods.
135      * Each test case is an array of integers, as follows:
136      * <ul>
137      *  <li>0: input year
138      *  <li>1:       month  (zero-based)
139      *  <li>2:       day
140      *  <li>3: field to roll or add to
141      *  <li>4: amount to roll or add
142      *  <li>5: result year
143      *  <li>6:        month (zero-based)
144      *  <li>7:        day
145      * </ul>
146      * For example:
147      * <pre>
148      *   //       input                add by          output
149      *   //  year  month     day     field amount    year  month     day
150      *   {   5759, HESHVAN,   2,     MONTH,   1,     5759, KISLEV,    2 },
151      * </pre>
152      *
153      * @param roll  <code>true</code> or <code>ROLL</code> to test the <code>roll</code> method;
154      *              <code>false</code> or <code>ADD</code> to test the <code>add</code method
155      */
doRollAdd(boolean roll, Calendar cal, int[][] tests)156     public void doRollAdd(boolean roll, Calendar cal, int[][] tests)
157     {
158         String name = roll ? "rolling" : "adding";
159 
160         for (int i = 0; i < tests.length; i++) {
161             int[] test = tests[i];
162 
163             cal.clear();
164             if (cal instanceof ChineseCalendar) {
165                 cal.set(Calendar.EXTENDED_YEAR, test[0]);
166                 cal.set(Calendar.MONTH, test[1]);
167                 cal.set(Calendar.DAY_OF_MONTH, test[2]);
168             } else {
169                 cal.set(test[0], test[1], test[2]);
170             }
171             double day0 = getJulianDay(cal);
172             if (roll) {
173                 cal.roll(test[3], test[4]);
174             } else {
175                 cal.add(test[3], test[4]);
176             }
177             int y = cal.get(cal instanceof ChineseCalendar ?
178                             Calendar.EXTENDED_YEAR : YEAR);
179             if (y != test[5] || cal.get(MONTH) != test[6]
180                     || cal.get(DATE) != test[7])
181             {
182                 errln("Fail: " + name + " "+ ymdToString(test[0], test[1], test[2])
183                     + " (" + day0 + ")"
184                     + " " + FIELD_NAME[test[3]] + " by " + test[4]
185                     + ": expected " + ymdToString(test[5], test[6], test[7])
186                     + ", got " + ymdToString(cal));
187             } else if (isVerbose()) {
188                 logln("OK: " + name + " "+ ymdToString(test[0], test[1], test[2])
189                     + " (" + day0 + ")"
190                     + " " + FIELD_NAME[test[3]] + " by " + test[4]
191                     + ": got " + ymdToString(cal));
192             }
193         }
194     }
195 
196     /**
197      * Test the functions getXxxMinimum() and getXxxMaximum() by marching a
198      * test calendar 'cal' through 'numberOfDays' sequential days starting
199      * with 'startDate'.  For each date, read a field value along with its
200      * reported actual minimum and actual maximum.  These values are
201      * checked against one another as well as against getMinimum(),
202      * getGreatestMinimum(), getLeastMaximum(), and getMaximum().  We
203      * expect to see:
204      *
205      * 1. minimum <= actualMinimum <= greatestMinimum <=
206      *    leastMaximum <= actualMaximum <= maximum
207      *
208      * 2. actualMinimum <= value <= actualMaximum
209      *
210      * Note: In addition to outright failures, this test reports some
211      * results as warnings.  These are not generally of concern, but they
212      * should be evaluated by a human.  To see these, run this test in
213      * verbose mode.
214      * @param cal the calendar to be tested
215      * @param fieldsToTest an array of field values to be tested, e.g., new
216      * int[] { Calendar.MONTH, Calendar.DAY_OF_MONTH }.  It only makes
217      * sense to test the day fields; the time fields are not tested by this
218      * method.  If null, then test all standard fields.
219      * @param startDate the first date to test
220      * @param testDuration if positive, the number of days to be tested.
221      * If negative, the number of seconds to run the test.
222      */
doLimitsTest(Calendar cal, int[] fieldsToTest, Date startDate, int testDuration)223     public void doLimitsTest(Calendar cal, int[] fieldsToTest,
224                                 Date startDate, int testDuration) {
225         GregorianCalendar greg = new GregorianCalendar();
226         greg.setTime(startDate);
227         logln("Start: " + startDate);
228 
229         if (fieldsToTest == null) {
230             fieldsToTest = new int[] {
231                 Calendar.ERA, Calendar.YEAR, Calendar.MONTH,
232                 Calendar.WEEK_OF_YEAR, Calendar.WEEK_OF_MONTH,
233                 Calendar.DAY_OF_MONTH, Calendar.DAY_OF_YEAR,
234                 Calendar.DAY_OF_WEEK_IN_MONTH, Calendar.YEAR_WOY,
235                 Calendar.EXTENDED_YEAR
236             };
237         }
238 
239         // Keep a record of minima and maxima that we actually see.
240         // These are kept in an array of arrays of hashes.
241         Map[][] limits = new Map[fieldsToTest.length][2];
242         Object nub = new Object(); // Meaningless placeholder
243 
244         // This test can run for a long time; show progress.
245         long millis = System.currentTimeMillis();
246         long mark = millis + 5000; // 5 sec
247         millis -= testDuration * 1000; // stop time if testDuration<0
248 
249         for (int i=0;
250              testDuration>0 ? i<testDuration
251                             : System.currentTimeMillis()<millis;
252              ++i) {
253             if (System.currentTimeMillis() >= mark) {
254                 logln("(" + i + " days)");
255                 mark += 5000; // 5 sec
256             }
257             cal.setTimeInMillis(greg.getTimeInMillis());
258             for (int j=0; j<fieldsToTest.length; ++j) {
259                 int f = fieldsToTest[j];
260                 int v = cal.get(f);
261                 int minActual = cal.getActualMinimum(f);
262                 int maxActual = cal.getActualMaximum(f);
263                 int minLow = cal.getMinimum(f);
264                 int minHigh = cal.getGreatestMinimum(f);
265                 int maxLow = cal.getLeastMaximum(f);
266                 int maxHigh = cal.getMaximum(f);
267 
268                 // Fetch the hash for this field and keep track of the
269                 // minima and maxima.
270                 Map[] h = limits[j];
271                 if (h[0] == null) {
272                     h[0] = new HashMap();
273                     h[1] = new HashMap();
274                 }
275                 h[0].put(new Integer(minActual), nub);
276                 h[1].put(new Integer(maxActual), nub);
277 
278                 if (minActual < minLow || minActual > minHigh) {
279                     errln("Fail: " + ymdToString(cal) +
280                           " Range for min of " + FIELD_NAME[f] + "(" + f +
281                           ")=" + minLow + ".." + minHigh +
282                           ", actual_min=" + minActual);
283                 }
284                 if (maxActual < maxLow || maxActual > maxHigh) {
285                     errln("Fail: " + ymdToString(cal) +
286                           " Range for max of " + FIELD_NAME[f] + "(" + f +
287                           ")=" + maxLow + ".." + maxHigh +
288                           ", actual_max=" + maxActual);
289                 }
290                 if (v < minActual || v > maxActual) {
291                     errln("Fail: " + ymdToString(cal) +
292                           " " + FIELD_NAME[f] + "(" + f + ")=" + v +
293                           ", actual range=" + minActual + ".." + maxActual +
294                           ", allowed=(" + minLow + ".." + minHigh + ")..(" +
295                           maxLow + ".." + maxHigh + ")");
296                 }
297             }
298             greg.add(Calendar.DAY_OF_YEAR, 1);
299         }
300 
301         // Check actual maxima and minima seen against ranges returned
302         // by API.
303         StringBuffer buf = new StringBuffer();
304         for (int j=0; j<fieldsToTest.length; ++j) {
305             int f = fieldsToTest[j];
306             buf.setLength(0);
307             buf.append(FIELD_NAME[f]);
308             Map[] h = limits[j];
309             boolean fullRangeSeen = true;
310             for (int k=0; k<2; ++k) {
311                 int rangeLow = (k==0) ?
312                     cal.getMinimum(f) : cal.getLeastMaximum(f);
313                 int rangeHigh = (k==0) ?
314                     cal.getGreatestMinimum(f) : cal.getMaximum(f);
315                 // If either the top of the range or the bottom was never
316                 // seen, then there may be a problem.
317                 if (h[k].get(new Integer(rangeLow)) == null ||
318                     h[k].get(new Integer(rangeHigh)) == null) {
319                     fullRangeSeen = false;
320                 }
321                 buf.append(k==0 ? " minima seen=(" : "; maxima seen=(");
322                 for (Object v : h[k].keySet()) {
323                     buf.append(" " + v);
324                 }
325                 buf.append(") range=" + rangeLow + ".." + rangeHigh);
326             }
327             if (fullRangeSeen) {
328                 logln("OK: " + buf.toString());
329             } else {
330                 // This may or may not be an error -- if the range of dates
331                 // we scan over doesn't happen to contain a minimum or
332                 // maximum, it doesn't mean some other range won't.
333                 logln("Warning: " + buf.toString());
334             }
335         }
336 
337         logln("End: " + greg.getTime());
338     }
339 
340     /**
341      * doLimitsTest with default test duration
342      */
doLimitsTest(Calendar cal, int[] fieldsToTest, Date startDate)343     public void doLimitsTest(Calendar cal, int[] fieldsToTest, Date startDate) {
344         int testTime = getInclusion() <= 5 ? -3 : -120; // in seconds
345         doLimitsTest(cal, fieldsToTest, startDate, testTime);
346     }
347 
348     /**
349      * Test the functions getMaximum/getGeratestMinimum logically correct.
350      * This method assumes day of week cycle is consistent.
351      * @param cal The calendar instance to be tested.
352      * @param leapMonth true if the calendar system has leap months
353      */
doTheoreticalLimitsTest(Calendar cal, boolean leapMonth)354     public void doTheoreticalLimitsTest(Calendar cal, boolean leapMonth) {
355         int nDOW = cal.getMaximum(Calendar.DAY_OF_WEEK);
356         int maxDOY = cal.getMaximum(Calendar.DAY_OF_YEAR);
357         int lmaxDOW = cal.getLeastMaximum(Calendar.DAY_OF_YEAR);
358         int maxWOY = cal.getMaximum(Calendar.WEEK_OF_YEAR);
359         int lmaxWOY = cal.getLeastMaximum(Calendar.WEEK_OF_YEAR);
360         int maxM = cal.getMaximum(Calendar.MONTH) + 1;
361         int lmaxM = cal.getLeastMaximum(Calendar.MONTH) + 1;
362         int maxDOM = cal.getMaximum(Calendar.DAY_OF_MONTH);
363         int lmaxDOM = cal.getLeastMaximum(Calendar.DAY_OF_MONTH);
364         int maxDOWIM = cal.getMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
365         int lmaxDOWIM = cal.getLeastMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
366         int maxWOM = cal.getMaximum(Calendar.WEEK_OF_MONTH);
367         int lmaxWOM = cal.getLeastMaximum(Calendar.WEEK_OF_MONTH);
368         int minDaysInFirstWeek = cal.getMinimalDaysInFirstWeek();
369 
370         // Day of year
371         int expected;
372         if (!leapMonth) {
373             expected = maxM*maxDOM;
374             if (maxDOY > expected) {
375                 errln("FAIL: Maximum value of DAY_OF_YEAR is too big: " + maxDOY + "/expected: <=" + expected);
376             }
377             expected = lmaxM*lmaxDOM;
378             if (lmaxDOW < expected) {
379                 errln("FAIL: Least maximum value of DAY_OF_YEAR is too small: " + lmaxDOW + "/expected: >=" + expected);
380             }
381         }
382 
383         // Week of year
384         expected = maxDOY/nDOW + 1;
385         if (maxWOY > expected) {
386             errln("FAIL: Maximum value of WEEK_OF_YEAR is too big: " + maxWOY + "/expected: <=" + expected);
387         }
388         expected = lmaxDOW/nDOW;
389         if (lmaxWOY < expected) {
390             errln("FAIL: Least maximum value of WEEK_OF_YEAR is too small: " + lmaxWOY + "/expected >=" + expected);
391         }
392 
393         // Day of week in month
394         expected = (maxDOM + nDOW - 1)/nDOW;
395         if (maxDOWIM != expected) {
396             errln("FAIL: Maximum value of DAY_OF_WEEK_IN_MONTH is incorrect: " + maxDOWIM + "/expected: " + expected);
397         }
398         expected = (lmaxDOM + nDOW - 1)/nDOW;
399         if (lmaxDOWIM != expected) {
400             errln("FAIL: Least maximum value of DAY_OF_WEEK_IN_MONTH is incorrect: " + lmaxDOWIM + "/expected: " + expected);
401         }
402 
403         // Week of month
404         expected = (maxDOM + (nDOW - 1) + (nDOW - minDaysInFirstWeek)) / nDOW;
405         if (maxWOM != expected) {
406             errln("FAIL: Maximum value of WEEK_OF_MONTH is incorrect: " + maxWOM + "/expected: " + expected);
407         }
408         expected = (lmaxDOM + (nDOW - minDaysInFirstWeek)) / nDOW;
409         if (lmaxWOM != expected) {
410             errln("FAIL: Least maximum value of WEEK_OF_MONTH is incorrect: " + lmaxWOM + "/expected: " + expected);
411         }
412     }
413 
414     /**
415      * Convert year,month,day values to the form "year/month/day".
416      * On input the month value is zero-based, but in the result string it is one-based.
417      */
ymdToString(int year, int month, int day)418     static public String ymdToString(int year, int month, int day) {
419         return "" + year + "/" + (month+1) + "/" + day;
420     }
421 
422     /**
423      * Convert year,month,day values to the form "year/month/day".
424      */
ymdToString(Calendar cal)425     static public String ymdToString(Calendar cal) {
426         double day = getJulianDay(cal);
427         if (cal instanceof ChineseCalendar) {
428             return "" + cal.get(Calendar.EXTENDED_YEAR) + "/" +
429                 (cal.get(Calendar.MONTH)+1) +
430                 (cal.get(Calendar.IS_LEAP_MONTH)==1?"(leap)":"") + "/" +
431                 cal.get(Calendar.DATE) + " (" + day + ", time=" + cal.getTimeInMillis() + ")";
432         }
433         return ymdToString(cal.get(Calendar.EXTENDED_YEAR),
434                             cal.get(MONTH), cal.get(DATE)) +
435                             " (" + day + ", time=" + cal.getTimeInMillis() + ")";
436     }
437 
getJulianDay(Calendar cal)438     static double getJulianDay(Calendar cal) {
439         return (cal.getTime().getTime() - JULIAN_EPOCH) / DAY_MS;
440     }
441 
442     static final double DAY_MS = 24*60*60*1000.0;
443     static final long JULIAN_EPOCH = -210866760000000L;   // 1/1/4713 BC 12:00
444 }
445