1 /* SimpleDateFormat.java -- A class for parsing/formating simple 2 date constructs 3 Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005 4 Free Software Foundation, Inc. 5 6 This file is part of GNU Classpath. 7 8 GNU Classpath is free software; you can redistribute it and/or modify 9 it under the terms of the GNU General Public License as published by 10 the Free Software Foundation; either version 2, or (at your option) 11 any later version. 12 13 GNU Classpath is distributed in the hope that it will be useful, but 14 WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 General Public License for more details. 17 18 You should have received a copy of the GNU General Public License 19 along with GNU Classpath; see the file COPYING. If not, write to the 20 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 21 02110-1301 USA. 22 23 Linking this library statically or dynamically with other modules is 24 making a combined work based on this library. Thus, the terms and 25 conditions of the GNU General Public License cover the whole 26 combination. 27 28 As a special exception, the copyright holders of this library give you 29 permission to link this library with independent modules to produce an 30 executable, regardless of the license terms of these independent 31 modules, and to copy and distribute the resulting executable under 32 terms of your choice, provided that you also meet, for each linked 33 independent module, the terms and conditions of the license of that 34 module. An independent module is a module which is not derived from 35 or based on this library. If you modify this library, you may extend 36 this exception to your version of the library, but you are not 37 obligated to do so. If you do not wish to do so, delete this 38 exception statement from your version. */ 39 40 41 package java.text; 42 43 import gnu.java.lang.CPStringBuilder; 44 45 import gnu.java.text.AttributedFormatBuffer; 46 import gnu.java.text.FormatBuffer; 47 import gnu.java.text.FormatCharacterIterator; 48 import gnu.java.text.StringFormatBuffer; 49 50 import java.io.IOException; 51 import java.io.InvalidObjectException; 52 import java.io.ObjectInputStream; 53 import java.util.ArrayList; 54 import java.util.Calendar; 55 import java.util.Date; 56 import java.util.GregorianCalendar; 57 import java.util.Iterator; 58 import java.util.Locale; 59 import java.util.TimeZone; 60 import java.util.regex.Matcher; 61 import java.util.regex.Pattern; 62 63 /** 64 * SimpleDateFormat provides convenient methods for parsing and formatting 65 * dates using Gregorian calendars (see java.util.GregorianCalendar). 66 * This class is not thread-safe; external synchronisation should be applied 67 * if an instance is to be accessed from multiple threads. 68 */ 69 public class SimpleDateFormat extends DateFormat 70 { 71 /** 72 * This class is used by <code>SimpleDateFormat</code> as a 73 * compiled representation of a format string. The field 74 * ID, size, and character used are stored for each sequence 75 * of pattern characters. 76 */ 77 private class CompiledField 78 { 79 /** 80 * The ID of the field within the local pattern characters. 81 * Package private for use in out class. 82 */ 83 int field; 84 85 /** 86 * The size of the character sequence. 87 * Package private for use in out class. 88 */ 89 int size; 90 91 /** 92 * The character used. 93 */ 94 private char character; 95 96 /** 97 * Constructs a compiled field using the 98 * the given field ID, size and character 99 * values. 100 * 101 * @param f the field ID. 102 * @param s the size of the field. 103 * @param c the character used. 104 */ CompiledField(int f, int s, char c)105 public CompiledField(int f, int s, char c) 106 { 107 field = f; 108 size = s; 109 character = c; 110 } 111 112 /** 113 * Retrieves the ID of the field relative to 114 * the local pattern characters. 115 */ getField()116 public int getField() 117 { 118 return field; 119 } 120 121 /** 122 * Retrieves the size of the character sequence. 123 */ getSize()124 public int getSize() 125 { 126 return size; 127 } 128 129 /** 130 * Retrieves the character used in the sequence. 131 */ getCharacter()132 public char getCharacter() 133 { 134 return character; 135 } 136 137 /** 138 * Returns a <code>String</code> representation 139 * of the compiled field, primarily for debugging 140 * purposes. 141 * 142 * @return a <code>String</code> representation. 143 */ toString()144 public String toString() 145 { 146 CPStringBuilder builder; 147 148 builder = new CPStringBuilder(getClass().getName()); 149 builder.append("[field="); 150 builder.append(field); 151 builder.append(", size="); 152 builder.append(size); 153 builder.append(", character="); 154 builder.append(character); 155 builder.append("]"); 156 157 return builder.toString(); 158 } 159 } 160 161 /** 162 * A list of <code>CompiledField</code>s and {@code String}s 163 * representing the compiled version of the pattern. 164 * 165 * @see CompiledField 166 * @serial Ignored. 167 */ 168 private transient ArrayList<Object> tokens; 169 170 /** 171 * The localised data used in formatting, 172 * such as the day and month names in the local 173 * language, and the localized pattern characters. 174 * 175 * @see DateFormatSymbols 176 * @serial The localisation data. May not be null. 177 */ 178 private DateFormatSymbols formatData; 179 180 /** 181 * The date representing the start of the century 182 * used for interpreting two digit years. For 183 * example, 24/10/2004 would cause two digit 184 * years to be interpreted as representing 185 * the years between 2004 and 2104. 186 * 187 * @see #get2DigitYearStart() 188 * @see #set2DigitYearStart(java.util.Date) 189 * @see Date 190 * @serial The start date of the century for parsing two digit years. 191 * May not be null. 192 */ 193 private Date defaultCenturyStart; 194 195 /** 196 * The year at which interpretation of two 197 * digit years starts. 198 * 199 * @see #get2DigitYearStart() 200 * @see #set2DigitYearStart(java.util.Date) 201 * @serial Ignored. 202 */ 203 private transient int defaultCentury; 204 205 /** 206 * The non-localized pattern string. This 207 * only ever contains the pattern characters 208 * stored in standardChars. Localized patterns 209 * are translated to this form. 210 * 211 * @see #applyPattern(String) 212 * @see #applyLocalizedPattern(String) 213 * @see #toPattern() 214 * @see #toLocalizedPattern() 215 * @serial The non-localized pattern string. May not be null. 216 */ 217 private String pattern; 218 219 /** 220 * The version of serialized data used by this class. 221 * Version 0 only includes the pattern and formatting 222 * data. Version 1 adds the start date for interpreting 223 * two digit years. 224 * 225 * @serial This specifies the version of the data being serialized. 226 * Version 0 (or no version) specifies just <code>pattern</code> 227 * and <code>formatData</code>. Version 1 adds 228 * the <code>defaultCenturyStart</code>. This implementation 229 * always writes out version 1 data. 230 */ 231 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier 232 233 /** 234 * For compatability. 235 */ 236 private static final long serialVersionUID = 4774881970558875024L; 237 238 // This string is specified in the root of the CLDR. 239 private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZvcL"; 240 241 /** 242 * Represents the position of the RFC822 timezone pattern character 243 * in the array of localized pattern characters. In the 244 * U.S. locale, this is 'Z'. The value is the offset of the current 245 * time from GMT e.g. -0500 would be five hours prior to GMT. 246 */ 247 private static final int RFC822_TIMEZONE_FIELD = 23; 248 249 /** 250 * Reads the serialized version of this object. 251 * If the serialized data is only version 0, 252 * then the date for the start of the century 253 * for interpreting two digit years is computed. 254 * The pattern is parsed and compiled following the process 255 * of reading in the serialized data. 256 * 257 * @param stream the object stream to read the data from. 258 * @throws IOException if an I/O error occurs. 259 * @throws ClassNotFoundException if the class of the serialized data 260 * could not be found. 261 * @throws InvalidObjectException if the pattern is invalid. 262 */ readObject(ObjectInputStream stream)263 private void readObject(ObjectInputStream stream) 264 throws IOException, ClassNotFoundException 265 { 266 stream.defaultReadObject(); 267 if (serialVersionOnStream < 1) 268 { 269 computeCenturyStart (); 270 serialVersionOnStream = 1; 271 } 272 else 273 // Ensure that defaultCentury gets set. 274 set2DigitYearStart(defaultCenturyStart); 275 276 // Set up items normally taken care of by the constructor. 277 tokens = new ArrayList<Object>(); 278 try 279 { 280 compileFormat(pattern); 281 } 282 catch (IllegalArgumentException e) 283 { 284 throw new InvalidObjectException("The stream pattern was invalid."); 285 } 286 } 287 288 /** 289 * Compiles the supplied non-localized pattern into a form 290 * from which formatting and parsing can be performed. 291 * This also detects errors in the pattern, which will 292 * be raised on later use of the compiled data. 293 * 294 * @param pattern the non-localized pattern to compile. 295 * @throws IllegalArgumentException if the pattern is invalid. 296 */ compileFormat(String pattern)297 private void compileFormat(String pattern) 298 { 299 // Any alphabetical characters are treated as pattern characters 300 // unless enclosed in single quotes. 301 302 char thisChar; 303 int pos; 304 int field; 305 CompiledField current = null; 306 307 for (int i = 0; i < pattern.length(); i++) 308 { 309 thisChar = pattern.charAt(i); 310 field = standardChars.indexOf(thisChar); 311 if (field == -1) 312 { 313 current = null; 314 if ((thisChar >= 'A' && thisChar <= 'Z') 315 || (thisChar >= 'a' && thisChar <= 'z')) 316 { 317 // Not a valid letter 318 throw new IllegalArgumentException("Invalid letter " 319 + thisChar + 320 " encountered at character " 321 + i + "."); 322 } 323 else if (thisChar == '\'') 324 { 325 // Quoted text section; skip to next single quote 326 pos = pattern.indexOf('\'', i + 1); 327 // First look for '' -- meaning a single quote. 328 if (pos == i + 1) 329 tokens.add("'"); 330 else 331 { 332 // Look for the terminating quote. However, if we 333 // see a '', that represents a literal quote and 334 // we must iterate. 335 CPStringBuilder buf = new CPStringBuilder(); 336 int oldPos = i + 1; 337 do 338 { 339 if (pos == -1) 340 throw new IllegalArgumentException("Quotes starting at character " 341 + i + 342 " not closed."); 343 buf.append(pattern.substring(oldPos, pos)); 344 if (pos + 1 >= pattern.length() 345 || pattern.charAt(pos + 1) != '\'') 346 break; 347 buf.append('\''); 348 oldPos = pos + 2; 349 pos = pattern.indexOf('\'', pos + 2); 350 } 351 while (true); 352 tokens.add(buf.toString()); 353 } 354 i = pos; 355 } 356 else 357 { 358 // A special character 359 tokens.add(Character.valueOf(thisChar)); 360 } 361 } 362 else 363 { 364 // A valid field 365 if ((current != null) && (field == current.field)) 366 current.size++; 367 else 368 { 369 current = new CompiledField(field, 1, thisChar); 370 tokens.add(current); 371 } 372 } 373 } 374 } 375 376 /** 377 * Returns a string representation of this 378 * class. 379 * 380 * @return a string representation of the <code>SimpleDateFormat</code> 381 * instance. 382 */ toString()383 public String toString() 384 { 385 CPStringBuilder output = new CPStringBuilder(getClass().getName()); 386 output.append("[tokens="); 387 output.append(tokens); 388 output.append(", formatData="); 389 output.append(formatData); 390 output.append(", defaultCenturyStart="); 391 output.append(defaultCenturyStart); 392 output.append(", defaultCentury="); 393 output.append(defaultCentury); 394 output.append(", pattern="); 395 output.append(pattern); 396 output.append(", serialVersionOnStream="); 397 output.append(serialVersionOnStream); 398 output.append(", standardChars="); 399 output.append(standardChars); 400 output.append("]"); 401 return output.toString(); 402 } 403 404 /** 405 * Constructs a SimpleDateFormat using the default pattern for 406 * the default locale. 407 */ SimpleDateFormat()408 public SimpleDateFormat() 409 { 410 /* 411 * There does not appear to be a standard API for determining 412 * what the default pattern for a locale is, so use package-scope 413 * variables in DateFormatSymbols to encapsulate this. 414 */ 415 super(); 416 Locale locale = Locale.getDefault(); 417 calendar = new GregorianCalendar(locale); 418 computeCenturyStart(); 419 tokens = new ArrayList<Object>(); 420 formatData = new DateFormatSymbols(locale); 421 pattern = (formatData.dateFormats[DEFAULT] + ' ' 422 + formatData.timeFormats[DEFAULT]); 423 compileFormat(pattern); 424 numberFormat = NumberFormat.getInstance(locale); 425 numberFormat.setGroupingUsed (false); 426 numberFormat.setParseIntegerOnly (true); 427 numberFormat.setMaximumFractionDigits (0); 428 } 429 430 /** 431 * Creates a date formatter using the specified non-localized pattern, 432 * with the default DateFormatSymbols for the default locale. 433 * 434 * @param pattern the pattern to use. 435 * @throws NullPointerException if the pattern is null. 436 * @throws IllegalArgumentException if the pattern is invalid. 437 */ SimpleDateFormat(String pattern)438 public SimpleDateFormat(String pattern) 439 { 440 this(pattern, Locale.getDefault()); 441 } 442 443 /** 444 * Creates a date formatter using the specified non-localized pattern, 445 * with the default DateFormatSymbols for the given locale. 446 * 447 * @param pattern the non-localized pattern to use. 448 * @param locale the locale to use for the formatting symbols. 449 * @throws NullPointerException if the pattern is null. 450 * @throws IllegalArgumentException if the pattern is invalid. 451 */ SimpleDateFormat(String pattern, Locale locale)452 public SimpleDateFormat(String pattern, Locale locale) 453 { 454 super(); 455 calendar = new GregorianCalendar(locale); 456 computeCenturyStart(); 457 tokens = new ArrayList<Object>(); 458 formatData = new DateFormatSymbols(locale); 459 compileFormat(pattern); 460 this.pattern = pattern; 461 numberFormat = NumberFormat.getInstance(locale); 462 numberFormat.setGroupingUsed (false); 463 numberFormat.setParseIntegerOnly (true); 464 numberFormat.setMaximumFractionDigits (0); 465 } 466 467 /** 468 * Creates a date formatter using the specified non-localized 469 * pattern. The specified DateFormatSymbols will be used when 470 * formatting. 471 * 472 * @param pattern the non-localized pattern to use. 473 * @param formatData the formatting symbols to use. 474 * @throws NullPointerException if the pattern or formatData is null. 475 * @throws IllegalArgumentException if the pattern is invalid. 476 */ SimpleDateFormat(String pattern, DateFormatSymbols formatData)477 public SimpleDateFormat(String pattern, DateFormatSymbols formatData) 478 { 479 super(); 480 calendar = new GregorianCalendar(); 481 computeCenturyStart (); 482 tokens = new ArrayList<Object>(); 483 if (formatData == null) 484 throw new NullPointerException("formatData"); 485 this.formatData = formatData; 486 compileFormat(pattern); 487 this.pattern = pattern; 488 numberFormat = NumberFormat.getInstance(); 489 numberFormat.setGroupingUsed (false); 490 numberFormat.setParseIntegerOnly (true); 491 numberFormat.setMaximumFractionDigits (0); 492 } 493 494 /** 495 * This method returns a string with the formatting pattern being used 496 * by this object. This string is unlocalized. 497 * 498 * @return The format string. 499 */ toPattern()500 public String toPattern() 501 { 502 return pattern; 503 } 504 505 /** 506 * This method returns a string with the formatting pattern being used 507 * by this object. This string is localized. 508 * 509 * @return The format string. 510 */ toLocalizedPattern()511 public String toLocalizedPattern() 512 { 513 String localChars = formatData.getLocalPatternChars(); 514 return translateLocalizedPattern(pattern, standardChars, localChars); 515 } 516 517 /** 518 * This method sets the formatting pattern that should be used by this 519 * object. This string is not localized. 520 * 521 * @param pattern The new format pattern. 522 * @throws NullPointerException if the pattern is null. 523 * @throws IllegalArgumentException if the pattern is invalid. 524 */ applyPattern(String pattern)525 public void applyPattern(String pattern) 526 { 527 tokens.clear(); 528 compileFormat(pattern); 529 this.pattern = pattern; 530 } 531 532 /** 533 * This method sets the formatting pattern that should be used by this 534 * object. This string is localized. 535 * 536 * @param pattern The new format pattern. 537 * @throws NullPointerException if the pattern is null. 538 * @throws IllegalArgumentException if the pattern is invalid. 539 */ applyLocalizedPattern(String pattern)540 public void applyLocalizedPattern(String pattern) 541 { 542 String localChars = formatData.getLocalPatternChars(); 543 pattern = translateLocalizedPattern(pattern, localChars, standardChars); 544 applyPattern(pattern); 545 } 546 547 /** 548 * Translates either from or to a localized variant of the pattern 549 * string. For example, in the German locale, 't' (for 'tag') is 550 * used instead of 'd' (for 'date'). This method translates 551 * a localized pattern (such as 'ttt') to a non-localized pattern 552 * (such as 'ddd'), or vice versa. Non-localized patterns use 553 * a standard set of characters, which match those of the U.S. English 554 * locale. 555 * 556 * @param pattern the pattern to translate. 557 * @param oldChars the old set of characters (used in the pattern). 558 * @param newChars the new set of characters (which will be used in the 559 * pattern). 560 * @return a version of the pattern using the characters in 561 * <code>newChars</code>. 562 */ translateLocalizedPattern(String pattern, String oldChars, String newChars)563 private String translateLocalizedPattern(String pattern, 564 String oldChars, String newChars) 565 { 566 int len = pattern.length(); 567 CPStringBuilder buf = new CPStringBuilder(len); 568 boolean quoted = false; 569 for (int i = 0; i < len; i++) 570 { 571 char ch = pattern.charAt(i); 572 if (ch == '\'') 573 quoted = ! quoted; 574 if (! quoted) 575 { 576 int j = oldChars.indexOf(ch); 577 if (j >= 0) 578 ch = newChars.charAt(j); 579 } 580 buf.append(ch); 581 } 582 return buf.toString(); 583 } 584 585 /** 586 * Returns the start of the century used for two digit years. 587 * 588 * @return A <code>Date</code> representing the start of the century 589 * for two digit years. 590 */ get2DigitYearStart()591 public Date get2DigitYearStart() 592 { 593 return defaultCenturyStart; 594 } 595 596 /** 597 * Sets the start of the century used for two digit years. 598 * 599 * @param date A <code>Date</code> representing the start of the century for 600 * two digit years. 601 */ set2DigitYearStart(Date date)602 public void set2DigitYearStart(Date date) 603 { 604 defaultCenturyStart = date; 605 calendar.clear(); 606 calendar.setTime(date); 607 int year = calendar.get(Calendar.YEAR); 608 defaultCentury = year - (year % 100); 609 } 610 611 /** 612 * This method returns a copy of the format symbol information used 613 * for parsing and formatting dates. 614 * 615 * @return a copy of the date format symbols. 616 */ getDateFormatSymbols()617 public DateFormatSymbols getDateFormatSymbols() 618 { 619 return (DateFormatSymbols) formatData.clone(); 620 } 621 622 /** 623 * This method sets the format symbols information used for parsing 624 * and formatting dates. 625 * 626 * @param formatData The date format symbols. 627 * @throws NullPointerException if <code>formatData</code> is null. 628 */ setDateFormatSymbols(DateFormatSymbols formatData)629 public void setDateFormatSymbols(DateFormatSymbols formatData) 630 { 631 if (formatData == null) 632 { 633 throw new 634 NullPointerException("The supplied format data was null."); 635 } 636 this.formatData = formatData; 637 } 638 639 /** 640 * This methods tests whether the specified object is equal to this 641 * object. This will be true if and only if the specified object: 642 * <p> 643 * <ul> 644 * <li>Is not <code>null</code>.</li> 645 * <li>Is an instance of <code>SimpleDateFormat</code>.</li> 646 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>) 647 * level.</li> 648 * <li>Has the same formatting pattern.</li> 649 * <li>Is using the same formatting symbols.</li> 650 * <li>Is using the same century for two digit years.</li> 651 * </ul> 652 * 653 * @param o The object to compare for equality against. 654 * 655 * @return <code>true</code> if the specified object is equal to this object, 656 * <code>false</code> otherwise. 657 */ equals(Object o)658 public boolean equals(Object o) 659 { 660 if (!super.equals(o)) 661 return false; 662 663 if (!(o instanceof SimpleDateFormat)) 664 return false; 665 666 SimpleDateFormat sdf = (SimpleDateFormat)o; 667 668 if (defaultCentury != sdf.defaultCentury) 669 return false; 670 671 if (!toPattern().equals(sdf.toPattern())) 672 return false; 673 674 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols())) 675 return false; 676 677 return true; 678 } 679 680 /** 681 * This method returns a hash value for this object. 682 * 683 * @return A hash value for this object. 684 */ hashCode()685 public int hashCode() 686 { 687 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^ 688 getDateFormatSymbols().hashCode(); 689 } 690 691 692 /** 693 * Formats the date input according to the format string in use, 694 * appending to the specified StringBuffer. The input StringBuffer 695 * is returned as output for convenience. 696 */ formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)697 private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos) 698 { 699 String temp; 700 calendar.setTime(date); 701 702 // go through vector, filling in fields where applicable, else toString 703 Iterator<Object> iter = tokens.iterator(); 704 while (iter.hasNext()) 705 { 706 Object o = iter.next(); 707 if (o instanceof CompiledField) 708 { 709 CompiledField cf = (CompiledField) o; 710 int beginIndex = buffer.length(); 711 712 switch (cf.getField()) 713 { 714 case ERA_FIELD: 715 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA); 716 break; 717 case YEAR_FIELD: 718 // If we have two digits, then we truncate. Otherwise, we 719 // use the size of the pattern, and zero pad. 720 buffer.setDefaultAttribute (DateFormat.Field.YEAR); 721 if (cf.getSize() == 2) 722 { 723 temp = "00"+String.valueOf (calendar.get (Calendar.YEAR)); 724 buffer.append (temp.substring (temp.length() - 2)); 725 } 726 else 727 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer); 728 break; 729 case MONTH_FIELD: 730 buffer.setDefaultAttribute (DateFormat.Field.MONTH); 731 if (cf.getSize() < 3) 732 withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer); 733 else if (cf.getSize() < 4) 734 buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]); 735 else 736 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]); 737 break; 738 case DATE_FIELD: 739 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH); 740 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer); 741 break; 742 case HOUR_OF_DAY1_FIELD: // 1-24 743 buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1); 744 withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1, 745 cf.getSize(), buffer); 746 break; 747 case HOUR_OF_DAY0_FIELD: // 0-23 748 buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0); 749 withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer); 750 break; 751 case MINUTE_FIELD: 752 buffer.setDefaultAttribute (DateFormat.Field.MINUTE); 753 withLeadingZeros (calendar.get (Calendar.MINUTE), 754 cf.getSize(), buffer); 755 break; 756 case SECOND_FIELD: 757 buffer.setDefaultAttribute (DateFormat.Field.SECOND); 758 withLeadingZeros(calendar.get (Calendar.SECOND), 759 cf.getSize(), buffer); 760 break; 761 case MILLISECOND_FIELD: 762 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND); 763 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer); 764 break; 765 case DAY_OF_WEEK_FIELD: 766 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK); 767 if (cf.getSize() < 4) 768 buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]); 769 else 770 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]); 771 break; 772 case DAY_OF_YEAR_FIELD: 773 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR); 774 withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer); 775 break; 776 case DAY_OF_WEEK_IN_MONTH_FIELD: 777 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH); 778 withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH), 779 cf.getSize(), buffer); 780 break; 781 case WEEK_OF_YEAR_FIELD: 782 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR); 783 withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR), 784 cf.getSize(), buffer); 785 break; 786 case WEEK_OF_MONTH_FIELD: 787 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH); 788 withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH), 789 cf.getSize(), buffer); 790 break; 791 case AM_PM_FIELD: 792 buffer.setDefaultAttribute (DateFormat.Field.AM_PM); 793 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]); 794 break; 795 case HOUR1_FIELD: // 1-12 796 buffer.setDefaultAttribute (DateFormat.Field.HOUR1); 797 withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1, 798 cf.getSize(), buffer); 799 break; 800 case HOUR0_FIELD: // 0-11 801 buffer.setDefaultAttribute (DateFormat.Field.HOUR0); 802 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer); 803 break; 804 case TIMEZONE_FIELD: 805 buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE); 806 TimeZone zone = calendar.getTimeZone(); 807 boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0; 808 // FIXME: XXX: This should be a localized time zone. 809 String zoneID = zone.getDisplayName 810 (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT); 811 buffer.append (zoneID); 812 break; 813 case RFC822_TIMEZONE_FIELD: 814 buffer.setDefaultAttribute(DateFormat.Field.TIME_ZONE); 815 int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) + 816 calendar.get(Calendar.DST_OFFSET)) / (1000 * 60); 817 String sign = (pureMinutes < 0) ? "-" : "+"; 818 pureMinutes = Math.abs(pureMinutes); 819 int hours = pureMinutes / 60; 820 int minutes = pureMinutes % 60; 821 buffer.append(sign); 822 withLeadingZeros(hours, 2, buffer); 823 withLeadingZeros(minutes, 2, buffer); 824 break; 825 default: 826 throw new IllegalArgumentException ("Illegal pattern character " + 827 cf.getCharacter()); 828 } 829 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute() 830 || cf.getField() == pos.getField())) 831 { 832 pos.setBeginIndex(beginIndex); 833 pos.setEndIndex(buffer.length()); 834 } 835 } 836 else 837 { 838 buffer.append(o.toString(), null); 839 } 840 } 841 } 842 format(Date date, StringBuffer buffer, FieldPosition pos)843 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos) 844 { 845 formatWithAttribute(date, new StringFormatBuffer (buffer), pos); 846 847 return buffer; 848 } 849 formatToCharacterIterator(Object date)850 public AttributedCharacterIterator formatToCharacterIterator(Object date) 851 throws IllegalArgumentException 852 { 853 if (date == null) 854 throw new NullPointerException("null argument"); 855 if (!(date instanceof Date)) 856 throw new IllegalArgumentException("argument should be an instance of java.util.Date"); 857 858 AttributedFormatBuffer buf = new AttributedFormatBuffer(); 859 formatWithAttribute((Date)date, buf, 860 null); 861 buf.sync(); 862 863 return new FormatCharacterIterator(buf.getBuffer().toString(), 864 buf.getRanges(), 865 buf.getAttributes()); 866 } 867 withLeadingZeros(int value, int length, FormatBuffer buffer)868 private void withLeadingZeros(int value, int length, FormatBuffer buffer) 869 { 870 String valStr = String.valueOf(value); 871 for (length -= valStr.length(); length > 0; length--) 872 buffer.append('0'); 873 buffer.append(valStr); 874 } 875 expect(String source, ParsePosition pos, char ch)876 private boolean expect(String source, ParsePosition pos, char ch) 877 { 878 int x = pos.getIndex(); 879 boolean r = x < source.length() && source.charAt(x) == ch; 880 if (r) 881 pos.setIndex(x + 1); 882 else 883 pos.setErrorIndex(x); 884 return r; 885 } 886 887 /** 888 * This method parses the specified string into a date. 889 * 890 * @param dateStr The date string to parse. 891 * @param pos The input and output parse position 892 * 893 * @return The parsed date, or <code>null</code> if the string cannot be 894 * parsed. 895 */ 896 public Date parse (String dateStr, ParsePosition pos) 897 { 898 int fmt_index = 0; 899 int fmt_max = pattern.length(); 900 901 calendar.clear(); 902 boolean saw_timezone = false; 903 int quote_start = -1; 904 boolean is2DigitYear = false; 905 try 906 { 907 for (; fmt_index < fmt_max; ++fmt_index) 908 { 909 char ch = pattern.charAt(fmt_index); 910 if (ch == '\'') 911 { 912 if (fmt_index < fmt_max - 1 913 && pattern.charAt(fmt_index + 1) == '\'') 914 { 915 if (! expect (dateStr, pos, ch)) 916 return null; 917 ++fmt_index; 918 } 919 else 920 quote_start = quote_start < 0 ? fmt_index : -1; 921 continue; 922 } 923 924 if (quote_start != -1 925 || ((ch < 'a' || ch > 'z') 926 && (ch < 'A' || ch > 'Z'))) 927 { 928 if (quote_start == -1 && ch == ' ') 929 { 930 // A single unquoted space in the pattern may match 931 // any number of spaces in the input. 932 int index = pos.getIndex(); 933 int save = index; 934 while (index < dateStr.length() 935 && Character.isWhitespace(dateStr.charAt(index))) 936 ++index; 937 if (index > save) 938 pos.setIndex(index); 939 else 940 { 941 // Didn't see any whitespace. 942 pos.setErrorIndex(index); 943 return null; 944 } 945 } 946 else if (! expect (dateStr, pos, ch)) 947 return null; 948 continue; 949 } 950 951 // We've arrived at a potential pattern character in the 952 // pattern. 953 int fmt_count = 1; 954 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch) 955 { 956 ++fmt_count; 957 } 958 959 // We might need to limit the number of digits to parse in 960 // some cases. We look to the next pattern character to 961 // decide. 962 boolean limit_digits = false; 963 if (fmt_index < fmt_max 964 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0) 965 limit_digits = true; 966 --fmt_index; 967 968 // We can handle most fields automatically: most either are 969 // numeric or are looked up in a string vector. In some cases 970 // we need an offset. When numeric, `offset' is added to the 971 // resulting value. When doing a string lookup, offset is the 972 // initial index into the string array. 973 int calendar_field; 974 boolean is_numeric = true; 975 int offset = 0; 976 boolean maybe2DigitYear = false; 977 boolean oneBasedHour = false; 978 boolean oneBasedHourOfDay = false; 979 Integer simpleOffset; 980 String[] set1 = null; 981 String[] set2 = null; 982 switch (ch) 983 { 984 case 'd': 985 calendar_field = Calendar.DATE; 986 break; 987 case 'D': 988 calendar_field = Calendar.DAY_OF_YEAR; 989 break; 990 case 'F': 991 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH; 992 break; 993 case 'E': 994 is_numeric = false; 995 offset = 1; 996 calendar_field = Calendar.DAY_OF_WEEK; 997 set1 = formatData.getWeekdays(); 998 set2 = formatData.getShortWeekdays(); 999 break; 1000 case 'w': 1001 calendar_field = Calendar.WEEK_OF_YEAR; 1002 break; 1003 case 'W': 1004 calendar_field = Calendar.WEEK_OF_MONTH; 1005 break; 1006 case 'M': 1007 calendar_field = Calendar.MONTH; 1008 if (fmt_count <= 2) 1009 offset = -1; 1010 else 1011 { 1012 is_numeric = false; 1013 set1 = formatData.getMonths(); 1014 set2 = formatData.getShortMonths(); 1015 } 1016 break; 1017 case 'y': 1018 calendar_field = Calendar.YEAR; 1019 if (fmt_count <= 2) 1020 maybe2DigitYear = true; 1021 break; 1022 case 'K': 1023 calendar_field = Calendar.HOUR; 1024 break; 1025 case 'h': 1026 calendar_field = Calendar.HOUR; 1027 oneBasedHour = true; 1028 break; 1029 case 'H': 1030 calendar_field = Calendar.HOUR_OF_DAY; 1031 break; 1032 case 'k': 1033 calendar_field = Calendar.HOUR_OF_DAY; 1034 oneBasedHourOfDay = true; 1035 break; 1036 case 'm': 1037 calendar_field = Calendar.MINUTE; 1038 break; 1039 case 's': 1040 calendar_field = Calendar.SECOND; 1041 break; 1042 case 'S': 1043 calendar_field = Calendar.MILLISECOND; 1044 break; 1045 case 'a': 1046 is_numeric = false; 1047 calendar_field = Calendar.AM_PM; 1048 set1 = formatData.getAmPmStrings(); 1049 break; 1050 case 'z': 1051 case 'Z': 1052 // We need a special case for the timezone, because it 1053 // uses a different data structure than the other cases. 1054 is_numeric = false; 1055 calendar_field = Calendar.ZONE_OFFSET; 1056 String[][] zoneStrings = formatData.getZoneStrings(); 1057 int zoneCount = zoneStrings.length; 1058 int index = pos.getIndex(); 1059 boolean found_zone = false; 1060 simpleOffset = computeOffset(dateStr.substring(index), pos); 1061 if (simpleOffset != null) 1062 { 1063 found_zone = true; 1064 saw_timezone = true; 1065 calendar.set(Calendar.DST_OFFSET, 0); 1066 offset = simpleOffset.intValue(); 1067 } 1068 else 1069 { 1070 for (int j = 0; j < zoneCount; j++) 1071 { 1072 String[] strings = zoneStrings[j]; 1073 int k; 1074 for (k = 0; k < strings.length; ++k) 1075 { 1076 if (dateStr.startsWith(strings[k], index)) 1077 break; 1078 } 1079 if (k != strings.length) 1080 { 1081 found_zone = true; 1082 saw_timezone = true; 1083 TimeZone tz = TimeZone.getTimeZone (strings[0]); 1084 // Check if it's a DST zone or ordinary 1085 if(k == 3 || k == 4) 1086 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings()); 1087 else 1088 calendar.set (Calendar.DST_OFFSET, 0); 1089 offset = tz.getRawOffset (); 1090 pos.setIndex(index + strings[k].length()); 1091 break; 1092 } 1093 } 1094 } 1095 if (! found_zone) 1096 { 1097 pos.setErrorIndex(pos.getIndex()); 1098 return null; 1099 } 1100 break; 1101 default: 1102 pos.setErrorIndex(pos.getIndex()); 1103 return null; 1104 } 1105 1106 // Compute the value we should assign to the field. 1107 int value; 1108 int index = -1; 1109 if (is_numeric) 1110 { 1111 numberFormat.setMinimumIntegerDigits(fmt_count); 1112 if (maybe2DigitYear) 1113 index = pos.getIndex(); 1114 Number n = null; 1115 if (limit_digits) 1116 { 1117 // numberFormat.setMaximumIntegerDigits(fmt_count) may 1118 // not work as expected. So we explicitly use substring 1119 // of dateStr. 1120 int origPos = pos.getIndex(); 1121 pos.setIndex(0); 1122 n = numberFormat.parse(dateStr.substring(origPos, origPos + fmt_count), pos); 1123 pos.setIndex(origPos + pos.getIndex()); 1124 } 1125 else 1126 n = numberFormat.parse(dateStr, pos); 1127 if (pos == null || ! (n instanceof Long)) 1128 return null; 1129 value = n.intValue() + offset; 1130 } 1131 else if (set1 != null) 1132 { 1133 index = pos.getIndex(); 1134 int i; 1135 boolean found = false; 1136 for (i = offset; i < set1.length; ++i) 1137 { 1138 if (set1[i] != null) 1139 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(), 1140 index)) 1141 { 1142 found = true; 1143 pos.setIndex(index + set1[i].length()); 1144 break; 1145 } 1146 } 1147 if (!found && set2 != null) 1148 { 1149 for (i = offset; i < set2.length; ++i) 1150 { 1151 if (set2[i] != null) 1152 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(), 1153 index)) 1154 { 1155 found = true; 1156 pos.setIndex(index + set2[i].length()); 1157 break; 1158 } 1159 } 1160 } 1161 if (!found) 1162 { 1163 pos.setErrorIndex(index); 1164 return null; 1165 } 1166 value = i; 1167 } 1168 else 1169 value = offset; 1170 1171 if (maybe2DigitYear) 1172 { 1173 // Parse into default century if the numeric year string has 1174 // exactly 2 digits. 1175 int digit_count = pos.getIndex() - index; 1176 if (digit_count == 2) 1177 { 1178 is2DigitYear = true; 1179 value += defaultCentury; 1180 } 1181 } 1182 1183 // Calendar uses 0-based hours. 1184 // I.e. 00:00 AM is midnight, not 12 AM or 24:00 1185 if (oneBasedHour && value == 12) 1186 value = 0; 1187 1188 if (oneBasedHourOfDay && value == 24) 1189 value = 0; 1190 1191 // Assign the value and move on. 1192 calendar.set(calendar_field, value); 1193 } 1194 1195 if (is2DigitYear) 1196 { 1197 // Apply the 80-20 heuristic to dermine the full year based on 1198 // defaultCenturyStart. 1199 int year = calendar.get(Calendar.YEAR); 1200 if (calendar.getTime().compareTo(defaultCenturyStart) < 0) 1201 calendar.set(Calendar.YEAR, year + 100); 1202 } 1203 if (! saw_timezone) 1204 { 1205 // Use the real rules to determine whether or not this 1206 // particular time is in daylight savings. 1207 calendar.clear (Calendar.DST_OFFSET); 1208 calendar.clear (Calendar.ZONE_OFFSET); 1209 } 1210 return calendar.getTime(); 1211 } 1212 catch (IllegalArgumentException x) 1213 { 1214 pos.setErrorIndex(pos.getIndex()); 1215 return null; 1216 } 1217 } 1218 1219 /** 1220 * <p> 1221 * Computes the time zone offset in milliseconds 1222 * relative to GMT, based on the supplied 1223 * <code>String</code> representation. 1224 * </p> 1225 * <p> 1226 * The supplied <code>String</code> must be a three 1227 * or four digit signed number, with an optional 'GMT' 1228 * prefix. The first one or two digits represents the hours, 1229 * while the last two represent the minutes. The 1230 * two sets of digits can optionally be separated by 1231 * ':'. The mandatory sign prefix (either '+' or '-') 1232 * indicates the direction of the offset from GMT. 1233 * </p> 1234 * <p> 1235 * For example, 'GMT+0200' specifies 2 hours after 1236 * GMT, while '-05:00' specifies 5 hours prior to 1237 * GMT. The special case of 'GMT' alone can be used 1238 * to represent the offset, 0. 1239 * </p> 1240 * <p> 1241 * If the <code>String</code> can not be parsed, 1242 * the result will be null. The resulting offset 1243 * is wrapped in an <code>Integer</code> object, in 1244 * order to allow such failure to be represented. 1245 * </p> 1246 * 1247 * @param zoneString a string in the form 1248 * (GMT)? sign hours : minutes 1249 * where sign = '+' or '-', hours 1250 * is a one or two digits representing 1251 * a number between 0 and 23, and 1252 * minutes is two digits representing 1253 * a number between 0 and 59. 1254 * @return the parsed offset, or null if parsing 1255 * failed. 1256 */ 1257 private Integer computeOffset(String zoneString, ParsePosition pos) 1258 { 1259 Pattern pattern = 1260 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})"); 1261 Matcher matcher = pattern.matcher(zoneString); 1262 1263 // Match from start, but ignore trailing parts 1264 boolean hasAll = matcher.lookingAt(); 1265 try 1266 { 1267 // Do we have at least the sign, hour and minute? 1268 matcher.group(2); 1269 matcher.group(4); 1270 matcher.group(5); 1271 } 1272 catch (IllegalStateException ise) 1273 { 1274 hasAll = false; 1275 } 1276 if (hasAll) 1277 { 1278 int sign = matcher.group(2).equals("+") ? 1 : -1; 1279 int hour = Integer.parseInt(matcher.group(4)); 1280 if (!matcher.group(3).equals("")) 1281 hour += (Integer.parseInt(matcher.group(3)) * 10); 1282 int minutes = Integer.parseInt(matcher.group(5)); 1283 1284 if (hour > 23) 1285 return null; 1286 int offset = sign * ((hour * 60) + minutes) * 60000; 1287 1288 // advance the index 1289 pos.setIndex(pos.getIndex() + matcher.end()); 1290 return Integer.valueOf(offset); 1291 } 1292 else if (zoneString.startsWith("GMT")) 1293 { 1294 pos.setIndex(pos.getIndex() + 3); 1295 return Integer.valueOf(0); 1296 } 1297 return null; 1298 } 1299 1300 // Compute the start of the current century as defined by 1301 // get2DigitYearStart. 1302 private void computeCenturyStart() 1303 { 1304 int year = calendar.get(Calendar.YEAR); 1305 calendar.set(Calendar.YEAR, year - 80); 1306 set2DigitYearStart(calendar.getTime()); 1307 } 1308 1309 /** 1310 * Returns a copy of this instance of 1311 * <code>SimpleDateFormat</code>. The copy contains 1312 * clones of the formatting symbols and the 2-digit 1313 * year century start date. 1314 */ 1315 public Object clone() 1316 { 1317 SimpleDateFormat clone = (SimpleDateFormat) super.clone(); 1318 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone()); 1319 clone.set2DigitYearStart((Date) defaultCenturyStart.clone()); 1320 return clone; 1321 } 1322 1323 } 1324