1 /*
  2  * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /*
 25  * @test
 26  * @summary test Date Format (Round Trip)
 27  * @bug 8008577
 28  * @run main/othervm -Djava.locale.providers=COMPAT,SPI DateFormatRoundTripTest
 29  */
 30 
 31 import java.text.DateFormat;
 32 import java.text.ParseException;
 33 import java.text.SimpleDateFormat;
 34 import java.util.ArrayList;
 35 import java.util.Arrays;
 36 import java.util.Calendar;
 37 import java.util.Date;
 38 import java.util.GregorianCalendar;
 39 import java.util.List;
 40 import java.util.Locale;
 41 import java.util.Random;
 42 import java.util.SimpleTimeZone;
 43 import java.util.TimeZone;
 44 
 45 public class DateFormatRoundTripTest {
 46 
 47     static Random RANDOM = null;
 48 
 49     static final long FIXED_SEED = 3141592653589793238L; // Arbitrary fixed value
 50 
 51     // Useful for turning up subtle bugs: Use -infinite and run while at lunch.
 52     boolean INFINITE = false; // Warning -- makes test run infinite loop!!!
 53 
 54     boolean random = false;
 55 
 56     // Options used to reproduce failures
 57     Locale locale = null;
 58     String pattern = null;
 59     Date initialDate = null;
 60 
 61     Locale[] avail;
 62     TimeZone defaultZone;
 63 
 64     // If SPARSENESS is > 0, we don't run each exhaustive possibility.
 65     // There are 24 total possible tests per each locale.  A SPARSENESS
 66     // of 12 means we run half of them.  A SPARSENESS of 23 means we run
 67     // 1 of them.  SPARSENESS _must_ be in the range 0..23.
 68     static final int SPARSENESS = 18;
 69 
 70     static final int TRIALS = 4;
 71 
 72     static final int DEPTH = 5;
 73 
 74     static SimpleDateFormat refFormat =
 75         new SimpleDateFormat("EEE MMM dd HH:mm:ss.SSS zzz yyyy G");
 76 
 77     public DateFormatRoundTripTest(boolean rand, long seed, boolean infinite,
 78                                    Date date, String pat, Locale loc) {
 79         random = rand;
 80         if (random) {
 81             RANDOM = new Random(seed);
 82         }
 83         INFINITE = infinite;
 84 
 85         initialDate = date;
 86         locale = loc;
 87         pattern = pat;
 88     }
 89 
 90     /**
 91      * Parse a name like "fr_FR" into Locale.of("fr", "FR", "");
 92      */
 93     static Locale createLocale(String name) {
 94         String country = "",
 95                variant = "";
 96         int i;
 97         if ((i = name.indexOf('_')) >= 0) {
 98             country = name.substring(i+1);
 99             name = name.substring(0, i);
100         }
101         if ((i = country.indexOf('_')) >= 0) {
102             variant = country.substring(i+1);
103             country = country.substring(0, i);
104         }
105         return Locale.of(name, country, variant);
106     }
107 
108     public static void main(String[] args) throws Exception {
109         // Command-line parameters
110         Locale loc = null;
111         boolean infinite = false;
112         boolean random = false;
113         long seed = FIXED_SEED;
114         String pat = null;
115         Date date = null;
116 
117         List<String> newArgs = new ArrayList<>();
118         for (int i=0; i<args.length; ++i) {
119             if (args[i].equals("-locale")
120                     && (i+1) < args.length) {
121                 loc = createLocale(args[i+1]);
122                 ++i;
123             } else if (args[i].equals("-date")
124                     && (i+1) < args.length) {
125                 date = new Date(Long.parseLong(args[i+1]));
126                 ++i;
127             } else if (args[i].equals("-pattern")
128                     && (i+1) < args.length) {
129                 pat = args[i+1];
130                 ++i;
131             } else if (args[i].equals("-INFINITE")) {
132                 infinite = true;
133             } else if (args[i].equals("-random")) {
134                 random = true;
135             } else if (args[i].equals("-randomseed")) {
136                 random = true;
137                 seed = System.currentTimeMillis();
138             } else if (args[i].equals("-seed")
139                     && (i+1) < args.length) {
140                 random = true;
141                 seed = Long.parseLong(args[i+1]);
142                 ++i;
143             } else {
144                 newArgs.add(args[i]);
145             }
146         }
147 
148         if (newArgs.size() != args.length) {
149             args = new String[newArgs.size()];
150             newArgs.addAll(Arrays.asList(args));
151         }
152 
153         new DateFormatRoundTripTest(random, seed, infinite, date, pat, loc)
154                 .TestDateFormatRoundTrip();
155     }
156 
157     /**
158      * Print a usage message for this test class.
159      */
160     void usage() {
161         System.out.println(getClass().getName() +
162                            ": [-pattern <pattern>] [-locale <locale>] [-date <ms>] [-INFINITE]");
163         System.out.println(" [-random | -randomseed | -seed <seed>]");
164         System.out.println("* Warning: Some patterns will fail with some locales.");
165         System.out.println("* Do not use -pattern unless you know what you are doing!");
166         System.out.println("When specifying a locale, use a format such as fr_FR.");
167         System.out.println("Use -pattern, -locale, and -date to reproduce a failure.");
168         System.out.println("-random     Random with fixed seed (same data every run).");
169         System.out.println("-randomseed Random with a random seed.");
170         System.out.println("-seed <s>   Random using <s> as seed.");
171     }
172 
173     static private class TestCase {
174         private int[] date;
175         TimeZone zone;
176         FormatFactory ff;
177         boolean timeOnly;
178         private Date _date;
179 
180         TestCase(int[] d, TimeZone z, FormatFactory f, boolean timeOnly) {
181             date = d;
182             zone = z;
183             ff  = f;
184             this.timeOnly = timeOnly;
185         }
186 
187         TestCase(Date d, TimeZone z, FormatFactory f, boolean timeOnly) {
188             date = null;
189             _date = d;
190             zone = z;
191             ff  = f;
192             this.timeOnly = timeOnly;
193         }
194 
195         /**
196          * Create a format for testing.
197          */
198         DateFormat createFormat() {
199             return ff.createFormat();
200         }
201 
202         /**
203          * Return the Date of this test case; must be called with the default
204          * zone set to this TestCase's zone.
205          */
206         @SuppressWarnings("deprecation")
207         Date getDate() {
208             if (_date == null) {
209                 // Date constructor will work right iff we are in the target zone
210                 int h = 0;
211                 int m = 0;
212                 int s = 0;
213                 if (date.length >= 4) {
214                     h = date[3];
215                     if (date.length >= 5) {
216                         m = date[4];
217                         if (date.length >= 6) {
218                             s = date[5];
219                         }
220                     }
221                 }
222                 _date = new Date(date[0] - 1900, date[1] - 1, date[2],
223                                  h, m, s);
224             }
225             return _date;
226         }
227 
228         public String toString() {
229             return String.valueOf(getDate().getTime()) + " " +
230                 refFormat.format(getDate()) + " : " + ff.createFormat().format(getDate());
231         }
232     };
233 
234     private interface FormatFactory {
235         DateFormat createFormat();
236     }
237 
238     TestCase[] TESTS = {
239         // Feb 29 2004 -- ordinary leap day
240         new TestCase(new int[] {2004, 2, 29}, null,
241                      new FormatFactory() { public DateFormat createFormat() {
242                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
243                                                                DateFormat.LONG);
244                      }}, false),
245 
246         // Feb 29 2000 -- century leap day
247         new TestCase(new int[] {2000, 2, 29}, null,
248                      new FormatFactory() { public DateFormat createFormat() {
249                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
250                                                                DateFormat.LONG);
251                      }}, false),
252 
253         // 0:00:00 Jan 1 1999 -- first second of normal year
254         new TestCase(new int[] {1999, 1, 1}, null,
255                      new FormatFactory() { public DateFormat createFormat() {
256                          return DateFormat.getDateTimeInstance();
257                      }}, false),
258 
259         // 23:59:59 Dec 31 1999 -- last second of normal year
260         new TestCase(new int[] {1999, 12, 31, 23, 59, 59}, null,
261                      new FormatFactory() { public DateFormat createFormat() {
262                          return DateFormat.getDateTimeInstance();
263                      }}, false),
264 
265         // 0:00:00 Jan 1 2004 -- first second of leap year
266         new TestCase(new int[] {2004, 1, 1}, null,
267                      new FormatFactory() { public DateFormat createFormat() {
268                          return DateFormat.getDateTimeInstance();
269                      }}, false),
270 
271         // 23:59:59 Dec 31 2004 -- last second of leap year
272         new TestCase(new int[] {2004, 12, 31, 23, 59, 59}, null,
273                      new FormatFactory() { public DateFormat createFormat() {
274                          return DateFormat.getDateTimeInstance();
275                      }}, false),
276 
277         // October 25, 1998 1:59:59 AM PDT -- just before DST cessation
278         new TestCase(new Date(909305999000L), TimeZone.getTimeZone("PST"),
279                      new FormatFactory() { public DateFormat createFormat() {
280                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
281                                                                DateFormat.LONG);
282                      }}, false),
283 
284         // October 25, 1998 1:00:00 AM PST -- just after DST cessation
285         new TestCase(new Date(909306000000L), TimeZone.getTimeZone("PST"),
286                      new FormatFactory() { public DateFormat createFormat() {
287                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
288                                                                DateFormat.LONG);
289                      }}, false),
290 
291         // April 4, 1999 1:59:59 AM PST -- just before DST onset
292         new TestCase(new int[] {1999, 4, 4, 1, 59, 59},
293                      TimeZone.getTimeZone("PST"),
294                      new FormatFactory() { public DateFormat createFormat() {
295                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
296                                                                DateFormat.LONG);
297                      }}, false),
298 
299         // April 4, 1999 3:00:00 AM PDT -- just after DST onset
300         new TestCase(new Date(923220000000L), TimeZone.getTimeZone("PST"),
301                      new FormatFactory() { public DateFormat createFormat() {
302                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
303                                                                DateFormat.LONG);
304                      }}, false),
305 
306         // October 4, 1582 11:59:59 PM PDT -- just before Gregorian change
307         new TestCase(new int[] {1582, 10, 4, 23, 59, 59}, null,
308                      new FormatFactory() { public DateFormat createFormat() {
309                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
310                                                                DateFormat.LONG);
311                      }}, false),
312 
313         // October 15, 1582 12:00:00 AM PDT -- just after Gregorian change
314         new TestCase(new int[] {1582, 10, 15, 0, 0, 0}, null,
315                      new FormatFactory() { public DateFormat createFormat() {
316                          return DateFormat.getDateTimeInstance(DateFormat.LONG,
317                                                                DateFormat.LONG);
318                      }}, false),
319     };
320 
321     public void TestDateFormatRoundTrip() {
322         avail = DateFormat.getAvailableLocales();
323         System.out.println("DateFormat available locales: " + avail.length);
324         System.out.println("Default TimeZone: " +
325               (defaultZone = TimeZone.getDefault()).getID());
326 
327         if (random || initialDate != null) {
328             if (RANDOM == null) {
329                 // Need this for sparse coverage to reduce combinatorial explosion,
330                 // even for non-random looped testing (i.e., with explicit date but
331                 // not pattern or locale).
332                 RANDOM = new Random(FIXED_SEED);
333             }
334             loopedTest();
335         } else {
336             for (int i=0; i<TESTS.length; ++i) {
337                 doTest(TESTS[i]);
338             }
339         }
340     }
341 
342     /**
343      * TimeZone must be set to tc.zone before this method is called.
344      */
345     private void doTestInZone(TestCase tc) {
346         System.out.println(escape(tc.toString()));
347         Locale save = Locale.getDefault();
348         try {
349             if (locale != null) {
350                 Locale.setDefault(locale);
351                 doTest(locale, tc.createFormat(), tc.timeOnly, tc.getDate());
352             } else {
353                 for (int i=0; i<avail.length; ++i) {
354                     Locale.setDefault(avail[i]);
355                     doTest(avail[i], tc.createFormat(), tc.timeOnly, tc.getDate());
356                 }
357             }
358         } finally {
359             Locale.setDefault(save);
360         }
361     }
362 
363     private void doTest(TestCase tc) {
364         if (tc.zone == null) {
365             // Just run in the default zone
366             doTestInZone(tc);
367         } else {
368             try {
369                 TimeZone.setDefault(tc.zone);
370                 doTestInZone(tc);
371             } finally {
372                 TimeZone.setDefault(defaultZone);
373             }
374         }
375     }
376 
377     private void loopedTest() {
378         if (INFINITE) {
379             // Special infinite loop test mode for finding hard to reproduce errors
380             if (locale != null) {
381                 System.out.println("ENTERING INFINITE TEST LOOP, LOCALE " + locale.getDisplayName());
382                 for (;;) doTest(locale);
383             } else {
384                 System.out.println("ENTERING INFINITE TEST LOOP, ALL LOCALES");
385                 for (;;) {
386                     for (int i=0; i<avail.length; ++i) {
387                         doTest(avail[i]);
388                     }
389                 }
390             }
391         }
392         else {
393             if (locale != null) {
394                 doTest(locale);
395             } else {
396                 doTest(Locale.getDefault());
397 
398                 for (int i=0; i<avail.length; ++i) {
399                     doTest(avail[i]);
400                 }
401             }
402         }
403     }
404 
405     void doTest(Locale loc) {
406         if (!INFINITE) System.out.println("Locale: " + loc.getDisplayName());
407 
408         if (pattern != null) {
409             doTest(loc, new SimpleDateFormat(pattern, loc));
410             return;
411         }
412 
413         // Total possibilities = 24
414         //  4 date
415         //  4 time
416         //  16 date-time
417         boolean[] TEST_TABLE = new boolean[24];
418         for (int i=0; i<24; ++i) TEST_TABLE[i] = true;
419 
420         // If we have some sparseness, implement it here.  Sparseness decreases
421         // test time by eliminating some tests, up to 23.
422         if (!INFINITE) {
423             for (int i=0; i<SPARSENESS; ) {
424                 int random = (int)(java.lang.Math.random() * 24);
425                 if (random >= 0 && random < 24 && TEST_TABLE[i]) {
426                     TEST_TABLE[i] = false;
427                     ++i;
428                 }
429             }
430         }
431 
432         int itable = 0;
433         for (int style=DateFormat.FULL; style<=DateFormat.SHORT; ++style) {
434             if (TEST_TABLE[itable++])
435                 doTest(loc, DateFormat.getDateInstance(style, loc));
436         }
437 
438         for (int style=DateFormat.FULL; style<=DateFormat.SHORT; ++style) {
439             if (TEST_TABLE[itable++])
440                 doTest(loc, DateFormat.getTimeInstance(style, loc), true);
441         }
442 
443         for (int dstyle=DateFormat.FULL; dstyle<=DateFormat.SHORT; ++dstyle) {
444             for (int tstyle=DateFormat.FULL; tstyle<=DateFormat.SHORT; ++tstyle) {
445                 if (TEST_TABLE[itable++])
446                     doTest(loc, DateFormat.getDateTimeInstance(dstyle, tstyle, loc));
447             }
448         }
449     }
450 
451     void doTest(Locale loc, DateFormat fmt) { doTest(loc, fmt, false); }
452 
453     void doTest(Locale loc, DateFormat fmt, boolean timeOnly) {
454         doTest(loc, fmt, timeOnly, initialDate != null ? initialDate : generateDate());
455     }
456 
457     void doTest(Locale loc, DateFormat fmt, boolean timeOnly, Date date) {
458         // Skip testing with the JapaneseImperialCalendar which
459         // doesn't support the Gregorian year semantices with 'y'.
460         if (fmt.getCalendar().getClass().getName().equals("java.util.JapaneseImperialCalendar")) {
461             return;
462         }
463 
464         String pat = ((SimpleDateFormat)fmt).toPattern();
465         String deqPat = dequotePattern(pat); // Remove quoted elements
466 
467         boolean hasEra = (deqPat.indexOf("G") != -1);
468         boolean hasZone = (deqPat.indexOf("z") != -1);
469 
470         Calendar cal = fmt.getCalendar();
471 
472         // Because patterns contain incomplete data representing the Date,
473         // we must be careful of how we do the roundtrip.  We start with
474         // a randomly generated Date because they're easier to generate.
475         // From this we get a string.  The string is our real starting point,
476         // because this string should parse the same way all the time.  Note
477         // that it will not necessarily parse back to the original date because
478         // of incompleteness in patterns.  For example, a time-only pattern won't
479         // parse back to the same date.
480 
481         try {
482             for (int i=0; i<TRIALS; ++i) {
483                 Date[] d = new Date[DEPTH];
484                 String[] s = new String[DEPTH];
485                 String error = null;
486 
487                 d[0] = date;
488 
489                 // We go through this loop until we achieve a match or until
490                 // the maximum loop count is reached.  We record the points at
491                 // which the date and the string starts to match.  Once matching
492                 // starts, it should continue.
493                 int loop;
494                 int dmatch = 0; // d[dmatch].getTime() == d[dmatch-1].getTime()
495                 int smatch = 0; // s[smatch].equals(s[smatch-1])
496                 for (loop=0; loop<DEPTH; ++loop) {
497                     if (loop > 0) d[loop] = fmt.parse(s[loop-1]);
498                     s[loop] = fmt.format(d[loop]);
499 
500                     if (loop > 0) {
501                         if (smatch == 0) {
502                             boolean match = s[loop].equals(s[loop-1]);
503                             if (smatch == 0) {
504                                 if (match) smatch = loop;
505                             }
506                             else if (!match) {
507                                 // This should never happen; if it does, fail.
508                                 smatch = -1;
509                                 error = "FAIL: String mismatch after match";
510                             }
511                         }
512 
513                         if (dmatch == 0) {
514                             boolean match = d[loop].getTime() == d[loop-1].getTime();
515                             if (dmatch == 0) {
516                                 if (match) dmatch = loop;
517                             }
518                             else if (!match) {
519                                 // This should never happen; if it does, fail.
520                                 dmatch = -1;
521                                 error = "FAIL: Date mismatch after match";
522                             }
523                         }
524 
525                         if (smatch != 0 && dmatch != 0) break;
526                     }
527                 }
528                 // At this point loop == DEPTH if we've failed, otherwise loop is the
529                 // max(smatch, dmatch), that is, the index at which we have string and
530                 // date matching.
531 
532                 // Date usually matches in 2.  Exceptions handled below.
533                 int maxDmatch = 2;
534                 int maxSmatch = 1;
535                 if (dmatch > maxDmatch) {
536                     // Time-only pattern with zone information and a starting date in PST.
537                     if (timeOnly && hasZone && fmt.getTimeZone().inDaylightTime(d[0])) {
538                         maxDmatch = 3;
539                         maxSmatch = 2;
540                     }
541                 }
542 
543                 // String usually matches in 1.  Exceptions are checked for here.
544                 if (smatch > maxSmatch) { // Don't compute unless necessary
545                     // Starts in BC, with no era in pattern
546                     if (!hasEra && getField(cal, d[0], Calendar.ERA) == GregorianCalendar.BC)
547                         maxSmatch = 2;
548                     // Starts in DST, no year in pattern
549                     else if (fmt.getTimeZone().inDaylightTime(d[0]) &&
550                              deqPat.indexOf("yyyy") == -1)
551                         maxSmatch = 2;
552                     // Two digit year with zone and year change and zone in pattern
553                     else if (hasZone &&
554                              fmt.getTimeZone().inDaylightTime(d[0]) !=
555                              fmt.getTimeZone().inDaylightTime(d[dmatch]) &&
556                              getField(cal, d[0], Calendar.YEAR) !=
557                              getField(cal, d[dmatch], Calendar.YEAR) &&
558                              deqPat.indexOf("y") != -1 &&
559                              deqPat.indexOf("yyyy") == -1)
560                         maxSmatch = 2;
561                     // Two digit year, year change, DST changeover hour.  Example:
562                     //    FAIL: Pattern: dd/MM/yy HH:mm:ss
563                     //     Date matched in 2, wanted 2
564                     //     String matched in 2, wanted 1
565                     //        Thu Apr 02 02:35:52.110 PST 1795 AD F> 02/04/95 02:35:52
566                     //     P> Sun Apr 02 01:35:52.000 PST 1995 AD F> 02/04/95 01:35:52
567                     //     P> Sun Apr 02 01:35:52.000 PST 1995 AD F> 02/04/95 01:35:52 d== s==
568                     // The problem is that the initial time is not a DST onset day, but
569                     // then the year changes, and the resultant parsed time IS a DST
570                     // onset day.  The hour "2:XX" makes no sense if 2:00 is the DST
571                     // onset, so DateFormat interprets it as 1:XX (arbitrary -- could
572                     // also be 3:XX, same problem).  This results in an extra iteration
573                     // for String match convergence.
574                     else if (!justBeforeOnset(cal, d[0]) && justBeforeOnset(cal, d[dmatch]) &&
575                              getField(cal, d[0], Calendar.YEAR) !=
576                              getField(cal, d[dmatch], Calendar.YEAR) &&
577                              deqPat.indexOf("y") != -1 &&
578                              deqPat.indexOf("yyyy") == -1)
579                         maxSmatch = 2;
580                     // Another spurious failure:
581                     // FAIL: Pattern: dd MMMM yyyy hh:mm:ss
582                     //  Date matched in 2, wanted 2
583                     //  String matched in 2, wanted 1
584                     //     Sun Apr 05 14:28:38.410 PDT 3998 AD F> 05 April 3998 02:28:38
585                     //  P> Sun Apr 05 01:28:38.000 PST 3998 AD F> 05 April 3998 01:28:38
586                     //  P> Sun Apr 05 01:28:38.000 PST 3998 AD F> 05 April 3998 01:28:38 d== s==
587                     // The problem here is that with an 'hh' pattern, hour from 1-12,
588                     // a lack of AM/PM -- that is, no 'a' in pattern, and an initial
589                     // time in the onset hour + 12:00.
590                     else if (deqPat.indexOf('h') >= 0
591                              && deqPat.indexOf('a') < 0
592                              && justBeforeOnset(cal, new Date(d[0].getTime() - 12*60*60*1000L))
593                              && justBeforeOnset(cal, d[1]))
594                         maxSmatch = 2;
595                 }
596 
597                 if (dmatch > maxDmatch || smatch > maxSmatch
598                     || dmatch < 0 || smatch < 0) {
599                     StringBuffer out = new StringBuffer();
600                     if (error != null) {
601                         out.append(error + '\n');
602                     }
603                     out.append("FAIL: Pattern: " + pat + ", Locale: " + loc + '\n');
604                     out.append("      Initial date (ms): " + d[0].getTime() + '\n');
605                     out.append("     Date matched in " + dmatch
606                                + ", wanted " + maxDmatch + '\n');
607                     out.append("     String matched in " + smatch
608                                + ", wanted " + maxSmatch);
609 
610                     for (int j=0; j<=loop && j<DEPTH; ++j) {
611                         out.append("\n    " +
612                                    (j>0?" P> ":"    ") + refFormat.format(d[j]) + " F> " +
613                                    escape(s[j]) +
614                                    (j>0&&d[j].getTime()==d[j-1].getTime()?" d==":"") +
615                                    (j>0&&s[j].equals(s[j-1])?" s==":""));
616                     }
617                     throw new RuntimeException(escape(out.toString()));
618                 }
619             }
620         }
621         catch (ParseException e) {
622             throw new RuntimeException(e.toString());
623         }
624     }
625 
626     /**
627      * Return a field of the given date
628      */
629     static int getField(Calendar cal, Date d, int f) {
630         // Should be synchronized, but we're single threaded so it's ok
631         cal.setTime(d);
632         return cal.get(f);
633     }
634 
635     /**
636      * Return true if the given Date is in the 1 hour window BEFORE the
637      * change from STD to DST for the given Calendar.
638      */
639     static final boolean justBeforeOnset(Calendar cal, Date d) {
640         return nearOnset(cal, d, false);
641     }
642 
643     /**
644      * Return true if the given Date is in the 1 hour window AFTER the
645      * change from STD to DST for the given Calendar.
646      */
647     static final boolean justAfterOnset(Calendar cal, Date d) {
648         return nearOnset(cal, d, true);
649     }
650 
651     /**
652      * Return true if the given Date is in the 1 hour (or whatever the
653      * DST savings is) window before or after the onset of DST.
654      */
655     static boolean nearOnset(Calendar cal, Date d, boolean after) {
656         cal.setTime(d);
657         if ((cal.get(Calendar.DST_OFFSET) == 0) == after) {
658             return false;
659         }
660         int delta;
661         try {
662             delta = ((SimpleTimeZone) cal.getTimeZone()).getDSTSavings();
663         } catch (ClassCastException e) {
664             delta = 60*60*1000; // One hour as ms
665         }
666         cal.setTime(new Date(d.getTime() + (after ? -delta : delta)));
667         return (cal.get(Calendar.DST_OFFSET) == 0) == after;
668     }
669 
670     static String escape(String s) {
671         StringBuffer buf = new StringBuffer();
672         for (int i=0; i<s.length(); ++i) {
673             char c = s.charAt(i);
674             if (c < '\u0080') buf.append(c);
675             else {
676                 buf.append("\\u");
677                 if (c < '\u1000') {
678                     buf.append('0');
679                     if (c < '\u0100') {
680                         buf.append('0');
681                         if (c < '\u0010') {
682                             buf.append('0');
683                         }
684                     }
685                 }
686                 buf.append(Integer.toHexString(c));
687             }
688         }
689         return buf.toString();
690     }
691 
692     /**
693      * Remove quoted elements from a pattern.  E.g., change "hh:mm 'o''clock'"
694      * to "hh:mm ?".  All quoted elements are replaced by one or more '?'
695      * characters.
696      */
697     static String dequotePattern(String pat) {
698         StringBuffer out = new StringBuffer();
699         boolean inQuote = false;
700         for (int i=0; i<pat.length(); ++i) {
701             char ch = pat.charAt(i);
702             if (ch == '\'') {
703                 if ((i+1)<pat.length()
704                     && pat.charAt(i+1) == '\'') {
705                     // Handle "''"
706                     out.append('?');
707                     ++i;
708                 } else {
709                     inQuote = !inQuote;
710                     if (inQuote) {
711                         out.append('?');
712                     }
713                 }
714             } else if (!inQuote) {
715                 out.append(ch);
716             }
717         }
718         return out.toString();
719     }
720 
721     static Date generateDate() {
722         double a = (RANDOM.nextLong() & 0x7FFFFFFFFFFFFFFFL ) /
723             ((double)0x7FFFFFFFFFFFFFFFL);
724 
725         // Now 'a' ranges from 0..1; scale it to range from 0 to 8000 years
726         a *= 8000;
727 
728         // Range from (4000-1970) BC to (8000-1970) AD
729         a -= 4000;
730 
731         // Now scale up to ms
732         a *= 365.25 * 24 * 60 * 60 * 1000;
733 
734         return new Date((long)a);
735     }
736 }
737 
738 //eof