libjava/ChangeLog:
[official-gcc.git] / libjava / classpath / java / text / SimpleDateFormat.java
blob4dcda4f487cf4a8c1ecc87d5c3f56bf7b7fa6c27
1 /* SimpleDateFormat.java -- A class for parsing/formating simple
2 date constructs
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)
11 any later version.
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
21 02110-1301 USA.
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
26 combination.
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. */
41 package java.text;
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;
63 /**
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
71 /**
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
79 /**
80 * The ID of the field within the local pattern characters.
81 * Package private for use in out class.
83 int field;
85 /**
86 * The size of the character sequence.
87 * Package private for use in out class.
89 int size;
91 /**
92 * The character used.
94 private char character;
96 /**
97 * Constructs a compiled field using the
98 * the given field ID, size and character
99 * values.
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)
107 field = f;
108 size = s;
109 character = c;
113 * Retrieves the ID of the field relative to
114 * the local pattern characters.
116 public int getField()
118 return field;
122 * Retrieves the size of the character sequence.
124 public int getSize()
126 return size;
130 * Retrieves the character used in the sequence.
132 public char getCharacter()
134 return character;
138 * Returns a <code>String</code> representation
139 * of the compiled field, primarily for debugging
140 * purposes.
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);
155 builder.append("]");
157 return builder.toString();
162 * A list of <code>CompiledField</code>s,
163 * representing the compiled version of the pattern.
165 * @see CompiledField
166 * @serial Ignored.
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)
189 * @see Date
190 * @serial The start date of the century for parsing two digit years.
191 * May not be null.
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)
201 * @serial Ignored.
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)
213 * @see #toPattern()
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
223 * two digit years.
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
234 * For compatability.
236 private static final long serialVersionUID = 4774881970558875024L;
238 // This string is specified in the Java class libraries.
239 private static final String standardChars = "GyMdkHmsSEDFwWahKzZ";
241 /**
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;
272 else
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.
302 char thisChar;
303 int pos;
304 int field;
305 CompiledField current = null;
307 for (int i = 0; i < pattern.length(); i++)
309 thisChar = pattern.charAt(i);
310 field = standardChars.indexOf(thisChar);
311 if (field == -1)
313 current = null;
314 if ((thisChar >= 'A' && thisChar <= 'Z')
315 || (thisChar >= 'a' && thisChar <= 'z'))
317 // Not a valid letter
318 throw new IllegalArgumentException("Invalid letter "
319 + thisChar +
320 " encountered at character "
321 + i + ".");
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.
328 if (pos == i + 1)
329 tokens.add("'");
330 else
332 // Look for the terminating quote. However, if we
333 // see a '', that represents a literal quote and
334 // we must iterate.
335 CPStringBuilder buf = new CPStringBuilder();
336 int oldPos = i + 1;
339 if (pos == -1)
340 throw new IllegalArgumentException("Quotes starting at character "
341 + i +
342 " not closed.");
343 buf.append(pattern.substring(oldPos, pos));
344 if (pos + 1 >= pattern.length()
345 || pattern.charAt(pos + 1) != '\'')
346 break;
347 buf.append('\'');
348 oldPos = pos + 2;
349 pos = pattern.indexOf('\'', pos + 2);
351 while (true);
352 tokens.add(buf.toString());
354 i = pos;
356 else
358 // A special character
359 tokens.add(Character.valueOf(thisChar));
362 else
364 // A valid field
365 if ((current != null) && (field == current.field))
366 current.size++;
367 else
369 current = new CompiledField(field, 1, thisChar);
370 tokens.add(current);
377 * Returns a string representation of this
378 * class.
380 * @return a string representation of the <code>SimpleDateFormat</code>
381 * instance.
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);
400 output.append("]");
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.
415 super();
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)
454 super();
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
470 * formatting.
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)
479 super();
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()
502 return pattern;
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
554 * locale.
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
559 * pattern).
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);
572 if (ch == '\'')
573 quoted = ! quoted;
574 if (! quoted)
576 int j = oldChars.indexOf(ch);
577 if (j >= 0)
578 ch = newChars.charAt(j);
580 buf.append(ch);
582 return buf.toString();
585 /**
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
600 * two digit years.
602 public void set2DigitYearStart(Date date)
604 defaultCenturyStart = date;
605 calendar.clear();
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)
633 throw new
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:
642 * <p>
643 * <ul>
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>)
647 * level.</li>
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>
651 * </ul>
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))
661 return false;
663 if (!(o instanceof SimpleDateFormat))
664 return false;
666 SimpleDateFormat sdf = (SimpleDateFormat)o;
668 if (defaultCentury != sdf.defaultCentury)
669 return false;
671 if (!toPattern().equals(sdf.toPattern()))
672 return false;
674 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
675 return false;
677 return true;
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)
699 String temp;
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())
715 case ERA_FIELD:
716 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
717 break;
718 case YEAR_FIELD:
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));
727 else
728 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
729 break;
730 case MONTH_FIELD:
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)]);
736 else
737 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
738 break;
739 case DATE_FIELD:
740 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
741 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
742 break;
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);
747 break;
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);
751 break;
752 case MINUTE_FIELD:
753 buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
754 withLeadingZeros (calendar.get (Calendar.MINUTE),
755 cf.getSize(), buffer);
756 break;
757 case SECOND_FIELD:
758 buffer.setDefaultAttribute (DateFormat.Field.SECOND);
759 withLeadingZeros(calendar.get (Calendar.SECOND),
760 cf.getSize(), buffer);
761 break;
762 case MILLISECOND_FIELD:
763 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
764 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
765 break;
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)]);
770 else
771 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
772 break;
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);
776 break;
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);
781 break;
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);
786 break;
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);
791 break;
792 case AM_PM_FIELD:
793 buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
794 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
795 break;
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);
800 break;
801 case HOUR0_FIELD: // 0-11
802 buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
803 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
804 break;
805 case TIMEZONE_FIELD:
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);
813 break;
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;
822 buffer.append(sign);
823 withLeadingZeros(hours, 2, buffer);
824 withLeadingZeros(minutes, 2, buffer);
825 break;
826 default:
827 throw new IllegalArgumentException ("Illegal pattern character " +
828 cf.getCharacter());
830 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
831 || cf.getField() == pos.getField()))
833 pos.setBeginIndex(beginIndex);
834 pos.setEndIndex(buffer.length());
837 else
839 buffer.append(o.toString(), null);
844 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
846 formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
848 return buffer;
851 public AttributedCharacterIterator formatToCharacterIterator(Object date)
852 throws IllegalArgumentException
854 if (date == null)
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,
861 null);
862 buf.sync();
864 return new FormatCharacterIterator(buf.getBuffer().toString(),
865 buf.getRanges(),
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--)
873 buffer.append('0');
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;
881 if (r)
882 pos.setIndex(x + 1);
883 else
884 pos.setErrorIndex(x);
885 return r;
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
895 * parsed.
897 public Date parse (String dateStr, ParsePosition pos)
899 int fmt_index = 0;
900 int fmt_max = pattern.length();
902 calendar.clear();
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);
911 if (ch == '\'')
913 int index = pos.getIndex();
914 if (fmt_index < fmt_max - 1
915 && pattern.charAt(fmt_index + 1) == '\'')
917 if (! expect (dateStr, pos, ch))
918 return null;
919 ++fmt_index;
921 else
922 quote_start = quote_start < 0 ? fmt_index : -1;
923 continue;
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();
935 int save = index;
936 while (index < dateStr.length()
937 && Character.isWhitespace(dateStr.charAt(index)))
938 ++index;
939 if (index > save)
940 pos.setIndex(index);
941 else
943 // Didn't see any whitespace.
944 pos.setErrorIndex(index);
945 return null;
948 else if (! expect (dateStr, pos, ch))
949 return null;
950 continue;
953 // We've arrived at a potential pattern character in the
954 // pattern.
955 int fmt_count = 1;
956 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
958 ++fmt_count;
961 // We might need to limit the number of digits to parse in
962 // some cases. We look to the next pattern character to
963 // decide.
964 boolean limit_digits = false;
965 if (fmt_index < fmt_max
966 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
967 limit_digits = true;
968 --fmt_index;
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.
975 int calendar_field;
976 boolean is_numeric = true;
977 int offset = 0;
978 boolean maybe2DigitYear = false;
979 boolean oneBasedHour = false;
980 boolean oneBasedHourOfDay = false;
981 Integer simpleOffset;
982 String[] set1 = null;
983 String[] set2 = null;
984 switch (ch)
986 case 'd':
987 calendar_field = Calendar.DATE;
988 break;
989 case 'D':
990 calendar_field = Calendar.DAY_OF_YEAR;
991 break;
992 case 'F':
993 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
994 break;
995 case 'E':
996 is_numeric = false;
997 offset = 1;
998 calendar_field = Calendar.DAY_OF_WEEK;
999 set1 = formatData.getWeekdays();
1000 set2 = formatData.getShortWeekdays();
1001 break;
1002 case 'w':
1003 calendar_field = Calendar.WEEK_OF_YEAR;
1004 break;
1005 case 'W':
1006 calendar_field = Calendar.WEEK_OF_MONTH;
1007 break;
1008 case 'M':
1009 calendar_field = Calendar.MONTH;
1010 if (fmt_count <= 2)
1011 offset = -1;
1012 else
1014 is_numeric = false;
1015 set1 = formatData.getMonths();
1016 set2 = formatData.getShortMonths();
1018 break;
1019 case 'y':
1020 calendar_field = Calendar.YEAR;
1021 if (fmt_count <= 2)
1022 maybe2DigitYear = true;
1023 break;
1024 case 'K':
1025 calendar_field = Calendar.HOUR;
1026 break;
1027 case 'h':
1028 calendar_field = Calendar.HOUR;
1029 oneBasedHour = true;
1030 break;
1031 case 'H':
1032 calendar_field = Calendar.HOUR_OF_DAY;
1033 break;
1034 case 'k':
1035 calendar_field = Calendar.HOUR_OF_DAY;
1036 oneBasedHourOfDay = true;
1037 break;
1038 case 'm':
1039 calendar_field = Calendar.MINUTE;
1040 break;
1041 case 's':
1042 calendar_field = Calendar.SECOND;
1043 break;
1044 case 'S':
1045 calendar_field = Calendar.MILLISECOND;
1046 break;
1047 case 'a':
1048 is_numeric = false;
1049 calendar_field = Calendar.AM_PM;
1050 set1 = formatData.getAmPmStrings();
1051 break;
1052 case 'z':
1053 case 'Z':
1054 // We need a special case for the timezone, because it
1055 // uses a different data structure than the other cases.
1056 is_numeric = false;
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)
1065 found_zone = true;
1066 saw_timezone = true;
1067 calendar.set(Calendar.DST_OFFSET, 0);
1068 offset = simpleOffset.intValue();
1070 else
1072 for (int j = 0; j < zoneCount; j++)
1074 String[] strings = zoneStrings[j];
1075 int k;
1076 for (k = 0; k < strings.length; ++k)
1078 if (dateStr.startsWith(strings[k], index))
1079 break;
1081 if (k != strings.length)
1083 found_zone = true;
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());
1089 else
1090 calendar.set (Calendar.DST_OFFSET, 0);
1091 offset = tz.getRawOffset ();
1092 pos.setIndex(index + strings[k].length());
1093 break;
1097 if (! found_zone)
1099 pos.setErrorIndex(pos.getIndex());
1100 return null;
1102 break;
1103 default:
1104 pos.setErrorIndex(pos.getIndex());
1105 return null;
1108 // Compute the value we should assign to the field.
1109 int value;
1110 int index = -1;
1111 if (is_numeric)
1113 numberFormat.setMinimumIntegerDigits(fmt_count);
1114 if (maybe2DigitYear)
1115 index = pos.getIndex();
1116 Number n = null;
1117 if (limit_digits)
1119 // numberFormat.setMaximumIntegerDigits(fmt_count) may
1120 // not work as expected. So we explicitly use substring
1121 // of dateStr.
1122 int origPos = pos.getIndex();
1123 pos.setIndex(0);
1124 n = numberFormat.parse(dateStr.substring(origPos, origPos + fmt_count), pos);
1125 pos.setIndex(origPos + pos.getIndex());
1127 else
1128 n = numberFormat.parse(dateStr, pos);
1129 if (pos == null || ! (n instanceof Long))
1130 return null;
1131 value = n.intValue() + offset;
1133 else if (set1 != null)
1135 index = pos.getIndex();
1136 int i;
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(),
1142 index))
1144 found = true;
1145 pos.setIndex(index + set1[i].length());
1146 break;
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(),
1155 index))
1157 found = true;
1158 pos.setIndex(index + set2[i].length());
1159 break;
1163 if (!found)
1165 pos.setErrorIndex(index);
1166 return null;
1168 value = i;
1170 else
1171 value = offset;
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)
1188 value = 0;
1190 if (oneBasedHourOfDay && value == 24)
1191 value = 0;
1193 // Assign the value and move on.
1194 calendar.set(calendar_field, value);
1197 if (is2DigitYear)
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);
1205 if (! saw_timezone)
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());
1217 return null;
1222 * <p>
1223 * Computes the time zone offset in milliseconds
1224 * relative to GMT, based on the supplied
1225 * <code>String</code> representation.
1226 * </p>
1227 * <p>
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.
1235 * </p>
1236 * <p>
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.
1241 * </p>
1242 * <p>
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.
1247 * </p>
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
1257 * failed.
1259 private Integer computeOffset(String zoneString, ParsePosition pos)
1261 Pattern pattern =
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?
1270 matcher.group(2);
1271 matcher.group(4);
1272 matcher.group(5);
1274 catch (IllegalStateException ise)
1276 hasAll = false;
1278 if (hasAll)
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));
1286 if (hour > 23)
1287 return null;
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);
1299 return null;
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());
1322 return clone;