1 /*
   2  * Copyright (c) 1997, 2016, 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 new Locale("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 new Locale(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