1 /* SimpleDateFormat.java -- A class for parsing/formating simple
3 Copyright (C) 1998, 1999, 2000, 2001 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 As a special exception, if you link this library with other files to
23 produce an executable, this library does not by itself cause the
24 resulting executable to be covered by the GNU General Public License.
25 This exception does not however invalidate any other reasons why the
26 executable file might be covered by the GNU General Public License. */
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.Enumeration;
34 import java.util.GregorianCalendar;
35 import java.util.Locale;
36 import java.util.TimeZone;
37 import java.util.SimpleTimeZone;
38 import java.util.Vector;
39 import java.io.ObjectInputStream;
40 import java.io.IOException;
43 * SimpleDateFormat provides convenient methods for parsing and formatting
44 * dates using Gregorian calendars (see java.util.GregorianCalendar).
46 public class SimpleDateFormat extends DateFormat
48 /** A pair class used by SimpleDateFormat as a compiled representation
51 private class FieldSizePair
56 /** Constructs a pair with the given field and size values */
57 public FieldSizePair(int f, int s) {
63 private transient Vector tokens;
64 private DateFormatSymbols formatData; // formatData
65 private Date defaultCenturyStart = computeCenturyStart ();
66 private String pattern;
67 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
68 private static final long serialVersionUID = 4774881970558875024L;
70 // This string is specified in the JCL. We set it here rather than
71 // do a DateFormatSymbols(Locale.US).getLocalPatternChars() since
72 // someone could theoretically change those values (though unlikely).
73 private static final String standardChars = "GyMdkHmsSEDFwWahKz";
75 private void readObject(ObjectInputStream stream)
76 throws IOException, ClassNotFoundException
78 stream.defaultReadObject();
79 if (serialVersionOnStream < 1)
81 defaultCenturyStart = computeCenturyStart ();
82 serialVersionOnStream = 1;
85 // Set up items normally taken care of by the constructor.
86 tokens = new Vector();
87 compileFormat(pattern);
90 private void compileFormat(String pattern)
92 // Any alphabetical characters are treated as pattern characters
93 // unless enclosed in single quotes.
98 FieldSizePair current = null;
100 for (int i=0; i<pattern.length(); i++) {
101 thisChar = pattern.charAt(i);
102 field = formatData.getLocalPatternChars().indexOf(thisChar);
105 if (Character.isLetter(thisChar)) {
106 // Not a valid letter
107 tokens.addElement(new FieldSizePair(-1,0));
108 } else if (thisChar == '\'') {
109 // Quoted text section; skip to next single quote
110 pos = pattern.indexOf('\'',i+1);
112 // This ought to be an exception, but spec does not
114 tokens.addElement(new FieldSizePair(-1,0));
116 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
117 tokens.addElement(pattern.substring(i+1,pos+1));
119 tokens.addElement(pattern.substring(i+1,pos));
123 // A special character
124 tokens.addElement(new Character(thisChar));
128 if ((current != null) && (field == current.field)) {
131 current = new FieldSizePair(field,1);
132 tokens.addElement(current);
138 public String toString()
140 StringBuffer output = new StringBuffer();
141 Enumeration e = tokens.elements();
142 while (e.hasMoreElements()) {
143 output.append(e.nextElement().toString());
145 return output.toString();
149 * Constructs a SimpleDateFormat using the default pattern for
150 * the default locale.
152 public SimpleDateFormat()
155 * There does not appear to be a standard API for determining
156 * what the default pattern for a locale is, so use package-scope
157 * variables in DateFormatSymbols to encapsulate this.
160 Locale locale = Locale.getDefault();
161 calendar = new GregorianCalendar(locale);
163 tokens = new Vector();
164 formatData = new DateFormatSymbols(locale);
165 pattern = (formatData.dateFormats[DEFAULT] + ' '
166 + formatData.timeFormats[DEFAULT]);
167 compileFormat(pattern);
168 numberFormat = NumberFormat.getInstance(locale);
169 numberFormat.setGroupingUsed (false);
173 * Creates a date formatter using the specified pattern, with the default
174 * DateFormatSymbols for the default locale.
176 public SimpleDateFormat(String pattern)
178 this(pattern, Locale.getDefault());
182 * Creates a date formatter using the specified pattern, with the default
183 * DateFormatSymbols for the given locale.
185 public SimpleDateFormat(String pattern, Locale locale)
188 calendar = new GregorianCalendar(locale);
190 tokens = new Vector();
191 formatData = new DateFormatSymbols(locale);
192 compileFormat(pattern);
193 this.pattern = pattern;
194 numberFormat = NumberFormat.getInstance(locale);
195 numberFormat.setGroupingUsed (false);
199 * Creates a date formatter using the specified pattern. The
200 * specified DateFormatSymbols will be used when formatting.
202 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
205 calendar = new GregorianCalendar();
207 // FIXME: XXX: Is it really necessary to set the timezone?
208 // The Calendar constructor is supposed to take care of this.
209 calendar.setTimeZone(TimeZone.getDefault());
210 tokens = new Vector();
211 this.formatData = formatData;
212 compileFormat(pattern);
213 this.pattern = pattern;
214 numberFormat = NumberFormat.getInstance();
215 numberFormat.setGroupingUsed (false);
218 // What is the difference between localized and unlocalized? The
222 * This method returns a string with the formatting pattern being used
223 * by this object. This string is unlocalized.
225 * @return The format string.
227 public String toPattern()
233 * This method returns a string with the formatting pattern being used
234 * by this object. This string is localized.
236 * @return The format string.
238 public String toLocalizedPattern()
240 String localChars = formatData.getLocalPatternChars();
241 return applyLocalizedPattern (pattern, standardChars, localChars);
245 * This method sets the formatting pattern that should be used by this
246 * object. This string is not localized.
248 * @param pattern The new format pattern.
250 public void applyPattern(String pattern)
252 tokens = new Vector();
253 compileFormat(pattern);
254 this.pattern = pattern;
258 * This method sets the formatting pattern that should be used by this
259 * object. This string is localized.
261 * @param pattern The new format pattern.
263 public void applyLocalizedPattern(String pattern)
265 String localChars = formatData.getLocalPatternChars();
266 pattern = applyLocalizedPattern (pattern, localChars, standardChars);
267 applyPattern(pattern);
270 private String applyLocalizedPattern(String pattern,
271 String oldChars, String newChars)
273 int len = pattern.length();
274 StringBuffer buf = new StringBuffer(len);
275 boolean quoted = false;
276 for (int i = 0; i < len; i++)
278 char ch = pattern.charAt(i);
283 int j = oldChars.indexOf(ch);
285 ch = newChars.charAt(j);
289 return buf.toString();
293 * Returns the start of the century used for two digit years.
295 * @return A <code>Date</code> representing the start of the century
296 * for two digit years.
298 public Date get2DigitYearStart()
300 return defaultCenturyStart;
304 * Sets the start of the century used for two digit years.
306 * @param date A <code>Date</code> representing the start of the century for
309 public void set2DigitYearStart(Date date)
311 defaultCenturyStart = date;
315 * This method returns the format symbol information used for parsing
316 * and formatting dates.
318 * @return The date format symbols.
320 public DateFormatSymbols getDateFormatSymbols()
326 * This method sets the format symbols information used for parsing
327 * and formatting dates.
329 * @param formatData The date format symbols.
331 public void setDateFormatSymbols(DateFormatSymbols formatData)
333 this.formatData = formatData;
337 * This methods tests whether the specified object is equal to this
338 * object. This will be true if and only if the specified object:
341 * <li>Is not <code>null</code>.
342 * <li>Is an instance of <code>SimpleDateFormat</code>.
343 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
345 * <li>Has the same formatting pattern.
346 * <li>Is using the same formatting symbols.
347 * <li>Is using the same century for two digit years.
350 * @param obj The object to compare for equality against.
352 * @return <code>true</code> if the specified object is equal to this object,
353 * <code>false</code> otherwise.
355 public boolean equals(Object o)
360 if (!super.equals(o))
363 if (!(o instanceof SimpleDateFormat))
366 SimpleDateFormat sdf = (SimpleDateFormat)o;
368 if (!toPattern().equals(sdf.toPattern()))
371 if (!get2DigitYearStart().equals(sdf.get2DigitYearStart()))
374 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
382 * Formats the date input according to the format string in use,
383 * appending to the specified StringBuffer. The input StringBuffer
384 * is returned as output for convenience.
386 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
389 Calendar theCalendar = (Calendar) calendar.clone();
390 theCalendar.setTime(date);
392 // go through vector, filling in fields where applicable, else toString
393 Enumeration e = tokens.elements();
394 while (e.hasMoreElements()) {
395 Object o = e.nextElement();
396 if (o instanceof FieldSizePair) {
397 FieldSizePair p = (FieldSizePair) o;
398 int beginIndex = buffer.length();
401 buffer.append(formatData.eras[theCalendar.get(Calendar.ERA)]);
404 temp = String.valueOf(theCalendar.get(Calendar.YEAR));
406 buffer.append(temp.substring(temp.length()-2));
412 withLeadingZeros(theCalendar.get(Calendar.MONTH)+1,p.size,buffer);
414 buffer.append(formatData.shortMonths[theCalendar.get(Calendar.MONTH)]);
416 buffer.append(formatData.months[theCalendar.get(Calendar.MONTH)]);
419 withLeadingZeros(theCalendar.get(Calendar.DATE),p.size,buffer);
421 case HOUR_OF_DAY1_FIELD: // 1-24
422 withLeadingZeros(((theCalendar.get(Calendar.HOUR_OF_DAY)+23)%24)+1,p.size,buffer);
424 case HOUR_OF_DAY0_FIELD: // 0-23
425 withLeadingZeros(theCalendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
428 withLeadingZeros(theCalendar.get(Calendar.MINUTE),p.size,buffer);
431 withLeadingZeros(theCalendar.get(Calendar.SECOND),p.size,buffer);
433 case MILLISECOND_FIELD:
434 withLeadingZeros(theCalendar.get(Calendar.MILLISECOND),p.size,buffer);
436 case DAY_OF_WEEK_FIELD:
438 buffer.append(formatData.shortWeekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
440 buffer.append(formatData.weekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
442 case DAY_OF_YEAR_FIELD:
443 withLeadingZeros(theCalendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
445 case DAY_OF_WEEK_IN_MONTH_FIELD:
446 withLeadingZeros(theCalendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
448 case WEEK_OF_YEAR_FIELD:
449 withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
451 case WEEK_OF_MONTH_FIELD:
452 withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
455 buffer.append(formatData.ampms[theCalendar.get(Calendar.AM_PM)]);
457 case HOUR1_FIELD: // 1-12
458 withLeadingZeros(((theCalendar.get(Calendar.HOUR)+11)%12)+1,p.size,buffer);
460 case HOUR0_FIELD: // 0-11
461 withLeadingZeros(theCalendar.get(Calendar.HOUR),p.size,buffer);
464 TimeZone zone = theCalendar.getTimeZone();
465 boolean isDST = theCalendar.get(Calendar.DST_OFFSET) != 0;
466 // FIXME: XXX: This should be a localized time zone.
467 String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
468 buffer.append(zoneID);
471 throw new IllegalArgumentException("Illegal pattern character");
473 if (pos != null && p.field == pos.getField())
475 pos.setBeginIndex(beginIndex);
476 pos.setEndIndex(buffer.length());
479 buffer.append(o.toString());
485 private void withLeadingZeros(int value, int length, StringBuffer buffer) {
486 String valStr = String.valueOf(value);
487 for (length -= valStr.length(); length > 0; length--)
489 buffer.append(valStr);
492 private final boolean expect (String source, ParsePosition pos, char ch)
494 int x = pos.getIndex();
495 boolean r = x < source.length() && source.charAt(x) == ch;
499 pos.setErrorIndex(x);
504 * This method parses the specified string into a date.
506 * @param dateStr The date string to parse.
507 * @param pos The input and output parse position
509 * @return The parsed date, or <code>null</code> if the string cannot be
512 public Date parse (String dateStr, ParsePosition pos)
515 int fmt_max = pattern.length();
517 // We copy the Calendar because if we don't we will modify it and
518 // then this.equals() will no longer have the desired result.
519 Calendar theCalendar = (Calendar) calendar.clone ();
521 int quote_start = -1;
522 for (; fmt_index < fmt_max; ++fmt_index)
524 char ch = pattern.charAt(fmt_index);
527 int index = pos.getIndex();
528 if (fmt_index < fmt_max - 1
529 && pattern.charAt(fmt_index + 1) == '\'')
531 if (! expect (dateStr, pos, ch))
536 quote_start = quote_start < 0 ? fmt_index : -1;
540 if (quote_start != -1
541 || ((ch < 'a' || ch > 'z')
542 && (ch < 'A' || ch > 'Z')))
544 if (! expect (dateStr, pos, ch))
549 // We've arrived at a potential pattern character in the
551 int first = fmt_index;
552 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
554 int count = fmt_index - first;
557 // We can handle most fields automatically: most either are
558 // numeric or are looked up in a string vector. In some cases
559 // we need an offset. When numeric, `offset' is added to the
560 // resulting value. When doing a string lookup, offset is the
561 // initial index into the string array.
563 boolean is_numeric = true;
564 String[] match = null;
569 calendar_field = Calendar.DATE;
572 calendar_field = Calendar.DAY_OF_YEAR;
575 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
580 calendar_field = Calendar.DAY_OF_WEEK;
582 ? formatData.getShortWeekdays()
583 : formatData.getWeekdays());
586 calendar_field = Calendar.WEEK_OF_YEAR;
589 calendar_field = Calendar.WEEK_OF_MONTH;
592 calendar_field = Calendar.MONTH;
599 ? formatData.getShortMonths()
600 : formatData.getMonths());
604 calendar_field = Calendar.YEAR;
609 calendar_field = Calendar.HOUR;
612 calendar_field = Calendar.HOUR;
615 calendar_field = Calendar.HOUR_OF_DAY;
618 calendar_field = Calendar.HOUR_OF_DAY;
621 calendar_field = Calendar.MINUTE;
624 calendar_field = Calendar.SECOND;
627 calendar_field = Calendar.MILLISECOND;
631 calendar_field = Calendar.AM_PM;
632 match = formatData.getAmPmStrings();
635 // We need a special case for the timezone, because it
636 // uses a different data structure than the other cases.
638 // We don't actually use this; see below.
639 calendar_field = Calendar.DST_OFFSET;
640 String[][] zoneStrings = formatData.getZoneStrings();
641 int zoneCount = zoneStrings.length;
642 int index = pos.getIndex();
643 boolean found_zone = false;
644 for (int j = 0; j < zoneCount; j++)
646 String[] strings = zoneStrings[j];
648 for (k = 1; k < strings.length; ++k)
650 if (dateStr.startsWith(strings[k], index))
653 if (k != strings.length)
656 TimeZone tz = TimeZone.getTimeZone (strings[0]);
657 theCalendar.setTimeZone (tz);
658 theCalendar.clear (Calendar.DST_OFFSET);
659 theCalendar.clear (Calendar.ZONE_OFFSET);
660 pos.setIndex(index + strings[k].length());
666 pos.setErrorIndex(pos.getIndex());
671 pos.setErrorIndex(pos.getIndex());
675 // Compute the value we should assign to the field.
679 numberFormat.setMinimumIntegerDigits(count);
680 Number n = numberFormat.parse(dateStr, pos);
681 if (pos == null || ! (n instanceof Long))
683 value = n.intValue() + offset;
685 else if (match != null)
687 int index = pos.getIndex();
689 for (i = offset; i < match.length; ++i)
691 if (dateStr.startsWith(match[i], index))
694 if (i == match.length)
696 pos.setErrorIndex(index);
699 pos.setIndex(index + match[i].length());
705 // Assign the value and move on.
706 if (calendar_field != Calendar.DST_OFFSET)
707 theCalendar.set(calendar_field, value);
712 return theCalendar.getTime();
714 catch (IllegalArgumentException x)
716 pos.setErrorIndex(pos.getIndex());
721 // Compute the start of the current century as defined by
722 // get2DigitYearStart.
723 private Date computeCenturyStart ()
725 // Compute the current year. We assume a year has 365 days. Then
726 // compute 80 years ago, and finally reconstruct the number of
727 // milliseconds. We do this computation in this strange way
728 // because it lets us easily truncate the milliseconds, seconds,
729 // etc, which don't matter and which confuse
730 // SimpleDateFormat.equals().
731 long now = System.currentTimeMillis ();
732 now /= 365L * 24L * 60L * 60L * 1000L;
734 now *= 365L * 24L * 60L * 60L * 1000L;
735 return new Date (now);