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
.lang
.CPStringBuilder
;
45 import gnu
.java
.text
.AttributedFormatBuffer
;
46 import gnu
.java
.text
.FormatBuffer
;
47 import gnu
.java
.text
.FormatCharacterIterator
;
48 import gnu
.java
.text
.StringFormatBuffer
;
50 import java
.io
.IOException
;
51 import java
.io
.InvalidObjectException
;
52 import java
.io
.ObjectInputStream
;
53 import java
.util
.ArrayList
;
54 import java
.util
.Calendar
;
55 import java
.util
.Date
;
56 import java
.util
.GregorianCalendar
;
57 import java
.util
.Iterator
;
58 import java
.util
.Locale
;
59 import java
.util
.TimeZone
;
60 import java
.util
.regex
.Matcher
;
61 import java
.util
.regex
.Pattern
;
64 * SimpleDateFormat provides convenient methods for parsing and formatting
65 * dates using Gregorian calendars (see java.util.GregorianCalendar).
66 * This class is not thread-safe; external synchronisation should be applied
67 * if an instance is to be accessed from multiple threads.
69 public class SimpleDateFormat
extends DateFormat
72 * This class is used by <code>SimpleDateFormat</code> as a
73 * compiled representation of a format string. The field
74 * ID, size, and character used are stored for each sequence
75 * of pattern characters.
77 private class CompiledField
80 * The ID of the field within the local pattern characters.
81 * Package private for use in out class.
86 * The size of the character sequence.
87 * Package private for use in out class.
94 private char character
;
97 * Constructs a compiled field using the
98 * the given field ID, size and character
101 * @param f the field ID.
102 * @param s the size of the field.
103 * @param c the character used.
105 public CompiledField(int f
, int s
, char c
)
113 * Retrieves the ID of the field relative to
114 * the local pattern characters.
116 public int getField()
122 * Retrieves the size of the character sequence.
130 * Retrieves the character used in the sequence.
132 public char getCharacter()
138 * Returns a <code>String</code> representation
139 * of the compiled field, primarily for debugging
142 * @return a <code>String</code> representation.
144 public String
toString()
146 CPStringBuilder builder
;
148 builder
= new CPStringBuilder(getClass().getName());
149 builder
.append("[field=");
150 builder
.append(field
);
151 builder
.append(", size=");
152 builder
.append(size
);
153 builder
.append(", character=");
154 builder
.append(character
);
157 return builder
.toString();
162 * A list of <code>CompiledField</code>s,
163 * representing the compiled version of the pattern.
168 private transient ArrayList tokens
;
171 * The localised data used in formatting,
172 * such as the day and month names in the local
173 * language, and the localized pattern characters.
175 * @see DateFormatSymbols
176 * @serial The localisation data. May not be null.
178 private DateFormatSymbols formatData
;
181 * The date representing the start of the century
182 * used for interpreting two digit years. For
183 * example, 24/10/2004 would cause two digit
184 * years to be interpreted as representing
185 * the years between 2004 and 2104.
187 * @see #get2DigitYearStart()
188 * @see #set2DigitYearStart(java.util.Date)
190 * @serial The start date of the century for parsing two digit years.
193 private Date defaultCenturyStart
;
196 * The year at which interpretation of two
197 * digit years starts.
199 * @see #get2DigitYearStart()
200 * @see #set2DigitYearStart(java.util.Date)
203 private transient int defaultCentury
;
206 * The non-localized pattern string. This
207 * only ever contains the pattern characters
208 * stored in standardChars. Localized patterns
209 * are translated to this form.
211 * @see #applyPattern(String)
212 * @see #applyLocalizedPattern(String)
214 * @see #toLocalizedPattern()
215 * @serial The non-localized pattern string. May not be null.
217 private String pattern
;
220 * The version of serialized data used by this class.
221 * Version 0 only includes the pattern and formatting
222 * data. Version 1 adds the start date for interpreting
225 * @serial This specifies the version of the data being serialized.
226 * Version 0 (or no version) specifies just <code>pattern</code>
227 * and <code>formatData</code>. Version 1 adds
228 * the <code>defaultCenturyStart</code>. This implementation
229 * always writes out version 1 data.
231 private int serialVersionOnStream
= 1; // 0 indicates JDK1.1.3 or earlier
236 private static final long serialVersionUID
= 4774881970558875024L;
238 // This string is specified in the Java class libraries.
239 private static final String standardChars
= "GyMdkHmsSEDFwWahKzZ";
242 * Represents the position of the RFC822 timezone pattern character
243 * in the array of localized pattern characters. In the
244 * U.S. locale, this is 'Z'. The value is the offset of the current
245 * time from GMT e.g. -0500 would be five hours prior to GMT.
247 private static final int RFC822_TIMEZONE_FIELD
= 18;
250 * Reads the serialized version of this object.
251 * If the serialized data is only version 0,
252 * then the date for the start of the century
253 * for interpreting two digit years is computed.
254 * The pattern is parsed and compiled following the process
255 * of reading in the serialized data.
257 * @param stream the object stream to read the data from.
258 * @throws IOException if an I/O error occurs.
259 * @throws ClassNotFoundException if the class of the serialized data
260 * could not be found.
261 * @throws InvalidObjectException if the pattern is invalid.
263 private void readObject(ObjectInputStream stream
)
264 throws IOException
, ClassNotFoundException
266 stream
.defaultReadObject();
267 if (serialVersionOnStream
< 1)
269 computeCenturyStart ();
270 serialVersionOnStream
= 1;
273 // Ensure that defaultCentury gets set.
274 set2DigitYearStart(defaultCenturyStart
);
276 // Set up items normally taken care of by the constructor.
277 tokens
= new ArrayList();
280 compileFormat(pattern
);
282 catch (IllegalArgumentException e
)
284 throw new InvalidObjectException("The stream pattern was invalid.");
289 * Compiles the supplied non-localized pattern into a form
290 * from which formatting and parsing can be performed.
291 * This also detects errors in the pattern, which will
292 * be raised on later use of the compiled data.
294 * @param pattern the non-localized pattern to compile.
295 * @throws IllegalArgumentException if the pattern is invalid.
297 private void compileFormat(String pattern
)
299 // Any alphabetical characters are treated as pattern characters
300 // unless enclosed in single quotes.
305 CompiledField current
= null;
307 for (int i
= 0; i
< pattern
.length(); i
++)
309 thisChar
= pattern
.charAt(i
);
310 field
= standardChars
.indexOf(thisChar
);
314 if ((thisChar
>= 'A' && thisChar
<= 'Z')
315 || (thisChar
>= 'a' && thisChar
<= 'z'))
317 // Not a valid letter
318 throw new IllegalArgumentException("Invalid letter "
320 " encountered at character "
323 else if (thisChar
== '\'')
325 // Quoted text section; skip to next single quote
326 pos
= pattern
.indexOf('\'', i
+ 1);
327 // First look for '' -- meaning a single quote.
332 // Look for the terminating quote. However, if we
333 // see a '', that represents a literal quote and
335 CPStringBuilder buf
= new CPStringBuilder();
340 throw new IllegalArgumentException("Quotes starting at character "
343 buf
.append(pattern
.substring(oldPos
, pos
));
344 if (pos
+ 1 >= pattern
.length()
345 || pattern
.charAt(pos
+ 1) != '\'')
349 pos
= pattern
.indexOf('\'', pos
+ 2);
352 tokens
.add(buf
.toString());
358 // A special character
359 tokens
.add(Character
.valueOf(thisChar
));
365 if ((current
!= null) && (field
== current
.field
))
369 current
= new CompiledField(field
, 1, thisChar
);
377 * Returns a string representation of this
380 * @return a string representation of the <code>SimpleDateFormat</code>
383 public String
toString()
385 CPStringBuilder output
= new CPStringBuilder(getClass().getName());
386 output
.append("[tokens=");
387 output
.append(tokens
);
388 output
.append(", formatData=");
389 output
.append(formatData
);
390 output
.append(", defaultCenturyStart=");
391 output
.append(defaultCenturyStart
);
392 output
.append(", defaultCentury=");
393 output
.append(defaultCentury
);
394 output
.append(", pattern=");
395 output
.append(pattern
);
396 output
.append(", serialVersionOnStream=");
397 output
.append(serialVersionOnStream
);
398 output
.append(", standardChars=");
399 output
.append(standardChars
);
401 return output
.toString();
405 * Constructs a SimpleDateFormat using the default pattern for
406 * the default locale.
408 public SimpleDateFormat()
411 * There does not appear to be a standard API for determining
412 * what the default pattern for a locale is, so use package-scope
413 * variables in DateFormatSymbols to encapsulate this.
416 Locale locale
= Locale
.getDefault();
417 calendar
= new GregorianCalendar(locale
);
418 computeCenturyStart();
419 tokens
= new ArrayList();
420 formatData
= new DateFormatSymbols(locale
);
421 pattern
= (formatData
.dateFormats
[DEFAULT
] + ' '
422 + formatData
.timeFormats
[DEFAULT
]);
423 compileFormat(pattern
);
424 numberFormat
= NumberFormat
.getInstance(locale
);
425 numberFormat
.setGroupingUsed (false);
426 numberFormat
.setParseIntegerOnly (true);
427 numberFormat
.setMaximumFractionDigits (0);
431 * Creates a date formatter using the specified non-localized pattern,
432 * with the default DateFormatSymbols for the default locale.
434 * @param pattern the pattern to use.
435 * @throws NullPointerException if the pattern is null.
436 * @throws IllegalArgumentException if the pattern is invalid.
438 public SimpleDateFormat(String pattern
)
440 this(pattern
, Locale
.getDefault());
444 * Creates a date formatter using the specified non-localized pattern,
445 * with the default DateFormatSymbols for the given locale.
447 * @param pattern the non-localized pattern to use.
448 * @param locale the locale to use for the formatting symbols.
449 * @throws NullPointerException if the pattern is null.
450 * @throws IllegalArgumentException if the pattern is invalid.
452 public SimpleDateFormat(String pattern
, Locale locale
)
455 calendar
= new GregorianCalendar(locale
);
456 computeCenturyStart();
457 tokens
= new ArrayList();
458 formatData
= new DateFormatSymbols(locale
);
459 compileFormat(pattern
);
460 this.pattern
= pattern
;
461 numberFormat
= NumberFormat
.getInstance(locale
);
462 numberFormat
.setGroupingUsed (false);
463 numberFormat
.setParseIntegerOnly (true);
464 numberFormat
.setMaximumFractionDigits (0);
468 * Creates a date formatter using the specified non-localized
469 * pattern. The specified DateFormatSymbols will be used when
472 * @param pattern the non-localized pattern to use.
473 * @param formatData the formatting symbols to use.
474 * @throws NullPointerException if the pattern or formatData is null.
475 * @throws IllegalArgumentException if the pattern is invalid.
477 public SimpleDateFormat(String pattern
, DateFormatSymbols formatData
)
480 calendar
= new GregorianCalendar();
481 computeCenturyStart ();
482 tokens
= new ArrayList();
483 if (formatData
== null)
484 throw new NullPointerException("formatData");
485 this.formatData
= formatData
;
486 compileFormat(pattern
);
487 this.pattern
= pattern
;
488 numberFormat
= NumberFormat
.getInstance();
489 numberFormat
.setGroupingUsed (false);
490 numberFormat
.setParseIntegerOnly (true);
491 numberFormat
.setMaximumFractionDigits (0);
495 * This method returns a string with the formatting pattern being used
496 * by this object. This string is unlocalized.
498 * @return The format string.
500 public String
toPattern()
506 * This method returns a string with the formatting pattern being used
507 * by this object. This string is localized.
509 * @return The format string.
511 public String
toLocalizedPattern()
513 String localChars
= formatData
.getLocalPatternChars();
514 return translateLocalizedPattern(pattern
, standardChars
, localChars
);
518 * This method sets the formatting pattern that should be used by this
519 * object. This string is not localized.
521 * @param pattern The new format pattern.
522 * @throws NullPointerException if the pattern is null.
523 * @throws IllegalArgumentException if the pattern is invalid.
525 public void applyPattern(String pattern
)
527 tokens
= new ArrayList();
528 compileFormat(pattern
);
529 this.pattern
= pattern
;
533 * This method sets the formatting pattern that should be used by this
534 * object. This string is localized.
536 * @param pattern The new format pattern.
537 * @throws NullPointerException if the pattern is null.
538 * @throws IllegalArgumentException if the pattern is invalid.
540 public void applyLocalizedPattern(String pattern
)
542 String localChars
= formatData
.getLocalPatternChars();
543 pattern
= translateLocalizedPattern(pattern
, localChars
, standardChars
);
544 applyPattern(pattern
);
548 * Translates either from or to a localized variant of the pattern
549 * string. For example, in the German locale, 't' (for 'tag') is
550 * used instead of 'd' (for 'date'). This method translates
551 * a localized pattern (such as 'ttt') to a non-localized pattern
552 * (such as 'ddd'), or vice versa. Non-localized patterns use
553 * a standard set of characters, which match those of the U.S. English
556 * @param pattern the pattern to translate.
557 * @param oldChars the old set of characters (used in the pattern).
558 * @param newChars the new set of characters (which will be used in the
560 * @return a version of the pattern using the characters in
561 * <code>newChars</code>.
563 private String
translateLocalizedPattern(String pattern
,
564 String oldChars
, String newChars
)
566 int len
= pattern
.length();
567 CPStringBuilder buf
= new CPStringBuilder(len
);
568 boolean quoted
= false;
569 for (int i
= 0; i
< len
; i
++)
571 char ch
= pattern
.charAt(i
);
576 int j
= oldChars
.indexOf(ch
);
578 ch
= newChars
.charAt(j
);
582 return buf
.toString();
586 * Returns the start of the century used for two digit years.
588 * @return A <code>Date</code> representing the start of the century
589 * for two digit years.
591 public Date
get2DigitYearStart()
593 return defaultCenturyStart
;
597 * Sets the start of the century used for two digit years.
599 * @param date A <code>Date</code> representing the start of the century for
602 public void set2DigitYearStart(Date date
)
604 defaultCenturyStart
= date
;
606 calendar
.setTime(date
);
607 int year
= calendar
.get(Calendar
.YEAR
);
608 defaultCentury
= year
- (year
% 100);
612 * This method returns a copy of the format symbol information used
613 * for parsing and formatting dates.
615 * @return a copy of the date format symbols.
617 public DateFormatSymbols
getDateFormatSymbols()
619 return (DateFormatSymbols
) formatData
.clone();
623 * This method sets the format symbols information used for parsing
624 * and formatting dates.
626 * @param formatData The date format symbols.
627 * @throws NullPointerException if <code>formatData</code> is null.
629 public void setDateFormatSymbols(DateFormatSymbols formatData
)
631 if (formatData
== null)
634 NullPointerException("The supplied format data was null.");
636 this.formatData
= formatData
;
640 * This methods tests whether the specified object is equal to this
641 * object. This will be true if and only if the specified object:
644 * <li>Is not <code>null</code>.</li>
645 * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
646 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
648 * <li>Has the same formatting pattern.</li>
649 * <li>Is using the same formatting symbols.</li>
650 * <li>Is using the same century for two digit years.</li>
653 * @param o The object to compare for equality against.
655 * @return <code>true</code> if the specified object is equal to this object,
656 * <code>false</code> otherwise.
658 public boolean equals(Object o
)
660 if (!super.equals(o
))
663 if (!(o
instanceof SimpleDateFormat
))
666 SimpleDateFormat sdf
= (SimpleDateFormat
)o
;
668 if (defaultCentury
!= sdf
.defaultCentury
)
671 if (!toPattern().equals(sdf
.toPattern()))
674 if (!getDateFormatSymbols().equals(sdf
.getDateFormatSymbols()))
681 * This method returns a hash value for this object.
683 * @return A hash value for this object.
685 public int hashCode()
687 return super.hashCode() ^
toPattern().hashCode() ^ defaultCentury ^
688 getDateFormatSymbols().hashCode();
693 * Formats the date input according to the format string in use,
694 * appending to the specified StringBuffer. The input StringBuffer
695 * is returned as output for convenience.
697 private void formatWithAttribute(Date date
, FormatBuffer buffer
, FieldPosition pos
)
700 AttributedCharacterIterator
.Attribute attribute
;
701 calendar
.setTime(date
);
703 // go through vector, filling in fields where applicable, else toString
704 Iterator iter
= tokens
.iterator();
705 while (iter
.hasNext())
707 Object o
= iter
.next();
708 if (o
instanceof CompiledField
)
710 CompiledField cf
= (CompiledField
) o
;
711 int beginIndex
= buffer
.length();
713 switch (cf
.getField())
716 buffer
.append (formatData
.eras
[calendar
.get (Calendar
.ERA
)], DateFormat
.Field
.ERA
);
719 // If we have two digits, then we truncate. Otherwise, we
720 // use the size of the pattern, and zero pad.
721 buffer
.setDefaultAttribute (DateFormat
.Field
.YEAR
);
722 if (cf
.getSize() == 2)
724 temp
= "00"+String
.valueOf (calendar
.get (Calendar
.YEAR
));
725 buffer
.append (temp
.substring (temp
.length() - 2));
728 withLeadingZeros (calendar
.get (Calendar
.YEAR
), cf
.getSize(), buffer
);
731 buffer
.setDefaultAttribute (DateFormat
.Field
.MONTH
);
732 if (cf
.getSize() < 3)
733 withLeadingZeros (calendar
.get (Calendar
.MONTH
) + 1, cf
.getSize(), buffer
);
734 else if (cf
.getSize() < 4)
735 buffer
.append (formatData
.shortMonths
[calendar
.get (Calendar
.MONTH
)]);
737 buffer
.append (formatData
.months
[calendar
.get (Calendar
.MONTH
)]);
740 buffer
.setDefaultAttribute (DateFormat
.Field
.DAY_OF_MONTH
);
741 withLeadingZeros (calendar
.get (Calendar
.DATE
), cf
.getSize(), buffer
);
743 case HOUR_OF_DAY1_FIELD
: // 1-24
744 buffer
.setDefaultAttribute(DateFormat
.Field
.HOUR_OF_DAY1
);
745 withLeadingZeros ( ((calendar
.get (Calendar
.HOUR_OF_DAY
) + 23) % 24) + 1,
746 cf
.getSize(), buffer
);
748 case HOUR_OF_DAY0_FIELD
: // 0-23
749 buffer
.setDefaultAttribute (DateFormat
.Field
.HOUR_OF_DAY0
);
750 withLeadingZeros (calendar
.get (Calendar
.HOUR_OF_DAY
), cf
.getSize(), buffer
);
753 buffer
.setDefaultAttribute (DateFormat
.Field
.MINUTE
);
754 withLeadingZeros (calendar
.get (Calendar
.MINUTE
),
755 cf
.getSize(), buffer
);
758 buffer
.setDefaultAttribute (DateFormat
.Field
.SECOND
);
759 withLeadingZeros(calendar
.get (Calendar
.SECOND
),
760 cf
.getSize(), buffer
);
762 case MILLISECOND_FIELD
:
763 buffer
.setDefaultAttribute (DateFormat
.Field
.MILLISECOND
);
764 withLeadingZeros (calendar
.get (Calendar
.MILLISECOND
), cf
.getSize(), buffer
);
766 case DAY_OF_WEEK_FIELD
:
767 buffer
.setDefaultAttribute (DateFormat
.Field
.DAY_OF_WEEK
);
768 if (cf
.getSize() < 4)
769 buffer
.append (formatData
.shortWeekdays
[calendar
.get (Calendar
.DAY_OF_WEEK
)]);
771 buffer
.append (formatData
.weekdays
[calendar
.get (Calendar
.DAY_OF_WEEK
)]);
773 case DAY_OF_YEAR_FIELD
:
774 buffer
.setDefaultAttribute (DateFormat
.Field
.DAY_OF_YEAR
);
775 withLeadingZeros (calendar
.get (Calendar
.DAY_OF_YEAR
), cf
.getSize(), buffer
);
777 case DAY_OF_WEEK_IN_MONTH_FIELD
:
778 buffer
.setDefaultAttribute (DateFormat
.Field
.DAY_OF_WEEK_IN_MONTH
);
779 withLeadingZeros (calendar
.get (Calendar
.DAY_OF_WEEK_IN_MONTH
),
780 cf
.getSize(), buffer
);
782 case WEEK_OF_YEAR_FIELD
:
783 buffer
.setDefaultAttribute (DateFormat
.Field
.WEEK_OF_YEAR
);
784 withLeadingZeros (calendar
.get (Calendar
.WEEK_OF_YEAR
),
785 cf
.getSize(), buffer
);
787 case WEEK_OF_MONTH_FIELD
:
788 buffer
.setDefaultAttribute (DateFormat
.Field
.WEEK_OF_MONTH
);
789 withLeadingZeros (calendar
.get (Calendar
.WEEK_OF_MONTH
),
790 cf
.getSize(), buffer
);
793 buffer
.setDefaultAttribute (DateFormat
.Field
.AM_PM
);
794 buffer
.append (formatData
.ampms
[calendar
.get (Calendar
.AM_PM
)]);
796 case HOUR1_FIELD
: // 1-12
797 buffer
.setDefaultAttribute (DateFormat
.Field
.HOUR1
);
798 withLeadingZeros (((calendar
.get (Calendar
.HOUR
) + 11) % 12) + 1,
799 cf
.getSize(), buffer
);
801 case HOUR0_FIELD
: // 0-11
802 buffer
.setDefaultAttribute (DateFormat
.Field
.HOUR0
);
803 withLeadingZeros (calendar
.get (Calendar
.HOUR
), cf
.getSize(), buffer
);
806 buffer
.setDefaultAttribute (DateFormat
.Field
.TIME_ZONE
);
807 TimeZone zone
= calendar
.getTimeZone();
808 boolean isDST
= calendar
.get (Calendar
.DST_OFFSET
) != 0;
809 // FIXME: XXX: This should be a localized time zone.
810 String zoneID
= zone
.getDisplayName
811 (isDST
, cf
.getSize() > 3 ? TimeZone
.LONG
: TimeZone
.SHORT
);
812 buffer
.append (zoneID
);
814 case RFC822_TIMEZONE_FIELD
:
815 buffer
.setDefaultAttribute(DateFormat
.Field
.TIME_ZONE
);
816 int pureMinutes
= (calendar
.get(Calendar
.ZONE_OFFSET
) +
817 calendar
.get(Calendar
.DST_OFFSET
)) / (1000 * 60);
818 String sign
= (pureMinutes
< 0) ?
"-" : "+";
819 pureMinutes
= Math
.abs(pureMinutes
);
820 int hours
= pureMinutes
/ 60;
821 int minutes
= pureMinutes
% 60;
823 withLeadingZeros(hours
, 2, buffer
);
824 withLeadingZeros(minutes
, 2, buffer
);
827 throw new IllegalArgumentException ("Illegal pattern character " +
830 if (pos
!= null && (buffer
.getDefaultAttribute() == pos
.getFieldAttribute()
831 || cf
.getField() == pos
.getField()))
833 pos
.setBeginIndex(beginIndex
);
834 pos
.setEndIndex(buffer
.length());
839 buffer
.append(o
.toString(), null);
844 public StringBuffer
format(Date date
, StringBuffer buffer
, FieldPosition pos
)
846 formatWithAttribute(date
, new StringFormatBuffer (buffer
), pos
);
851 public AttributedCharacterIterator
formatToCharacterIterator(Object date
)
852 throws IllegalArgumentException
855 throw new NullPointerException("null argument");
856 if (!(date
instanceof Date
))
857 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
859 AttributedFormatBuffer buf
= new AttributedFormatBuffer();
860 formatWithAttribute((Date
)date
, buf
,
864 return new FormatCharacterIterator(buf
.getBuffer().toString(),
866 buf
.getAttributes());
869 private void withLeadingZeros(int value
, int length
, FormatBuffer buffer
)
871 String valStr
= String
.valueOf(value
);
872 for (length
-= valStr
.length(); length
> 0; length
--)
874 buffer
.append(valStr
);
877 private boolean expect(String source
, ParsePosition pos
, char ch
)
879 int x
= pos
.getIndex();
880 boolean r
= x
< source
.length() && source
.charAt(x
) == ch
;
884 pos
.setErrorIndex(x
);
889 * This method parses the specified string into a date.
891 * @param dateStr The date string to parse.
892 * @param pos The input and output parse position
894 * @return The parsed date, or <code>null</code> if the string cannot be
897 public Date
parse (String dateStr
, ParsePosition pos
)
900 int fmt_max
= pattern
.length();
903 boolean saw_timezone
= false;
904 int quote_start
= -1;
905 boolean is2DigitYear
= false;
908 for (; fmt_index
< fmt_max
; ++fmt_index
)
910 char ch
= pattern
.charAt(fmt_index
);
913 int index
= pos
.getIndex();
914 if (fmt_index
< fmt_max
- 1
915 && pattern
.charAt(fmt_index
+ 1) == '\'')
917 if (! expect (dateStr
, pos
, ch
))
922 quote_start
= quote_start
< 0 ? fmt_index
: -1;
926 if (quote_start
!= -1
927 || ((ch
< 'a' || ch
> 'z')
928 && (ch
< 'A' || ch
> 'Z')))
930 if (quote_start
== -1 && ch
== ' ')
932 // A single unquoted space in the pattern may match
933 // any number of spaces in the input.
934 int index
= pos
.getIndex();
936 while (index
< dateStr
.length()
937 && Character
.isWhitespace(dateStr
.charAt(index
)))
943 // Didn't see any whitespace.
944 pos
.setErrorIndex(index
);
948 else if (! expect (dateStr
, pos
, ch
))
953 // We've arrived at a potential pattern character in the
956 while (++fmt_index
< fmt_max
&& pattern
.charAt(fmt_index
) == ch
)
961 // We might need to limit the number of digits to parse in
962 // some cases. We look to the next pattern character to
964 boolean limit_digits
= false;
965 if (fmt_index
< fmt_max
966 && standardChars
.indexOf(pattern
.charAt(fmt_index
)) >= 0)
970 // We can handle most fields automatically: most either are
971 // numeric or are looked up in a string vector. In some cases
972 // we need an offset. When numeric, `offset' is added to the
973 // resulting value. When doing a string lookup, offset is the
974 // initial index into the string array.
976 boolean is_numeric
= true;
978 boolean maybe2DigitYear
= false;
979 boolean oneBasedHour
= false;
980 boolean oneBasedHourOfDay
= false;
981 Integer simpleOffset
;
982 String
[] set1
= null;
983 String
[] set2
= null;
987 calendar_field
= Calendar
.DATE
;
990 calendar_field
= Calendar
.DAY_OF_YEAR
;
993 calendar_field
= Calendar
.DAY_OF_WEEK_IN_MONTH
;
998 calendar_field
= Calendar
.DAY_OF_WEEK
;
999 set1
= formatData
.getWeekdays();
1000 set2
= formatData
.getShortWeekdays();
1003 calendar_field
= Calendar
.WEEK_OF_YEAR
;
1006 calendar_field
= Calendar
.WEEK_OF_MONTH
;
1009 calendar_field
= Calendar
.MONTH
;
1015 set1
= formatData
.getMonths();
1016 set2
= formatData
.getShortMonths();
1020 calendar_field
= Calendar
.YEAR
;
1022 maybe2DigitYear
= true;
1025 calendar_field
= Calendar
.HOUR
;
1028 calendar_field
= Calendar
.HOUR
;
1029 oneBasedHour
= true;
1032 calendar_field
= Calendar
.HOUR_OF_DAY
;
1035 calendar_field
= Calendar
.HOUR_OF_DAY
;
1036 oneBasedHourOfDay
= true;
1039 calendar_field
= Calendar
.MINUTE
;
1042 calendar_field
= Calendar
.SECOND
;
1045 calendar_field
= Calendar
.MILLISECOND
;
1049 calendar_field
= Calendar
.AM_PM
;
1050 set1
= formatData
.getAmPmStrings();
1054 // We need a special case for the timezone, because it
1055 // uses a different data structure than the other cases.
1057 calendar_field
= Calendar
.ZONE_OFFSET
;
1058 String
[][] zoneStrings
= formatData
.getZoneStrings();
1059 int zoneCount
= zoneStrings
.length
;
1060 int index
= pos
.getIndex();
1061 boolean found_zone
= false;
1062 simpleOffset
= computeOffset(dateStr
.substring(index
), pos
);
1063 if (simpleOffset
!= null)
1066 saw_timezone
= true;
1067 calendar
.set(Calendar
.DST_OFFSET
, 0);
1068 offset
= simpleOffset
.intValue();
1072 for (int j
= 0; j
< zoneCount
; j
++)
1074 String
[] strings
= zoneStrings
[j
];
1076 for (k
= 0; k
< strings
.length
; ++k
)
1078 if (dateStr
.startsWith(strings
[k
], index
))
1081 if (k
!= strings
.length
)
1084 saw_timezone
= true;
1085 TimeZone tz
= TimeZone
.getTimeZone (strings
[0]);
1086 // Check if it's a DST zone or ordinary
1087 if(k
== 3 || k
== 4)
1088 calendar
.set (Calendar
.DST_OFFSET
, tz
.getDSTSavings());
1090 calendar
.set (Calendar
.DST_OFFSET
, 0);
1091 offset
= tz
.getRawOffset ();
1092 pos
.setIndex(index
+ strings
[k
].length());
1099 pos
.setErrorIndex(pos
.getIndex());
1104 pos
.setErrorIndex(pos
.getIndex());
1108 // Compute the value we should assign to the field.
1113 numberFormat
.setMinimumIntegerDigits(fmt_count
);
1114 if (maybe2DigitYear
)
1115 index
= pos
.getIndex();
1119 // numberFormat.setMaximumIntegerDigits(fmt_count) may
1120 // not work as expected. So we explicitly use substring
1122 int origPos
= pos
.getIndex();
1124 n
= numberFormat
.parse(dateStr
.substring(origPos
, origPos
+ fmt_count
), pos
);
1125 pos
.setIndex(origPos
+ pos
.getIndex());
1128 n
= numberFormat
.parse(dateStr
, pos
);
1129 if (pos
== null || ! (n
instanceof Long
))
1131 value
= n
.intValue() + offset
;
1133 else if (set1
!= null)
1135 index
= pos
.getIndex();
1137 boolean found
= false;
1138 for (i
= offset
; i
< set1
.length
; ++i
)
1140 if (set1
[i
] != null)
1141 if (dateStr
.toUpperCase().startsWith(set1
[i
].toUpperCase(),
1145 pos
.setIndex(index
+ set1
[i
].length());
1149 if (!found
&& set2
!= null)
1151 for (i
= offset
; i
< set2
.length
; ++i
)
1153 if (set2
[i
] != null)
1154 if (dateStr
.toUpperCase().startsWith(set2
[i
].toUpperCase(),
1158 pos
.setIndex(index
+ set2
[i
].length());
1165 pos
.setErrorIndex(index
);
1173 if (maybe2DigitYear
)
1175 // Parse into default century if the numeric year string has
1176 // exactly 2 digits.
1177 int digit_count
= pos
.getIndex() - index
;
1178 if (digit_count
== 2)
1180 is2DigitYear
= true;
1181 value
+= defaultCentury
;
1185 // Calendar uses 0-based hours.
1186 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1187 if (oneBasedHour
&& value
== 12)
1190 if (oneBasedHourOfDay
&& value
== 24)
1193 // Assign the value and move on.
1194 calendar
.set(calendar_field
, value
);
1199 // Apply the 80-20 heuristic to dermine the full year based on
1200 // defaultCenturyStart.
1201 int year
= calendar
.get(Calendar
.YEAR
);
1202 if (calendar
.getTime().compareTo(defaultCenturyStart
) < 0)
1203 calendar
.set(Calendar
.YEAR
, year
+ 100);
1207 // Use the real rules to determine whether or not this
1208 // particular time is in daylight savings.
1209 calendar
.clear (Calendar
.DST_OFFSET
);
1210 calendar
.clear (Calendar
.ZONE_OFFSET
);
1212 return calendar
.getTime();
1214 catch (IllegalArgumentException x
)
1216 pos
.setErrorIndex(pos
.getIndex());
1223 * Computes the time zone offset in milliseconds
1224 * relative to GMT, based on the supplied
1225 * <code>String</code> representation.
1228 * The supplied <code>String</code> must be a three
1229 * or four digit signed number, with an optional 'GMT'
1230 * prefix. The first one or two digits represents the hours,
1231 * while the last two represent the minutes. The
1232 * two sets of digits can optionally be separated by
1233 * ':'. The mandatory sign prefix (either '+' or '-')
1234 * indicates the direction of the offset from GMT.
1237 * For example, 'GMT+0200' specifies 2 hours after
1238 * GMT, while '-05:00' specifies 5 hours prior to
1239 * GMT. The special case of 'GMT' alone can be used
1240 * to represent the offset, 0.
1243 * If the <code>String</code> can not be parsed,
1244 * the result will be null. The resulting offset
1245 * is wrapped in an <code>Integer</code> object, in
1246 * order to allow such failure to be represented.
1249 * @param zoneString a string in the form
1250 * (GMT)? sign hours : minutes
1251 * where sign = '+' or '-', hours
1252 * is a one or two digits representing
1253 * a number between 0 and 23, and
1254 * minutes is two digits representing
1255 * a number between 0 and 59.
1256 * @return the parsed offset, or null if parsing
1259 private Integer
computeOffset(String zoneString
, ParsePosition pos
)
1262 Pattern
.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1263 Matcher matcher
= pattern
.matcher(zoneString
);
1265 // Match from start, but ignore trailing parts
1266 boolean hasAll
= matcher
.lookingAt();
1269 // Do we have at least the sign, hour and minute?
1274 catch (IllegalStateException ise
)
1280 int sign
= matcher
.group(2).equals("+") ?
1 : -1;
1281 int hour
= Integer
.parseInt(matcher
.group(4));
1282 if (!matcher
.group(3).equals(""))
1283 hour
+= (Integer
.parseInt(matcher
.group(3)) * 10);
1284 int minutes
= Integer
.parseInt(matcher
.group(5));
1288 int offset
= sign
* ((hour
* 60) + minutes
) * 60000;
1290 // advance the index
1291 pos
.setIndex(pos
.getIndex() + matcher
.end());
1292 return Integer
.valueOf(offset
);
1294 else if (zoneString
.startsWith("GMT"))
1296 pos
.setIndex(pos
.getIndex() + 3);
1297 return Integer
.valueOf(0);
1302 // Compute the start of the current century as defined by
1303 // get2DigitYearStart.
1304 private void computeCenturyStart()
1306 int year
= calendar
.get(Calendar
.YEAR
);
1307 calendar
.set(Calendar
.YEAR
, year
- 80);
1308 set2DigitYearStart(calendar
.getTime());
1312 * Returns a copy of this instance of
1313 * <code>SimpleDateFormat</code>. The copy contains
1314 * clones of the formatting symbols and the 2-digit
1315 * year century start date.
1317 public Object
clone()
1319 SimpleDateFormat clone
= (SimpleDateFormat
) super.clone();
1320 clone
.setDateFormatSymbols((DateFormatSymbols
) formatData
.clone());
1321 clone
.set2DigitYearStart((Date
) defaultCenturyStart
.clone());