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., 59 Temple Place, Suite 330, 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,
81 * The size of the character sequence.
88 private char character;
91 * Constructs a compiled field using the
92 * the given field ID, size and character
95 * @param f the field ID.
96 * @param s the size of the field.
97 * @param c the character used.
99 public CompiledField(int f, int s, char c)
107 * Retrieves the ID of the field relative to
108 * the local pattern characters.
110 public int getField()
116 * Retrieves the size of the character sequence.
124 * Retrieves the character used in the sequence.
126 public char getCharacter()
132 * Returns a <code>String</code> representation
133 * of the compiled field, primarily for debugging
136 * @return a <code>String</code> representation.
138 public String toString()
140 StringBuffer builder;
142 builder = new StringBuffer(getClass().getName());
143 builder.append("[field=");
144 builder.append(field);
145 builder.append(", size=");
146 builder.append(size);
147 builder.append(", character=");
148 builder.append(character);
151 return builder.toString();
156 * A list of <code>CompiledField</code>s,
157 * representing the compiled version of the pattern.
162 private transient ArrayList tokens;
165 * The localised data used in formatting,
166 * such as the day and month names in the local
167 * language, and the localized pattern characters.
169 * @see DateFormatSymbols
170 * @serial The localisation data. May not be null.
172 private DateFormatSymbols formatData;
175 * The date representing the start of the century
176 * used for interpreting two digit years. For
177 * example, 24/10/2004 would cause two digit
178 * years to be interpreted as representing
179 * the years between 2004 and 2104.
181 * @see get2DigitYearStart()
182 * @see set2DigitYearStart(java.util.Date)
184 * @serial The start date of the century for parsing two digit years.
187 private Date defaultCenturyStart;
190 * The year at which interpretation of two
191 * digit years starts.
193 * @see get2DigitYearStart()
194 * @see set2DigitYearStart(java.util.Date)
197 private transient int defaultCentury;
200 * The non-localized pattern string. This
201 * only ever contains the pattern characters
202 * stored in standardChars. Localized patterns
203 * are translated to this form.
205 * @see applyPattern(String)
206 * @see applyLocalizedPattern(String)
208 * @see toLocalizedPattern()
209 * @serial The non-localized pattern string. May not be null.
211 private String pattern;
214 * The version of serialized data used by this class.
215 * Version 0 only includes the pattern and formatting
216 * data. Version 1 adds the start date for interpreting
219 * @serial This specifies the version of the data being serialized.
220 * Version 0 (or no version) specifies just <code>pattern</code>
221 * and <code>formatData</code>. Version 1 adds
222 * the <code>defaultCenturyStart</code>. This implementation
223 * always writes out version 1 data.
225 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
230 private static final long serialVersionUID = 4774881970558875024L;
232 // This string is specified in the root of the CLDR. We set it here
233 // rather than doing a DateFormatSymbols(Locale.US).getLocalPatternChars()
234 // since someone could theoretically change those values (though unlikely).
235 private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZ";
238 * Reads the serialized version of this object.
239 * If the serialized data is only version 0,
240 * then the date for the start of the century
241 * for interpreting two digit years is computed.
242 * The pattern is parsed and compiled following the process
243 * of reading in the serialized data.
245 * @param stream the object stream to read the data from.
246 * @throws IOException if an I/O error occurs.
247 * @throws ClassNotFoundException if the class of the serialized data
248 * could not be found.
249 * @throws InvalidObjectException if the pattern is invalid.
251 private void readObject(ObjectInputStream stream)
252 throws IOException, ClassNotFoundException
254 stream.defaultReadObject();
255 if (serialVersionOnStream < 1)
257 computeCenturyStart ();
258 serialVersionOnStream = 1;
261 // Ensure that defaultCentury gets set.
262 set2DigitYearStart(defaultCenturyStart);
264 // Set up items normally taken care of by the constructor.
265 tokens = new ArrayList();
268 compileFormat(pattern);
270 catch (IllegalArgumentException e)
272 throw new InvalidObjectException("The stream pattern was invalid.");
277 * Compiles the supplied non-localized pattern into a form
278 * from which formatting and parsing can be performed.
279 * This also detects errors in the pattern, which will
280 * be raised on later use of the compiled data.
282 * @param pattern the non-localized pattern to compile.
283 * @throws IllegalArgumentException if the pattern is invalid.
285 private void compileFormat(String pattern)
287 // Any alphabetical characters are treated as pattern characters
288 // unless enclosed in single quotes.
293 CompiledField current = null;
295 for (int i=0; i<pattern.length(); i++) {
296 thisChar = pattern.charAt(i);
297 field = standardChars.indexOf(thisChar);
300 if ((thisChar >= 'A' && thisChar <= 'Z')
301 || (thisChar >= 'a' && thisChar <= 'z')) {
302 // Not a valid letter
303 throw new IllegalArgumentException("Invalid letter " + thisChar +
304 "encountered at character " + i
306 } else if (thisChar == '\'') {
307 // Quoted text section; skip to next single quote
308 pos = pattern.indexOf('\'',i+1);
310 throw new IllegalArgumentException("Quotes starting at character "
311 + i + " not closed.");
313 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
314 tokens.add(pattern.substring(i+1,pos+1));
316 tokens.add(pattern.substring(i+1,pos));
320 // A special character
321 tokens.add(new Character(thisChar));
325 if ((current != null) && (field == current.field)) {
328 current = new CompiledField(field,1,thisChar);
336 * Returns a string representation of this
339 * @return a string representation of the <code>SimpleDateFormat</code>
342 public String toString()
344 StringBuffer output = new StringBuffer(getClass().getName());
345 output.append("[tokens=");
346 output.append(tokens);
347 output.append(", formatData=");
348 output.append(formatData);
349 output.append(", defaultCenturyStart=");
350 output.append(defaultCenturyStart);
351 output.append(", defaultCentury=");
352 output.append(defaultCentury);
353 output.append(", pattern=");
354 output.append(pattern);
355 output.append(", serialVersionOnStream=");
356 output.append(serialVersionOnStream);
357 output.append(", standardChars=");
358 output.append(standardChars);
360 return output.toString();
364 * Constructs a SimpleDateFormat using the default pattern for
365 * the default locale.
367 public SimpleDateFormat()
370 * There does not appear to be a standard API for determining
371 * what the default pattern for a locale is, so use package-scope
372 * variables in DateFormatSymbols to encapsulate this.
375 Locale locale = Locale.getDefault();
376 calendar = new GregorianCalendar(locale);
377 computeCenturyStart();
378 tokens = new ArrayList();
379 formatData = new DateFormatSymbols(locale);
380 pattern = (formatData.dateFormats[DEFAULT] + ' '
381 + formatData.timeFormats[DEFAULT]);
382 compileFormat(pattern);
383 numberFormat = NumberFormat.getInstance(locale);
384 numberFormat.setGroupingUsed (false);
385 numberFormat.setParseIntegerOnly (true);
386 numberFormat.setMaximumFractionDigits (0);
390 * Creates a date formatter using the specified non-localized pattern,
391 * with the default DateFormatSymbols for the default locale.
393 * @param pattern the pattern to use.
394 * @throws NullPointerException if the pattern is null.
395 * @throws IllegalArgumentException if the pattern is invalid.
397 public SimpleDateFormat(String pattern)
399 this(pattern, Locale.getDefault());
403 * Creates a date formatter using the specified non-localized pattern,
404 * with the default DateFormatSymbols for the given locale.
406 * @param pattern the non-localized pattern to use.
407 * @param locale the locale to use for the formatting symbols.
408 * @throws NullPointerException if the pattern is null.
409 * @throws IllegalArgumentException if the pattern is invalid.
411 public SimpleDateFormat(String pattern, Locale locale)
414 calendar = new GregorianCalendar(locale);
415 computeCenturyStart();
416 tokens = new ArrayList();
417 formatData = new DateFormatSymbols(locale);
418 compileFormat(pattern);
419 this.pattern = pattern;
420 numberFormat = NumberFormat.getInstance(locale);
421 numberFormat.setGroupingUsed (false);
422 numberFormat.setParseIntegerOnly (true);
423 numberFormat.setMaximumFractionDigits (0);
427 * Creates a date formatter using the specified non-localized
428 * pattern. The specified DateFormatSymbols will be used when
431 * @param pattern the non-localized pattern to use.
432 * @param formatData the formatting symbols to use.
433 * @throws NullPointerException if the pattern or formatData is null.
434 * @throws IllegalArgumentException if the pattern is invalid.
436 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
439 calendar = new GregorianCalendar();
440 computeCenturyStart ();
441 tokens = new ArrayList();
442 if (formatData == null)
443 throw new NullPointerException("formatData");
444 this.formatData = formatData;
445 compileFormat(pattern);
446 this.pattern = pattern;
447 numberFormat = NumberFormat.getInstance();
448 numberFormat.setGroupingUsed (false);
449 numberFormat.setParseIntegerOnly (true);
450 numberFormat.setMaximumFractionDigits (0);
454 * This method returns a string with the formatting pattern being used
455 * by this object. This string is unlocalized.
457 * @return The format string.
459 public String toPattern()
465 * This method returns a string with the formatting pattern being used
466 * by this object. This string is localized.
468 * @return The format string.
470 public String toLocalizedPattern()
472 String localChars = formatData.getLocalPatternChars();
473 return translateLocalizedPattern(pattern, standardChars, localChars);
477 * This method sets the formatting pattern that should be used by this
478 * object. This string is not localized.
480 * @param pattern The new format pattern.
481 * @throws NullPointerException if the pattern is null.
482 * @throws IllegalArgumentException if the pattern is invalid.
484 public void applyPattern(String pattern)
486 tokens = new ArrayList();
487 compileFormat(pattern);
488 this.pattern = pattern;
492 * This method sets the formatting pattern that should be used by this
493 * object. This string is localized.
495 * @param pattern The new format pattern.
496 * @throws NullPointerException if the pattern is null.
497 * @throws IllegalArgumentException if the pattern is invalid.
499 public void applyLocalizedPattern(String pattern)
501 String localChars = formatData.getLocalPatternChars();
502 pattern = translateLocalizedPattern(pattern, localChars, standardChars);
503 applyPattern(pattern);
507 * Translates either from or to a localized variant of the pattern
508 * string. For example, in the German locale, 't' (for 'tag') is
509 * used instead of 'd' (for 'date'). This method translates
510 * a localized pattern (such as 'ttt') to a non-localized pattern
511 * (such as 'ddd'), or vice versa. Non-localized patterns use
512 * a standard set of characters, which match those of the U.S. English
515 * @param pattern the pattern to translate.
516 * @param oldChars the old set of characters (used in the pattern).
517 * @param newChars the new set of characters (which will be used in the
519 * @return a version of the pattern using the characters in
520 * <code>newChars</code>.
522 private String translateLocalizedPattern(String pattern,
523 String oldChars, String newChars)
525 int len = pattern.length();
526 StringBuffer buf = new StringBuffer(len);
527 boolean quoted = false;
528 for (int i = 0; i < len; i++)
530 char ch = pattern.charAt(i);
535 int j = oldChars.indexOf(ch);
537 ch = newChars.charAt(j);
541 return buf.toString();
545 * Returns the start of the century used for two digit years.
547 * @return A <code>Date</code> representing the start of the century
548 * for two digit years.
550 public Date get2DigitYearStart()
552 return defaultCenturyStart;
556 * Sets the start of the century used for two digit years.
558 * @param date A <code>Date</code> representing the start of the century for
561 public void set2DigitYearStart(Date date)
563 defaultCenturyStart = date;
565 calendar.setTime(date);
566 int year = calendar.get(Calendar.YEAR);
567 defaultCentury = year - (year % 100);
571 * This method returns a copy of the format symbol information used
572 * for parsing and formatting dates.
574 * @return a copy of the date format symbols.
576 public DateFormatSymbols getDateFormatSymbols()
578 return (DateFormatSymbols) formatData.clone();
582 * This method sets the format symbols information used for parsing
583 * and formatting dates.
585 * @param formatData The date format symbols.
586 * @throws NullPointerException if <code>formatData</code> is null.
588 public void setDateFormatSymbols(DateFormatSymbols formatData)
590 if (formatData == null)
593 NullPointerException("The supplied format data was null.");
595 this.formatData = formatData;
599 * This methods tests whether the specified object is equal to this
600 * object. This will be true if and only if the specified object:
603 * <li>Is not <code>null</code>.</li>
604 * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
605 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
607 * <li>Has the same formatting pattern.</li>
608 * <li>Is using the same formatting symbols.</li>
609 * <li>Is using the same century for two digit years.</li>
612 * @param obj The object to compare for equality against.
614 * @return <code>true</code> if the specified object is equal to this object,
615 * <code>false</code> otherwise.
617 public boolean equals(Object o)
619 if (!super.equals(o))
622 if (!(o instanceof SimpleDateFormat))
625 SimpleDateFormat sdf = (SimpleDateFormat)o;
627 if (defaultCentury != sdf.defaultCentury)
630 if (!toPattern().equals(sdf.toPattern()))
633 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
640 * This method returns a hash value for this object.
642 * @return A hash value for this object.
644 public int hashCode()
646 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
647 getDateFormatSymbols().hashCode();
652 * Formats the date input according to the format string in use,
653 * appending to the specified StringBuffer. The input StringBuffer
654 * is returned as output for convenience.
656 private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
659 AttributedCharacterIterator.Attribute attribute;
660 calendar.setTime(date);
662 // go through vector, filling in fields where applicable, else toString
663 Iterator iter = tokens.iterator();
664 while (iter.hasNext())
666 Object o = iter.next();
667 if (o instanceof CompiledField)
669 CompiledField cf = (CompiledField) o;
670 int beginIndex = buffer.length();
672 switch (cf.getField())
675 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
678 // If we have two digits, then we truncate. Otherwise, we
679 // use the size of the pattern, and zero pad.
680 buffer.setDefaultAttribute (DateFormat.Field.YEAR);
681 if (cf.getSize() == 2)
683 temp = String.valueOf (calendar.get (Calendar.YEAR));
684 buffer.append (temp.substring (temp.length() - 2));
687 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
690 buffer.setDefaultAttribute (DateFormat.Field.MONTH);
691 if (cf.getSize() < 3)
692 withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer);
693 else if (cf.getSize() < 4)
694 buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]);
696 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
699 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
700 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
702 case HOUR_OF_DAY1_FIELD: // 1-24
703 buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1);
704 withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1,
705 cf.getSize(), buffer);
707 case HOUR_OF_DAY0_FIELD: // 0-23
708 buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0);
709 withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer);
712 buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
713 withLeadingZeros (calendar.get (Calendar.MINUTE),
714 cf.getSize(), buffer);
717 buffer.setDefaultAttribute (DateFormat.Field.SECOND);
718 withLeadingZeros(calendar.get (Calendar.SECOND),
719 cf.getSize(), buffer);
721 case MILLISECOND_FIELD:
722 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
723 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
725 case DAY_OF_WEEK_FIELD:
726 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK);
727 if (cf.getSize() < 4)
728 buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
730 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
732 case DAY_OF_YEAR_FIELD:
733 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR);
734 withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer);
736 case DAY_OF_WEEK_IN_MONTH_FIELD:
737 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH);
738 withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH),
739 cf.getSize(), buffer);
741 case WEEK_OF_YEAR_FIELD:
742 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR);
743 withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR),
744 cf.getSize(), buffer);
746 case WEEK_OF_MONTH_FIELD:
747 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH);
748 withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH),
749 cf.getSize(), buffer);
752 buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
753 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
755 case HOUR1_FIELD: // 1-12
756 buffer.setDefaultAttribute (DateFormat.Field.HOUR1);
757 withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1,
758 cf.getSize(), buffer);
760 case HOUR0_FIELD: // 0-11
761 buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
762 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
765 buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE);
766 TimeZone zone = calendar.getTimeZone();
767 boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0;
768 // FIXME: XXX: This should be a localized time zone.
769 String zoneID = zone.getDisplayName
770 (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT);
771 buffer.append (zoneID);
773 case RFC822_TIMEZONE_FIELD:
774 buffer.setDefaultAttribute(DateFormat.Field.RFC822_TIME_ZONE);
775 int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) +
776 calendar.get(Calendar.DST_OFFSET)) / (1000 * 60);
777 String sign = (pureMinutes < 0) ? "-" : "+";
778 int hours = pureMinutes / 60;
779 int minutes = pureMinutes % 60;
781 withLeadingZeros(hours, 2, buffer);
782 withLeadingZeros(minutes, 2, buffer);
785 throw new IllegalArgumentException ("Illegal pattern character " +
788 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
789 || cf.getField() == pos.getField()))
791 pos.setBeginIndex(beginIndex);
792 pos.setEndIndex(buffer.length());
797 buffer.append(o.toString(), null);
802 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
804 formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
809 public AttributedCharacterIterator formatToCharacterIterator(Object date)
810 throws IllegalArgumentException
813 throw new NullPointerException("null argument");
814 if (!(date instanceof Date))
815 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
817 AttributedFormatBuffer buf = new AttributedFormatBuffer();
818 formatWithAttribute((Date)date, buf,
822 return new FormatCharacterIterator(buf.getBuffer().toString(),
824 buf.getAttributes());
827 private void withLeadingZeros(int value, int length, FormatBuffer buffer)
829 String valStr = String.valueOf(value);
830 for (length -= valStr.length(); length > 0; length--)
832 buffer.append(valStr);
835 private boolean expect(String source, ParsePosition pos, char ch)
837 int x = pos.getIndex();
838 boolean r = x < source.length() && source.charAt(x) == ch;
842 pos.setErrorIndex(x);
847 * This method parses the specified string into a date.
849 * @param dateStr The date string to parse.
850 * @param pos The input and output parse position
852 * @return The parsed date, or <code>null</code> if the string cannot be
855 public Date parse (String dateStr, ParsePosition pos)
858 int fmt_max = pattern.length();
861 boolean saw_timezone = false;
862 int quote_start = -1;
863 boolean is2DigitYear = false;
866 for (; fmt_index < fmt_max; ++fmt_index)
868 char ch = pattern.charAt(fmt_index);
871 int index = pos.getIndex();
872 if (fmt_index < fmt_max - 1
873 && pattern.charAt(fmt_index + 1) == '\'')
875 if (! expect (dateStr, pos, ch))
880 quote_start = quote_start < 0 ? fmt_index : -1;
884 if (quote_start != -1
885 || ((ch < 'a' || ch > 'z')
886 && (ch < 'A' || ch > 'Z')))
888 if (! expect (dateStr, pos, ch))
893 // We've arrived at a potential pattern character in the
896 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
901 // We might need to limit the number of digits to parse in
902 // some cases. We look to the next pattern character to
904 boolean limit_digits = false;
905 if (fmt_index < fmt_max
906 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
910 // We can handle most fields automatically: most either are
911 // numeric or are looked up in a string vector. In some cases
912 // we need an offset. When numeric, `offset' is added to the
913 // resulting value. When doing a string lookup, offset is the
914 // initial index into the string array.
916 boolean is_numeric = true;
918 boolean maybe2DigitYear = false;
919 boolean oneBasedHour = false;
920 boolean oneBasedHourOfDay = false;
921 Integer simpleOffset;
922 String[] set1 = null;
923 String[] set2 = null;
927 calendar_field = Calendar.DATE;
930 calendar_field = Calendar.DAY_OF_YEAR;
933 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
938 calendar_field = Calendar.DAY_OF_WEEK;
939 set1 = formatData.getWeekdays();
940 set2 = formatData.getShortWeekdays();
943 calendar_field = Calendar.WEEK_OF_YEAR;
946 calendar_field = Calendar.WEEK_OF_MONTH;
949 calendar_field = Calendar.MONTH;
955 set1 = formatData.getMonths();
956 set2 = formatData.getShortMonths();
960 calendar_field = Calendar.YEAR;
962 maybe2DigitYear = true;
965 calendar_field = Calendar.HOUR;
968 calendar_field = Calendar.HOUR;
972 calendar_field = Calendar.HOUR_OF_DAY;
975 calendar_field = Calendar.HOUR_OF_DAY;
976 oneBasedHourOfDay = true;
979 calendar_field = Calendar.MINUTE;
982 calendar_field = Calendar.SECOND;
985 calendar_field = Calendar.MILLISECOND;
989 calendar_field = Calendar.AM_PM;
990 set1 = formatData.getAmPmStrings();
994 // We need a special case for the timezone, because it
995 // uses a different data structure than the other cases.
997 calendar_field = Calendar.ZONE_OFFSET;
998 String[][] zoneStrings = formatData.getZoneStrings();
999 int zoneCount = zoneStrings.length;
1000 int index = pos.getIndex();
1001 boolean found_zone = false;
1002 simpleOffset = computeOffset(dateStr.substring(index));
1003 if (simpleOffset != null)
1006 saw_timezone = true;
1007 calendar.set(Calendar.DST_OFFSET, 0);
1008 offset = simpleOffset.intValue();
1012 for (int j = 0; j < zoneCount; j++)
1014 String[] strings = zoneStrings[j];
1016 for (k = 0; k < strings.length; ++k)
1018 if (dateStr.startsWith(strings[k], index))
1021 if (k != strings.length)
1024 saw_timezone = true;
1025 TimeZone tz = TimeZone.getTimeZone (strings[0]);
1026 // Check if it's a DST zone or ordinary
1027 if(k == 3 || k == 4)
1028 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1030 calendar.set (Calendar.DST_OFFSET, 0);
1031 offset = tz.getRawOffset ();
1032 pos.setIndex(index + strings[k].length());
1039 pos.setErrorIndex(pos.getIndex());
1044 pos.setErrorIndex(pos.getIndex());
1048 // Compute the value we should assign to the field.
1053 numberFormat.setMinimumIntegerDigits(fmt_count);
1055 numberFormat.setMaximumIntegerDigits(fmt_count);
1056 if (maybe2DigitYear)
1057 index = pos.getIndex();
1058 Number n = numberFormat.parse(dateStr, pos);
1059 if (pos == null || ! (n instanceof Long))
1061 value = n.intValue() + offset;
1063 else if (set1 != null)
1065 index = pos.getIndex();
1067 boolean found = false;
1068 for (i = offset; i < set1.length; ++i)
1070 if (set1[i] != null)
1071 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1075 pos.setIndex(index + set1[i].length());
1079 if (!found && set2 != null)
1081 for (i = offset; i < set2.length; ++i)
1083 if (set2[i] != null)
1084 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1088 pos.setIndex(index + set2[i].length());
1095 pos.setErrorIndex(index);
1103 if (maybe2DigitYear)
1105 // Parse into default century if the numeric year string has
1106 // exactly 2 digits.
1107 int digit_count = pos.getIndex() - index;
1108 if (digit_count == 2)
1110 is2DigitYear = true;
1111 value += defaultCentury;
1115 // Calendar uses 0-based hours.
1116 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1117 if (oneBasedHour && value == 12)
1120 if (oneBasedHourOfDay && value == 24)
1123 // Assign the value and move on.
1124 calendar.set(calendar_field, value);
1129 // Apply the 80-20 heuristic to dermine the full year based on
1130 // defaultCenturyStart.
1131 int year = calendar.get(Calendar.YEAR);
1132 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1133 calendar.set(Calendar.YEAR, year + 100);
1137 // Use the real rules to determine whether or not this
1138 // particular time is in daylight savings.
1139 calendar.clear (Calendar.DST_OFFSET);
1140 calendar.clear (Calendar.ZONE_OFFSET);
1142 return calendar.getTime();
1144 catch (IllegalArgumentException x)
1146 pos.setErrorIndex(pos.getIndex());
1153 * Computes the time zone offset in milliseconds
1154 * relative to GMT, based on the supplied
1155 * <code>String</code> representation.
1158 * The supplied <code>String</code> must be a three
1159 * or four digit signed number, with an optional 'GMT'
1160 * prefix. The first one or two digits represents the hours,
1161 * while the last two represent the minutes. The
1162 * two sets of digits can optionally be separated by
1163 * ':'. The mandatory sign prefix (either '+' or '-')
1164 * indicates the direction of the offset from GMT.
1167 * For example, 'GMT+0200' specifies 2 hours after
1168 * GMT, while '-05:00' specifies 5 hours prior to
1169 * GMT. The special case of 'GMT' alone can be used
1170 * to represent the offset, 0.
1173 * If the <code>String</code> can not be parsed,
1174 * the result will be null. The resulting offset
1175 * is wrapped in an <code>Integer</code> object, in
1176 * order to allow such failure to be represented.
1179 * @param zoneString a string in the form
1180 * (GMT)? sign hours : minutes
1181 * where sign = '+' or '-', hours
1182 * is a one or two digits representing
1183 * a number between 0 and 23, and
1184 * minutes is two digits representing
1185 * a number between 0 and 59.
1186 * @return the parsed offset, or null if parsing
1189 private Integer computeOffset(String zoneString)
1192 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1193 Matcher matcher = pattern.matcher(zoneString);
1194 if (matcher.matches())
1196 int sign = matcher.group(2).equals("+") ? 1 : -1;
1197 int hour = (Integer.parseInt(matcher.group(3)) * 10)
1198 + Integer.parseInt(matcher.group(4));
1199 int minutes = Integer.parseInt(matcher.group(5));
1204 int offset = sign * ((hour * 60) + minutes) * 60000;
1205 return new Integer(offset);
1207 else if (zoneString.startsWith("GMT"))
1209 return new Integer(0);
1214 // Compute the start of the current century as defined by
1215 // get2DigitYearStart.
1216 private void computeCenturyStart()
1218 int year = calendar.get(Calendar.YEAR);
1219 calendar.set(Calendar.YEAR, year - 80);
1220 set2DigitYearStart(calendar.getTime());
1224 * Returns a copy of this instance of
1225 * <code>SimpleDateFormat</code>. The copy contains
1226 * clones of the formatting symbols and the 2-digit
1227 * year century start date.
1229 public Object clone()
1231 SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1232 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1233 clone.set2DigitYearStart((Date) defaultCenturyStart.clone());