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