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