1 /* SimpleDateFormat.java -- A class for parsing/formating simple
3 Copyright (C) 1998, 1999, 2000, 2001, 2003 Free Software Foundation, Inc.
5 This file is part of GNU Classpath.
7 GNU Classpath is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2, or (at your option)
12 GNU Classpath is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with GNU Classpath; see the file COPYING. If not, write to the
19 Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
22 Linking this library statically or dynamically with other modules is
23 making a combined work based on this library. Thus, the terms and
24 conditions of the GNU General Public License cover the whole
27 As a special exception, the copyright holders of this library give you
28 permission to link this library with independent modules to produce an
29 executable, regardless of the license terms of these independent
30 modules, and to copy and distribute the resulting executable under
31 terms of your choice, provided that you also meet, for each linked
32 independent module, the terms and conditions of the license of that
33 module. An independent module is a module which is not derived from
34 or based on this library. If you modify this library, you may extend
35 this exception to your version of the library, but you are not
36 obligated to do so. If you do not wish to do so, delete this
37 exception statement from your version. */
42 import java.util.Calendar;
43 import java.util.Date;
44 import java.util.Enumeration;
45 import java.util.GregorianCalendar;
46 import java.util.Locale;
47 import java.util.TimeZone;
48 import java.util.SimpleTimeZone;
49 import java.util.Vector;
50 import java.io.ObjectInputStream;
51 import java.io.IOException;
54 * SimpleDateFormat provides convenient methods for parsing and formatting
55 * dates using Gregorian calendars (see java.util.GregorianCalendar).
57 public class SimpleDateFormat extends DateFormat
59 /** A pair class used by SimpleDateFormat as a compiled representation
62 private class FieldSizePair
67 /** Constructs a pair with the given field and size values */
68 public FieldSizePair(int f, int s) {
74 private transient Vector tokens;
75 private DateFormatSymbols formatData; // formatData
76 private Date defaultCenturyStart;
77 private transient int defaultCentury;
78 private String pattern;
79 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
80 private static final long serialVersionUID = 4774881970558875024L;
82 // This string is specified in the JCL. We set it here rather than
83 // do a DateFormatSymbols(Locale.US).getLocalPatternChars() since
84 // someone could theoretically change those values (though unlikely).
85 private static final String standardChars = "GyMdkHmsSEDFwWahKz";
87 private void readObject(ObjectInputStream stream)
88 throws IOException, ClassNotFoundException
90 stream.defaultReadObject();
91 if (serialVersionOnStream < 1)
93 computeCenturyStart ();
94 serialVersionOnStream = 1;
97 // Ensure that defaultCentury gets set.
98 set2DigitYearStart(defaultCenturyStart);
100 // Set up items normally taken care of by the constructor.
101 tokens = new Vector();
102 compileFormat(pattern);
105 private void compileFormat(String pattern)
107 // Any alphabetical characters are treated as pattern characters
108 // unless enclosed in single quotes.
113 FieldSizePair current = null;
115 for (int i=0; i<pattern.length(); i++) {
116 thisChar = pattern.charAt(i);
117 field = formatData.getLocalPatternChars().indexOf(thisChar);
120 if (Character.isLetter(thisChar)) {
121 // Not a valid letter
122 tokens.addElement(new FieldSizePair(-1,0));
123 } else if (thisChar == '\'') {
124 // Quoted text section; skip to next single quote
125 pos = pattern.indexOf('\'',i+1);
127 // This ought to be an exception, but spec does not
129 tokens.addElement(new FieldSizePair(-1,0));
131 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
132 tokens.addElement(pattern.substring(i+1,pos+1));
134 tokens.addElement(pattern.substring(i+1,pos));
138 // A special character
139 tokens.addElement(new Character(thisChar));
143 if ((current != null) && (field == current.field)) {
146 current = new FieldSizePair(field,1);
147 tokens.addElement(current);
153 public String toString()
155 StringBuffer output = new StringBuffer();
156 Enumeration e = tokens.elements();
157 while (e.hasMoreElements()) {
158 output.append(e.nextElement().toString());
160 return output.toString();
164 * Constructs a SimpleDateFormat using the default pattern for
165 * the default locale.
167 public SimpleDateFormat()
170 * There does not appear to be a standard API for determining
171 * what the default pattern for a locale is, so use package-scope
172 * variables in DateFormatSymbols to encapsulate this.
175 Locale locale = Locale.getDefault();
176 calendar = new GregorianCalendar(locale);
177 computeCenturyStart();
178 tokens = new Vector();
179 formatData = new DateFormatSymbols(locale);
180 pattern = (formatData.dateFormats[DEFAULT] + ' '
181 + formatData.timeFormats[DEFAULT]);
182 compileFormat(pattern);
183 numberFormat = NumberFormat.getInstance(locale);
184 numberFormat.setGroupingUsed (false);
185 numberFormat.setParseIntegerOnly (true);
189 * Creates a date formatter using the specified pattern, with the default
190 * DateFormatSymbols for the default locale.
192 public SimpleDateFormat(String pattern)
194 this(pattern, Locale.getDefault());
198 * Creates a date formatter using the specified pattern, with the default
199 * DateFormatSymbols for the given locale.
201 public SimpleDateFormat(String pattern, Locale locale)
204 calendar = new GregorianCalendar(locale);
205 computeCenturyStart();
206 tokens = new Vector();
207 formatData = new DateFormatSymbols(locale);
208 compileFormat(pattern);
209 this.pattern = pattern;
210 numberFormat = NumberFormat.getInstance(locale);
211 numberFormat.setGroupingUsed (false);
212 numberFormat.setParseIntegerOnly (true);
216 * Creates a date formatter using the specified pattern. The
217 * specified DateFormatSymbols will be used when formatting.
219 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
222 calendar = new GregorianCalendar();
223 computeCenturyStart ();
224 tokens = new Vector();
225 this.formatData = formatData;
226 compileFormat(pattern);
227 this.pattern = pattern;
228 numberFormat = NumberFormat.getInstance();
229 numberFormat.setGroupingUsed (false);
230 numberFormat.setParseIntegerOnly (true);
233 // What is the difference between localized and unlocalized? The
237 * This method returns a string with the formatting pattern being used
238 * by this object. This string is unlocalized.
240 * @return The format string.
242 public String toPattern()
248 * This method returns a string with the formatting pattern being used
249 * by this object. This string is localized.
251 * @return The format string.
253 public String toLocalizedPattern()
255 String localChars = formatData.getLocalPatternChars();
256 return applyLocalizedPattern (pattern, standardChars, localChars);
260 * This method sets the formatting pattern that should be used by this
261 * object. This string is not localized.
263 * @param pattern The new format pattern.
265 public void applyPattern(String pattern)
267 tokens = new Vector();
268 compileFormat(pattern);
269 this.pattern = pattern;
273 * This method sets the formatting pattern that should be used by this
274 * object. This string is localized.
276 * @param pattern The new format pattern.
278 public void applyLocalizedPattern(String pattern)
280 String localChars = formatData.getLocalPatternChars();
281 pattern = applyLocalizedPattern (pattern, localChars, standardChars);
282 applyPattern(pattern);
285 private String applyLocalizedPattern(String pattern,
286 String oldChars, String newChars)
288 int len = pattern.length();
289 StringBuffer buf = new StringBuffer(len);
290 boolean quoted = false;
291 for (int i = 0; i < len; i++)
293 char ch = pattern.charAt(i);
298 int j = oldChars.indexOf(ch);
300 ch = newChars.charAt(j);
304 return buf.toString();
308 * Returns the start of the century used for two digit years.
310 * @return A <code>Date</code> representing the start of the century
311 * for two digit years.
313 public Date get2DigitYearStart()
315 return defaultCenturyStart;
319 * Sets the start of the century used for two digit years.
321 * @param date A <code>Date</code> representing the start of the century for
324 public void set2DigitYearStart(Date date)
326 defaultCenturyStart = date;
328 calendar.setTime(date);
329 int year = calendar.get(Calendar.YEAR);
330 defaultCentury = year - (year % 100);
334 * This method returns the format symbol information used for parsing
335 * and formatting dates.
337 * @return The date format symbols.
339 public DateFormatSymbols getDateFormatSymbols()
345 * This method sets the format symbols information used for parsing
346 * and formatting dates.
348 * @param formatData The date format symbols.
350 public void setDateFormatSymbols(DateFormatSymbols formatData)
352 this.formatData = formatData;
356 * This methods tests whether the specified object is equal to this
357 * object. This will be true if and only if the specified object:
360 * <li>Is not <code>null</code>.
361 * <li>Is an instance of <code>SimpleDateFormat</code>.
362 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
364 * <li>Has the same formatting pattern.
365 * <li>Is using the same formatting symbols.
366 * <li>Is using the same century for two digit years.
369 * @param obj The object to compare for equality against.
371 * @return <code>true</code> if the specified object is equal to this object,
372 * <code>false</code> otherwise.
374 public boolean equals(Object o)
379 if (!super.equals(o))
382 if (!(o instanceof SimpleDateFormat))
385 SimpleDateFormat sdf = (SimpleDateFormat)o;
387 if (defaultCentury != sdf.defaultCentury)
390 if (!toPattern().equals(sdf.toPattern()))
393 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
400 * This method returns a hash value for this object.
402 * @return A hash value for this object.
404 public int hashCode()
406 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
407 getDateFormatSymbols().hashCode();
412 * Formats the date input according to the format string in use,
413 * appending to the specified StringBuffer. The input StringBuffer
414 * is returned as output for convenience.
416 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
419 calendar.setTime(date);
421 // go through vector, filling in fields where applicable, else toString
422 Enumeration e = tokens.elements();
423 while (e.hasMoreElements()) {
424 Object o = e.nextElement();
425 if (o instanceof FieldSizePair) {
426 FieldSizePair p = (FieldSizePair) o;
427 int beginIndex = buffer.length();
430 buffer.append(formatData.eras[calendar.get(Calendar.ERA)]);
433 // If we have two digits, then we truncate. Otherwise, we
434 // use the size of the pattern, and zero pad.
437 temp = String.valueOf(calendar.get(Calendar.YEAR));
438 buffer.append(temp.substring(temp.length() - 2));
441 withLeadingZeros(calendar.get(Calendar.YEAR), p.size, buffer);
445 withLeadingZeros(calendar.get(Calendar.MONTH)+1,p.size,buffer);
447 buffer.append(formatData.shortMonths[calendar.get(Calendar.MONTH)]);
449 buffer.append(formatData.months[calendar.get(Calendar.MONTH)]);
452 withLeadingZeros(calendar.get(Calendar.DATE),p.size,buffer);
454 case HOUR_OF_DAY1_FIELD: // 1-24
455 withLeadingZeros(((calendar.get(Calendar.HOUR_OF_DAY)+23)%24)+1,p.size,buffer);
457 case HOUR_OF_DAY0_FIELD: // 0-23
458 withLeadingZeros(calendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
461 withLeadingZeros(calendar.get(Calendar.MINUTE),p.size,buffer);
464 withLeadingZeros(calendar.get(Calendar.SECOND),p.size,buffer);
466 case MILLISECOND_FIELD:
467 withLeadingZeros(calendar.get(Calendar.MILLISECOND),p.size,buffer);
469 case DAY_OF_WEEK_FIELD:
471 buffer.append(formatData.shortWeekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
473 buffer.append(formatData.weekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
475 case DAY_OF_YEAR_FIELD:
476 withLeadingZeros(calendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
478 case DAY_OF_WEEK_IN_MONTH_FIELD:
479 withLeadingZeros(calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
481 case WEEK_OF_YEAR_FIELD:
482 withLeadingZeros(calendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
484 case WEEK_OF_MONTH_FIELD:
485 withLeadingZeros(calendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
488 buffer.append(formatData.ampms[calendar.get(Calendar.AM_PM)]);
490 case HOUR1_FIELD: // 1-12
491 withLeadingZeros(((calendar.get(Calendar.HOUR)+11)%12)+1,p.size,buffer);
493 case HOUR0_FIELD: // 0-11
494 withLeadingZeros(calendar.get(Calendar.HOUR),p.size,buffer);
497 TimeZone zone = calendar.getTimeZone();
498 boolean isDST = calendar.get(Calendar.DST_OFFSET) != 0;
499 // FIXME: XXX: This should be a localized time zone.
500 String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
501 buffer.append(zoneID);
504 throw new IllegalArgumentException("Illegal pattern character");
506 if (pos != null && p.field == pos.getField())
508 pos.setBeginIndex(beginIndex);
509 pos.setEndIndex(buffer.length());
512 buffer.append(o.toString());
518 private void withLeadingZeros(int value, int length, StringBuffer buffer)
520 String valStr = String.valueOf(value);
521 for (length -= valStr.length(); length > 0; length--)
523 buffer.append(valStr);
526 private final boolean expect (String source, ParsePosition pos, char ch)
528 int x = pos.getIndex();
529 boolean r = x < source.length() && source.charAt(x) == ch;
533 pos.setErrorIndex(x);
538 * This method parses the specified string into a date.
540 * @param dateStr The date string to parse.
541 * @param pos The input and output parse position
543 * @return The parsed date, or <code>null</code> if the string cannot be
546 public Date parse (String dateStr, ParsePosition pos)
549 int fmt_max = pattern.length();
552 boolean saw_timezone = false;
553 int quote_start = -1;
554 boolean is2DigitYear = false;
555 for (; fmt_index < fmt_max; ++fmt_index)
557 char ch = pattern.charAt(fmt_index);
560 int index = pos.getIndex();
561 if (fmt_index < fmt_max - 1
562 && pattern.charAt(fmt_index + 1) == '\'')
564 if (! expect (dateStr, pos, ch))
569 quote_start = quote_start < 0 ? fmt_index : -1;
573 if (quote_start != -1
574 || ((ch < 'a' || ch > 'z')
575 && (ch < 'A' || ch > 'Z')))
577 if (! expect (dateStr, pos, ch))
582 // We've arrived at a potential pattern character in the
584 int first = fmt_index;
585 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
587 int fmt_count = fmt_index - first;
589 // We might need to limit the number of digits to parse in
590 // some cases. We look to the next pattern character to
592 boolean limit_digits = false;
593 if (fmt_index < fmt_max
594 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
598 // We can handle most fields automatically: most either are
599 // numeric or are looked up in a string vector. In some cases
600 // we need an offset. When numeric, `offset' is added to the
601 // resulting value. When doing a string lookup, offset is the
602 // initial index into the string array.
604 boolean is_numeric = true;
605 String[] match = null;
607 boolean maybe2DigitYear = false;
611 calendar_field = Calendar.DATE;
614 calendar_field = Calendar.DAY_OF_YEAR;
617 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
622 calendar_field = Calendar.DAY_OF_WEEK;
623 match = (fmt_count <= 3
624 ? formatData.getShortWeekdays()
625 : formatData.getWeekdays());
628 calendar_field = Calendar.WEEK_OF_YEAR;
631 calendar_field = Calendar.WEEK_OF_MONTH;
634 calendar_field = Calendar.MONTH;
640 match = (fmt_count <= 3
641 ? formatData.getShortMonths()
642 : formatData.getMonths());
646 calendar_field = Calendar.YEAR;
648 maybe2DigitYear = true;
651 calendar_field = Calendar.HOUR;
654 calendar_field = Calendar.HOUR;
657 calendar_field = Calendar.HOUR_OF_DAY;
660 calendar_field = Calendar.HOUR_OF_DAY;
663 calendar_field = Calendar.MINUTE;
666 calendar_field = Calendar.SECOND;
669 calendar_field = Calendar.MILLISECOND;
673 calendar_field = Calendar.AM_PM;
674 match = formatData.getAmPmStrings();
677 // We need a special case for the timezone, because it
678 // uses a different data structure than the other cases.
680 calendar_field = Calendar.DST_OFFSET;
681 String[][] zoneStrings = formatData.getZoneStrings();
682 int zoneCount = zoneStrings.length;
683 int index = pos.getIndex();
684 boolean found_zone = false;
685 for (int j = 0; j < zoneCount; j++)
687 String[] strings = zoneStrings[j];
689 for (k = 1; k < strings.length; ++k)
691 if (dateStr.startsWith(strings[k], index))
694 if (k != strings.length)
698 TimeZone tz = TimeZone.getTimeZone (strings[0]);
699 calendar.set (Calendar.ZONE_OFFSET, tz.getRawOffset ());
701 if (k > 2 && tz instanceof SimpleTimeZone)
703 SimpleTimeZone stz = (SimpleTimeZone) tz;
704 offset = stz.getDSTSavings ();
706 pos.setIndex(index + strings[k].length());
712 pos.setErrorIndex(pos.getIndex());
717 pos.setErrorIndex(pos.getIndex());
721 // Compute the value we should assign to the field.
726 numberFormat.setMinimumIntegerDigits(fmt_count);
728 numberFormat.setMaximumIntegerDigits(fmt_count);
730 index = pos.getIndex();
731 Number n = numberFormat.parse(dateStr, pos);
732 if (pos == null || ! (n instanceof Long))
734 value = n.intValue() + offset;
736 else if (match != null)
738 index = pos.getIndex();
740 for (i = offset; i < match.length; ++i)
742 if (dateStr.startsWith(match[i], index))
745 if (i == match.length)
747 pos.setErrorIndex(index);
750 pos.setIndex(index + match[i].length());
758 // Parse into default century if the numeric year string has
760 int digit_count = pos.getIndex() - index;
761 if (digit_count == 2)
765 // Assign the value and move on.
766 calendar.set(calendar_field, value);
771 // Apply the 80-20 heuristic to dermine the full year based on
772 // defaultCenturyStart.
773 int year = defaultCentury + calendar.get(Calendar.YEAR);
774 calendar.set(Calendar.YEAR, year);
775 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
776 calendar.set(Calendar.YEAR, year + 100);
783 // Use the real rules to determine whether or not this
784 // particular time is in daylight savings.
785 calendar.clear (Calendar.DST_OFFSET);
786 calendar.clear (Calendar.ZONE_OFFSET);
788 return calendar.getTime();
790 catch (IllegalArgumentException x)
792 pos.setErrorIndex(pos.getIndex());
797 // Compute the start of the current century as defined by
798 // get2DigitYearStart.
799 private void computeCenturyStart()
801 int year = calendar.get(Calendar.YEAR);
802 calendar.set(Calendar.YEAR, year - 80);
803 set2DigitYearStart(calendar.getTime());