/*
 * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * @test
 * @summary test Date Format (Round Trip)
 * @bug 8008577 8174269
 * @run junit DateFormatRoundTripTest
 */

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.SimpleTimeZone;
import java.util.TimeZone;

public class DateFormatRoundTripTest {

    static Random RANDOM = null;

    static final long FIXED_SEED = 3141592653589793238L; // Arbitrary fixed value

    // Useful for turning up subtle bugs: Use -infinite and run while at lunch.
    boolean INFINITE = false; // Warning -- makes test run infinite loop!!!

    boolean random = false;

    // Options used to reproduce failures
    Locale locale = null;
    String pattern = null;
    Date initialDate = null;

    Locale[] avail;
    TimeZone defaultZone;

    // If SPARSENESS is > 0, we don't run each exhaustive possibility.
    // There are 24 total possible tests per each locale.  A SPARSENESS
    // of 12 means we run half of them.  A SPARSENESS of 23 means we run
    // 1 of them.  SPARSENESS _must_ be in the range 0..23.
    static final int SPARSENESS = 18;

    static final int TRIALS = 4;

    static final int DEPTH = 5;

    static SimpleDateFormat refFormat =
        new SimpleDateFormat("EEE MMM dd HH:mm:ss.SSS zzz yyyy G");

    public DateFormatRoundTripTest(boolean rand, long seed, boolean infinite,
                                   Date date, String pat, Locale loc) {
        random = rand;
        if (random) {
            RANDOM = new Random(seed);
        }
        INFINITE = infinite;

        initialDate = date;
        locale = loc;
        pattern = pat;
    }

    /**
     * Parse a name like "fr_FR" into Locale.of("fr", "FR", "");
     */
    static Locale createLocale(String name) {
        String country = "",
               variant = "";
        int i;
        if ((i = name.indexOf('_')) >= 0) {
            country = name.substring(i+1);
            name = name.substring(0, i);
        }
        if ((i = country.indexOf('_')) >= 0) {
            variant = country.substring(i+1);
            country = country.substring(0, i);
        }
        return Locale.of(name, country, variant);
    }

    public static void main(String[] args) throws Exception {
        // Command-line parameters
        Locale loc = null;
        boolean infinite = false;
        boolean random = false;
        long seed = FIXED_SEED;
        String pat = null;
        Date date = null;

        List<String> newArgs = new ArrayList<>();
        for (int i=0; i<args.length; ++i) {
            if (args[i].equals("-locale")
                    && (i+1) < args.length) {
                loc = createLocale(args[i+1]);
                ++i;
            } else if (args[i].equals("-date")
                    && (i+1) < args.length) {
                date = new Date(Long.parseLong(args[i+1]));
                ++i;
            } else if (args[i].equals("-pattern")
                    && (i+1) < args.length) {
                pat = args[i+1];
                ++i;
            } else if (args[i].equals("-INFINITE")) {
                infinite = true;
            } else if (args[i].equals("-random")) {
                random = true;
            } else if (args[i].equals("-randomseed")) {
                random = true;
                seed = System.currentTimeMillis();
            } else if (args[i].equals("-seed")
                    && (i+1) < args.length) {
                random = true;
                seed = Long.parseLong(args[i+1]);
                ++i;
            } else {
                newArgs.add(args[i]);
            }
        }

        if (newArgs.size() != args.length) {
            args = new String[newArgs.size()];
            newArgs.addAll(Arrays.asList(args));
        }

        new DateFormatRoundTripTest(random, seed, infinite, date, pat, loc)
                .TestDateFormatRoundTrip();
    }

    /**
     * Print a usage message for this test class.
     */
    void usage() {
        System.out.println(getClass().getName() +
                           ": [-pattern <pattern>] [-locale <locale>] [-date <ms>] [-INFINITE]");
        System.out.println(" [-random | -randomseed | -seed <seed>]");
        System.out.println("* Warning: Some patterns will fail with some locales.");
        System.out.println("* Do not use -pattern unless you know what you are doing!");
        System.out.println("When specifying a locale, use a format such as fr_FR.");
        System.out.println("Use -pattern, -locale, and -date to reproduce a failure.");
        System.out.println("-random     Random with fixed seed (same data every run).");
        System.out.println("-randomseed Random with a random seed.");
        System.out.println("-seed <s>   Random using <s> as seed.");
    }

    static private class TestCase {
        private int[] date;
        TimeZone zone;
        FormatFactory ff;
        boolean timeOnly;
        private Date _date;

        TestCase(int[] d, TimeZone z, FormatFactory f, boolean timeOnly) {
            date = d;
            zone = z;
            ff  = f;
            this.timeOnly = timeOnly;
        }

        TestCase(Date d, TimeZone z, FormatFactory f, boolean timeOnly) {
            date = null;
            _date = d;
            zone = z;
            ff  = f;
            this.timeOnly = timeOnly;
        }

        /**
         * Create a format for testing.
         */
        DateFormat createFormat() {
            return ff.createFormat();
        }

        /**
         * Return the Date of this test case; must be called with the default
         * zone set to this TestCase's zone.
         */
        @SuppressWarnings("deprecation")
        Date getDate() {
            if (_date == null) {
                // Date constructor will work right iff we are in the target zone
                int h = 0;
                int m = 0;
                int s = 0;
                if (date.length >= 4) {
                    h = date[3];
                    if (date.length >= 5) {
                        m = date[4];
                        if (date.length >= 6) {
                            s = date[5];
                        }
                    }
                }
                _date = new Date(date[0] - 1900, date[1] - 1, date[2],
                                 h, m, s);
            }
            return _date;
        }

        public String toString() {
            return String.valueOf(getDate().getTime()) + " " +
                refFormat.format(getDate()) + " : " + ff.createFormat().format(getDate());
        }
    };

    private interface FormatFactory {
        DateFormat createFormat();
    }

    TestCase[] TESTS = {
        // Feb 29 2004 -- ordinary leap day
        new TestCase(new int[] {2004, 2, 29}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // Feb 29 2000 -- century leap day
        new TestCase(new int[] {2000, 2, 29}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // 0:00:00 Jan 1 1999 -- first second of normal year
        new TestCase(new int[] {1999, 1, 1}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance();
                     }}, false),

        // 23:59:59 Dec 31 1999 -- last second of normal year
        new TestCase(new int[] {1999, 12, 31, 23, 59, 59}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance();
                     }}, false),

        // 0:00:00 Jan 1 2004 -- first second of leap year
        new TestCase(new int[] {2004, 1, 1}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance();
                     }}, false),

        // 23:59:59 Dec 31 2004 -- last second of leap year
        new TestCase(new int[] {2004, 12, 31, 23, 59, 59}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance();
                     }}, false),

        // October 25, 1998 1:59:59 AM PDT -- just before DST cessation
        new TestCase(new Date(909305999000L), TimeZone.getTimeZone("PST"),
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // October 25, 1998 1:00:00 AM PST -- just after DST cessation
        new TestCase(new Date(909306000000L), TimeZone.getTimeZone("PST"),
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // April 4, 1999 1:59:59 AM PST -- just before DST onset
        new TestCase(new int[] {1999, 4, 4, 1, 59, 59},
                     TimeZone.getTimeZone("PST"),
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // April 4, 1999 3:00:00 AM PDT -- just after DST onset
        new TestCase(new Date(923220000000L), TimeZone.getTimeZone("PST"),
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // October 4, 1582 11:59:59 PM PDT -- just before Gregorian change
        new TestCase(new int[] {1582, 10, 4, 23, 59, 59}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),

        // October 15, 1582 12:00:00 AM PDT -- just after Gregorian change
        new TestCase(new int[] {1582, 10, 15, 0, 0, 0}, null,
                     new FormatFactory() { public DateFormat createFormat() {
                         return DateFormat.getDateTimeInstance(DateFormat.LONG,
                                                               DateFormat.LONG);
                     }}, false),
    };

    public void TestDateFormatRoundTrip() {
        avail = DateFormat.getAvailableLocales();
        System.out.println("DateFormat available locales: " + avail.length);
        System.out.println("Default TimeZone: " +
              (defaultZone = TimeZone.getDefault()).getID());

        if (random || initialDate != null) {
            if (RANDOM == null) {
                // Need this for sparse coverage to reduce combinatorial explosion,
                // even for non-random looped testing (i.e., with explicit date but
                // not pattern or locale).
                RANDOM = new Random(FIXED_SEED);
            }
            loopedTest();
        } else {
            for (int i=0; i<TESTS.length; ++i) {
                doTest(TESTS[i]);
            }
        }
    }

    /**
     * TimeZone must be set to tc.zone before this method is called.
     */
    private void doTestInZone(TestCase tc) {
        System.out.println(escape(tc.toString()));
        Locale save = Locale.getDefault();
        try {
            if (locale != null) {
                Locale.setDefault(locale);
                doTest(locale, tc.createFormat(), tc.timeOnly, tc.getDate());
            } else {
                for (int i=0; i<avail.length; ++i) {
                    Locale.setDefault(avail[i]);
                    doTest(avail[i], tc.createFormat(), tc.timeOnly, tc.getDate());
                }
            }
        } finally {
            Locale.setDefault(save);
        }
    }

    private void doTest(TestCase tc) {
        if (tc.zone == null) {
            // Just run in the default zone
            doTestInZone(tc);
        } else {
            try {
                TimeZone.setDefault(tc.zone);
                doTestInZone(tc);
            } finally {
                TimeZone.setDefault(defaultZone);
            }
        }
    }

    private void loopedTest() {
        if (INFINITE) {
            // Special infinite loop test mode for finding hard to reproduce errors
            if (locale != null) {
                System.out.println("ENTERING INFINITE TEST LOOP, LOCALE " + locale.getDisplayName());
                for (;;) doTest(locale);
            } else {
                System.out.println("ENTERING INFINITE TEST LOOP, ALL LOCALES");
                for (;;) {
                    for (int i=0; i<avail.length; ++i) {
                        doTest(avail[i]);
                    }
                }
            }
        }
        else {
            if (locale != null) {
                doTest(locale);
            } else {
                doTest(Locale.getDefault());

                for (int i=0; i<avail.length; ++i) {
                    doTest(avail[i]);
                }
            }
        }
    }

    void doTest(Locale loc) {
        if (!INFINITE) System.out.println("Locale: " + loc.getDisplayName());

        if (pattern != null) {
            doTest(loc, new SimpleDateFormat(pattern, loc));
            return;
        }

        // Total possibilities = 24
        //  4 date
        //  4 time
        //  16 date-time
        boolean[] TEST_TABLE = new boolean[24];
        for (int i=0; i<24; ++i) TEST_TABLE[i] = true;

        // If we have some sparseness, implement it here.  Sparseness decreases
        // test time by eliminating some tests, up to 23.
        if (!INFINITE) {
            for (int i=0; i<SPARSENESS; ) {
                int random = (int)(java.lang.Math.random() * 24);
                if (random >= 0 && random < 24 && TEST_TABLE[i]) {
                    TEST_TABLE[i] = false;
                    ++i;
                }
            }
        }

        int itable = 0;
        for (int style=DateFormat.FULL; style<=DateFormat.SHORT; ++style) {
            if (TEST_TABLE[itable++])
                doTest(loc, DateFormat.getDateInstance(style, loc));
        }

        for (int style=DateFormat.FULL; style<=DateFormat.SHORT; ++style) {
            if (TEST_TABLE[itable++])
                doTest(loc, DateFormat.getTimeInstance(style, loc), true);
        }

        for (int dstyle=DateFormat.FULL; dstyle<=DateFormat.SHORT; ++dstyle) {
            for (int tstyle=DateFormat.FULL; tstyle<=DateFormat.SHORT; ++tstyle) {
                if (TEST_TABLE[itable++])
                    doTest(loc, DateFormat.getDateTimeInstance(dstyle, tstyle, loc));
            }
        }
    }

    void doTest(Locale loc, DateFormat fmt) { doTest(loc, fmt, false); }

    void doTest(Locale loc, DateFormat fmt, boolean timeOnly) {
        doTest(loc, fmt, timeOnly, initialDate != null ? initialDate : generateDate());
    }

    void doTest(Locale loc, DateFormat fmt, boolean timeOnly, Date date) {
        // Skip testing with the JapaneseImperialCalendar which
        // doesn't support the Gregorian year semantices with 'y'.
        if (fmt.getCalendar().getClass().getName().equals("java.util.JapaneseImperialCalendar")) {
            return;
        }

        String pat = ((SimpleDateFormat)fmt).toPattern();
        String deqPat = dequotePattern(pat); // Remove quoted elements

        boolean hasEra = (deqPat.indexOf("G") != -1);
        boolean hasZone = (deqPat.indexOf("z") != -1);

        Calendar cal = fmt.getCalendar();

        // Because patterns contain incomplete data representing the Date,
        // we must be careful of how we do the roundtrip.  We start with
        // a randomly generated Date because they're easier to generate.
        // From this we get a string.  The string is our real starting point,
        // because this string should parse the same way all the time.  Note
        // that it will not necessarily parse back to the original date because
        // of incompleteness in patterns.  For example, a time-only pattern won't
        // parse back to the same date.

        try {
            for (int i=0; i<TRIALS; ++i) {
                Date[] d = new Date[DEPTH];
                String[] s = new String[DEPTH];
                String error = null;

                d[0] = date;

                // We go through this loop until we achieve a match or until
                // the maximum loop count is reached.  We record the points at
                // which the date and the string starts to match.  Once matching
                // starts, it should continue.
                int loop;
                int dmatch = 0; // d[dmatch].getTime() == d[dmatch-1].getTime()
                int smatch = 0; // s[smatch].equals(s[smatch-1])
                for (loop=0; loop<DEPTH; ++loop) {
                    if (loop > 0) d[loop] = fmt.parse(s[loop-1]);
                    s[loop] = fmt.format(d[loop]);

                    if (loop > 0) {
                        if (smatch == 0) {
                            boolean match = s[loop].equals(s[loop-1]);
                            if (smatch == 0) {
                                if (match) smatch = loop;
                            }
                            else if (!match) {
                                // This should never happen; if it does, fail.
                                smatch = -1;
                                error = "FAIL: String mismatch after match";
                            }
                        }

                        if (dmatch == 0) {
                            boolean match = d[loop].getTime() == d[loop-1].getTime();
                            if (dmatch == 0) {
                                if (match) dmatch = loop;
                            }
                            else if (!match) {
                                // This should never happen; if it does, fail.
                                dmatch = -1;
                                error = "FAIL: Date mismatch after match";
                            }
                        }

                        if (smatch != 0 && dmatch != 0) break;
                    }
                }
                // At this point loop == DEPTH if we've failed, otherwise loop is the
                // max(smatch, dmatch), that is, the index at which we have string and
                // date matching.

                // Date usually matches in 2.  Exceptions handled below.
                int maxDmatch = 2;
                int maxSmatch = 1;
                if (dmatch > maxDmatch) {
                    // Time-only pattern with zone information and a starting date in PST.
                    if (timeOnly && hasZone && fmt.getTimeZone().inDaylightTime(d[0])) {
                        maxDmatch = 3;
                        maxSmatch = 2;
                    }
                }

                // String usually matches in 1.  Exceptions are checked for here.
                if (smatch > maxSmatch) { // Don't compute unless necessary
                    // Starts in BC, with no era in pattern
                    if (!hasEra && getField(cal, d[0], Calendar.ERA) == GregorianCalendar.BC)
                        maxSmatch = 2;
                    // Starts in DST, no year in pattern
                    else if (fmt.getTimeZone().inDaylightTime(d[0]) &&
                             deqPat.indexOf("yyyy") == -1)
                        maxSmatch = 2;
                    // Two digit year with zone and year change and zone in pattern
                    else if (hasZone &&
                             fmt.getTimeZone().inDaylightTime(d[0]) !=
                             fmt.getTimeZone().inDaylightTime(d[dmatch]) &&
                             getField(cal, d[0], Calendar.YEAR) !=
                             getField(cal, d[dmatch], Calendar.YEAR) &&
                             deqPat.indexOf("y") != -1 &&
                             deqPat.indexOf("yyyy") == -1)
                        maxSmatch = 2;
                    // Two digit year, year change, DST changeover hour.  Example:
                    //    FAIL: Pattern: dd/MM/yy HH:mm:ss
                    //     Date matched in 2, wanted 2
                    //     String matched in 2, wanted 1
                    //        Thu Apr 02 02:35:52.110 PST 1795 AD F> 02/04/95 02:35:52
                    //     P> Sun Apr 02 01:35:52.000 PST 1995 AD F> 02/04/95 01:35:52
                    //     P> Sun Apr 02 01:35:52.000 PST 1995 AD F> 02/04/95 01:35:52 d== s==
                    // The problem is that the initial time is not a DST onset day, but
                    // then the year changes, and the resultant parsed time IS a DST
                    // onset day.  The hour "2:XX" makes no sense if 2:00 is the DST
                    // onset, so DateFormat interprets it as 1:XX (arbitrary -- could
                    // also be 3:XX, same problem).  This results in an extra iteration
                    // for String match convergence.
                    else if (!justBeforeOnset(cal, d[0]) && justBeforeOnset(cal, d[dmatch]) &&
                             getField(cal, d[0], Calendar.YEAR) !=
                             getField(cal, d[dmatch], Calendar.YEAR) &&
                             deqPat.indexOf("y") != -1 &&
                             deqPat.indexOf("yyyy") == -1)
                        maxSmatch = 2;
                    // Another spurious failure:
                    // FAIL: Pattern: dd MMMM yyyy hh:mm:ss
                    //  Date matched in 2, wanted 2
                    //  String matched in 2, wanted 1
                    //     Sun Apr 05 14:28:38.410 PDT 3998 AD F> 05 April 3998 02:28:38
                    //  P> Sun Apr 05 01:28:38.000 PST 3998 AD F> 05 April 3998 01:28:38
                    //  P> Sun Apr 05 01:28:38.000 PST 3998 AD F> 05 April 3998 01:28:38 d== s==
                    // The problem here is that with an 'hh' pattern, hour from 1-12,
                    // a lack of AM/PM -- that is, no 'a' in pattern, and an initial
                    // time in the onset hour + 12:00.
                    else if (deqPat.indexOf('h') >= 0
                             && deqPat.indexOf('a') < 0
                             && justBeforeOnset(cal, new Date(d[0].getTime() - 12*60*60*1000L))
                             && justBeforeOnset(cal, d[1]))
                        maxSmatch = 2;
                }

                if (dmatch > maxDmatch || smatch > maxSmatch
                    || dmatch < 0 || smatch < 0) {
                    StringBuffer out = new StringBuffer();
                    if (error != null) {
                        out.append(error + '\n');
                    }
                    out.append("FAIL: Pattern: " + pat + ", Locale: " + loc + '\n');
                    out.append("      Initial date (ms): " + d[0].getTime() + '\n');
                    out.append("     Date matched in " + dmatch
                               + ", wanted " + maxDmatch + '\n');
                    out.append("     String matched in " + smatch
                               + ", wanted " + maxSmatch);

                    for (int j=0; j<=loop && j<DEPTH; ++j) {
                        out.append("\n    " +
                                   (j>0?" P> ":"    ") + refFormat.format(d[j]) + " F> " +
                                   escape(s[j]) +
                                   (j>0&&d[j].getTime()==d[j-1].getTime()?" d==":"") +
                                   (j>0&&s[j].equals(s[j-1])?" s==":""));
                    }
                    throw new RuntimeException(escape(out.toString()));
                }
            }
        }
        catch (ParseException e) {
            throw new RuntimeException(e.toString());
        }
    }

    /**
     * Return a field of the given date
     */
    static int getField(Calendar cal, Date d, int f) {
        // Should be synchronized, but we're single threaded so it's ok
        cal.setTime(d);
        return cal.get(f);
    }

    /**
     * Return true if the given Date is in the 1 hour window BEFORE the
     * change from STD to DST for the given Calendar.
     */
    static final boolean justBeforeOnset(Calendar cal, Date d) {
        return nearOnset(cal, d, false);
    }

    /**
     * Return true if the given Date is in the 1 hour window AFTER the
     * change from STD to DST for the given Calendar.
     */
    static final boolean justAfterOnset(Calendar cal, Date d) {
        return nearOnset(cal, d, true);
    }

    /**
     * Return true if the given Date is in the 1 hour (or whatever the
     * DST savings is) window before or after the onset of DST.
     */
    static boolean nearOnset(Calendar cal, Date d, boolean after) {
        cal.setTime(d);
        if ((cal.get(Calendar.DST_OFFSET) == 0) == after) {
            return false;
        }
        int delta;
        try {
            delta = ((SimpleTimeZone) cal.getTimeZone()).getDSTSavings();
        } catch (ClassCastException e) {
            delta = 60*60*1000; // One hour as ms
        }
        cal.setTime(new Date(d.getTime() + (after ? -delta : delta)));
        return (cal.get(Calendar.DST_OFFSET) == 0) == after;
    }

    static String escape(String s) {
        StringBuffer buf = new StringBuffer();
        for (int i=0; i<s.length(); ++i) {
            char c = s.charAt(i);
            if (c < '\u0080') buf.append(c);
            else {
                buf.append("\\u");
                if (c < '\u1000') {
                    buf.append('0');
                    if (c < '\u0100') {
                        buf.append('0');
                        if (c < '\u0010') {
                            buf.append('0');
                        }
                    }
                }
                buf.append(Integer.toHexString(c));
            }
        }
        return buf.toString();
    }

    /**
     * Remove quoted elements from a pattern.  E.g., change "hh:mm 'o''clock'"
     * to "hh:mm ?".  All quoted elements are replaced by one or more '?'
     * characters.
     */
    static String dequotePattern(String pat) {
        StringBuffer out = new StringBuffer();
        boolean inQuote = false;
        for (int i=0; i<pat.length(); ++i) {
            char ch = pat.charAt(i);
            if (ch == '\'') {
                if ((i+1)<pat.length()
                    && pat.charAt(i+1) == '\'') {
                    // Handle "''"
                    out.append('?');
                    ++i;
                } else {
                    inQuote = !inQuote;
                    if (inQuote) {
                        out.append('?');
                    }
                }
            } else if (!inQuote) {
                out.append(ch);
            }
        }
        return out.toString();
    }

    static Date generateDate() {
        double a = (RANDOM.nextLong() & 0x7FFFFFFFFFFFFFFFL ) /
            ((double)0x7FFFFFFFFFFFFFFFL);

        // Now 'a' ranges from 0..1; scale it to range from 0 to 8000 years
        a *= 8000;

        // Range from (4000-1970) BC to (8000-1970) AD
        a -= 4000;

        // Now scale up to ms
        a *= 365.25 * 24 * 60 * 60 * 1000;

        return new Date((long)a);
    }
}

//eof
