2003-12-26 Guilhem Lavaux <guilhem@kaffe.org>
[official-gcc.git] / libjava / java / text / SimpleDateFormat.java
blob89f84dde5c6a00596426e38f7ca00d1edd4cfa19
1 /* SimpleDateFormat.java -- A class for parsing/formating simple
2 date constructs
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)
10 any later version.
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
20 02111-1307 USA.
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
25 combination.
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. */
40 package java.text;
42 import java.util.ArrayList;
43 import java.util.Calendar;
44 import java.util.Date;
45 import java.util.GregorianCalendar;
46 import java.util.Iterator;
47 import java.util.Locale;
48 import java.util.TimeZone;
49 import java.util.SimpleTimeZone;
50 import java.io.ObjectInputStream;
51 import java.io.IOException;
53 /**
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
60 * of a format string.
62 private class FieldSizePair
64 public int field;
65 public int size;
67 /** Constructs a pair with the given field and size values */
68 public FieldSizePair(int f, int s) {
69 field = f;
70 size = s;
74 private transient ArrayList 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;
96 else
97 // Ensure that defaultCentury gets set.
98 set2DigitYearStart(defaultCenturyStart);
100 // Set up items normally taken care of by the constructor.
101 tokens = new ArrayList();
102 compileFormat(pattern);
105 private void compileFormat(String pattern)
107 // Any alphabetical characters are treated as pattern characters
108 // unless enclosed in single quotes.
110 char thisChar;
111 int pos;
112 int field;
113 FieldSizePair current = null;
115 for (int i=0; i<pattern.length(); i++) {
116 thisChar = pattern.charAt(i);
117 field = formatData.getLocalPatternChars().indexOf(thisChar);
118 if (field == -1) {
119 current = null;
120 if ((thisChar >= 'A' && thisChar <= 'Z')
121 || (thisChar >= 'a' && thisChar <= 'z')) {
122 // Not a valid letter
123 tokens.add(new FieldSizePair(-1,0));
124 } else if (thisChar == '\'') {
125 // Quoted text section; skip to next single quote
126 pos = pattern.indexOf('\'',i+1);
127 if (pos == -1) {
128 // This ought to be an exception, but spec does not
129 // let us throw one.
130 tokens.add(new FieldSizePair(-1,0));
132 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
133 tokens.add(pattern.substring(i+1,pos+1));
134 } else {
135 tokens.add(pattern.substring(i+1,pos));
137 i = pos;
138 } else {
139 // A special character
140 tokens.add(new Character(thisChar));
142 } else {
143 // A valid field
144 if ((current != null) && (field == current.field)) {
145 current.size++;
146 } else {
147 current = new FieldSizePair(field,1);
148 tokens.add(current);
154 public String toString()
156 StringBuffer output = new StringBuffer();
157 Iterator i = tokens.iterator();
158 while (i.hasNext()) {
159 output.append(i.next().toString());
161 return output.toString();
165 * Constructs a SimpleDateFormat using the default pattern for
166 * the default locale.
168 public SimpleDateFormat()
171 * There does not appear to be a standard API for determining
172 * what the default pattern for a locale is, so use package-scope
173 * variables in DateFormatSymbols to encapsulate this.
175 super();
176 Locale locale = Locale.getDefault();
177 calendar = new GregorianCalendar(locale);
178 computeCenturyStart();
179 tokens = new ArrayList();
180 formatData = new DateFormatSymbols(locale);
181 pattern = (formatData.dateFormats[DEFAULT] + ' '
182 + formatData.timeFormats[DEFAULT]);
183 compileFormat(pattern);
184 numberFormat = NumberFormat.getInstance(locale);
185 numberFormat.setGroupingUsed (false);
186 numberFormat.setParseIntegerOnly (true);
190 * Creates a date formatter using the specified pattern, with the default
191 * DateFormatSymbols for the default locale.
193 public SimpleDateFormat(String pattern)
195 this(pattern, Locale.getDefault());
199 * Creates a date formatter using the specified pattern, with the default
200 * DateFormatSymbols for the given locale.
202 public SimpleDateFormat(String pattern, Locale locale)
204 super();
205 calendar = new GregorianCalendar(locale);
206 computeCenturyStart();
207 tokens = new ArrayList();
208 formatData = new DateFormatSymbols(locale);
209 compileFormat(pattern);
210 this.pattern = pattern;
211 numberFormat = NumberFormat.getInstance(locale);
212 numberFormat.setGroupingUsed (false);
213 numberFormat.setParseIntegerOnly (true);
217 * Creates a date formatter using the specified pattern. The
218 * specified DateFormatSymbols will be used when formatting.
220 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
222 super();
223 calendar = new GregorianCalendar();
224 computeCenturyStart ();
225 tokens = new ArrayList();
226 this.formatData = formatData;
227 compileFormat(pattern);
228 this.pattern = pattern;
229 numberFormat = NumberFormat.getInstance();
230 numberFormat.setGroupingUsed (false);
231 numberFormat.setParseIntegerOnly (true);
234 // What is the difference between localized and unlocalized? The
235 // docs don't say.
238 * This method returns a string with the formatting pattern being used
239 * by this object. This string is unlocalized.
241 * @return The format string.
243 public String toPattern()
245 return pattern;
249 * This method returns a string with the formatting pattern being used
250 * by this object. This string is localized.
252 * @return The format string.
254 public String toLocalizedPattern()
256 String localChars = formatData.getLocalPatternChars();
257 return applyLocalizedPattern (pattern, standardChars, localChars);
261 * This method sets the formatting pattern that should be used by this
262 * object. This string is not localized.
264 * @param pattern The new format pattern.
266 public void applyPattern(String pattern)
268 tokens = new ArrayList();
269 compileFormat(pattern);
270 this.pattern = pattern;
274 * This method sets the formatting pattern that should be used by this
275 * object. This string is localized.
277 * @param pattern The new format pattern.
279 public void applyLocalizedPattern(String pattern)
281 String localChars = formatData.getLocalPatternChars();
282 pattern = applyLocalizedPattern (pattern, localChars, standardChars);
283 applyPattern(pattern);
286 private String applyLocalizedPattern(String pattern,
287 String oldChars, String newChars)
289 int len = pattern.length();
290 StringBuffer buf = new StringBuffer(len);
291 boolean quoted = false;
292 for (int i = 0; i < len; i++)
294 char ch = pattern.charAt(i);
295 if (ch == '\'')
296 quoted = ! quoted;
297 if (! quoted)
299 int j = oldChars.indexOf(ch);
300 if (j >= 0)
301 ch = newChars.charAt(j);
303 buf.append(ch);
305 return buf.toString();
308 /**
309 * Returns the start of the century used for two digit years.
311 * @return A <code>Date</code> representing the start of the century
312 * for two digit years.
314 public Date get2DigitYearStart()
316 return defaultCenturyStart;
320 * Sets the start of the century used for two digit years.
322 * @param date A <code>Date</code> representing the start of the century for
323 * two digit years.
325 public void set2DigitYearStart(Date date)
327 defaultCenturyStart = date;
328 calendar.clear();
329 calendar.setTime(date);
330 int year = calendar.get(Calendar.YEAR);
331 defaultCentury = year - (year % 100);
335 * This method returns the format symbol information used for parsing
336 * and formatting dates.
338 * @return The date format symbols.
340 public DateFormatSymbols getDateFormatSymbols()
342 return formatData;
346 * This method sets the format symbols information used for parsing
347 * and formatting dates.
349 * @param formatData The date format symbols.
351 public void setDateFormatSymbols(DateFormatSymbols formatData)
353 this.formatData = formatData;
357 * This methods tests whether the specified object is equal to this
358 * object. This will be true if and only if the specified object:
359 * <p>
360 * <ul>
361 * <li>Is not <code>null</code>.
362 * <li>Is an instance of <code>SimpleDateFormat</code>.
363 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
364 * level.
365 * <li>Has the same formatting pattern.
366 * <li>Is using the same formatting symbols.
367 * <li>Is using the same century for two digit years.
368 * </ul>
370 * @param obj The object to compare for equality against.
372 * @return <code>true</code> if the specified object is equal to this object,
373 * <code>false</code> otherwise.
375 public boolean equals(Object o)
377 if (!super.equals(o))
378 return false;
380 if (!(o instanceof SimpleDateFormat))
381 return false;
383 SimpleDateFormat sdf = (SimpleDateFormat)o;
385 if (defaultCentury != sdf.defaultCentury)
386 return false;
388 if (!toPattern().equals(sdf.toPattern()))
389 return false;
391 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
392 return false;
394 return true;
398 * This method returns a hash value for this object.
400 * @return A hash value for this object.
402 public int hashCode()
404 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
405 getDateFormatSymbols().hashCode();
410 * Formats the date input according to the format string in use,
411 * appending to the specified StringBuffer. The input StringBuffer
412 * is returned as output for convenience.
414 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
416 String temp;
417 calendar.setTime(date);
419 // go through ArrayList, filling in fields where applicable, else toString
420 Iterator i = tokens.iterator();
421 while (i.hasNext()) {
422 Object o = i.next();
423 if (o instanceof FieldSizePair) {
424 FieldSizePair p = (FieldSizePair) o;
425 int beginIndex = buffer.length();
426 switch (p.field) {
427 case ERA_FIELD:
428 buffer.append(formatData.eras[calendar.get(Calendar.ERA)]);
429 break;
430 case YEAR_FIELD:
431 // If we have two digits, then we truncate. Otherwise, we
432 // use the size of the pattern, and zero pad.
433 if (p.size == 2)
435 temp = String.valueOf(calendar.get(Calendar.YEAR));
436 buffer.append(temp.substring(temp.length() - 2));
438 else
439 withLeadingZeros(calendar.get(Calendar.YEAR), p.size, buffer);
440 break;
441 case MONTH_FIELD:
442 if (p.size < 3)
443 withLeadingZeros(calendar.get(Calendar.MONTH)+1,p.size,buffer);
444 else if (p.size < 4)
445 buffer.append(formatData.shortMonths[calendar.get(Calendar.MONTH)]);
446 else
447 buffer.append(formatData.months[calendar.get(Calendar.MONTH)]);
448 break;
449 case DATE_FIELD:
450 withLeadingZeros(calendar.get(Calendar.DATE),p.size,buffer);
451 break;
452 case HOUR_OF_DAY1_FIELD: // 1-24
453 withLeadingZeros(((calendar.get(Calendar.HOUR_OF_DAY)+23)%24)+1,p.size,buffer);
454 break;
455 case HOUR_OF_DAY0_FIELD: // 0-23
456 withLeadingZeros(calendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
457 break;
458 case MINUTE_FIELD:
459 withLeadingZeros(calendar.get(Calendar.MINUTE),p.size,buffer);
460 break;
461 case SECOND_FIELD:
462 withLeadingZeros(calendar.get(Calendar.SECOND),p.size,buffer);
463 break;
464 case MILLISECOND_FIELD:
465 withLeadingZeros(calendar.get(Calendar.MILLISECOND),p.size,buffer);
466 break;
467 case DAY_OF_WEEK_FIELD:
468 if (p.size < 4)
469 buffer.append(formatData.shortWeekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
470 else
471 buffer.append(formatData.weekdays[calendar.get(Calendar.DAY_OF_WEEK)]);
472 break;
473 case DAY_OF_YEAR_FIELD:
474 withLeadingZeros(calendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
475 break;
476 case DAY_OF_WEEK_IN_MONTH_FIELD:
477 withLeadingZeros(calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
478 break;
479 case WEEK_OF_YEAR_FIELD:
480 withLeadingZeros(calendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
481 break;
482 case WEEK_OF_MONTH_FIELD:
483 withLeadingZeros(calendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
484 break;
485 case AM_PM_FIELD:
486 buffer.append(formatData.ampms[calendar.get(Calendar.AM_PM)]);
487 break;
488 case HOUR1_FIELD: // 1-12
489 withLeadingZeros(((calendar.get(Calendar.HOUR)+11)%12)+1,p.size,buffer);
490 break;
491 case HOUR0_FIELD: // 0-11
492 withLeadingZeros(calendar.get(Calendar.HOUR),p.size,buffer);
493 break;
494 case TIMEZONE_FIELD:
495 TimeZone zone = calendar.getTimeZone();
496 boolean isDST = calendar.get(Calendar.DST_OFFSET) != 0;
497 // FIXME: XXX: This should be a localized time zone.
498 String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
499 buffer.append(zoneID);
500 break;
501 default:
502 throw new IllegalArgumentException("Illegal pattern character");
504 if (pos != null && p.field == pos.getField())
506 pos.setBeginIndex(beginIndex);
507 pos.setEndIndex(buffer.length());
509 } else {
510 buffer.append(o.toString());
513 return buffer;
516 private void withLeadingZeros(int value, int length, StringBuffer buffer)
518 String valStr = String.valueOf(value);
519 for (length -= valStr.length(); length > 0; length--)
520 buffer.append('0');
521 buffer.append(valStr);
524 private final boolean expect (String source, ParsePosition pos, char ch)
526 int x = pos.getIndex();
527 boolean r = x < source.length() && source.charAt(x) == ch;
528 if (r)
529 pos.setIndex(x + 1);
530 else
531 pos.setErrorIndex(x);
532 return r;
536 * This method parses the specified string into a date.
538 * @param dateStr The date string to parse.
539 * @param pos The input and output parse position
541 * @return The parsed date, or <code>null</code> if the string cannot be
542 * parsed.
544 public Date parse (String dateStr, ParsePosition pos)
546 int fmt_index = 0;
547 int fmt_max = pattern.length();
549 calendar.clear();
550 boolean saw_timezone = false;
551 int quote_start = -1;
552 boolean is2DigitYear = false;
553 for (; fmt_index < fmt_max; ++fmt_index)
555 char ch = pattern.charAt(fmt_index);
556 if (ch == '\'')
558 int index = pos.getIndex();
559 if (fmt_index < fmt_max - 1
560 && pattern.charAt(fmt_index + 1) == '\'')
562 if (! expect (dateStr, pos, ch))
563 return null;
564 ++fmt_index;
566 else
567 quote_start = quote_start < 0 ? fmt_index : -1;
568 continue;
571 if (quote_start != -1
572 || ((ch < 'a' || ch > 'z')
573 && (ch < 'A' || ch > 'Z')))
575 if (! expect (dateStr, pos, ch))
576 return null;
577 continue;
580 // We've arrived at a potential pattern character in the
581 // pattern.
582 int first = fmt_index;
583 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
585 int fmt_count = fmt_index - first;
587 // We might need to limit the number of digits to parse in
588 // some cases. We look to the next pattern character to
589 // decide.
590 boolean limit_digits = false;
591 if (fmt_index < fmt_max
592 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
593 limit_digits = true;
594 --fmt_index;
596 // We can handle most fields automatically: most either are
597 // numeric or are looked up in a string vector. In some cases
598 // we need an offset. When numeric, `offset' is added to the
599 // resulting value. When doing a string lookup, offset is the
600 // initial index into the string array.
601 int calendar_field;
602 boolean is_numeric = true;
603 String[] match = null;
604 int offset = 0;
605 boolean maybe2DigitYear = false;
606 switch (ch)
608 case 'd':
609 calendar_field = Calendar.DATE;
610 break;
611 case 'D':
612 calendar_field = Calendar.DAY_OF_YEAR;
613 break;
614 case 'F':
615 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
616 break;
617 case 'E':
618 is_numeric = false;
619 offset = 1;
620 calendar_field = Calendar.DAY_OF_WEEK;
621 match = (fmt_count <= 3
622 ? formatData.getShortWeekdays()
623 : formatData.getWeekdays());
624 break;
625 case 'w':
626 calendar_field = Calendar.WEEK_OF_YEAR;
627 break;
628 case 'W':
629 calendar_field = Calendar.WEEK_OF_MONTH;
630 break;
631 case 'M':
632 calendar_field = Calendar.MONTH;
633 if (fmt_count <= 2)
634 offset = -1;
635 else
637 is_numeric = false;
638 match = (fmt_count <= 3
639 ? formatData.getShortMonths()
640 : formatData.getMonths());
642 break;
643 case 'y':
644 calendar_field = Calendar.YEAR;
645 if (fmt_count <= 2)
646 maybe2DigitYear = true;
647 break;
648 case 'K':
649 calendar_field = Calendar.HOUR;
650 break;
651 case 'h':
652 calendar_field = Calendar.HOUR;
653 break;
654 case 'H':
655 calendar_field = Calendar.HOUR_OF_DAY;
656 break;
657 case 'k':
658 calendar_field = Calendar.HOUR_OF_DAY;
659 break;
660 case 'm':
661 calendar_field = Calendar.MINUTE;
662 break;
663 case 's':
664 calendar_field = Calendar.SECOND;
665 break;
666 case 'S':
667 calendar_field = Calendar.MILLISECOND;
668 break;
669 case 'a':
670 is_numeric = false;
671 calendar_field = Calendar.AM_PM;
672 match = formatData.getAmPmStrings();
673 break;
674 case 'z':
675 // We need a special case for the timezone, because it
676 // uses a different data structure than the other cases.
677 is_numeric = false;
678 calendar_field = Calendar.DST_OFFSET;
679 String[][] zoneStrings = formatData.getZoneStrings();
680 int zoneCount = zoneStrings.length;
681 int index = pos.getIndex();
682 boolean found_zone = false;
683 for (int j = 0; j < zoneCount; j++)
685 String[] strings = zoneStrings[j];
686 int k;
687 for (k = 1; k < strings.length; ++k)
689 if (dateStr.startsWith(strings[k], index))
690 break;
692 if (k != strings.length)
694 found_zone = true;
695 saw_timezone = true;
696 TimeZone tz = TimeZone.getTimeZone (strings[0]);
697 calendar.set (Calendar.ZONE_OFFSET, tz.getRawOffset ());
698 offset = 0;
699 if (k > 2 && tz instanceof SimpleTimeZone)
701 SimpleTimeZone stz = (SimpleTimeZone) tz;
702 offset = stz.getDSTSavings ();
704 pos.setIndex(index + strings[k].length());
705 break;
708 if (! found_zone)
710 pos.setErrorIndex(pos.getIndex());
711 return null;
713 break;
714 default:
715 pos.setErrorIndex(pos.getIndex());
716 return null;
719 // Compute the value we should assign to the field.
720 int value;
721 int index = -1;
722 if (is_numeric)
724 numberFormat.setMinimumIntegerDigits(fmt_count);
725 if (limit_digits)
726 numberFormat.setMaximumIntegerDigits(fmt_count);
727 if (maybe2DigitYear)
728 index = pos.getIndex();
729 Number n = numberFormat.parse(dateStr, pos);
730 if (pos == null || ! (n instanceof Long))
731 return null;
732 value = n.intValue() + offset;
734 else if (match != null)
736 index = pos.getIndex();
737 int i;
738 for (i = offset; i < match.length; ++i)
740 if (dateStr.startsWith(match[i], index))
741 break;
743 if (i == match.length)
745 pos.setErrorIndex(index);
746 return null;
748 pos.setIndex(index + match[i].length());
749 value = i;
751 else
752 value = offset;
754 if (maybe2DigitYear)
756 // Parse into default century if the numeric year string has
757 // exactly 2 digits.
758 int digit_count = pos.getIndex() - index;
759 if (digit_count == 2)
760 is2DigitYear = true;
763 // Assign the value and move on.
764 calendar.set(calendar_field, value);
767 if (is2DigitYear)
769 // Apply the 80-20 heuristic to dermine the full year based on
770 // defaultCenturyStart.
771 int year = defaultCentury + calendar.get(Calendar.YEAR);
772 calendar.set(Calendar.YEAR, year);
773 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
774 calendar.set(Calendar.YEAR, year + 100);
779 if (! saw_timezone)
781 // Use the real rules to determine whether or not this
782 // particular time is in daylight savings.
783 calendar.clear (Calendar.DST_OFFSET);
784 calendar.clear (Calendar.ZONE_OFFSET);
786 return calendar.getTime();
788 catch (IllegalArgumentException x)
790 pos.setErrorIndex(pos.getIndex());
791 return null;
795 // Compute the start of the current century as defined by
796 // get2DigitYearStart.
797 private void computeCenturyStart()
799 int year = calendar.get(Calendar.YEAR);
800 calendar.set(Calendar.YEAR, year - 80);
801 set2DigitYearStart(calendar.getTime());