Merge from the pain train
[official-gcc.git] / libjava / java / text / SimpleDateFormat.java
blobc1eb3cd3a7078c944f5023a70eb77e7027e4101c
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., 59 Temple Place, Suite 330, Boston, MA
21 02111-1307 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.text.AttributedFormatBuffer;
44 import gnu.java.text.FormatBuffer;
45 import gnu.java.text.FormatCharacterIterator;
46 import gnu.java.text.StringFormatBuffer;
48 import java.io.IOException;
49 import java.io.InvalidObjectException;
50 import java.io.ObjectInputStream;
51 import java.util.ArrayList;
52 import java.util.Calendar;
53 import java.util.Date;
54 import java.util.GregorianCalendar;
55 import java.util.Iterator;
56 import java.util.Locale;
57 import java.util.TimeZone;
58 import java.util.regex.Matcher;
59 import java.util.regex.Pattern;
61 /**
62 * SimpleDateFormat provides convenient methods for parsing and formatting
63 * dates using Gregorian calendars (see java.util.GregorianCalendar).
65 public class SimpleDateFormat extends DateFormat
67 /**
68 * This class is used by <code>SimpleDateFormat</code> as a
69 * compiled representation of a format string. The field
70 * ID, size, and character used are stored for each sequence
71 * of pattern characters.
73 private class CompiledField
75 /**
76 * The ID of the field within the local pattern characters,
78 private int field;
80 /**
81 * The size of the character sequence.
83 private int size;
85 /**
86 * The character used.
88 private char character;
90 /**
91 * Constructs a compiled field using the
92 * the given field ID, size and character
93 * values.
95 * @param f the field ID.
96 * @param s the size of the field.
97 * @param c the character used.
99 public CompiledField(int f, int s, char c)
101 field = f;
102 size = s;
103 character = c;
107 * Retrieves the ID of the field relative to
108 * the local pattern characters.
110 public int getField()
112 return field;
116 * Retrieves the size of the character sequence.
118 public int getSize()
120 return size;
124 * Retrieves the character used in the sequence.
126 public char getCharacter()
128 return character;
132 * Returns a <code>String</code> representation
133 * of the compiled field, primarily for debugging
134 * purposes.
136 * @return a <code>String</code> representation.
138 public String toString()
140 StringBuffer builder;
142 builder = new StringBuffer(getClass().getName());
143 builder.append("[field=");
144 builder.append(field);
145 builder.append(", size=");
146 builder.append(size);
147 builder.append(", character=");
148 builder.append(character);
149 builder.append("]");
151 return builder.toString();
156 * A list of <code>CompiledField</code>s,
157 * representing the compiled version of the pattern.
159 * @see CompiledField
160 * @serial Ignored.
162 private transient ArrayList tokens;
165 * The localised data used in formatting,
166 * such as the day and month names in the local
167 * language, and the localized pattern characters.
169 * @see DateFormatSymbols
170 * @serial The localisation data. May not be null.
172 private DateFormatSymbols formatData;
175 * The date representing the start of the century
176 * used for interpreting two digit years. For
177 * example, 24/10/2004 would cause two digit
178 * years to be interpreted as representing
179 * the years between 2004 and 2104.
181 * @see get2DigitYearStart()
182 * @see set2DigitYearStart(java.util.Date)
183 * @see Date
184 * @serial The start date of the century for parsing two digit years.
185 * May not be null.
187 private Date defaultCenturyStart;
190 * The year at which interpretation of two
191 * digit years starts.
193 * @see get2DigitYearStart()
194 * @see set2DigitYearStart(java.util.Date)
195 * @serial Ignored.
197 private transient int defaultCentury;
200 * The non-localized pattern string. This
201 * only ever contains the pattern characters
202 * stored in standardChars. Localized patterns
203 * are translated to this form.
205 * @see applyPattern(String)
206 * @see applyLocalizedPattern(String)
207 * @see toPattern()
208 * @see toLocalizedPattern()
209 * @serial The non-localized pattern string. May not be null.
211 private String pattern;
214 * The version of serialized data used by this class.
215 * Version 0 only includes the pattern and formatting
216 * data. Version 1 adds the start date for interpreting
217 * two digit years.
219 * @serial This specifies the version of the data being serialized.
220 * Version 0 (or no version) specifies just <code>pattern</code>
221 * and <code>formatData</code>. Version 1 adds
222 * the <code>defaultCenturyStart</code>. This implementation
223 * always writes out version 1 data.
225 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
228 * For compatability.
230 private static final long serialVersionUID = 4774881970558875024L;
232 // This string is specified in the root of the CLDR. We set it here
233 // rather than doing a DateFormatSymbols(Locale.US).getLocalPatternChars()
234 // since someone could theoretically change those values (though unlikely).
235 private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZ";
238 * Reads the serialized version of this object.
239 * If the serialized data is only version 0,
240 * then the date for the start of the century
241 * for interpreting two digit years is computed.
242 * The pattern is parsed and compiled following the process
243 * of reading in the serialized data.
245 * @param stream the object stream to read the data from.
246 * @throws IOException if an I/O error occurs.
247 * @throws ClassNotFoundException if the class of the serialized data
248 * could not be found.
249 * @throws InvalidObjectException if the pattern is invalid.
251 private void readObject(ObjectInputStream stream)
252 throws IOException, ClassNotFoundException
254 stream.defaultReadObject();
255 if (serialVersionOnStream < 1)
257 computeCenturyStart ();
258 serialVersionOnStream = 1;
260 else
261 // Ensure that defaultCentury gets set.
262 set2DigitYearStart(defaultCenturyStart);
264 // Set up items normally taken care of by the constructor.
265 tokens = new ArrayList();
268 compileFormat(pattern);
270 catch (IllegalArgumentException e)
272 throw new InvalidObjectException("The stream pattern was invalid.");
277 * Compiles the supplied non-localized pattern into a form
278 * from which formatting and parsing can be performed.
279 * This also detects errors in the pattern, which will
280 * be raised on later use of the compiled data.
282 * @param pattern the non-localized pattern to compile.
283 * @throws IllegalArgumentException if the pattern is invalid.
285 private void compileFormat(String pattern)
287 // Any alphabetical characters are treated as pattern characters
288 // unless enclosed in single quotes.
290 char thisChar;
291 int pos;
292 int field;
293 CompiledField current = null;
295 for (int i=0; i<pattern.length(); i++) {
296 thisChar = pattern.charAt(i);
297 field = standardChars.indexOf(thisChar);
298 if (field == -1) {
299 current = null;
300 if ((thisChar >= 'A' && thisChar <= 'Z')
301 || (thisChar >= 'a' && thisChar <= 'z')) {
302 // Not a valid letter
303 throw new IllegalArgumentException("Invalid letter " + thisChar +
304 "encountered at character " + i
305 + ".");
306 } else if (thisChar == '\'') {
307 // Quoted text section; skip to next single quote
308 pos = pattern.indexOf('\'',i+1);
309 if (pos == -1) {
310 throw new IllegalArgumentException("Quotes starting at character "
311 + i + " not closed.");
313 if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
314 tokens.add(pattern.substring(i+1,pos+1));
315 } else {
316 tokens.add(pattern.substring(i+1,pos));
318 i = pos;
319 } else {
320 // A special character
321 tokens.add(new Character(thisChar));
323 } else {
324 // A valid field
325 if ((current != null) && (field == current.field)) {
326 current.size++;
327 } else {
328 current = new CompiledField(field,1,thisChar);
329 tokens.add(current);
336 * Returns a string representation of this
337 * class.
339 * @return a string representation of the <code>SimpleDateFormat</code>
340 * instance.
342 public String toString()
344 StringBuffer output = new StringBuffer(getClass().getName());
345 output.append("[tokens=");
346 output.append(tokens);
347 output.append(", formatData=");
348 output.append(formatData);
349 output.append(", defaultCenturyStart=");
350 output.append(defaultCenturyStart);
351 output.append(", defaultCentury=");
352 output.append(defaultCentury);
353 output.append(", pattern=");
354 output.append(pattern);
355 output.append(", serialVersionOnStream=");
356 output.append(serialVersionOnStream);
357 output.append(", standardChars=");
358 output.append(standardChars);
359 output.append("]");
360 return output.toString();
364 * Constructs a SimpleDateFormat using the default pattern for
365 * the default locale.
367 public SimpleDateFormat()
370 * There does not appear to be a standard API for determining
371 * what the default pattern for a locale is, so use package-scope
372 * variables in DateFormatSymbols to encapsulate this.
374 super();
375 Locale locale = Locale.getDefault();
376 calendar = new GregorianCalendar(locale);
377 computeCenturyStart();
378 tokens = new ArrayList();
379 formatData = new DateFormatSymbols(locale);
380 pattern = (formatData.dateFormats[DEFAULT] + ' '
381 + formatData.timeFormats[DEFAULT]);
382 compileFormat(pattern);
383 numberFormat = NumberFormat.getInstance(locale);
384 numberFormat.setGroupingUsed (false);
385 numberFormat.setParseIntegerOnly (true);
386 numberFormat.setMaximumFractionDigits (0);
390 * Creates a date formatter using the specified non-localized pattern,
391 * with the default DateFormatSymbols for the default locale.
393 * @param pattern the pattern to use.
394 * @throws NullPointerException if the pattern is null.
395 * @throws IllegalArgumentException if the pattern is invalid.
397 public SimpleDateFormat(String pattern)
399 this(pattern, Locale.getDefault());
403 * Creates a date formatter using the specified non-localized pattern,
404 * with the default DateFormatSymbols for the given locale.
406 * @param pattern the non-localized pattern to use.
407 * @param locale the locale to use for the formatting symbols.
408 * @throws NullPointerException if the pattern is null.
409 * @throws IllegalArgumentException if the pattern is invalid.
411 public SimpleDateFormat(String pattern, Locale locale)
413 super();
414 calendar = new GregorianCalendar(locale);
415 computeCenturyStart();
416 tokens = new ArrayList();
417 formatData = new DateFormatSymbols(locale);
418 compileFormat(pattern);
419 this.pattern = pattern;
420 numberFormat = NumberFormat.getInstance(locale);
421 numberFormat.setGroupingUsed (false);
422 numberFormat.setParseIntegerOnly (true);
423 numberFormat.setMaximumFractionDigits (0);
427 * Creates a date formatter using the specified non-localized
428 * pattern. The specified DateFormatSymbols will be used when
429 * formatting.
431 * @param pattern the non-localized pattern to use.
432 * @param formatData the formatting symbols to use.
433 * @throws NullPointerException if the pattern or formatData is null.
434 * @throws IllegalArgumentException if the pattern is invalid.
436 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
438 super();
439 calendar = new GregorianCalendar();
440 computeCenturyStart ();
441 tokens = new ArrayList();
442 if (formatData == null)
443 throw new NullPointerException("formatData");
444 this.formatData = formatData;
445 compileFormat(pattern);
446 this.pattern = pattern;
447 numberFormat = NumberFormat.getInstance();
448 numberFormat.setGroupingUsed (false);
449 numberFormat.setParseIntegerOnly (true);
450 numberFormat.setMaximumFractionDigits (0);
454 * This method returns a string with the formatting pattern being used
455 * by this object. This string is unlocalized.
457 * @return The format string.
459 public String toPattern()
461 return pattern;
465 * This method returns a string with the formatting pattern being used
466 * by this object. This string is localized.
468 * @return The format string.
470 public String toLocalizedPattern()
472 String localChars = formatData.getLocalPatternChars();
473 return translateLocalizedPattern(pattern, standardChars, localChars);
477 * This method sets the formatting pattern that should be used by this
478 * object. This string is not localized.
480 * @param pattern The new format pattern.
481 * @throws NullPointerException if the pattern is null.
482 * @throws IllegalArgumentException if the pattern is invalid.
484 public void applyPattern(String pattern)
486 tokens = new ArrayList();
487 compileFormat(pattern);
488 this.pattern = pattern;
492 * This method sets the formatting pattern that should be used by this
493 * object. This string is localized.
495 * @param pattern The new format pattern.
496 * @throws NullPointerException if the pattern is null.
497 * @throws IllegalArgumentException if the pattern is invalid.
499 public void applyLocalizedPattern(String pattern)
501 String localChars = formatData.getLocalPatternChars();
502 pattern = translateLocalizedPattern(pattern, localChars, standardChars);
503 applyPattern(pattern);
507 * Translates either from or to a localized variant of the pattern
508 * string. For example, in the German locale, 't' (for 'tag') is
509 * used instead of 'd' (for 'date'). This method translates
510 * a localized pattern (such as 'ttt') to a non-localized pattern
511 * (such as 'ddd'), or vice versa. Non-localized patterns use
512 * a standard set of characters, which match those of the U.S. English
513 * locale.
515 * @param pattern the pattern to translate.
516 * @param oldChars the old set of characters (used in the pattern).
517 * @param newChars the new set of characters (which will be used in the
518 * pattern).
519 * @return a version of the pattern using the characters in
520 * <code>newChars</code>.
522 private String translateLocalizedPattern(String pattern,
523 String oldChars, String newChars)
525 int len = pattern.length();
526 StringBuffer buf = new StringBuffer(len);
527 boolean quoted = false;
528 for (int i = 0; i < len; i++)
530 char ch = pattern.charAt(i);
531 if (ch == '\'')
532 quoted = ! quoted;
533 if (! quoted)
535 int j = oldChars.indexOf(ch);
536 if (j >= 0)
537 ch = newChars.charAt(j);
539 buf.append(ch);
541 return buf.toString();
544 /**
545 * Returns the start of the century used for two digit years.
547 * @return A <code>Date</code> representing the start of the century
548 * for two digit years.
550 public Date get2DigitYearStart()
552 return defaultCenturyStart;
556 * Sets the start of the century used for two digit years.
558 * @param date A <code>Date</code> representing the start of the century for
559 * two digit years.
561 public void set2DigitYearStart(Date date)
563 defaultCenturyStart = date;
564 calendar.clear();
565 calendar.setTime(date);
566 int year = calendar.get(Calendar.YEAR);
567 defaultCentury = year - (year % 100);
571 * This method returns a copy of the format symbol information used
572 * for parsing and formatting dates.
574 * @return a copy of the date format symbols.
576 public DateFormatSymbols getDateFormatSymbols()
578 return (DateFormatSymbols) formatData.clone();
582 * This method sets the format symbols information used for parsing
583 * and formatting dates.
585 * @param formatData The date format symbols.
586 * @throws NullPointerException if <code>formatData</code> is null.
588 public void setDateFormatSymbols(DateFormatSymbols formatData)
590 if (formatData == null)
592 throw new
593 NullPointerException("The supplied format data was null.");
595 this.formatData = formatData;
599 * This methods tests whether the specified object is equal to this
600 * object. This will be true if and only if the specified object:
601 * <p>
602 * <ul>
603 * <li>Is not <code>null</code>.</li>
604 * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
605 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
606 * level.</li>
607 * <li>Has the same formatting pattern.</li>
608 * <li>Is using the same formatting symbols.</li>
609 * <li>Is using the same century for two digit years.</li>
610 * </ul>
612 * @param obj The object to compare for equality against.
614 * @return <code>true</code> if the specified object is equal to this object,
615 * <code>false</code> otherwise.
617 public boolean equals(Object o)
619 if (!super.equals(o))
620 return false;
622 if (!(o instanceof SimpleDateFormat))
623 return false;
625 SimpleDateFormat sdf = (SimpleDateFormat)o;
627 if (defaultCentury != sdf.defaultCentury)
628 return false;
630 if (!toPattern().equals(sdf.toPattern()))
631 return false;
633 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
634 return false;
636 return true;
640 * This method returns a hash value for this object.
642 * @return A hash value for this object.
644 public int hashCode()
646 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
647 getDateFormatSymbols().hashCode();
652 * Formats the date input according to the format string in use,
653 * appending to the specified StringBuffer. The input StringBuffer
654 * is returned as output for convenience.
656 private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
658 String temp;
659 AttributedCharacterIterator.Attribute attribute;
660 calendar.setTime(date);
662 // go through vector, filling in fields where applicable, else toString
663 Iterator iter = tokens.iterator();
664 while (iter.hasNext())
666 Object o = iter.next();
667 if (o instanceof CompiledField)
669 CompiledField cf = (CompiledField) o;
670 int beginIndex = buffer.length();
672 switch (cf.getField())
674 case ERA_FIELD:
675 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
676 break;
677 case YEAR_FIELD:
678 // If we have two digits, then we truncate. Otherwise, we
679 // use the size of the pattern, and zero pad.
680 buffer.setDefaultAttribute (DateFormat.Field.YEAR);
681 if (cf.getSize() == 2)
683 temp = String.valueOf (calendar.get (Calendar.YEAR));
684 buffer.append (temp.substring (temp.length() - 2));
686 else
687 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
688 break;
689 case MONTH_FIELD:
690 buffer.setDefaultAttribute (DateFormat.Field.MONTH);
691 if (cf.getSize() < 3)
692 withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer);
693 else if (cf.getSize() < 4)
694 buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]);
695 else
696 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
697 break;
698 case DATE_FIELD:
699 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
700 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
701 break;
702 case HOUR_OF_DAY1_FIELD: // 1-24
703 buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1);
704 withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1,
705 cf.getSize(), buffer);
706 break;
707 case HOUR_OF_DAY0_FIELD: // 0-23
708 buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0);
709 withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer);
710 break;
711 case MINUTE_FIELD:
712 buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
713 withLeadingZeros (calendar.get (Calendar.MINUTE),
714 cf.getSize(), buffer);
715 break;
716 case SECOND_FIELD:
717 buffer.setDefaultAttribute (DateFormat.Field.SECOND);
718 withLeadingZeros(calendar.get (Calendar.SECOND),
719 cf.getSize(), buffer);
720 break;
721 case MILLISECOND_FIELD:
722 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
723 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
724 break;
725 case DAY_OF_WEEK_FIELD:
726 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK);
727 if (cf.getSize() < 4)
728 buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
729 else
730 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
731 break;
732 case DAY_OF_YEAR_FIELD:
733 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR);
734 withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer);
735 break;
736 case DAY_OF_WEEK_IN_MONTH_FIELD:
737 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH);
738 withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH),
739 cf.getSize(), buffer);
740 break;
741 case WEEK_OF_YEAR_FIELD:
742 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR);
743 withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR),
744 cf.getSize(), buffer);
745 break;
746 case WEEK_OF_MONTH_FIELD:
747 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH);
748 withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH),
749 cf.getSize(), buffer);
750 break;
751 case AM_PM_FIELD:
752 buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
753 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
754 break;
755 case HOUR1_FIELD: // 1-12
756 buffer.setDefaultAttribute (DateFormat.Field.HOUR1);
757 withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1,
758 cf.getSize(), buffer);
759 break;
760 case HOUR0_FIELD: // 0-11
761 buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
762 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
763 break;
764 case TIMEZONE_FIELD:
765 buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE);
766 TimeZone zone = calendar.getTimeZone();
767 boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0;
768 // FIXME: XXX: This should be a localized time zone.
769 String zoneID = zone.getDisplayName
770 (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT);
771 buffer.append (zoneID);
772 break;
773 case RFC822_TIMEZONE_FIELD:
774 buffer.setDefaultAttribute(DateFormat.Field.RFC822_TIME_ZONE);
775 int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) +
776 calendar.get(Calendar.DST_OFFSET)) / (1000 * 60);
777 String sign = (pureMinutes < 0) ? "-" : "+";
778 int hours = pureMinutes / 60;
779 int minutes = pureMinutes % 60;
780 buffer.append(sign);
781 withLeadingZeros(hours, 2, buffer);
782 withLeadingZeros(minutes, 2, buffer);
783 break;
784 default:
785 throw new IllegalArgumentException ("Illegal pattern character " +
786 cf.getCharacter());
788 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
789 || cf.getField() == pos.getField()))
791 pos.setBeginIndex(beginIndex);
792 pos.setEndIndex(buffer.length());
795 else
797 buffer.append(o.toString(), null);
802 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
804 formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
806 return buffer;
809 public AttributedCharacterIterator formatToCharacterIterator(Object date)
810 throws IllegalArgumentException
812 if (date == null)
813 throw new NullPointerException("null argument");
814 if (!(date instanceof Date))
815 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
817 AttributedFormatBuffer buf = new AttributedFormatBuffer();
818 formatWithAttribute((Date)date, buf,
819 null);
820 buf.sync();
822 return new FormatCharacterIterator(buf.getBuffer().toString(),
823 buf.getRanges(),
824 buf.getAttributes());
827 private void withLeadingZeros(int value, int length, FormatBuffer buffer)
829 String valStr = String.valueOf(value);
830 for (length -= valStr.length(); length > 0; length--)
831 buffer.append('0');
832 buffer.append(valStr);
835 private boolean expect(String source, ParsePosition pos, char ch)
837 int x = pos.getIndex();
838 boolean r = x < source.length() && source.charAt(x) == ch;
839 if (r)
840 pos.setIndex(x + 1);
841 else
842 pos.setErrorIndex(x);
843 return r;
847 * This method parses the specified string into a date.
849 * @param dateStr The date string to parse.
850 * @param pos The input and output parse position
852 * @return The parsed date, or <code>null</code> if the string cannot be
853 * parsed.
855 public Date parse (String dateStr, ParsePosition pos)
857 int fmt_index = 0;
858 int fmt_max = pattern.length();
860 calendar.clear();
861 boolean saw_timezone = false;
862 int quote_start = -1;
863 boolean is2DigitYear = false;
866 for (; fmt_index < fmt_max; ++fmt_index)
868 char ch = pattern.charAt(fmt_index);
869 if (ch == '\'')
871 int index = pos.getIndex();
872 if (fmt_index < fmt_max - 1
873 && pattern.charAt(fmt_index + 1) == '\'')
875 if (! expect (dateStr, pos, ch))
876 return null;
877 ++fmt_index;
879 else
880 quote_start = quote_start < 0 ? fmt_index : -1;
881 continue;
884 if (quote_start != -1
885 || ((ch < 'a' || ch > 'z')
886 && (ch < 'A' || ch > 'Z')))
888 if (! expect (dateStr, pos, ch))
889 return null;
890 continue;
893 // We've arrived at a potential pattern character in the
894 // pattern.
895 int fmt_count = 1;
896 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
898 ++fmt_count;
901 // We might need to limit the number of digits to parse in
902 // some cases. We look to the next pattern character to
903 // decide.
904 boolean limit_digits = false;
905 if (fmt_index < fmt_max
906 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
907 limit_digits = true;
908 --fmt_index;
910 // We can handle most fields automatically: most either are
911 // numeric or are looked up in a string vector. In some cases
912 // we need an offset. When numeric, `offset' is added to the
913 // resulting value. When doing a string lookup, offset is the
914 // initial index into the string array.
915 int calendar_field;
916 boolean is_numeric = true;
917 int offset = 0;
918 boolean maybe2DigitYear = false;
919 Integer simpleOffset;
920 String[] set1 = null;
921 String[] set2 = null;
922 switch (ch)
924 case 'd':
925 calendar_field = Calendar.DATE;
926 break;
927 case 'D':
928 calendar_field = Calendar.DAY_OF_YEAR;
929 break;
930 case 'F':
931 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
932 break;
933 case 'E':
934 is_numeric = false;
935 offset = 1;
936 calendar_field = Calendar.DAY_OF_WEEK;
937 set1 = formatData.getWeekdays();
938 set2 = formatData.getShortWeekdays();
939 break;
940 case 'w':
941 calendar_field = Calendar.WEEK_OF_YEAR;
942 break;
943 case 'W':
944 calendar_field = Calendar.WEEK_OF_MONTH;
945 break;
946 case 'M':
947 calendar_field = Calendar.MONTH;
948 if (fmt_count <= 2)
949 offset = -1;
950 else
952 is_numeric = false;
953 set1 = formatData.getMonths();
954 set2 = formatData.getShortMonths();
956 break;
957 case 'y':
958 calendar_field = Calendar.YEAR;
959 if (fmt_count <= 2)
960 maybe2DigitYear = true;
961 break;
962 case 'K':
963 calendar_field = Calendar.HOUR;
964 break;
965 case 'h':
966 calendar_field = Calendar.HOUR;
967 break;
968 case 'H':
969 calendar_field = Calendar.HOUR_OF_DAY;
970 break;
971 case 'k':
972 calendar_field = Calendar.HOUR_OF_DAY;
973 break;
974 case 'm':
975 calendar_field = Calendar.MINUTE;
976 break;
977 case 's':
978 calendar_field = Calendar.SECOND;
979 break;
980 case 'S':
981 calendar_field = Calendar.MILLISECOND;
982 break;
983 case 'a':
984 is_numeric = false;
985 calendar_field = Calendar.AM_PM;
986 set1 = formatData.getAmPmStrings();
987 break;
988 case 'z':
989 case 'Z':
990 // We need a special case for the timezone, because it
991 // uses a different data structure than the other cases.
992 is_numeric = false;
993 calendar_field = Calendar.ZONE_OFFSET;
994 String[][] zoneStrings = formatData.getZoneStrings();
995 int zoneCount = zoneStrings.length;
996 int index = pos.getIndex();
997 boolean found_zone = false;
998 simpleOffset = computeOffset(dateStr.substring(index));
999 if (simpleOffset != null)
1001 found_zone = true;
1002 saw_timezone = true;
1003 calendar.set(Calendar.DST_OFFSET, 0);
1004 offset = simpleOffset.intValue();
1006 else
1008 for (int j = 0; j < zoneCount; j++)
1010 String[] strings = zoneStrings[j];
1011 int k;
1012 for (k = 0; k < strings.length; ++k)
1014 if (dateStr.startsWith(strings[k], index))
1015 break;
1017 if (k != strings.length)
1019 found_zone = true;
1020 saw_timezone = true;
1021 TimeZone tz = TimeZone.getTimeZone (strings[0]);
1022 // Check if it's a DST zone or ordinary
1023 if(k == 3 || k == 4)
1024 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1025 else
1026 calendar.set (Calendar.DST_OFFSET, 0);
1027 offset = tz.getRawOffset ();
1028 pos.setIndex(index + strings[k].length());
1029 break;
1033 if (! found_zone)
1035 pos.setErrorIndex(pos.getIndex());
1036 return null;
1038 break;
1039 default:
1040 pos.setErrorIndex(pos.getIndex());
1041 return null;
1044 // Compute the value we should assign to the field.
1045 int value;
1046 int index = -1;
1047 if (is_numeric)
1049 numberFormat.setMinimumIntegerDigits(fmt_count);
1050 if (limit_digits)
1051 numberFormat.setMaximumIntegerDigits(fmt_count);
1052 if (maybe2DigitYear)
1053 index = pos.getIndex();
1054 Number n = numberFormat.parse(dateStr, pos);
1055 if (pos == null || ! (n instanceof Long))
1056 return null;
1057 value = n.intValue() + offset;
1059 else if (set1 != null)
1061 index = pos.getIndex();
1062 int i;
1063 boolean found = false;
1064 for (i = offset; i < set1.length; ++i)
1066 if (set1[i] != null)
1067 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1068 index))
1070 found = true;
1071 pos.setIndex(index + set1[i].length());
1072 break;
1075 if (!found && set2 != null)
1077 for (i = offset; i < set2.length; ++i)
1079 if (set2[i] != null)
1080 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1081 index))
1083 found = true;
1084 pos.setIndex(index + set2[i].length());
1085 break;
1089 if (!found)
1091 pos.setErrorIndex(index);
1092 return null;
1094 value = i;
1096 else
1097 value = offset;
1099 if (maybe2DigitYear)
1101 // Parse into default century if the numeric year string has
1102 // exactly 2 digits.
1103 int digit_count = pos.getIndex() - index;
1104 if (digit_count == 2)
1106 is2DigitYear = true;
1107 value += defaultCentury;
1111 // Assign the value and move on.
1112 calendar.set(calendar_field, value);
1115 if (is2DigitYear)
1117 // Apply the 80-20 heuristic to dermine the full year based on
1118 // defaultCenturyStart.
1119 int year = calendar.get(Calendar.YEAR);
1120 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1121 calendar.set(Calendar.YEAR, year + 100);
1123 if (! saw_timezone)
1125 // Use the real rules to determine whether or not this
1126 // particular time is in daylight savings.
1127 calendar.clear (Calendar.DST_OFFSET);
1128 calendar.clear (Calendar.ZONE_OFFSET);
1130 return calendar.getTime();
1132 catch (IllegalArgumentException x)
1134 pos.setErrorIndex(pos.getIndex());
1135 return null;
1140 * <p>
1141 * Computes the time zone offset in milliseconds
1142 * relative to GMT, based on the supplied
1143 * <code>String</code> representation.
1144 * </p>
1145 * <p>
1146 * The supplied <code>String</code> must be a three
1147 * or four digit signed number, with an optional 'GMT'
1148 * prefix. The first one or two digits represents the hours,
1149 * while the last two represent the minutes. The
1150 * two sets of digits can optionally be separated by
1151 * ':'. The mandatory sign prefix (either '+' or '-')
1152 * indicates the direction of the offset from GMT.
1153 * </p>
1154 * <p>
1155 * For example, 'GMT+0200' specifies 2 hours after
1156 * GMT, while '-05:00' specifies 5 hours prior to
1157 * GMT. The special case of 'GMT' alone can be used
1158 * to represent the offset, 0.
1159 * </p>
1160 * <p>
1161 * If the <code>String</code> can not be parsed,
1162 * the result will be null. The resulting offset
1163 * is wrapped in an <code>Integer</code> object, in
1164 * order to allow such failure to be represented.
1165 * </p>
1167 * @param zoneString a string in the form
1168 * (GMT)? sign hours : minutes
1169 * where sign = '+' or '-', hours
1170 * is a one or two digits representing
1171 * a number between 0 and 23, and
1172 * minutes is two digits representing
1173 * a number between 0 and 59.
1174 * @return the parsed offset, or null if parsing
1175 * failed.
1177 private Integer computeOffset(String zoneString)
1179 Pattern pattern =
1180 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1181 Matcher matcher = pattern.matcher(zoneString);
1182 if (matcher.matches())
1184 int sign = matcher.group(2).equals("+") ? 1 : -1;
1185 int hour = (Integer.parseInt(matcher.group(3)) * 10)
1186 + Integer.parseInt(matcher.group(4));
1187 int minutes = Integer.parseInt(matcher.group(5));
1189 if (hour > 23)
1190 return null;
1192 int offset = sign * ((hour * 60) + minutes) * 60000;
1193 return new Integer(offset);
1195 else if (zoneString.startsWith("GMT"))
1197 return new Integer(0);
1199 return null;
1202 // Compute the start of the current century as defined by
1203 // get2DigitYearStart.
1204 private void computeCenturyStart()
1206 int year = calendar.get(Calendar.YEAR);
1207 calendar.set(Calendar.YEAR, year - 80);
1208 set2DigitYearStart(calendar.getTime());
1212 * Returns a copy of this instance of
1213 * <code>SimpleDateFormat</code>. The copy contains
1214 * clones of the formatting symbols and the 2-digit
1215 * year century start date.
1217 public Object clone()
1219 SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1220 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1221 clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1222 return clone;