1/*
2 * Copyright (C) 2013 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package libcore.java.util;
18
19import java.time.Instant;
20import java.time.LocalDateTime;
21import java.time.ZoneId;
22import java.time.ZonedDateTime;
23import java.util.Calendar;
24import java.util.Date;
25import java.util.GregorianCalendar;
26import java.util.Locale;
27import java.util.SimpleTimeZone;
28import java.util.TimeZone;
29import junit.framework.TestCase;
30
31public class GregorianCalendarTest extends TestCase {
32
33    private static final TimeZone LOS_ANGELES = TimeZone.getTimeZone("America/Los_Angeles");
34
35    private static final TimeZone LONDON = TimeZone.getTimeZone("Europe/London");
36
37    private static final int HOUR_IN_MILLIS = 3600000;
38
39    private static final SimpleTimeZone CUSTOM_LOS_ANGELES_TIME_ZONE = new SimpleTimeZone(-28800000,
40            "Custom America/Los_Angeles",
41            Calendar.MARCH, 9, 0, hours(2),
42            Calendar.NOVEMBER, 2, 0, hours(2),
43            hours(1));
44
45    private Locale defaultLocale;
46
47    @Override
48    public void setUp() throws Exception {
49        super.setUp();
50        defaultLocale = Locale.getDefault();
51        // Most tests are locale independent, but locale does affect start-of-week.
52        Locale.setDefault(Locale.US);
53    }
54
55    @Override
56    public void tearDown() throws Exception {
57        Locale.setDefault(defaultLocale);
58        super.tearDown();
59    }
60
61    // Documented a previous difference in behavior between this and the RI, see
62    // https://code.google.com/p/android/issues/detail?id=61993 for more details.
63    // Switching to OpenJDK has fixed that issue and so this test has been changed to reflect
64    // the correct behavior.
65    public void test_computeFields_dayOfWeekAndWeekOfYearSet() {
66        Calendar greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
67
68        // Ensure we use different values to the default ones.
69        int differentWeekOfYear = greg.get(Calendar.WEEK_OF_YEAR) == 1 ? 2 : 1;
70        int differentDayOfWeek = greg.get(Calendar.DAY_OF_WEEK) == Calendar.MONDAY
71                ? Calendar.TUESDAY : Calendar.MONDAY;
72
73        // Setting WEEK_OF_YEAR and DAY_OF_WEEK with an intervening
74        // call to computeFields will work.
75        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
76        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
77        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
78        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
79
80        // Setting WEEK_OF_YEAR after DAY_OF_WEEK with no intervening
81        // call to computeFields will work.
82        greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
83        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
84        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
85        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
86        assertEquals(differentDayOfWeek, greg.get(Calendar.DAY_OF_WEEK));
87
88        // Setting DAY_OF_WEEK after WEEK_OF_YEAR with no intervening
89        // call to computeFields will work.
90        greg = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
91        greg.set(Calendar.WEEK_OF_YEAR, differentWeekOfYear);
92        greg.set(Calendar.DAY_OF_WEEK, differentDayOfWeek);
93        assertEquals(differentWeekOfYear, greg.get(Calendar.WEEK_OF_YEAR));
94        assertEquals(differentDayOfWeek, greg.get(Calendar.DAY_OF_WEEK));
95    }
96
97    /**
98     * Specialized tests for those fields affected by GregorianCalendar cut over date.
99     *
100     * <p>Expands on a regression test created for harmony-2947.
101     */
102    public void test_fieldsAffectedByGregorianCutOver() {
103
104        Date date = new Date(Date.parse("Jan 1 00:00:01 GMT 2000"));
105        assertEquals(946684801000L, date.getTime());
106
107        GregorianCalendar gc;
108
109        // Test in America/Los_Angeles
110        gc = new GregorianCalendar(LOS_ANGELES, Locale.ENGLISH);
111        gc.setGregorianChange(date);
112        gc.setTime(date);
113
114        // Check the date to ensure that it is 18th Dec 1999. The reason this is not 1st Jan 2000
115        // is that the offset for Los Angeles is GMT-08:00. setGregorianChange() is interpreted as
116        // a wall time, not an instant. So, the instant that corresponds to
117        // "1st Jan 2000 00:00:01 GMT" is 8 hours before the Julian/Gregorian switch in LA. The day
118        // before 1st Jan 2000 (Gregorian Calendar) would be 18th Dec 1999 in the Julian calendar.
119        // The reason it is not the 31st Dec 1999 is simply a result of the discontinuity that
120        // occurred when switching calendars. That happened for real when the calendars were
121        // switched in 1582.
122        //
123        // A different year explains why we get very different results for the methods being tested.
124        assertEquals(1999, gc.get(Calendar.YEAR));
125        assertEquals(Calendar.DECEMBER, gc.get(Calendar.MONTH));
126        assertEquals(18, gc.get(Calendar.DAY_OF_MONTH));
127
128        assertEquals(50, gc.getActualMaximum(Calendar.WEEK_OF_YEAR));
129        assertEquals(50, gc.getLeastMaximum(Calendar.WEEK_OF_YEAR));
130        assertEquals(3, gc.getActualMaximum(Calendar.WEEK_OF_MONTH));
131        assertEquals(3, gc.getLeastMaximum(Calendar.WEEK_OF_MONTH));
132        assertEquals(18, gc.getActualMaximum(Calendar.DAY_OF_MONTH));
133        assertEquals(18, gc.getLeastMaximum(Calendar.DAY_OF_MONTH));
134        assertEquals(352, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
135        assertEquals(352, gc.getLeastMaximum(Calendar.DAY_OF_YEAR));
136        assertEquals(3, gc.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
137        assertEquals(3, gc.getLeastMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
138
139        // Test in Europe/London
140        gc = new GregorianCalendar(LONDON, Locale.ENGLISH);
141        gc.setGregorianChange(date);
142        gc.setTime(date);
143
144        // Check the date is actually 1st Jan 2000.
145        assertEquals(2000, gc.get(Calendar.YEAR));
146        assertEquals(Calendar.JANUARY, gc.get(Calendar.MONTH));
147        assertEquals(1, gc.get(Calendar.DAY_OF_MONTH));
148
149        assertEquals(53, gc.getActualMaximum(Calendar.WEEK_OF_YEAR));
150        assertEquals(52, gc.getLeastMaximum(Calendar.WEEK_OF_YEAR));
151        assertEquals(5, gc.getActualMaximum(Calendar.WEEK_OF_MONTH));
152        assertEquals(4, gc.getLeastMaximum(Calendar.WEEK_OF_MONTH));
153        assertEquals(31, gc.getActualMaximum(Calendar.DAY_OF_MONTH));
154        assertEquals(28, gc.getLeastMaximum(Calendar.DAY_OF_MONTH));
155        assertEquals(366, gc.getActualMaximum(Calendar.DAY_OF_YEAR));
156        assertEquals(365, gc.getLeastMaximum(Calendar.DAY_OF_YEAR));
157        assertEquals(5, gc.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
158        assertEquals(4, gc.getLeastMaximum(Calendar.DAY_OF_WEEK_IN_MONTH));
159    }
160
161    public void test_computeTime_enteringDst_TimeZone_LosAngeles_2014() {
162        TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
163        checkDstLosAngeles2014(timeZone);
164    }
165
166    /**
167     * This test will fail in the RI.
168     *
169     * <p>The AOSP behavior is different for backwards compatibility with previous versions of
170     * Android.
171     *
172     * <p>Search in this file for 'OpenJDK Failure' to see more details.
173     */
174    public void test_computeTime_enteringDst_DelegatingTimeZone_LosAngeles_2014() {
175        TimeZone timeZone = TimeZone.getTimeZone("America/Los_Angeles");
176        timeZone = new DelegatingTimeZone(timeZone);
177        checkDstLosAngeles2014(timeZone);
178    }
179
180    /**
181     * This test will fail in the RI.
182     *
183     * <p>The AOSP behavior is different for backwards compatibility with previous versions of
184     * Android.
185     *
186     * <p>Search in this file for 'OpenJDK Failure' to see more details.
187     */
188    public void test_computeTime_enteringDst_SimpleTimeZone_LosAngeles_2014() {
189        checkDstLosAngeles2014(CUSTOM_LOS_ANGELES_TIME_ZONE);
190    }
191
192    public void test_computeTime_enteringDst() {
193        // Get the DST entry time with a ZoneInfo implementation of TimeZone.
194        TimeZone zoneInfo = TimeZone.getTimeZone("America/Los_Angeles");
195        long zoneInfoTime = getDstLosAngeles2014(zoneInfo);
196
197        // Check that the time is correct.
198        assertTrue(zoneInfo.inDaylightTime(new Date(zoneInfoTime)));
199        assertFalse(zoneInfo.inDaylightTime(new Date(zoneInfoTime - 1)));
200
201        // Get the DST entry time with a SimpleTimeZone implementation of TimeZone.
202        SimpleTimeZone simpleTimeZone = new SimpleTimeZone(-28800000,
203                "Custom America/Los_Angeles",
204                Calendar.MARCH, 9, 0, 7200000,
205                Calendar.NOVEMBER, 2, 0, 7200000,
206                3600000);
207        long simpleTimeZoneTime = getDstLosAngeles2014(simpleTimeZone);
208
209    }
210
211    public void test_isWeekDateSupported() {
212        assertTrue(new GregorianCalendar().isWeekDateSupported());
213    }
214
215    public void test_setWeekDate() {
216        GregorianCalendar cal = new GregorianCalendar();
217
218        // When first week should have at least 4 days
219        cal.setMinimalDaysInFirstWeek(4);
220        cal.setWeekDate(2016, 13, Calendar.TUESDAY);
221        assertEquals(Calendar.TUESDAY, cal.get(Calendar.DAY_OF_WEEK));
222        assertEquals(29, cal.get(Calendar.DAY_OF_MONTH));
223
224        // When first week can have single day
225        cal.setMinimalDaysInFirstWeek(1);
226        cal.setWeekDate(2016, 13, Calendar.TUESDAY);
227        assertEquals(Calendar.TUESDAY, cal.get(Calendar.DAY_OF_WEEK));
228        assertEquals(22, cal.get(Calendar.DAY_OF_MONTH));
229
230        try {
231            cal.setWeekDate(2016, 13, Calendar.SATURDAY + 1);
232            fail();
233        } catch (IllegalArgumentException expected) {
234        }
235
236        try {
237            cal.setWeekDate(2016, 13, Calendar.SUNDAY - 1);
238            fail();
239        } catch (IllegalArgumentException expected) {
240        }
241
242        // in non-lenient mode
243        cal.setLenient(false);
244        try {
245            cal.setWeekDate(2016, 60, Calendar.SATURDAY);
246            fail();
247        } catch (IllegalArgumentException expected) {}
248
249        try {
250            cal.setWeekDate(-1, 60, Calendar.SATURDAY);
251            fail();
252        } catch (IllegalArgumentException expected) {}
253    }
254
255    public void test_getWeekYear() {
256        GregorianCalendar cal = new GregorianCalendar();
257
258        cal.set(2016, Calendar.MARCH, 29);
259        assertEquals(2016, cal.getWeekYear());
260
261        // With minimal days in first week is set to 4
262        cal.setMinimalDaysInFirstWeek(4);
263        cal.set(2016, Calendar.JANUARY, 1);
264        assertEquals(2015, cal.getWeekYear());
265        cal.set(2015, Calendar.DECEMBER, 31);
266        assertEquals(2015, cal.getWeekYear());
267
268        // With minimal days in first week is set to 1
269        cal.setMinimalDaysInFirstWeek(1);
270        cal.set(2016, Calendar.JANUARY, 1);
271        assertEquals(2016, cal.getWeekYear());
272        cal.set(2015, Calendar.DECEMBER, 31);
273        assertEquals(2016, cal.getWeekYear());
274    }
275
276    public void test_getWeeksInWeekYear() {
277        GregorianCalendar cal = new GregorianCalendar();
278
279        // With minimal days in first week is set to 1
280        cal.setMinimalDaysInFirstWeek(1);
281        cal.set(2016, Calendar.JANUARY, 10);
282        assertEquals(53, cal.getWeeksInWeekYear());
283
284        // With minimal days in first week is set to 4
285        cal.setMinimalDaysInFirstWeek(4);
286        cal.set(2016, Calendar.JANUARY, 10);
287        assertEquals(52, cal.getWeeksInWeekYear());
288    }
289
290    public void test_fromZonedDateTime() {
291        ZonedDateTime zdt = ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]");
292        GregorianCalendar calendar = GregorianCalendar.from(zdt);
293        TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
294        assertEquals(timeZone, calendar.getTimeZone());
295        assertEquals(2007, calendar.get(Calendar.YEAR));
296        assertEquals(Calendar.DECEMBER, calendar.get(Calendar.MONTH));
297        assertEquals(3, calendar.get(Calendar.DAY_OF_MONTH));
298        assertEquals(10, calendar.get(Calendar.HOUR_OF_DAY));
299        assertEquals(15, calendar.get(Calendar.MINUTE));
300        assertEquals(3600 * 1000, calendar.getTimeZone().getRawOffset()); // in milliseconds
301    }
302
303    public void test_fromZonedDateTime_invalidValues() {
304        ZoneId gmt = ZoneId.of("GMT");
305        ZonedDateTime[] invalidValues = {
306                ZonedDateTime.of(LocalDateTime.MAX, gmt),
307                ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.MAX_VALUE).plusMillis(1), gmt),
308                ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.MIN_VALUE).minusMillis(1), gmt),
309                ZonedDateTime.of(LocalDateTime.MAX, gmt) };
310        for (ZonedDateTime invalidValue : invalidValues) {
311            try {
312                GregorianCalendar.from(invalidValue);
313                fail("GregorianCalendar.from() should have failed with " + invalidValue);
314            } catch (IllegalArgumentException expected) {}
315        }
316    }
317
318    public void test_toZonedDateTime() {
319        TimeZone timeZone = TimeZone.getTimeZone("Europe/Paris");
320        GregorianCalendar calendar = new GregorianCalendar(timeZone);
321        calendar.set(2007, Calendar.DECEMBER, 3, 10, 15, 30);
322        calendar.set(Calendar.MILLISECOND, 0);
323        ZonedDateTime zdt = calendar.toZonedDateTime();
324        assertEquals(ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), zdt);
325    }
326
327    private long getDstLosAngeles2014(TimeZone timeZone) {
328        GregorianCalendar cal = new GregorianCalendar(timeZone, Locale.ENGLISH);
329        cal.set(Calendar.MILLISECOND, 0);
330        cal.set(2014, Calendar.MARCH, 9, 2, 0, 0);
331
332        return cal.getTimeInMillis();
333    }
334
335    private void checkDstLosAngeles2014(TimeZone timeZone) {
336        Calendar cal = new GregorianCalendar(timeZone, Locale.ENGLISH);
337        // Clear the milliseconds field.
338        cal.set(Calendar.MILLISECOND, 0);
339
340        String description;
341
342        // Check milliseconds one second before the transition.
343        description = "01:59:59 - March 9th 2014";
344        cal.set(2014, Calendar.MARCH, 9, 1, 59, 59);
345        checkMillis(cal, description, 1394359199000L);
346
347        // Outside DST time.
348        checkOutsideDst(cal, description);
349
350        // Check milliseconds at the transition point but using an invalid wall clock
351        // (02:00 - 02:59:59.999) do not actually exist.
352        description = "02:00:00 - March 9th 2014";
353        cal.set(2014, Calendar.MARCH, 9, 2, 0, 0);
354
355        // OpenJDK Failure:
356        //   This fails on OpenJDK when running with SimpleTimeZone (or any custom TimeZone
357        //   implementation). It incorrectly calculates the time in millis to be 1394355600000.
358        //   That is because GregorianCalendar treats the implementation that underpins
359        //   TimeZone.getTimeZone(String) specially and the code that runs for other classes does
360        //   not handle the invalid wall clock period on entry to DST properly.
361        checkMillis(cal, description, 1394359200000L);
362
363        // Invalid wall clock but treated as being inside DST time.
364        checkInsideDst(cal, description);
365
366        // Check milliseconds at the first valid wall clock time after transition, 03:00 - should
367        // be treated the same as 02:00.
368        description = "03:00:00 - March 9th 2014";
369        cal.set(2014, Calendar.MARCH, 9, 3, 0, 0);
370        checkMillis(cal, description, 1394359200000L);
371
372        // Valid wall clock treated as being inside DST time.
373        checkInsideDst(cal, description);
374
375        // Check milliseconds at the last invalid wall clock time, 02:59:59.999.
376        description = "02:59:59.999 - March 9th 2014";
377        cal.set(2014, Calendar.MARCH, 9, 2, 59, 59);
378        cal.set(Calendar.MILLISECOND, 999);
379        checkMillis(cal, description, 1394362799999L);
380
381        // Invalid wall clock but treated as being inside DST time.
382        checkInsideDst(cal, description);
383
384        // Check milliseconds at 03:59:59.999 - should be treated the same as 02:59:59.999
385        description = "03:59:59.999 - March 9th 2014";
386        cal.set(2014, Calendar.MARCH, 9, 3, 59, 59);
387        cal.set(Calendar.MILLISECOND, 999);
388        checkMillis(cal, description, 1394362799999L);
389
390        // Valid wall clock treated as being inside DST time.
391        checkInsideDst(cal, description);
392    }
393
394    private void checkMillis(Calendar cal, String description, long expectedMillis) {
395        assertEquals("Incorrect millis: " + description, expectedMillis, cal.getTimeInMillis());
396    }
397
398    private void checkOutsideDst(Calendar cal, String description) {
399        TimeZone timeZone = cal.getTimeZone();
400        checkOutsideDst(cal, description, timeZone.getRawOffset());
401    }
402
403    private void checkOutsideDst(Calendar cal, String description, int expectedZoneOffset) {
404        checkDstFields(cal, description, expectedZoneOffset, 0);
405    }
406
407    private void checkInsideDst(Calendar cal, String description) {
408        TimeZone timeZone = cal.getTimeZone();
409        checkDstFields(cal, description, timeZone.getRawOffset(), timeZone.getDSTSavings());
410    }
411
412    private void checkDstFields(Calendar cal, String description, int expectedZoneOffset, int expectedDstOffset) {
413        assertEquals("Incorrect ZONE_OFFSET: " + description, expectedZoneOffset, cal.get(Calendar.ZONE_OFFSET));
414        assertEquals("Incorrect DST_OFFSET: " + description, expectedDstOffset, cal.get(Calendar.DST_OFFSET));
415    }
416
417    /**
418     * A custom {@link TimeZone} implementation.
419     *
420     * <p>Used to show the behavior of {@link GregorianCalendar} when provided with a custom
421     * implementation of {@link TimeZone}, i.e. one that is unknown to the runtime,
422     */
423    private static class DelegatingTimeZone extends TimeZone {
424
425        private final TimeZone timeZone;
426
427        public DelegatingTimeZone(TimeZone timeZone) {
428            this.timeZone = timeZone;
429        }
430
431        @Override
432        public int getOffset(int era, int year, int month, int day, int dayOfWeek,
433                int milliseconds) {
434            return timeZone.getOffset(era, year, month, day, dayOfWeek, milliseconds);
435        }
436
437        @Override
438        public int getOffset(long date) {
439            return timeZone.getOffset(date);
440        }
441
442        @Override
443        public void setRawOffset(int offsetMillis) {
444            timeZone.setRawOffset(offsetMillis);
445        }
446
447        @Override
448        public int getRawOffset() {
449            return timeZone.getRawOffset();
450        }
451
452        @Override
453        public String getID() {
454            return timeZone.getID();
455        }
456
457        @Override
458        public void setID(String ID) {
459            timeZone.setID(ID);
460        }
461
462        @Override
463        public String getDisplayName(boolean daylightTime, int style, Locale locale) {
464            return timeZone.getDisplayName(daylightTime, style, locale);
465        }
466
467        @Override
468        public int getDSTSavings() {
469            return timeZone.getDSTSavings();
470        }
471
472        @Override
473        public boolean useDaylightTime() {
474            return timeZone.useDaylightTime();
475        }
476
477        @Override
478        public boolean observesDaylightTime() {
479            return timeZone.observesDaylightTime();
480        }
481
482        @Override
483        public boolean inDaylightTime(Date date) {
484            return timeZone.inDaylightTime(date);
485        }
486
487        @Override
488        public boolean hasSameRules(TimeZone other) {
489            return timeZone.hasSameRules(other);
490        }
491    }
492
493    private static int hours(int count) {
494        return HOUR_IN_MILLIS * count;
495    }
496}
497