1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.java.text;
18
19import java.text.DateFormat;
20import java.text.DateFormatSymbols;
21import java.text.ParseException;
22import java.text.ParsePosition;
23import java.text.SimpleDateFormat;
24import java.util.Calendar;
25import java.util.Date;
26import java.util.GregorianCalendar;
27import java.util.Locale;
28import java.util.TimeZone;
29
30public class SimpleDateFormatTest extends junit.framework.TestCase {
31
32    private static final TimeZone AMERICA_LOS_ANGELES = TimeZone.getTimeZone("America/Los_Angeles");
33    private static final TimeZone AUSTRALIA_LORD_HOWE = TimeZone.getTimeZone("Australia/Lord_Howe");
34    private static final TimeZone UTC = TimeZone.getTimeZone("Etc/UTC");
35
36    /**
37     * The list of time zone ids formatted as "UTC".
38     */
39    private static final String[] UTC_ZONE_IDS = new String[] {
40            "Etc/UCT", "Etc/UTC", "Etc/Universal", "Etc/Zulu", "UCT", "UTC", "Universal", "Zulu"
41    };
42
43
44    private Locale defaultLocale;
45
46    @Override
47    public void setUp() throws Exception {
48        super.setUp();
49        defaultLocale = Locale.getDefault();
50        // Locale affects timezone names / abbreviations so can affect formatting and parsing.
51        Locale.setDefault(Locale.US);
52    }
53
54    @Override
55    public void tearDown() throws Exception {
56        Locale.setDefault(defaultLocale);
57        super.tearDown();
58    }
59
60    public void testDefaultConstructor_localeUS() {
61        SimpleDateFormat sdf = new SimpleDateFormat();
62        sdf.setTimeZone(UTC);
63        assertEquals("M/d/yy h:mm a", sdf.toPattern());
64        assertEquals("1/1/70 12:00 AM", sdf.format(new Date(0)));
65    }
66
67    // The RI fails this test.
68    public void test2DigitYearStartIsCloned() throws Exception {
69        // Test that get2DigitYearStart returns a clone.
70        SimpleDateFormat sdf = new SimpleDateFormat();
71        sdf.setTimeZone(UTC);
72
73        Date originalDate = sdf.get2DigitYearStart();
74        assertNotSame(sdf.get2DigitYearStart(), originalDate);
75        assertEquals(sdf.get2DigitYearStart(), originalDate);
76        originalDate.setTime(0);
77        assertFalse(sdf.get2DigitYearStart().equals(originalDate));
78        // Test that set2DigitYearStart takes a clone.
79        Date newDate = new Date();
80        sdf.set2DigitYearStart(newDate);
81        assertNotSame(sdf.get2DigitYearStart(), newDate);
82        assertEquals(sdf.get2DigitYearStart(), newDate);
83        newDate.setTime(0);
84        assertFalse(sdf.get2DigitYearStart().equals(newDate));
85    }
86
87    // The RI fails this test because this is an ICU-compatible Android extension.
88    // Necessary for correct localization in various languages (http://b/2633414).
89    public void testStandAloneNames() throws Exception {
90        Locale en = Locale.ENGLISH;
91        Locale pl = new Locale("pl");
92        Locale ru = new Locale("ru");
93
94        assertEquals("January", formatDateUtc(en, "MMMM"));
95        assertEquals("January", formatDateUtc(en, "LLLL"));
96        assertEquals("stycznia", formatDateUtc(pl, "MMMM"));
97        assertEquals("stycze\u0144", formatDateUtc(pl, "LLLL"));
98
99        assertEquals("Thursday", formatDateUtc(en, "EEEE"));
100        assertEquals("Thursday", formatDateUtc(en, "cccc"));
101        assertEquals("\u0447\u0435\u0442\u0432\u0435\u0440\u0433", formatDateUtc(ru, "EEEE"));
102        assertEquals("\u0447\u0435\u0442\u0432\u0435\u0440\u0433", formatDateUtc(ru, "cccc"));
103
104        assertEquals(Calendar.JUNE, parseDateUtc(en, "yyyy-MMMM-dd", "1980-June-12").get(Calendar.MONTH));
105        assertEquals(Calendar.JUNE, parseDateUtc(en, "yyyy-LLLL-dd", "1980-June-12").get(Calendar.MONTH));
106        assertEquals(Calendar.JUNE, parseDateUtc(pl, "yyyy-MMMM-dd", "1980-czerwca-12").get(Calendar.MONTH));
107        assertEquals(Calendar.JUNE, parseDateUtc(pl, "yyyy-LLLL-dd", "1980-czerwiec-12").get(Calendar.MONTH));
108
109        assertEquals(Calendar.TUESDAY, parseDateUtc(en, "EEEE", "Tuesday").get(Calendar.DAY_OF_WEEK));
110        assertEquals(Calendar.TUESDAY, parseDateUtc(en, "cccc", "Tuesday").get(Calendar.DAY_OF_WEEK));
111        assertEquals(Calendar.TUESDAY, parseDateUtc(ru, "EEEE", "\u0432\u0442\u043e\u0440\u043d\u0438\u043a").get(Calendar.DAY_OF_WEEK));
112        assertEquals(Calendar.TUESDAY, parseDateUtc(ru, "cccc", "\u0412\u0442\u043e\u0440\u043d\u0438\u043a").get(Calendar.DAY_OF_WEEK));
113    }
114
115    // The RI fails this test because it doesn't fully support UTS #35.
116    // https://code.google.com/p/android/issues/detail?id=39616
117    public void testFiveCount_parsing() throws Exception {
118      // It's pretty silly to try to parse the shortest names, because they're almost always
119      // ambiguous.
120      assertCannotParse(Locale.ENGLISH, "MMMMM", "J");
121      assertCannotParse(Locale.ENGLISH, "LLLLL", "J");
122      assertCannotParse(Locale.ENGLISH, "EEEEE", "T");
123      assertCannotParse(Locale.ENGLISH, "ccccc", "T");
124    }
125
126    // The RI fails this test because it doesn't fully support UTS #35.
127    // https://code.google.com/p/android/issues/detail?id=39616
128    public void testFiveCount_M() throws Exception {
129      assertEquals("1", formatDateUtc(Locale.ENGLISH, "M"));
130      assertEquals("01", formatDateUtc(Locale.ENGLISH, "MM"));
131      assertEquals("Jan", formatDateUtc(Locale.ENGLISH, "MMM"));
132      assertEquals("January", formatDateUtc(Locale.ENGLISH, "MMMM"));
133      assertEquals("J", formatDateUtc(Locale.ENGLISH, "MMMMM"));
134    }
135
136    // The RI fails this test because it doesn't fully support UTS #35.
137    // https://code.google.com/p/android/issues/detail?id=39616
138    public void testFiveCount_L() throws Exception {
139      assertEquals("1", formatDateUtc(Locale.ENGLISH, "L"));
140      assertEquals("01", formatDateUtc(Locale.ENGLISH, "LL"));
141      assertEquals("Jan", formatDateUtc(Locale.ENGLISH, "LLL"));
142      assertEquals("January", formatDateUtc(Locale.ENGLISH, "LLLL"));
143      assertEquals("J", formatDateUtc(Locale.ENGLISH, "LLLLL"));
144    }
145
146    // The RI fails this test because it doesn't fully support UTS #35.
147    // https://code.google.com/p/android/issues/detail?id=39616
148    public void testFiveCount_E() throws Exception {
149      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "E"));
150      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "EE"));
151      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "EEE"));
152      assertEquals("Thursday", formatDateUtc(Locale.ENGLISH, "EEEE"));
153      assertEquals("T", formatDateUtc(Locale.ENGLISH, "EEEEE"));
154      // assertEquals("Th", formatDate(Locale.ENGLISH, "EEEEEE")); // icu4c doesn't support 6.
155    }
156
157    // The RI fails this test because it doesn't fully support UTS #35.
158    // https://code.google.com/p/android/issues/detail?id=39616
159    public void testFiveCount_c() throws Exception {
160      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "c"));
161      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "cc"));
162      assertEquals("Thu", formatDateUtc(Locale.ENGLISH, "ccc"));
163      assertEquals("Thursday", formatDateUtc(Locale.ENGLISH, "cccc"));
164      assertEquals("T", formatDateUtc(Locale.ENGLISH, "ccccc"));
165      // assertEquals("Th", formatDate(Locale.ENGLISH, "cccccc")); // icu4c doesn't support 6.
166    }
167
168    // The RI fails this test because it doesn't fully support UTS #35.
169    // https://code.google.com/p/android/issues/detail?id=39616
170    public void testFiveCount_Z() throws Exception {
171      assertEquals("+0000", formatDateUtc(Locale.ENGLISH, "Z"));
172      assertEquals("+0000", formatDateUtc(Locale.ENGLISH, "ZZ"));
173      assertEquals("+0000", formatDateUtc(Locale.ENGLISH, "ZZZ"));
174      assertEquals("GMT+00:00", formatDateUtc(Locale.ENGLISH, "ZZZZ"));
175      assertEquals("+00:00", formatDateUtc(Locale.ENGLISH, "ZZZZZ"));
176
177      TimeZone tz = AMERICA_LOS_ANGELES;
178      assertEquals("-0800", formatDate(Locale.ENGLISH, "Z", tz));
179      assertEquals("-0800", formatDate(Locale.ENGLISH, "ZZ", tz));
180      assertEquals("-0800", formatDate(Locale.ENGLISH, "ZZZ", tz));
181      assertEquals("GMT-08:00", formatDate(Locale.ENGLISH, "ZZZZ", tz));
182      assertEquals("-08:00", formatDate(Locale.ENGLISH, "ZZZZZ", tz));
183    }
184
185    // The RI fails this test because it doesn't fully support UTS #35.
186    // https://code.google.com/p/android/issues/detail?id=39616
187    public void test_parsing_Z() throws Exception {
188      assertEquals(1325421240000L, parseTimeUtc("yyyy-MM-dd' 'Z", "2012-01-01 -1234"));
189      assertEquals(1325421240000L, parseTimeUtc("yyyy-MM-dd' 'ZZ", "2012-01-01 -1234"));
190      assertEquals(1325421240000L, parseTimeUtc("yyyy-MM-dd' 'ZZZ", "2012-01-01 -1234"));
191      assertEquals(1325421240000L, parseTimeUtc("yyyy-MM-dd' 'ZZZZ", "2012-01-01 GMT-12:34"));
192      assertEquals(1325421240000L, parseTimeUtc("yyyy-MM-dd' 'ZZZZZ", "2012-01-01 -12:34"));
193    }
194
195    private static long parseTimeUtc(String fmt, String value) {
196      return parseDateUtc(Locale.ENGLISH, fmt, value).getTime().getTime();
197    }
198
199    public void test2038() {
200        SimpleDateFormat format = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US);
201        format.setTimeZone(UTC);
202
203        assertEquals("Sun Nov 24 17:31:44 1833",
204                format.format(new Date(((long) Integer.MIN_VALUE + Integer.MIN_VALUE) * 1000L)));
205        assertEquals("Fri Dec 13 20:45:52 1901",
206                format.format(new Date(Integer.MIN_VALUE * 1000L)));
207        assertEquals("Thu Jan 01 00:00:00 1970",
208                format.format(new Date(0L)));
209        assertEquals("Tue Jan 19 03:14:07 2038",
210                format.format(new Date(Integer.MAX_VALUE * 1000L)));
211        assertEquals("Sun Feb 07 06:28:16 2106",
212                format.format(new Date((2L + Integer.MAX_VALUE + Integer.MAX_VALUE) * 1000L)));
213    }
214
215    private String formatDateUtc(Locale l, String fmt) {
216        return formatDate(l, fmt, UTC);
217    }
218
219    private String formatDate(Locale l, String fmt, TimeZone tz) {
220        DateFormat dateFormat = new SimpleDateFormat(fmt, l);
221        dateFormat.setTimeZone(tz);
222        return dateFormat.format(new Date(0));
223    }
224
225    private static void assertCannotParse(Locale l, String fmt, String value) {
226        SimpleDateFormat sdf = new SimpleDateFormat(fmt, l);
227        sdf.setTimeZone(UTC);
228        ParsePosition pp = new ParsePosition(0);
229        Date d = sdf.parse(value, pp);
230        assertNull("Value " + value + " must not parse in locale " + l + " with format " + fmt, d);
231    }
232
233    /**
234     * Parse a date with a SimpleDateFormat set to use UTC. If fmt contains a pattern for zone the
235     * use of UTC should have no effect, but in other cases it can affect the outcome. The returned
236     * calendar will also be set to UTC.
237     */
238    private static Calendar parseDateUtc(Locale l, String fmt, String value) {
239        return parseDate(l, fmt, value, UTC);
240    }
241
242    private static Calendar parseDate(Locale l, String fmt, String value, TimeZone tz) {
243        SimpleDateFormat sdf = new SimpleDateFormat(fmt, l);
244        sdf.setTimeZone(tz);
245        ParsePosition pp = new ParsePosition(0);
246        Date d = sdf.parse(value, pp);
247        if (d == null) {
248            fail(pp.toString());
249        }
250        if (pp.getIndex() != value.length()) {
251            fail("Value " + value + " must be fully consumed: " +  pp.toString());
252        }
253        Calendar c = Calendar.getInstance(tz);
254        c.setTime(d);
255        return c;
256    }
257
258    // http://code.google.com/p/android/issues/detail?id=13420
259    public void testParsingUncommonTimeZoneAbbreviations() {
260        String fmt = "yyyy-MM-dd HH:mm:ss.SSS z";
261        String date = "2010-12-23 12:44:57.0 CET";
262        // ICU considers "CET" (Central European Time) to be common in Britain...
263        assertEquals(1293104697000L, parseDateUtc(Locale.UK, fmt, date).getTimeInMillis());
264        // ...but not in the US.
265        assertCannotParse(Locale.US, fmt, date);
266    }
267
268    // In Honeycomb, only one Olson id was associated with CET (or any other "uncommon"
269    // abbreviation). This was changed after KitKat to avoid Java hacks on top of ICU data.
270    // ICU data only provides abbreviations for timezones in the locales where they would
271    // not be ambiguous to most people of that locale.
272    public void testFormattingUncommonTimeZoneAbbreviations() {
273        String fmt = "yyyy-MM-dd HH:mm:ss.SSS z";
274        String unambiguousDate = "1970-01-01 01:00:00.000 CET";
275        String ambiguousDate = "1970-01-01 01:00:00.000 GMT+01:00";
276
277        // The locale to use when formatting. Not every Locale renders "Europe/Berlin" as "CET". The
278        // UK is one that does, the US is one that does not.
279        Locale cetUnambiguousLocale = Locale.UK;
280        Locale cetAmbiguousLocale = Locale.US;
281        TimeZone europeBerlin = TimeZone.getTimeZone("Europe/Berlin");
282        TimeZone europeZurich = TimeZone.getTimeZone("Europe/Zurich");
283
284        SimpleDateFormat sdf = new SimpleDateFormat(fmt, cetUnambiguousLocale);
285        sdf.setTimeZone(europeBerlin);
286        assertEquals(unambiguousDate, sdf.format(new Date(0)));
287        sdf = new SimpleDateFormat(fmt, cetUnambiguousLocale);
288        sdf.setTimeZone(europeZurich);
289        assertEquals(unambiguousDate, sdf.format(new Date(0)));
290
291        sdf = new SimpleDateFormat(fmt, cetAmbiguousLocale);
292        sdf.setTimeZone(europeBerlin);
293        assertEquals(ambiguousDate, sdf.format(new Date(0)));
294        sdf = new SimpleDateFormat(fmt, cetAmbiguousLocale);
295        sdf.setTimeZone(europeZurich);
296        assertEquals(ambiguousDate, sdf.format(new Date(0)));
297    }
298
299    // http://code.google.com/p/android/issues/detail?id=8258
300    public void testTimeZoneFormatting() throws Exception {
301        Date epoch = new Date(0);
302
303        // Create a SimpleDateFormat that defaults to America/Chicago...
304        TimeZone americaChicago = TimeZone.getTimeZone("America/Chicago");
305        TimeZone.setDefault(americaChicago);
306        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
307        assertEquals(americaChicago, sdf.getTimeZone());
308
309        // We should see something appropriate to America/Chicago...
310        assertEquals("1969-12-31 18:00:00 -0600", sdf.format(epoch));
311        // We can set any TimeZone we want:
312        sdf.setTimeZone(AMERICA_LOS_ANGELES);
313        assertEquals("1969-12-31 16:00:00 -0800", sdf.format(epoch));
314        sdf.setTimeZone(UTC);
315        assertEquals("1970-01-01 00:00:00 +0000", sdf.format(epoch));
316
317        // A new SimpleDateFormat will default to America/Chicago...
318        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
319        assertEquals(americaChicago, sdf.getTimeZone());
320
321        // ...and parsing an America/Los_Angeles time will *not* change that...
322        sdf.parse("2010-12-03 00:00:00 -0800");
323        assertEquals(americaChicago, sdf.getTimeZone());
324
325        // ...so our time zone here is "America/Chicago":
326        assertEquals("1969-12-31 18:00:00 -0600", sdf.format(epoch));
327        // We can set any TimeZone we want:
328        sdf.setTimeZone(AMERICA_LOS_ANGELES);
329        assertEquals("1969-12-31 16:00:00 -0800", sdf.format(epoch));
330        sdf.setTimeZone(UTC);
331        assertEquals("1970-01-01 00:00:00 +0000", sdf.format(epoch));
332
333        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
334        sdf.setTimeZone(UTC);
335        Date date = sdf.parse("2010-07-08 02:44:48");
336        assertEquals(UTC, sdf.getTimeZone());
337        assertEquals(1278557088000L, date.getTime());
338
339        sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
340        sdf.setTimeZone(AMERICA_LOS_ANGELES);
341        assertEquals("2010-07-07T19:44:48-0700", sdf.format(date));
342        assertEquals(AMERICA_LOS_ANGELES, sdf.getTimeZone());
343        sdf.setTimeZone(UTC);
344        assertEquals("2010-07-08T02:44:48+0000", sdf.format(date));
345        assertEquals(UTC, sdf.getTimeZone());
346    }
347
348    public void testDstZoneNameWithNonDstTimestamp() throws Exception {
349        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm zzzz", Locale.US);
350        Calendar calendar = new GregorianCalendar(AMERICA_LOS_ANGELES);
351        calendar.setTime(format.parse("2011-06-21T10:00 Pacific Standard Time")); // 18:00 GMT-8
352        assertEquals(11, calendar.get(Calendar.HOUR_OF_DAY)); // 18:00 GMT-7
353        assertEquals(0, calendar.get(Calendar.MINUTE));
354    }
355
356    public void testNonDstZoneNameWithDstTimestamp() throws Exception {
357        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm zzzz", Locale.US);
358        Calendar calendar = new GregorianCalendar(AMERICA_LOS_ANGELES);
359        calendar.setTime(format.parse("2010-12-21T10:00 Pacific Daylight Time")); // 17:00 GMT-7
360        assertEquals(9, calendar.get(Calendar.HOUR_OF_DAY)); // 17:00 GMT-8
361        assertEquals(0, calendar.get(Calendar.MINUTE));
362    }
363
364    // http://b/4723412
365    public void testDstZoneWithNonDstTimestampForNonHourDstZone() throws Exception {
366        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm zzzz", Locale.US);
367        Calendar calendar = new GregorianCalendar(AUSTRALIA_LORD_HOWE);
368        calendar.setTime(format.parse("2011-06-21T20:00 Lord Howe Daylight Time")); // 9:00 GMT+11
369        assertEquals(19, calendar.get(Calendar.HOUR_OF_DAY)); // 9:00 GMT+10:30
370        assertEquals(30, calendar.get(Calendar.MINUTE));
371    }
372
373    public void testNonDstZoneWithDstTimestampForNonHourDstZone() throws Exception {
374        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm zzzz", Locale.US);
375        Calendar calendar = new GregorianCalendar(AUSTRALIA_LORD_HOWE);
376        calendar.setTime(format.parse("2010-12-21T19:30 Lord Howe Standard Time")); //9:00 GMT+10:30
377        assertEquals(20, calendar.get(Calendar.HOUR_OF_DAY)); // 9:00 GMT+11:00
378        assertEquals(0, calendar.get(Calendar.MINUTE));
379    }
380
381    public void testLocales() throws Exception {
382        // Just run through them all. Handy as a poor man's benchmark, and a sanity check.
383        for (Locale l : Locale.getAvailableLocales()) {
384            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzzz", l);
385            sdf.format(new Date(0));
386        }
387    }
388
389    // http://code.google.com/p/android/issues/detail?id=14963
390    public void testParseTimezoneOnly() throws Exception {
391        new SimpleDateFormat("z", Locale.FRANCE).parse("UTC");
392        new SimpleDateFormat("z", Locale.US).parse("UTC");
393    }
394
395    // http://code.google.com/p/android/issues/detail?id=36689
396    public void testParseArabic() throws Exception {
397        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", new Locale("ar", "EG"));
398        sdf.setTimeZone(AMERICA_LOS_ANGELES);
399
400        // Can we parse an ASCII-formatted date in an Arabic locale?
401        Date d = sdf.parse("2012-08-29 10:02:45");
402        assertEquals(1346259765000L, d.getTime());
403
404        // Can we format a date correctly in an Arabic locale?
405        String formatted = sdf.format(d);
406        assertEquals("٢٠١٢-٠٨-٢٩ ١٠:٠٢:٤٥", formatted);
407
408        // Can we parse the Arabic-formatted date in an Arabic locale, and get the same date
409        // we started with?
410        Date d2 = sdf.parse(formatted);
411        assertEquals(d, d2);
412    }
413
414    public void test_59383() throws Exception {
415        SimpleDateFormat sdf = new SimpleDateFormat("d. MMM yyyy H:mm", Locale.GERMAN);
416        sdf.setTimeZone(AMERICA_LOS_ANGELES);
417        assertEquals(1376927400000L, sdf.parse("19. Aug 2013 8:50").getTime());
418        assertEquals(1376927400000L, sdf.parse("19. Aug. 2013 8:50").getTime());
419    }
420
421    // http://b/16969112
422    public void test_fractionalSeconds() throws Exception {
423        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S");
424        sdf.setTimeZone(UTC);
425        assertEquals("1970-01-02 02:17:36.7", sdf.format(sdf.parse("1970-01-02 02:17:36.7")));
426
427        // We only have millisecond precision for Date objects, so we'll lose
428        // information from the fractional seconds section of the string presentation.
429        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSSSS");
430        sdf.setTimeZone(UTC);
431        assertEquals("1970-01-02 02:17:36.789000", sdf.format(sdf.parse("1970-01-02 02:17:36.789564")));
432    }
433
434    public void test_nullLocales() {
435        try {
436            SimpleDateFormat.getDateInstance(DateFormat.SHORT, null);
437            fail();
438        } catch (NullPointerException expected) {}
439
440        try {
441            SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, null);
442            fail();
443        } catch (NullPointerException expected) {}
444
445        try {
446            SimpleDateFormat.getTimeInstance(DateFormat.SHORT, null);
447            fail();
448        } catch (NullPointerException expected) {}
449    }
450
451    // http://b/17431155
452    public void test_sl_dates() throws Exception {
453        DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, new Locale("sl"));
454        assertEquals(TimeZone.getDefault(), df.getTimeZone());
455        df.setTimeZone(UTC);
456        assertEquals("1. 1. 70", df.format(0L));
457    }
458
459    public void testLenientParsingForZ() throws Exception {
460        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
461        Date date = sdf.parse("2016-01-06T23:05:49.480+00:00");
462        Calendar calendar = Calendar.getInstance(UTC);
463        calendar.setTime(date);
464        assertEquals(11, calendar.get(Calendar.HOUR));
465        assertEquals(5, calendar.get(Calendar.MINUTE));
466        assertEquals(49, calendar.get(Calendar.SECOND));
467
468        Date date2 = sdf.parse("2016-01-06T23:05:49.480+00:00");
469        assertEquals(date, date2);
470
471        try {
472            date = sdf.parse("2016-01-06T23:05:49.480+00pissoff");
473            fail();
474        } catch (ParseException expected) {
475        }
476
477        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
478        Date date3 = sdf2.parse("2016-01-06T23:05:49.480+00:00");
479        assertEquals(date, date3);
480        try {
481            sdf2.parse("2016-01-06T23:05:49.480+0000");
482            fail();
483        } catch (ParseException expected) {
484        }
485    }
486
487    // http://b/27760434
488    public void testTimeZoneNotChangedByParse() throws Exception {
489        SimpleDateFormat df = new SimpleDateFormat("dd MMM yyyy HH:mm:ss zzz");
490        df.setTimeZone(UTC);
491        df.parse("22 Jul 1977 12:23:45 HST");
492        assertEquals(UTC, df.getTimeZone());
493    }
494
495    public void testZoneStringsUsedForParsingWhenPresent() throws ParseException {
496        DateFormatSymbols symbols = DateFormatSymbols.getInstance(Locale.ENGLISH);
497        String[][] zoneStrings = symbols.getZoneStrings();
498        TimeZone tz = TimeZone.getTimeZone(zoneStrings[0][0]);
499        zoneStrings[0][1] = "CustomTimeZone";
500        symbols.setZoneStrings(zoneStrings);
501
502        SimpleDateFormat sdf = new SimpleDateFormat("dd MM yyyy HH:mm zzz", symbols);
503
504        Date gmtDate = sdf.parse("1 1 2000 12:00 GMT");
505        Date customDate = sdf.parse("1 1 2000 12:00 CustomTimeZone");
506        assertEquals(tz.getOffset(gmtDate.getTime()), customDate.getTime() - gmtDate.getTime());
507    }
508
509    public void testTimeZoneFormattingRespectsSetZoneStrings() throws ParseException {
510        DateFormatSymbols symbols = DateFormatSymbols.getInstance(Locale.ENGLISH);
511        String[][] zoneStrings = symbols.getZoneStrings();
512        TimeZone tz = TimeZone.getTimeZone(zoneStrings[0][0]);
513        String originalTzName = zoneStrings[0][1];
514        symbols.setZoneStrings(zoneStrings);
515        SimpleDateFormat sdf = new SimpleDateFormat("zzzz", symbols);
516        sdf.setTimeZone(tz);
517
518        // just re-setting the default values
519        assertEquals(originalTzName, sdf.format(new Date(1376927400000L)));
520
521        // providing a custom name
522        zoneStrings[0][1] = "CustomTimeZone";
523        symbols.setZoneStrings(zoneStrings);
524        sdf = new SimpleDateFormat("zzzz", symbols);
525        sdf.setTimeZone(tz);
526        assertEquals("CustomTimeZone", sdf.format(new Date(1376927400000L)));
527
528        // setting the name to null should format as GMT[+-]...
529        zoneStrings[0][1] = null;
530        symbols.setZoneStrings(zoneStrings);
531        sdf = new SimpleDateFormat("zzzz", symbols);
532        sdf.setTimeZone(tz);
533        assertTrue(sdf.format(new Date(1376927400000L)).startsWith("GMT"));
534    }
535
536    // http://b/30323478
537    public void testStandaloneWeekdayParsing() throws Exception {
538        Locale fi = new Locale("fi"); // Finnish has separate standalone weekday names
539        // tiistaina = Tuesday (regular)
540        // tiistai = Tuesday (standalone)
541        assertEquals(Calendar.TUESDAY,
542                parseDateUtc(fi, "cccc yyyy", "tiistai 2000").get(Calendar.DAY_OF_WEEK));
543        assertEquals(Calendar.TUESDAY,
544                parseDateUtc(fi, "EEEE yyyy", "tiistaina 2000").get(Calendar.DAY_OF_WEEK));
545        assertCannotParse(fi, "cccc yyyy", "tiistaina 2000");
546        assertCannotParse(fi, "EEEE yyyy", "tiistai 2000");
547    }
548
549    // http://b/30323478
550    public void testStandaloneWeekdayFormatting() throws Exception {
551        Locale fi = new Locale("fi"); // Finnish has separate standalone weekday names
552        assertEquals("torstai", formatDateUtc(fi, "cccc"));
553        assertEquals("torstaina", formatDateUtc(fi, "EEEE"));
554    }
555
556    public void testDayNumberOfWeek() throws Exception {
557        Locale en = Locale.ENGLISH;
558        Locale pl = new Locale("pl");
559
560        assertEquals("4", formatDateUtc(en, "u"));
561        assertEquals("04", formatDateUtc(en, "uu"));
562        assertEquals("4", formatDateUtc(pl, "u"));
563        assertEquals("04", formatDateUtc(pl, "uu"));
564
565        assertEquals(Calendar.THURSDAY, parseDateUtc(en, "u", "4").get(Calendar.DAY_OF_WEEK));
566        assertEquals(Calendar.MONDAY, parseDateUtc(en, "uu", "1").get(Calendar.DAY_OF_WEEK));
567    }
568
569    // Tests that Android's SimpleDateFormat provides localized short strings for UTC
570    // (http://b/36337342) i.e. it does not fall back to "GMT" or "GMT+00:00".
571    public void testFormatUtcShort() {
572        String timeZonePattern = "z";
573        int timeZoneStyle = TimeZone.SHORT;
574
575        doTestFormat(Locale.ENGLISH, timeZoneStyle, timeZonePattern, "UTC");
576        doTestFormat(Locale.FRANCE, timeZoneStyle, timeZonePattern, "UTC");
577        doTestFormat(Locale.SIMPLIFIED_CHINESE, timeZoneStyle, timeZonePattern, "UTC");
578    }
579
580    // Tests that Android's SimpleDateFormat provides localized long strings for UTC
581    // (http://b/36337342)
582    public void testFormatUtcLong() {
583        String timeZonePattern = "zzzz";
584        int timeZoneStyle = TimeZone.LONG;
585        doTestFormat(Locale.ENGLISH, timeZoneStyle, timeZonePattern, "Coordinated Universal Time");
586        doTestFormat(Locale.FRANCE, timeZoneStyle, timeZonePattern, "Temps universel coordonné");
587        doTestFormat(Locale.SIMPLIFIED_CHINESE, timeZoneStyle, timeZonePattern, "协调世界时");
588    }
589
590    private static void doTestFormat(Locale locale, int timeZoneStyle, String timeZonePattern,
591            String expectedString) {
592        DateFormat dateFormat = new SimpleDateFormat(timeZonePattern, locale);
593        for (String timeZoneId : UTC_ZONE_IDS) {
594            TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
595
596            // Confirm the time zone ID was recognized and we didn't just get "GMT".
597            assertEquals(timeZoneId, timeZone.getID());
598
599            dateFormat.setTimeZone(timeZone);
600            String timeZoneString = dateFormat.format(new Date(0));
601            assertEquals(timeZone.getDisplayName(
602                    false /* daylight */, timeZoneStyle, locale), timeZoneString);
603
604            assertEquals(expectedString, timeZoneString);
605        }
606    }
607
608    // Tests that Android's SimpleDateFormat can parse localized short strings for UTC
609    // (http://b/36337342)
610    public void testParseUtcShort() throws Exception {
611        String timeZonePattern = "z";
612        int timeZoneStyle = TimeZone.SHORT;
613        doUtcParsingTest(Locale.ENGLISH, timeZonePattern, timeZoneStyle, "UTC");
614        doUtcParsingTest(Locale.FRENCH, timeZonePattern, timeZoneStyle, "UTC");
615        doUtcParsingTest(Locale.SIMPLIFIED_CHINESE, timeZonePattern, timeZoneStyle, "UTC");
616    }
617
618    // Tests that Android's SimpleDateFormat can parse localized long strings for UTC
619    // (http://b/36337342)
620    public void testParseUtcLong() throws Exception {
621        String timeZonePattern = "zzzz";
622        int timeZoneStyle = TimeZone.LONG;
623        doUtcParsingTest(Locale.ENGLISH, timeZonePattern, timeZoneStyle,
624                "Coordinated Universal Time");
625        doUtcParsingTest(Locale.FRENCH, timeZonePattern, timeZoneStyle,
626                "Temps universel coordonné");
627        doUtcParsingTest(Locale.SIMPLIFIED_CHINESE, timeZonePattern, timeZoneStyle,
628                "协调世界时");
629    }
630
631    private static void doUtcParsingTest(Locale locale, String timeZonePattern, int timeZoneStyle,
632            String timeZoneString) throws Exception {
633        String basePattern = "yyyyMMdd HH:mm:ss.SSS";
634        String fullPattern = basePattern + " " + timeZonePattern;
635
636        TimeZone nonUtcZone = TimeZone.getTimeZone("America/Los_Angeles");
637
638        DateFormat formatter = new SimpleDateFormat(basePattern, locale);
639        DateFormat parser = new SimpleDateFormat(fullPattern, locale);
640
641        for (String timeZoneId : UTC_ZONE_IDS) {
642            TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
643
644            // Confirm the time zone ID was recognized and we didn't just get "GMT".
645            assertEquals(timeZoneId, timeZone.getID());
646
647            assertEquals(timeZoneString,
648                    timeZone.getDisplayName(false /* daylight */, timeZoneStyle, locale));
649
650            // Format an arbitrary instant in the chosen time zone. We should get something like
651            // "20180126 13:23:34.456".
652            Date dateToFormat = new Date();
653
654            formatter.setTimeZone(timeZone);
655            String dateTimeString = formatter.format(dateToFormat);
656
657            // Append the time zone. e.g. "20180126 13:23:34.456 Coordinated Universal Time".
658            String dateTimeStringWithTimeZone = dateTimeString + " " + timeZoneString;
659
660            // Androidism: The formatter always resets the time zone of the formatter after parsing
661            // but we set it here to make it very clear the parser must be using a non-UTC time
662            // zone by default even though the string provides all the time zone information.
663            parser.setTimeZone(nonUtcZone);
664
665            // Parse the date with time zone back, which should be interpreted as being in UTC.
666            Date parsedDate = parser.parse(dateTimeStringWithTimeZone);
667
668            // The original instant should be returned, which means the formatter / parser were able
669            // to understand the time zone in the string.
670            assertEquals(dateToFormat, parsedDate);
671        }
672    }
673
674    // http://b/35134326
675    public void testTimeZoneParsingErrorIndex() {
676        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy z", Locale.ENGLISH);
677
678        checkTimeZoneParsingErrorIndex(dateFormat);
679    }
680
681    // http://b/35134326
682    public void testTimeZoneParsingErrorIndexWithZoneStrings() {
683        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy z", Locale.ENGLISH);
684        // Force legacy code path by using zone strings.
685        DateFormatSymbols dfs = dateFormat.getDateFormatSymbols();
686        dfs.setZoneStrings(dfs.getZoneStrings());
687        dateFormat.setDateFormatSymbols(dfs);
688
689        checkTimeZoneParsingErrorIndex(dateFormat);
690    }
691
692    // Tests that 'b' and 'B' pattern symbols are silently ignored so that CLDR 32 patterns
693    // can be used. http://b/68139386
694    public void testDayPeriodFormat() throws Exception {
695        SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
696        isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
697        Date date = isoFormat.parse("2017-01-01T08:00:00");
698
699        for (Locale locale : new Locale[] { Locale.US, Locale.FRANCE }) {
700            // Pattern letter 'b'
701            assertDayPeriodFormat("HHb", date, "08", locale);
702            assertDayPeriodFormat("HHbb", date, "08", locale);
703            assertDayPeriodFormat("HHbbb", date, "08", locale);
704            assertDayPeriodFormat("HHbbbb", date, "08", locale);
705            assertDayPeriodFormat("HHbbbbb", date, "08", locale);
706
707            // Pattern letter 'B'
708            assertDayPeriodFormat("HHB", date, "08", locale);
709            assertDayPeriodFormat("HHBB", date, "08", locale);
710            assertDayPeriodFormat("HHBBB", date, "08", locale);
711            assertDayPeriodFormat("HHBBBB", date, "08", locale);
712            assertDayPeriodFormat("HHBBBBB", date, "08", locale);
713        }
714    }
715
716    // Tests that SimpleDateFormat with 'b' and 'B' pattern symbols can't parse any date
717    public void testDayPeriodParse() {
718        assertDayPeriodParseFailure("b", "");
719        assertDayPeriodParseFailure("HHb", "1");
720        assertDayPeriodParseFailure("HHb", "12");
721        assertDayPeriodParseFailure("HH b", "12 AM");
722        assertDayPeriodParseFailure("HH b", "12 midnight");
723
724        assertDayPeriodParseFailure("B", "");
725        assertDayPeriodParseFailure("HHB", "8");
726        assertDayPeriodParseFailure("HHB", "08");
727        assertDayPeriodParseFailure("HH B", "08 AM");
728        assertDayPeriodParseFailure("HH B", "08 in the morning");
729    }
730
731    private void assertDayPeriodParseFailure(String pattern, String source) {
732        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, Locale.US);
733        ParsePosition parsePosition = new ParsePosition(0);
734        Date d = simpleDateFormat.parse(source, parsePosition);
735        assertNull(d);
736        assertEquals(0, parsePosition.getIndex());
737    }
738
739    private void assertDayPeriodFormat(String pattern, Date date, String expected, Locale locale) {
740        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern, locale);
741        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
742        assertEquals(expected, simpleDateFormat.format(date));
743    }
744
745    private void checkTimeZoneParsingErrorIndex(SimpleDateFormat dateFormat) {
746        ParsePosition pos = new ParsePosition(0);
747        Date parsed;
748        parsed = dateFormat.parse("2000 foobar", pos);
749        assertNull(parsed);
750        assertEquals("Wrong error index", 5, pos.getErrorIndex());
751    }
752
753    // http://b/38396219
754    public void testDisplayNamesOnNonGregorianCalendar() {
755        assertEquals("Jan", formatDateNonGregorianCalendar("MMM")); // MONTH
756        assertEquals("Jan", formatDateNonGregorianCalendar("LLL")); // MONTH_STANDALONE
757        assertEquals("Thu", formatDateNonGregorianCalendar("EEE")); // DAY_OF_WEEK
758        assertEquals("Thu", formatDateNonGregorianCalendar("ccc")); // STANDALONE_DAY_OF_WEEK
759    }
760
761    /**
762     * Format a date using a "non-gregorian" calendar. This means that we use a calendar that is not
763     * exactly {@code java.util.GregorianCalendar} as checked by
764     * {@link SimpleDateFormat#isGregorianCalendar()}.
765     */
766    private static String formatDateNonGregorianCalendar(String fmt) {
767        DateFormat dateFormat = new SimpleDateFormat(fmt, Locale.US);
768        NonGregorianCalendar cal = new NonGregorianCalendar();
769        cal.clear();
770        cal.setTimeZone(UTC);
771        dateFormat.setCalendar(cal);
772        return dateFormat.format(new Date(0));
773    }
774
775    /**
776     * Calendar that pretends that it's not a GregorianCalendar, for {@link
777     * #testDisplayNamesOnNonGregorianCalendar()}.
778     */
779    private static class NonGregorianCalendar extends GregorianCalendar {
780    }
781}
782