View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNotSame;
23  import static org.junit.jupiter.api.Assertions.assertSame;
24  import static org.junit.jupiter.api.Assertions.fail;
25  
26  import java.text.FieldPosition;
27  import java.text.Format;
28  import java.text.ParseException;
29  import java.text.ParsePosition;
30  import java.text.SimpleDateFormat;
31  import java.time.Instant;
32  import java.time.LocalDate;
33  import java.time.ZoneId;
34  import java.time.format.DateTimeFormatter;
35  import java.util.Calendar;
36  import java.util.Date;
37  import java.util.Locale;
38  import java.util.SimpleTimeZone;
39  import java.util.TimeZone;
40  import java.util.concurrent.ExecutorService;
41  import java.util.concurrent.Executors;
42  import java.util.concurrent.TimeUnit;
43  import java.util.concurrent.atomic.AtomicInteger;
44  import java.util.concurrent.atomic.AtomicLongArray;
45  
46  import org.apache.commons.lang3.AbstractLangTest;
47  import org.junit.jupiter.api.Test;
48  import org.junitpioneer.jupiter.DefaultLocale;
49  import org.junitpioneer.jupiter.DefaultTimeZone;
50  import org.junitpioneer.jupiter.ReadsDefaultLocale;
51  import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
52  
53  /**
54   * Tests {@link FastDateFormat}.
55   */
56  class FastDateFormatTest extends AbstractLangTest {
57  
58      private static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZZ";
59  
60      private static final int NTHREADS = 10;
61  
62      private static final int NROUNDS = 10000;
63  
64      final Locale FINNISH = Locale.forLanguageTag("fi");
65      final Locale HUNGARIAN = Locale.forLanguageTag("hu");
66  
67      private void assertCalendar(final TimeZone expectedTimeZone, final Date expectedDate, final Calendar actual) {
68          assertSame(expectedTimeZone, actual.getTimeZone());
69          // Calendar.getTime() returns a new Date in Java 8.
70          assertEquals(expectedDate, actual.getTime());
71      }
72  
73      private AtomicLongArray measureTime(final Format printer, final Format parser) throws InterruptedException {
74          final ExecutorService pool = Executors.newFixedThreadPool(NTHREADS);
75          final AtomicInteger failures = new AtomicInteger();
76          final AtomicLongArray totalElapsed = new AtomicLongArray(2);
77          try {
78              for (int i = 0; i < NTHREADS; ++i) {
79                  pool.submit(() -> {
80                      for (int j = 0; j < NROUNDS; ++j) {
81                          try {
82                              final Date date = new Date();
83  
84                              final long t0Millis = System.currentTimeMillis();
85                              final String formattedDate = printer.format(date);
86                              totalElapsed.addAndGet(0, System.currentTimeMillis() - t0Millis);
87  
88                              final long t1Millis = System.currentTimeMillis();
89                              final Object pd = parser.parseObject(formattedDate);
90                              totalElapsed.addAndGet(1, System.currentTimeMillis() - t1Millis);
91  
92                              if (!date.equals(pd)) {
93                                  failures.incrementAndGet();
94                              }
95                          } catch (final Exception e) {
96                              failures.incrementAndGet();
97                              e.printStackTrace();
98                          }
99                      }
100                 });
101             }
102         } finally {
103             pool.shutdown();
104             // depending on the performance of the machine used to run the parsing,
105             // the tests can run for a while. It should however complete within
106             // 30 seconds. Might need increase on very slow machines.
107             if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
108                 pool.shutdownNow();
109                 fail("did not complete tasks");
110             }
111         }
112         assertEquals(0, failures.get());
113         return totalElapsed;
114     }
115 
116     @DefaultLocale(language = "en", country = "US")
117     @Test
118     void test_changeDefault_Locale_DateInstance() {
119         final FastDateFormat format1 = FastDateFormat.getDateInstance(FastDateFormat.FULL, Locale.GERMANY);
120         final FastDateFormat format2 = FastDateFormat.getDateInstance(FastDateFormat.FULL);
121         Locale.setDefault(Locale.GERMANY);
122         final FastDateFormat format3 = FastDateFormat.getDateInstance(FastDateFormat.FULL);
123 
124         assertSame(Locale.GERMANY, format1.getLocale());
125         assertEquals(Locale.US, format2.getLocale());
126         assertSame(Locale.GERMANY, format3.getLocale());
127         assertNotSame(format1, format2);
128         assertNotSame(format2, format3);
129     }
130 
131     @DefaultLocale(language = "en", country = "US")
132     @Test
133     void test_changeDefault_Locale_DateTimeInstance() {
134         final FastDateFormat format1 = FastDateFormat.getDateTimeInstance(FastDateFormat.FULL, FastDateFormat.FULL, Locale.GERMANY);
135         final FastDateFormat format2 = FastDateFormat.getDateTimeInstance(FastDateFormat.FULL, FastDateFormat.FULL);
136         Locale.setDefault(Locale.GERMANY);
137         final FastDateFormat format3 = FastDateFormat.getDateTimeInstance(FastDateFormat.FULL, FastDateFormat.FULL);
138 
139         assertSame(Locale.GERMANY, format1.getLocale());
140         assertEquals(Locale.US, format2.getLocale());
141         assertSame(Locale.GERMANY, format3.getLocale());
142         assertNotSame(format1, format2);
143         assertNotSame(format2, format3);
144     }
145 
146     /*
147      * Only the cache methods need to be tested here.
148      * The print methods are tested by {@link FastDateFormat_PrinterTest}
149      * and the parse methods are tested by {@link FastDateFormat_ParserTest}
150      */
151     @Test
152     void test_getInstance() {
153         final FastDateFormat format1 = FastDateFormat.getInstance();
154         final FastDateFormat format2 = FastDateFormat.getInstance();
155         assertSame(format1, format2);
156     }
157 
158     @Test
159     void test_getInstance_String() {
160         final FastDateFormat format1 = FastDateFormat.getInstance("MM/DD/yyyy");
161         final FastDateFormat format2 = FastDateFormat.getInstance("MM-DD-yyyy");
162         final FastDateFormat format3 = FastDateFormat.getInstance("MM-DD-yyyy");
163         assertNotSame(format1, format2);
164         assertSame(format2, format3);
165         assertEquals("MM/DD/yyyy", format1.getPattern());
166         assertEquals(TimeZone.getDefault(), format1.getTimeZone());
167         assertEquals(TimeZone.getDefault(), format2.getTimeZone());
168         assertNotNull(FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZ"));
169     }
170 
171     @DefaultLocale(language = "en", country = "US")
172     @Test
173     void test_getInstance_String_Locale() {
174         final FastDateFormat format1 = FastDateFormat.getInstance("MM/DD/yyyy", Locale.GERMANY);
175         final FastDateFormat format2 = FastDateFormat.getInstance("MM/DD/yyyy");
176         final FastDateFormat format3 = FastDateFormat.getInstance("MM/DD/yyyy", Locale.GERMANY);
177 
178         assertNotSame(format1, format2);
179         assertSame(format1, format3);
180         assertEquals(Locale.GERMANY, format1.getLocale());
181     }
182 
183     @DefaultLocale(language = "en", country = "US")
184     @DefaultTimeZone("America/New_York")
185     @Test
186     void test_getInstance_String_TimeZone() {
187 
188         final FastDateFormat format1 = FastDateFormat.getInstance("MM/DD/yyyy",
189                 TimeZones.getTimeZone("Atlantic/Reykjavik"));
190         final FastDateFormat format2 = FastDateFormat.getInstance("MM/DD/yyyy");
191         final FastDateFormat format3 = FastDateFormat.getInstance("MM/DD/yyyy", TimeZone.getDefault());
192         final FastDateFormat format4 = FastDateFormat.getInstance("MM/DD/yyyy", TimeZone.getDefault());
193         final FastDateFormat format5 = FastDateFormat.getInstance("MM-DD-yyyy", TimeZone.getDefault());
194         final FastDateFormat format6 = FastDateFormat.getInstance("MM-DD-yyyy");
195 
196         assertNotSame(format1, format2);
197         assertEquals(TimeZones.getTimeZone("Atlantic/Reykjavik"), format1.getTimeZone());
198         assertEquals(TimeZone.getDefault(), format2.getTimeZone());
199         assertSame(format3, format4);
200         assertNotSame(format3, format5);
201         assertNotSame(format4, format6);
202     }
203 
204     @DefaultLocale(language = "en", country = "US")
205     @DefaultTimeZone("America/New_York")
206     @Test
207     void test_getInstance_String_TimeZone_Locale() {
208         final FastDateFormat format1 = FastDateFormat.getInstance("MM/DD/yyyy",
209                 TimeZones.getTimeZone("Atlantic/Reykjavik"), Locale.GERMANY);
210         final FastDateFormat format2 = FastDateFormat.getInstance("MM/DD/yyyy", Locale.GERMANY);
211         final FastDateFormat format3 = FastDateFormat.getInstance("MM/DD/yyyy",
212                 TimeZone.getDefault(), Locale.GERMANY);
213 
214         assertNotSame(format1, format2);
215         assertEquals(TimeZones.getTimeZone("Atlantic/Reykjavik"), format1.getTimeZone());
216         assertEquals(TimeZone.getDefault(), format2.getTimeZone());
217         assertEquals(TimeZone.getDefault(), format3.getTimeZone());
218         assertEquals(Locale.GERMANY, format1.getLocale());
219         assertEquals(Locale.GERMANY, format2.getLocale());
220         assertEquals(Locale.GERMANY, format3.getLocale());
221     }
222 
223     /**
224      * The instance cache is bounded: once it reaches its size limit, it is flushed rather than
225      * growing without bound per distinct pattern.
226      */
227     @Test
228     void test_instanceCacheIsBounded() {
229         FastDateFormat.clear();
230         final FastDateFormat first = FastDateFormat.getInstance("yyyy-MM-dd");
231         assertSame(first, FastDateFormat.getInstance("yyyy-MM-dd"));
232         // Exceed the cache bound with distinct patterns; the cache must flush, not grow forever.
233         for (int i = 0; i <= AbstractFormatCache.MAX_CACHE_SIZE; i++) {
234             FastDateFormat.getInstance("'p" + i + "'yyyy");
235         }
236         assertNotSame(first, FastDateFormat.getInstance("yyyy-MM-dd"), "Cache was not flushed at its bound");
237         FastDateFormat.clear();
238     }
239 
240     /**
241      * Tests the public cache-flush entry point.
242      */
243     @Test
244     void test_publicClearCache() {
245         final FastDateFormat format1 = FastDateFormat.getInstance("yyyy-MM-dd'T'HH");
246         assertSame(format1, FastDateFormat.getInstance("yyyy-MM-dd'T'HH"));
247         FastDateFormat.clear();
248         final FastDateFormat format2 = FastDateFormat.getInstance("yyyy-MM-dd'T'HH");
249         assertNotSame(format1, format2);
250         assertEquals(format1, format2);
251     }
252 
253     @Test
254     @ReadsDefaultLocale
255     @ReadsDefaultTimeZone
256     void testCheckDefaults() {
257         final FastDateFormat format = FastDateFormat.getInstance();
258         final FastDateFormat medium = FastDateFormat.getDateTimeInstance(FastDateFormat.SHORT, FastDateFormat.SHORT);
259         assertEquals(medium, format);
260         final SimpleDateFormat sdf = new SimpleDateFormat();
261         assertEquals(sdf.toPattern(), format.getPattern());
262         assertEquals(Locale.getDefault(), format.getLocale());
263         assertEquals(TimeZone.getDefault(), format.getTimeZone());
264     }
265 
266     @Test
267     void testCheckDifferingStyles() {
268         final FastDateFormat shortShort = FastDateFormat.getDateTimeInstance(FastDateFormat.SHORT, FastDateFormat.SHORT, Locale.US);
269         final FastDateFormat shortLong = FastDateFormat.getDateTimeInstance(FastDateFormat.SHORT, FastDateFormat.LONG, Locale.US);
270         final FastDateFormat longShort = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT, Locale.US);
271         final FastDateFormat longLong = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.LONG, Locale.US);
272 
273         assertNotEquals(shortShort, shortLong);
274         assertNotEquals(shortShort, longShort);
275         assertNotEquals(shortShort, longLong);
276         assertNotEquals(shortLong, longShort);
277         assertNotEquals(shortLong, longLong);
278         assertNotEquals(longShort, longLong);
279     }
280 
281     @Test
282     @ReadsDefaultLocale
283     @ReadsDefaultTimeZone
284     void testDateDefaults() {
285         assertEquals(FastDateFormat.getDateInstance(FastDateFormat.LONG, Locale.CANADA),
286                 FastDateFormat.getDateInstance(FastDateFormat.LONG, TimeZone.getDefault(), Locale.CANADA));
287         assertEquals(FastDateFormat.getDateInstance(FastDateFormat.LONG, TimeZones.getTimeZone("America/New_York")),
288                 FastDateFormat.getDateInstance(FastDateFormat.LONG, TimeZones.getTimeZone("America/New_York"), Locale.getDefault()));
289         assertEquals(FastDateFormat.getDateInstance(FastDateFormat.LONG),
290                 FastDateFormat.getDateInstance(FastDateFormat.LONG, TimeZone.getDefault(), Locale.getDefault()));
291     }
292 
293     /**
294      * Pre-patch: UnsupportedOperationException when formatting a Japanese Imperial
295      * <p>
296      * Calendar with 'Y' (week-year) pattern. Post-patch: falls back to Calendar.YEAR and formats successfully.
297      * </p>
298      * <p>
299      * Also test that a regular Gregorian calendar works fine with YYYY.
300      * </p>
301      */
302     @Test
303     void testGregorianCalendarWeekYearWorks() {
304         final Calendar cal = Calendar.getInstance();
305         final FastDateFormat fdp = FastDateFormat.getInstance("YYYY-MM-dd");
306         assertNotNull(fdp.format(cal), "Formatting Gregorian Calendar with YYYY pattern should always work");
307     }
308 
309     /**
310      * Pre-patch: UnsupportedOperationException when formatting a Japanese Imperial Calendar with 'Y' (week-year) pattern.
311      * <p>
312      * Post-patch: falls back to Calendar.YEAR and formats successfully.
313      * </p>
314      */
315     @Test
316     void testJapaneseImperialCalendarWeekYearDoesNotThrow() {
317         final Locale japaneseImperial = new Locale("ja", "JP", "JP");
318         final Calendar japaneseCal = Calendar.getInstance(japaneseImperial);
319         final FastDateFormat fdp = FastDateFormat.getInstance("YYYY-MM-dd");
320         assertNotNull(fdp.format(japaneseCal), "Formatting JapaneseImperialCalendar with YYYY pattern must not throw UnsupportedOperationException");
321     }
322 
323     @Test
324     void testLang1152() {
325         final TimeZone utc = FastTimeZone.getGmtTimeZone();
326         final Date date = new Date(Long.MAX_VALUE);
327 
328         String dateAsString = FastDateFormat.getInstance("yyyy-MM-dd", utc, Locale.US).format(date);
329         assertEquals("292278994-08-17", dateAsString);
330 
331         dateAsString = FastDateFormat.getInstance("dd/MM/yyyy", utc, Locale.US).format(date);
332         assertEquals("17/08/292278994", dateAsString);
333     }
334     @Test
335     void testLang1267() {
336         FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
337     }
338 
339     @Test
340     void testLang1641() {
341         assertSame(FastDateFormat.getInstance(ISO_8601_DATE_FORMAT), FastDateFormat.getInstance(ISO_8601_DATE_FORMAT));
342         // commons-lang's GMT TimeZone
343         assertSame(FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, FastTimeZone.getGmtTimeZone()),
344                 FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, FastTimeZone.getGmtTimeZone()));
345         // default TimeZone
346         assertSame(FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, TimeZone.getDefault()),
347                 FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, TimeZone.getDefault()));
348         // TimeZones that are identical in every way except ID
349         assertNotSame(FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, TimeZones.getTimeZone("Australia/Broken_Hill")),
350                 FastDateFormat.getInstance(ISO_8601_DATE_FORMAT, TimeZones.getTimeZone("Australia/Yancowinna")));
351     }
352 
353     /**
354      * See LANG-1791 https://issues.apache.org/jira/browse/LANG-1791.
355      */
356     @Test
357     @DefaultTimeZone("America/Toronto")
358     void testLang1791() {
359         final Instant now = Instant.now();
360         final String pattern = "yyyyMMddHH";
361         final FastDateFormat gmtFormatter = FastDateFormat.getInstance(pattern, TimeZones.GMT);
362         final Calendar gmtCal = Calendar.getInstance(TimeZones.GMT);
363         final TimeZone timeZone = gmtCal.getTimeZone();
364         final Date date = gmtCal.getTime();
365         final String gmtString = gmtFormatter.format(gmtCal);
366         // Asserts formatting doesn't edit the given Calendar
367         assertCalendar(timeZone, date, gmtCal);
368         assertEquals(DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.of("GMT")).format(now), gmtString);
369         final FastDateFormat defaultFormatter = FastDateFormat.getInstance(pattern);
370         final String defaultString = defaultFormatter.format(gmtCal);
371         // Asserts formatting doesn't edit the given Calendar
372         assertCalendar(timeZone, date, gmtCal);
373         assertEquals(DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.systemDefault()).format(now), defaultString);
374     }
375 
376     /**
377      * According to LANG-954 (https://issues.apache.org/jira/browse/LANG-954) this is broken in Android 2.1.
378      */
379     @Test
380     void testLang954() {
381         final String pattern = "yyyy-MM-dd'T'";
382         FastDateFormat.getInstance(pattern);
383     }
384 
385     /**
386      * Tests [LANG-1767] FastDateFormat.parse can not recognize "CEST" Timezone.
387      *
388      * @throws ParseException Thrown if an operation in the test fails.
389      */
390     @Test
391     void testParseCentralEuropeanSummerTime() throws ParseException {
392         assertNotNull(FastDateFormat.getInstance("dd.MM.yyyy HH:mm:ss", Locale.GERMANY).parse("26.10.2014 02:00:00"));
393         assertNotNull(FastDateFormat.getInstance("dd.MM.yyyy HH:mm:ss z", Locale.US).parse("26.10.2014 02:00:00 CEST"));
394         assertNotNull(FastDateFormat.getInstance("dd.MM.yyyy HH:mm:ss z", Locale.GERMANY).parse("26.10.2014 02:00:00 MESZ"));
395     }
396 
397     @Test
398     void testParseSync() throws InterruptedException {
399         final String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";
400         final SimpleDateFormat inner = new SimpleDateFormat(pattern);
401         final Format sdf = new Format() {
402             private static final long serialVersionUID = 1L;
403 
404             @Override
405             public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition fieldPosition) {
406                 synchronized (this) {
407                     return inner.format(obj, toAppendTo, fieldPosition);
408                 }
409             }
410 
411             @Override
412             public Object parseObject(final String source, final ParsePosition pos) {
413                 synchronized (this) {
414                     return inner.parseObject(source, pos);
415                 }
416             }
417         };
418         final AtomicLongArray sdfTime = measureTime(sdf, sdf);
419 
420         final Format fdf = FastDateFormat.getInstance(pattern);
421         final AtomicLongArray fdfTime = measureTime(fdf, fdf);
422 
423         // System.out.println(">>FastDateFormatTest: FastDatePrinter:"+fdfTime.get(0)+" SimpleDateFormat:"+sdfTime.get(0));
424         // System.out.println(">>FastDateFormatTest: FastDateParser:"+fdfTime.get(1)+" SimpleDateFormat:"+sdfTime.get(1));
425     }
426 
427     @Test
428     void testStandaloneLongMonthForm() {
429         final TimeZone utc = FastTimeZone.getGmtTimeZone();
430         final Instant testInstant = LocalDate.of(1970, 9, 15).atStartOfDay(ZoneId.of("UTC")).toInstant();
431         final Date date = Date.from(testInstant);
432 
433         String dateAsString = FastDateFormat.getInstance("yyyy-LLLL-dd", utc, Locale.GERMAN).format(date);
434         assertEquals("1970-September-15", dateAsString);
435 
436         dateAsString = FastDateFormat.getInstance("yyyy-LLLL-dd", utc, FINNISH).format(date);
437         assertEquals("1970-syyskuu-15", dateAsString);
438 
439         dateAsString = FastDateFormat.getInstance("yyyy-LLLL-dd", utc, HUNGARIAN).format(date);
440         assertEquals("1970-szeptember-15", dateAsString);
441     }
442 
443     @Test
444     void testStandaloneShortMonthForm() {
445         final TimeZone utc = FastTimeZone.getGmtTimeZone();
446         final Instant testInstant = LocalDate.of(1970, 9, 15).atStartOfDay(ZoneId.of("UTC")).toInstant();
447         final Date date = Date.from(testInstant);
448 
449         String dateAsString = FastDateFormat.getInstance("yyyy-LLL-dd", utc, Locale.GERMAN).format(date);
450         assertEquals("1970-Sep-15", dateAsString);
451 
452         dateAsString = FastDateFormat.getInstance("yyyy-LLL-dd", utc, FINNISH).format(date);
453         assertEquals("1970-syys-15", dateAsString);
454 
455         dateAsString = FastDateFormat.getInstance("yyyy-LLL-dd", utc, HUNGARIAN).format(date);
456         assertEquals("1970-szept.-15", dateAsString);
457     }
458 
459     @Test
460     @ReadsDefaultLocale
461     @ReadsDefaultTimeZone
462     void testTimeDateDefaults() {
463         assertEquals(FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM, Locale.CANADA),
464                 FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM, TimeZone.getDefault(), Locale.CANADA));
465         assertEquals(FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM, TimeZones.getTimeZone("America/New_York")),
466                 FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM, TimeZones.getTimeZone("America/New_York"), Locale.getDefault()));
467         assertEquals(FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM),
468                 FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.MEDIUM, TimeZone.getDefault(), Locale.getDefault()));
469     }
470 
471     @Test
472     @ReadsDefaultLocale
473     @ReadsDefaultTimeZone
474     void testTimeDefaults() {
475         assertEquals(FastDateFormat.getTimeInstance(FastDateFormat.LONG, Locale.CANADA),
476                 FastDateFormat.getTimeInstance(FastDateFormat.LONG, TimeZone.getDefault(), Locale.CANADA));
477         assertEquals(FastDateFormat.getTimeInstance(FastDateFormat.LONG, TimeZones.getTimeZone("America/New_York")),
478                 FastDateFormat.getTimeInstance(FastDateFormat.LONG, TimeZones.getTimeZone("America/New_York"), Locale.getDefault()));
479         assertEquals(FastDateFormat.getTimeInstance(FastDateFormat.LONG),
480                 FastDateFormat.getTimeInstance(FastDateFormat.LONG, TimeZone.getDefault(), Locale.getDefault()));
481     }
482 
483     /**
484      * Mutating the TimeZone passed to the factory must not change the cached, shared instance.
485      */
486     @Test
487     void testTimeZoneArgumentIsCopied() throws ParseException {
488         final TimeZone timeZone = TimeZones.getTimeZone("UTC");
489         final FastDateFormat printer = FastDateFormat.getInstance("yyyy-MM-dd HH:mm Z", timeZone, Locale.US);
490         final FastDateFormat parser = FastDateFormat.getInstance("yyyy-MM-dd HH:mm", timeZone, Locale.US);
491         timeZone.setRawOffset(5 * 3_600_000);
492         assertEquals(TimeZones.getTimeZone("UTC"), printer.getTimeZone());
493         assertEquals("1970-01-01 00:00 +0000", printer.format(new Date(0)));
494         assertEquals(new Date(0), parser.parse("1970-01-01 00:00"));
495     }
496 
497     @Test
498     void testTimeZoneCacheKeyIsCopied() throws ParseException {
499         final SimpleTimeZone timeZone = new SimpleTimeZone(0, "CacheKeyCopy", Calendar.MARCH, 1, 0, 0, Calendar.OCTOBER, 1, 0, 0);
500         timeZone.setStartYear(2000);
501         final TimeZone originalTimeZone = (TimeZone) timeZone.clone();
502         final String pattern = "yyyy-MM-dd HH:mm";
503         final FastDateFormat original = FastDateFormat.getInstance(pattern, timeZone, Locale.US);
504         final Date date = Date.from(Instant.parse("2026-06-01T00:00:00Z"));
505         assertEquals("2026-06-01 01:00", original.format(date));
506 
507         // Changing the DST start year preserves the hash code but changes the zone's rules and equality.
508         timeZone.setStartYear(2100);
509         final FastDateFormat changed = FastDateFormat.getInstance(pattern, (TimeZone) timeZone.clone(), Locale.US);
510         assertNotSame(original, changed);
511         assertSame(original, FastDateFormat.getInstance(pattern, originalTimeZone, Locale.US));
512         assertEquals("2026-06-01 01:00", original.format(date));
513         assertEquals("2026-06-01 00:00", changed.format(date));
514         assertEquals(date, original.parse("2026-06-01 01:00"));
515         assertEquals(date, changed.parse("2026-06-01 00:00"));
516     }
517 
518     /**
519      * Mutating the TimeZone returned by the getter must not change the cached, shared instance.
520      */
521     @Test
522     void testTimeZoneGetterReturnsCopy() throws ParseException {
523         final FastDateFormat printer = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss Z", TimeZones.getTimeZone("UTC"), Locale.US);
524         final FastDateFormat parser = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss", TimeZones.getTimeZone("UTC"), Locale.US);
525         printer.getTimeZone().setRawOffset(5 * 3_600_000);
526         parser.getTimeZone().setRawOffset(5 * 3_600_000);
527         assertEquals(TimeZones.getTimeZone("UTC"), printer.getTimeZone());
528         assertEquals("1970-01-01 00:00:00 +0000", printer.format(new Date(0)));
529         assertEquals(new Date(0), parser.parse("1970-01-01 00:00:00"));
530     }
531 }