1 /* SimpleDateFormat.java -- A class for parsing/formating simple
3 Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005
4 Free Software Foundation, Inc.
6 This file is part of GNU Classpath.
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)
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.
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
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
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. */
43 import gnu
.java
.text
.AttributedFormatBuffer
;
44 import gnu
.java
.text
.FormatBuffer
;
45 import gnu
.java
.text
.FormatCharacterIterator
;
46 import gnu
.java
.text
.StringFormatBuffer
;
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
;
62 * SimpleDateFormat provides convenient methods for parsing and formatting
63 * dates using Gregorian calendars (see java.util.GregorianCalendar).
65 public class SimpleDateFormat
extends DateFormat
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.
73 private class CompiledField
76 * The ID of the field within the local pattern characters.
77 * Package private for use in out class.
82 * The size of the character sequence.
83 * Package private for use in out class.
90 private char character
;
93 * Constructs a compiled field using the
94 * the given field ID, size and character
97 * @param f the field ID.
98 * @param s the size of the field.
99 * @param c the character used.
101 public CompiledField(int f
, int s
, char c
)
109 * Retrieves the ID of the field relative to
110 * the local pattern characters.
112 public int getField()
118 * Retrieves the size of the character sequence.
126 * Retrieves the character used in the sequence.
128 public char getCharacter()
134 * Returns a <code>String</code> representation
135 * of the compiled field, primarily for debugging
138 * @return a <code>String</code> representation.
140 public String
toString()
142 StringBuilder builder
;
144 builder
= new StringBuilder(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
);
153 return builder
.toString();
158 * A list of <code>CompiledField</code>s,
159 * representing the compiled version of the pattern.
164 private transient ArrayList tokens
;
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.
171 * @see DateFormatSymbols
172 * @serial The localisation data. May not be null.
174 private DateFormatSymbols formatData
;
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.
183 * @see #get2DigitYearStart()
184 * @see #set2DigitYearStart(java.util.Date)
186 * @serial The start date of the century for parsing two digit years.
189 private Date defaultCenturyStart
;
192 * The year at which interpretation of two
193 * digit years starts.
195 * @see #get2DigitYearStart()
196 * @see #set2DigitYearStart(java.util.Date)
199 private transient int defaultCentury
;
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.
207 * @see #applyPattern(String)
208 * @see #applyLocalizedPattern(String)
210 * @see #toLocalizedPattern()
211 * @serial The non-localized pattern string. May not be null.
213 private String pattern
;
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
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.
227 private int serialVersionOnStream
= 1; // 0 indicates JDK1.1.3 or earlier
232 private static final long serialVersionUID
= 4774881970558875024L;
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";
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.
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.
253 private void readObject(ObjectInputStream stream
)
254 throws IOException
, ClassNotFoundException
256 stream
.defaultReadObject();
257 if (serialVersionOnStream
< 1)
259 computeCenturyStart ();
260 serialVersionOnStream
= 1;
263 // Ensure that defaultCentury gets set.
264 set2DigitYearStart(defaultCenturyStart
);
266 // Set up items normally taken care of by the constructor.
267 tokens
= new ArrayList();
270 compileFormat(pattern
);
272 catch (IllegalArgumentException e
)
274 throw new InvalidObjectException("The stream pattern was invalid.");
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.
284 * @param pattern the non-localized pattern to compile.
285 * @throws IllegalArgumentException if the pattern is invalid.
287 private void compileFormat(String pattern
)
289 // Any alphabetical characters are treated as pattern characters
290 // unless enclosed in single quotes.
295 CompiledField current
= null;
297 for (int i
= 0; i
< pattern
.length(); i
++)
299 thisChar
= pattern
.charAt(i
);
300 field
= standardChars
.indexOf(thisChar
);
304 if ((thisChar
>= 'A' && thisChar
<= 'Z')
305 || (thisChar
>= 'a' && thisChar
<= 'z'))
307 // Not a valid letter
308 throw new IllegalArgumentException("Invalid letter "
310 " encountered at character "
313 else if (thisChar
== '\'')
315 // Quoted text section; skip to next single quote
316 pos
= pattern
.indexOf('\'', i
+ 1);
317 // First look for '' -- meaning a single quote.
322 // Look for the terminating quote. However, if we
323 // see a '', that represents a literal quote and
325 StringBuilder buf
= new StringBuilder();
330 throw new IllegalArgumentException("Quotes starting at character "
333 buf
.append(pattern
.substring(oldPos
, pos
));
334 if (pos
+ 1 >= pattern
.length()
335 || pattern
.charAt(pos
+ 1) != '\'')
339 pos
= pattern
.indexOf('\'', pos
+ 2);
342 tokens
.add(buf
.toString());
348 // A special character
349 tokens
.add(Character
.valueOf(thisChar
));
355 if ((current
!= null) && (field
== current
.field
))
359 current
= new CompiledField(field
, 1, thisChar
);
367 * Returns a string representation of this
370 * @return a string representation of the <code>SimpleDateFormat</code>
373 public String
toString()
375 StringBuilder output
= new StringBuilder(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
);
391 return output
.toString();
395 * Constructs a SimpleDateFormat using the default pattern for
396 * the default locale.
398 public SimpleDateFormat()
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.
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);
421 * Creates a date formatter using the specified non-localized pattern,
422 * with the default DateFormatSymbols for the default locale.
424 * @param pattern the pattern to use.
425 * @throws NullPointerException if the pattern is null.
426 * @throws IllegalArgumentException if the pattern is invalid.
428 public SimpleDateFormat(String pattern
)
430 this(pattern
, Locale
.getDefault());
434 * Creates a date formatter using the specified non-localized pattern,
435 * with the default DateFormatSymbols for the given locale.
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.
442 public SimpleDateFormat(String pattern
, Locale locale
)
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);
458 * Creates a date formatter using the specified non-localized
459 * pattern. The specified DateFormatSymbols will be used when
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.
467 public SimpleDateFormat(String pattern
, DateFormatSymbols formatData
)
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);
485 * This method returns a string with the formatting pattern being used
486 * by this object. This string is unlocalized.
488 * @return The format string.
490 public String
toPattern()
496 * This method returns a string with the formatting pattern being used
497 * by this object. This string is localized.
499 * @return The format string.
501 public String
toLocalizedPattern()
503 String localChars
= formatData
.getLocalPatternChars();
504 return translateLocalizedPattern(pattern
, standardChars
, localChars
);
508 * This method sets the formatting pattern that should be used by this
509 * object. This string is not localized.
511 * @param pattern The new format pattern.
512 * @throws NullPointerException if the pattern is null.
513 * @throws IllegalArgumentException if the pattern is invalid.
515 public void applyPattern(String pattern
)
517 tokens
= new ArrayList();
518 compileFormat(pattern
);
519 this.pattern
= pattern
;
523 * This method sets the formatting pattern that should be used by this
524 * object. This string is localized.
526 * @param pattern The new format pattern.
527 * @throws NullPointerException if the pattern is null.
528 * @throws IllegalArgumentException if the pattern is invalid.
530 public void applyLocalizedPattern(String pattern
)
532 String localChars
= formatData
.getLocalPatternChars();
533 pattern
= translateLocalizedPattern(pattern
, localChars
, standardChars
);
534 applyPattern(pattern
);
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
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
550 * @return a version of the pattern using the characters in
551 * <code>newChars</code>.
553 private String
translateLocalizedPattern(String pattern
,
554 String oldChars
, String newChars
)
556 int len
= pattern
.length();
557 StringBuilder buf
= new StringBuilder(len
);
558 boolean quoted
= false;
559 for (int i
= 0; i
< len
; i
++)
561 char ch
= pattern
.charAt(i
);
566 int j
= oldChars
.indexOf(ch
);
568 ch
= newChars
.charAt(j
);
572 return buf
.toString();
576 * Returns the start of the century used for two digit years.
578 * @return A <code>Date</code> representing the start of the century
579 * for two digit years.
581 public Date
get2DigitYearStart()
583 return defaultCenturyStart
;
587 * Sets the start of the century used for two digit years.
589 * @param date A <code>Date</code> representing the start of the century for
592 public void set2DigitYearStart(Date date
)
594 defaultCenturyStart
= date
;
596 calendar
.setTime(date
);
597 int year
= calendar
.get(Calendar
.YEAR
);
598 defaultCentury
= year
- (year
% 100);
602 * This method returns a copy of the format symbol information used
603 * for parsing and formatting dates.
605 * @return a copy of the date format symbols.
607 public DateFormatSymbols
getDateFormatSymbols()
609 return (DateFormatSymbols
) formatData
.clone();
613 * This method sets the format symbols information used for parsing
614 * and formatting dates.
616 * @param formatData The date format symbols.
617 * @throws NullPointerException if <code>formatData</code> is null.
619 public void setDateFormatSymbols(DateFormatSymbols formatData
)
621 if (formatData
== null)
624 NullPointerException("The supplied format data was null.");
626 this.formatData
= formatData
;
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:
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>)
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>
643 * @param o The object to compare for equality against.
645 * @return <code>true</code> if the specified object is equal to this object,
646 * <code>false</code> otherwise.
648 public boolean equals(Object o
)
650 if (!super.equals(o
))
653 if (!(o
instanceof SimpleDateFormat
))
656 SimpleDateFormat sdf
= (SimpleDateFormat
)o
;
658 if (defaultCentury
!= sdf
.defaultCentury
)
661 if (!toPattern().equals(sdf
.toPattern()))
664 if (!getDateFormatSymbols().equals(sdf
.getDateFormatSymbols()))
671 * This method returns a hash value for this object.
673 * @return A hash value for this object.
675 public int hashCode()
677 return super.hashCode() ^
toPattern().hashCode() ^ defaultCentury ^
678 getDateFormatSymbols().hashCode();
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.
687 private void formatWithAttribute(Date date
, FormatBuffer buffer
, FieldPosition pos
)
690 AttributedCharacterIterator
.Attribute attribute
;
691 calendar
.setTime(date
);
693 // go through vector, filling in fields where applicable, else toString
694 Iterator iter
= tokens
.iterator();
695 while (iter
.hasNext())
697 Object o
= iter
.next();
698 if (o
instanceof CompiledField
)
700 CompiledField cf
= (CompiledField
) o
;
701 int beginIndex
= buffer
.length();
703 switch (cf
.getField())
706 buffer
.append (formatData
.eras
[calendar
.get (Calendar
.ERA
)], DateFormat
.Field
.ERA
);
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)
714 temp
= "00"+String
.valueOf (calendar
.get (Calendar
.YEAR
));
715 buffer
.append (temp
.substring (temp
.length() - 2));
718 withLeadingZeros (calendar
.get (Calendar
.YEAR
), cf
.getSize(), buffer
);
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
)]);
727 buffer
.append (formatData
.months
[calendar
.get (Calendar
.MONTH
)]);
730 buffer
.setDefaultAttribute (DateFormat
.Field
.DAY_OF_MONTH
);
731 withLeadingZeros (calendar
.get (Calendar
.DATE
), cf
.getSize(), buffer
);
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
);
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
);
743 buffer
.setDefaultAttribute (DateFormat
.Field
.MINUTE
);
744 withLeadingZeros (calendar
.get (Calendar
.MINUTE
),
745 cf
.getSize(), buffer
);
748 buffer
.setDefaultAttribute (DateFormat
.Field
.SECOND
);
749 withLeadingZeros(calendar
.get (Calendar
.SECOND
),
750 cf
.getSize(), buffer
);
752 case MILLISECOND_FIELD
:
753 buffer
.setDefaultAttribute (DateFormat
.Field
.MILLISECOND
);
754 withLeadingZeros (calendar
.get (Calendar
.MILLISECOND
), cf
.getSize(), buffer
);
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
)]);
761 buffer
.append (formatData
.weekdays
[calendar
.get (Calendar
.DAY_OF_WEEK
)]);
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
);
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
);
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
);
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
);
783 buffer
.setDefaultAttribute (DateFormat
.Field
.AM_PM
);
784 buffer
.append (formatData
.ampms
[calendar
.get (Calendar
.AM_PM
)]);
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
);
791 case HOUR0_FIELD
: // 0-11
792 buffer
.setDefaultAttribute (DateFormat
.Field
.HOUR0
);
793 withLeadingZeros (calendar
.get (Calendar
.HOUR
), cf
.getSize(), buffer
);
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
);
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;
813 withLeadingZeros(hours
, 2, buffer
);
814 withLeadingZeros(minutes
, 2, buffer
);
817 throw new IllegalArgumentException ("Illegal pattern character " +
820 if (pos
!= null && (buffer
.getDefaultAttribute() == pos
.getFieldAttribute()
821 || cf
.getField() == pos
.getField()))
823 pos
.setBeginIndex(beginIndex
);
824 pos
.setEndIndex(buffer
.length());
829 buffer
.append(o
.toString(), null);
834 public StringBuffer
format(Date date
, StringBuffer buffer
, FieldPosition pos
)
836 formatWithAttribute(date
, new StringFormatBuffer (buffer
), pos
);
841 public AttributedCharacterIterator
formatToCharacterIterator(Object date
)
842 throws IllegalArgumentException
845 throw new NullPointerException("null argument");
846 if (!(date
instanceof Date
))
847 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
849 AttributedFormatBuffer buf
= new AttributedFormatBuffer();
850 formatWithAttribute((Date
)date
, buf
,
854 return new FormatCharacterIterator(buf
.getBuffer().toString(),
856 buf
.getAttributes());
859 private void withLeadingZeros(int value
, int length
, FormatBuffer buffer
)
861 String valStr
= String
.valueOf(value
);
862 for (length
-= valStr
.length(); length
> 0; length
--)
864 buffer
.append(valStr
);
867 private boolean expect(String source
, ParsePosition pos
, char ch
)
869 int x
= pos
.getIndex();
870 boolean r
= x
< source
.length() && source
.charAt(x
) == ch
;
874 pos
.setErrorIndex(x
);
879 * This method parses the specified string into a date.
881 * @param dateStr The date string to parse.
882 * @param pos The input and output parse position
884 * @return The parsed date, or <code>null</code> if the string cannot be
887 public Date
parse (String dateStr
, ParsePosition pos
)
890 int fmt_max
= pattern
.length();
893 boolean saw_timezone
= false;
894 int quote_start
= -1;
895 boolean is2DigitYear
= false;
898 for (; fmt_index
< fmt_max
; ++fmt_index
)
900 char ch
= pattern
.charAt(fmt_index
);
903 int index
= pos
.getIndex();
904 if (fmt_index
< fmt_max
- 1
905 && pattern
.charAt(fmt_index
+ 1) == '\'')
907 if (! expect (dateStr
, pos
, ch
))
912 quote_start
= quote_start
< 0 ? fmt_index
: -1;
916 if (quote_start
!= -1
917 || ((ch
< 'a' || ch
> 'z')
918 && (ch
< 'A' || ch
> 'Z')))
920 if (quote_start
== -1 && ch
== ' ')
922 // A single unquoted space in the pattern may match
923 // any number of spaces in the input.
924 int index
= pos
.getIndex();
926 while (index
< dateStr
.length()
927 && Character
.isWhitespace(dateStr
.charAt(index
)))
933 // Didn't see any whitespace.
934 pos
.setErrorIndex(index
);
938 else if (! expect (dateStr
, pos
, ch
))
943 // We've arrived at a potential pattern character in the
946 while (++fmt_index
< fmt_max
&& pattern
.charAt(fmt_index
) == ch
)
951 // We might need to limit the number of digits to parse in
952 // some cases. We look to the next pattern character to
954 boolean limit_digits
= false;
955 if (fmt_index
< fmt_max
956 && standardChars
.indexOf(pattern
.charAt(fmt_index
)) >= 0)
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.
966 boolean is_numeric
= true;
968 boolean maybe2DigitYear
= false;
969 boolean oneBasedHour
= false;
970 boolean oneBasedHourOfDay
= false;
971 Integer simpleOffset
;
972 String
[] set1
= null;
973 String
[] set2
= null;
977 calendar_field
= Calendar
.DATE
;
980 calendar_field
= Calendar
.DAY_OF_YEAR
;
983 calendar_field
= Calendar
.DAY_OF_WEEK_IN_MONTH
;
988 calendar_field
= Calendar
.DAY_OF_WEEK
;
989 set1
= formatData
.getWeekdays();
990 set2
= formatData
.getShortWeekdays();
993 calendar_field
= Calendar
.WEEK_OF_YEAR
;
996 calendar_field
= Calendar
.WEEK_OF_MONTH
;
999 calendar_field
= Calendar
.MONTH
;
1005 set1
= formatData
.getMonths();
1006 set2
= formatData
.getShortMonths();
1010 calendar_field
= Calendar
.YEAR
;
1012 maybe2DigitYear
= true;
1015 calendar_field
= Calendar
.HOUR
;
1018 calendar_field
= Calendar
.HOUR
;
1019 oneBasedHour
= true;
1022 calendar_field
= Calendar
.HOUR_OF_DAY
;
1025 calendar_field
= Calendar
.HOUR_OF_DAY
;
1026 oneBasedHourOfDay
= true;
1029 calendar_field
= Calendar
.MINUTE
;
1032 calendar_field
= Calendar
.SECOND
;
1035 calendar_field
= Calendar
.MILLISECOND
;
1039 calendar_field
= Calendar
.AM_PM
;
1040 set1
= formatData
.getAmPmStrings();
1044 // We need a special case for the timezone, because it
1045 // uses a different data structure than the other cases.
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)
1056 saw_timezone
= true;
1057 calendar
.set(Calendar
.DST_OFFSET
, 0);
1058 offset
= simpleOffset
.intValue();
1062 for (int j
= 0; j
< zoneCount
; j
++)
1064 String
[] strings
= zoneStrings
[j
];
1066 for (k
= 0; k
< strings
.length
; ++k
)
1068 if (dateStr
.startsWith(strings
[k
], index
))
1071 if (k
!= strings
.length
)
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());
1080 calendar
.set (Calendar
.DST_OFFSET
, 0);
1081 offset
= tz
.getRawOffset ();
1082 pos
.setIndex(index
+ strings
[k
].length());
1089 pos
.setErrorIndex(pos
.getIndex());
1094 pos
.setErrorIndex(pos
.getIndex());
1098 // Compute the value we should assign to the field.
1103 numberFormat
.setMinimumIntegerDigits(fmt_count
);
1104 if (maybe2DigitYear
)
1105 index
= pos
.getIndex();
1109 // numberFormat.setMaximumIntegerDigits(fmt_count) may
1110 // not work as expected. So we explicitly use substring
1112 int origPos
= pos
.getIndex();
1114 n
= numberFormat
.parse(dateStr
.substring(origPos
, origPos
+ fmt_count
), pos
);
1115 pos
.setIndex(origPos
+ pos
.getIndex());
1118 n
= numberFormat
.parse(dateStr
, pos
);
1119 if (pos
== null || ! (n
instanceof Long
))
1121 value
= n
.intValue() + offset
;
1123 else if (set1
!= null)
1125 index
= pos
.getIndex();
1127 boolean found
= false;
1128 for (i
= offset
; i
< set1
.length
; ++i
)
1130 if (set1
[i
] != null)
1131 if (dateStr
.toUpperCase().startsWith(set1
[i
].toUpperCase(),
1135 pos
.setIndex(index
+ set1
[i
].length());
1139 if (!found
&& set2
!= null)
1141 for (i
= offset
; i
< set2
.length
; ++i
)
1143 if (set2
[i
] != null)
1144 if (dateStr
.toUpperCase().startsWith(set2
[i
].toUpperCase(),
1148 pos
.setIndex(index
+ set2
[i
].length());
1155 pos
.setErrorIndex(index
);
1163 if (maybe2DigitYear
)
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)
1170 is2DigitYear
= true;
1171 value
+= defaultCentury
;
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)
1180 if (oneBasedHourOfDay
&& value
== 24)
1183 // Assign the value and move on.
1184 calendar
.set(calendar_field
, value
);
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);
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
);
1202 return calendar
.getTime();
1204 catch (IllegalArgumentException x
)
1206 pos
.setErrorIndex(pos
.getIndex());
1213 * Computes the time zone offset in milliseconds
1214 * relative to GMT, based on the supplied
1215 * <code>String</code> representation.
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.
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.
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.
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
1249 private Integer
computeOffset(String zoneString
, ParsePosition pos
)
1252 Pattern
.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1253 Matcher matcher
= pattern
.matcher(zoneString
);
1255 // Match from start, but ignore trailing parts
1256 boolean hasAll
= matcher
.lookingAt();
1259 // Do we have at least the sign, hour and minute?
1264 catch (IllegalStateException ise
)
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));
1278 int offset
= sign
* ((hour
* 60) + minutes
) * 60000;
1280 // advance the index
1281 pos
.setIndex(pos
.getIndex() + matcher
.end());
1282 return Integer
.valueOf(offset
);
1284 else if (zoneString
.startsWith("GMT"))
1286 pos
.setIndex(pos
.getIndex() + 3);
1287 return Integer
.valueOf(0);
1292 // Compute the start of the current century as defined by
1293 // get2DigitYearStart.
1294 private void computeCenturyStart()
1296 int year
= calendar
.get(Calendar
.YEAR
);
1297 calendar
.set(Calendar
.YEAR
, year
- 80);
1298 set2DigitYearStart(calendar
.getTime());
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.
1307 public Object
clone()
1309 SimpleDateFormat clone
= (SimpleDateFormat
) super.clone();
1310 clone
.setDateFormatSymbols((DateFormatSymbols
) formatData
.clone());
1311 clone
.set2DigitYearStart((Date
) defaultCenturyStart
.clone());