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
32import java.text.*;
33import java.util.*;
34
35public 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
729