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 StringBuffer builder
;
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
);
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 StringBuffer buf
= new StringBuffer();
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(new Character(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 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
);
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 StringBuffer buf
= new StringBuffer(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 int hours
= pureMinutes
/ 60;
810 int minutes
= pureMinutes
% 60;
812 withLeadingZeros(hours
, 2, buffer
);
813 withLeadingZeros(minutes
, 2, buffer
);
816 throw new IllegalArgumentException ("Illegal pattern character " +
819 if (pos
!= null && (buffer
.getDefaultAttribute() == pos
.getFieldAttribute()
820 || cf
.getField() == pos
.getField()))
822 pos
.setBeginIndex(beginIndex
);
823 pos
.setEndIndex(buffer
.length());
828 buffer
.append(o
.toString(), null);
833 public StringBuffer
format(Date date
, StringBuffer buffer
, FieldPosition pos
)
835 formatWithAttribute(date
, new StringFormatBuffer (buffer
), pos
);
840 public AttributedCharacterIterator
formatToCharacterIterator(Object date
)
841 throws IllegalArgumentException
844 throw new NullPointerException("null argument");
845 if (!(date
instanceof Date
))
846 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
848 AttributedFormatBuffer buf
= new AttributedFormatBuffer();
849 formatWithAttribute((Date
)date
, buf
,
853 return new FormatCharacterIterator(buf
.getBuffer().toString(),
855 buf
.getAttributes());
858 private void withLeadingZeros(int value
, int length
, FormatBuffer buffer
)
860 String valStr
= String
.valueOf(value
);
861 for (length
-= valStr
.length(); length
> 0; length
--)
863 buffer
.append(valStr
);
866 private boolean expect(String source
, ParsePosition pos
, char ch
)
868 int x
= pos
.getIndex();
869 boolean r
= x
< source
.length() && source
.charAt(x
) == ch
;
873 pos
.setErrorIndex(x
);
878 * This method parses the specified string into a date.
880 * @param dateStr The date string to parse.
881 * @param pos The input and output parse position
883 * @return The parsed date, or <code>null</code> if the string cannot be
886 public Date
parse (String dateStr
, ParsePosition pos
)
889 int fmt_max
= pattern
.length();
892 boolean saw_timezone
= false;
893 int quote_start
= -1;
894 boolean is2DigitYear
= false;
897 for (; fmt_index
< fmt_max
; ++fmt_index
)
899 char ch
= pattern
.charAt(fmt_index
);
902 int index
= pos
.getIndex();
903 if (fmt_index
< fmt_max
- 1
904 && pattern
.charAt(fmt_index
+ 1) == '\'')
906 if (! expect (dateStr
, pos
, ch
))
911 quote_start
= quote_start
< 0 ? fmt_index
: -1;
915 if (quote_start
!= -1
916 || ((ch
< 'a' || ch
> 'z')
917 && (ch
< 'A' || ch
> 'Z')))
919 if (! expect (dateStr
, pos
, ch
))
924 // We've arrived at a potential pattern character in the
927 while (++fmt_index
< fmt_max
&& pattern
.charAt(fmt_index
) == ch
)
932 // We might need to limit the number of digits to parse in
933 // some cases. We look to the next pattern character to
935 boolean limit_digits
= false;
936 if (fmt_index
< fmt_max
937 && standardChars
.indexOf(pattern
.charAt(fmt_index
)) >= 0)
941 // We can handle most fields automatically: most either are
942 // numeric or are looked up in a string vector. In some cases
943 // we need an offset. When numeric, `offset' is added to the
944 // resulting value. When doing a string lookup, offset is the
945 // initial index into the string array.
947 boolean is_numeric
= true;
949 boolean maybe2DigitYear
= false;
950 boolean oneBasedHour
= false;
951 boolean oneBasedHourOfDay
= false;
952 Integer simpleOffset
;
953 String
[] set1
= null;
954 String
[] set2
= null;
958 calendar_field
= Calendar
.DATE
;
961 calendar_field
= Calendar
.DAY_OF_YEAR
;
964 calendar_field
= Calendar
.DAY_OF_WEEK_IN_MONTH
;
969 calendar_field
= Calendar
.DAY_OF_WEEK
;
970 set1
= formatData
.getWeekdays();
971 set2
= formatData
.getShortWeekdays();
974 calendar_field
= Calendar
.WEEK_OF_YEAR
;
977 calendar_field
= Calendar
.WEEK_OF_MONTH
;
980 calendar_field
= Calendar
.MONTH
;
986 set1
= formatData
.getMonths();
987 set2
= formatData
.getShortMonths();
991 calendar_field
= Calendar
.YEAR
;
993 maybe2DigitYear
= true;
996 calendar_field
= Calendar
.HOUR
;
999 calendar_field
= Calendar
.HOUR
;
1000 oneBasedHour
= true;
1003 calendar_field
= Calendar
.HOUR_OF_DAY
;
1006 calendar_field
= Calendar
.HOUR_OF_DAY
;
1007 oneBasedHourOfDay
= true;
1010 calendar_field
= Calendar
.MINUTE
;
1013 calendar_field
= Calendar
.SECOND
;
1016 calendar_field
= Calendar
.MILLISECOND
;
1020 calendar_field
= Calendar
.AM_PM
;
1021 set1
= formatData
.getAmPmStrings();
1025 // We need a special case for the timezone, because it
1026 // uses a different data structure than the other cases.
1028 calendar_field
= Calendar
.ZONE_OFFSET
;
1029 String
[][] zoneStrings
= formatData
.getZoneStrings();
1030 int zoneCount
= zoneStrings
.length
;
1031 int index
= pos
.getIndex();
1032 boolean found_zone
= false;
1033 simpleOffset
= computeOffset(dateStr
.substring(index
), pos
);
1034 if (simpleOffset
!= null)
1037 saw_timezone
= true;
1038 calendar
.set(Calendar
.DST_OFFSET
, 0);
1039 offset
= simpleOffset
.intValue();
1043 for (int j
= 0; j
< zoneCount
; j
++)
1045 String
[] strings
= zoneStrings
[j
];
1047 for (k
= 0; k
< strings
.length
; ++k
)
1049 if (dateStr
.startsWith(strings
[k
], index
))
1052 if (k
!= strings
.length
)
1055 saw_timezone
= true;
1056 TimeZone tz
= TimeZone
.getTimeZone (strings
[0]);
1057 // Check if it's a DST zone or ordinary
1058 if(k
== 3 || k
== 4)
1059 calendar
.set (Calendar
.DST_OFFSET
, tz
.getDSTSavings());
1061 calendar
.set (Calendar
.DST_OFFSET
, 0);
1062 offset
= tz
.getRawOffset ();
1063 pos
.setIndex(index
+ strings
[k
].length());
1070 pos
.setErrorIndex(pos
.getIndex());
1075 pos
.setErrorIndex(pos
.getIndex());
1079 // Compute the value we should assign to the field.
1084 numberFormat
.setMinimumIntegerDigits(fmt_count
);
1086 numberFormat
.setMaximumIntegerDigits(fmt_count
);
1087 if (maybe2DigitYear
)
1088 index
= pos
.getIndex();
1089 Number n
= numberFormat
.parse(dateStr
, pos
);
1090 if (pos
== null || ! (n
instanceof Long
))
1092 value
= n
.intValue() + offset
;
1094 else if (set1
!= null)
1096 index
= pos
.getIndex();
1098 boolean found
= false;
1099 for (i
= offset
; i
< set1
.length
; ++i
)
1101 if (set1
[i
] != null)
1102 if (dateStr
.toUpperCase().startsWith(set1
[i
].toUpperCase(),
1106 pos
.setIndex(index
+ set1
[i
].length());
1110 if (!found
&& set2
!= null)
1112 for (i
= offset
; i
< set2
.length
; ++i
)
1114 if (set2
[i
] != null)
1115 if (dateStr
.toUpperCase().startsWith(set2
[i
].toUpperCase(),
1119 pos
.setIndex(index
+ set2
[i
].length());
1126 pos
.setErrorIndex(index
);
1134 if (maybe2DigitYear
)
1136 // Parse into default century if the numeric year string has
1137 // exactly 2 digits.
1138 int digit_count
= pos
.getIndex() - index
;
1139 if (digit_count
== 2)
1141 is2DigitYear
= true;
1142 value
+= defaultCentury
;
1146 // Calendar uses 0-based hours.
1147 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1148 if (oneBasedHour
&& value
== 12)
1151 if (oneBasedHourOfDay
&& value
== 24)
1154 // Assign the value and move on.
1155 calendar
.set(calendar_field
, value
);
1160 // Apply the 80-20 heuristic to dermine the full year based on
1161 // defaultCenturyStart.
1162 int year
= calendar
.get(Calendar
.YEAR
);
1163 if (calendar
.getTime().compareTo(defaultCenturyStart
) < 0)
1164 calendar
.set(Calendar
.YEAR
, year
+ 100);
1168 // Use the real rules to determine whether or not this
1169 // particular time is in daylight savings.
1170 calendar
.clear (Calendar
.DST_OFFSET
);
1171 calendar
.clear (Calendar
.ZONE_OFFSET
);
1173 return calendar
.getTime();
1175 catch (IllegalArgumentException x
)
1177 pos
.setErrorIndex(pos
.getIndex());
1184 * Computes the time zone offset in milliseconds
1185 * relative to GMT, based on the supplied
1186 * <code>String</code> representation.
1189 * The supplied <code>String</code> must be a three
1190 * or four digit signed number, with an optional 'GMT'
1191 * prefix. The first one or two digits represents the hours,
1192 * while the last two represent the minutes. The
1193 * two sets of digits can optionally be separated by
1194 * ':'. The mandatory sign prefix (either '+' or '-')
1195 * indicates the direction of the offset from GMT.
1198 * For example, 'GMT+0200' specifies 2 hours after
1199 * GMT, while '-05:00' specifies 5 hours prior to
1200 * GMT. The special case of 'GMT' alone can be used
1201 * to represent the offset, 0.
1204 * If the <code>String</code> can not be parsed,
1205 * the result will be null. The resulting offset
1206 * is wrapped in an <code>Integer</code> object, in
1207 * order to allow such failure to be represented.
1210 * @param zoneString a string in the form
1211 * (GMT)? sign hours : minutes
1212 * where sign = '+' or '-', hours
1213 * is a one or two digits representing
1214 * a number between 0 and 23, and
1215 * minutes is two digits representing
1216 * a number between 0 and 59.
1217 * @return the parsed offset, or null if parsing
1220 private Integer
computeOffset(String zoneString
, ParsePosition pos
)
1223 Pattern
.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1224 Matcher matcher
= pattern
.matcher(zoneString
);
1226 // Match from start, but ignore trailing parts
1227 boolean hasAll
= matcher
.lookingAt();
1230 // Do we have at least the sign, hour and minute?
1235 catch (IllegalStateException ise
)
1241 int sign
= matcher
.group(2).equals("+") ?
1 : -1;
1242 int hour
= Integer
.parseInt(matcher
.group(4));
1243 if (!matcher
.group(3).equals(""))
1244 hour
+= (Integer
.parseInt(matcher
.group(3)) * 10);
1245 int minutes
= Integer
.parseInt(matcher
.group(5));
1249 int offset
= sign
* ((hour
* 60) + minutes
) * 60000;
1251 // advance the index
1252 pos
.setIndex(pos
.getIndex() + matcher
.end());
1253 return new Integer(offset
);
1255 else if (zoneString
.startsWith("GMT"))
1257 pos
.setIndex(pos
.getIndex() + 3);
1258 return new Integer(0);
1263 // Compute the start of the current century as defined by
1264 // get2DigitYearStart.
1265 private void computeCenturyStart()
1267 int year
= calendar
.get(Calendar
.YEAR
);
1268 calendar
.set(Calendar
.YEAR
, year
- 80);
1269 set2DigitYearStart(calendar
.getTime());
1273 * Returns a copy of this instance of
1274 * <code>SimpleDateFormat</code>. The copy contains
1275 * clones of the formatting symbols and the 2-digit
1276 * year century start date.
1278 public Object
clone()
1280 SimpleDateFormat clone
= (SimpleDateFormat
) super.clone();
1281 clone
.setDateFormatSymbols((DateFormatSymbols
) formatData
.clone());
1282 clone
.set2DigitYearStart((Date
) defaultCenturyStart
.clone());