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