/* * Copyright (c) 1997, 2016, 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 * @library /java/text/testlib * @run main/othervm -Djava.locale.providers=COMPAT,SPI DateFormatRoundTripTest */ import java.text.*; import java.util.*; public class DateFormatRoundTripTest extends IntlTest { 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 new Locale("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 new Locale(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; Vector newArgs = new Vector(); for (int i=0; i] [-locale ] [-date ] [-INFINITE]"); System.out.println(" [-random | -randomseed | -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 Random using as seed."); super.usage(); } 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. */ 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(); logln("DateFormat available locales: " + avail.length); logln("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= 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 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 && j0?" 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==":"")); } errln(escape(out.toString())); } } } catch (ParseException e) { errln(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