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