Merge with trank @ 137446
[official-gcc.git] / libjava / classpath / java / text / SimpleDateFormat.java
blob934fb42e2a1d079c65fe10255582b6bfdb95d8cb
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.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.
77 * Package private for use in out class.
79 int field;
81 /**
82 * The size of the character sequence.
83 * Package private for use in out class.
85 int size;
87 /**
88 * The character used.
90 private char character;
92 /**
93 * Constructs a compiled field using the
94 * the given field ID, size and character
95 * values.
97 * @param f the field ID.
98 * @param s the size of the field.
99 * @param c the character used.
101 public CompiledField(int f, int s, char c)
103 field = f;
104 size = s;
105 character = c;
109 * Retrieves the ID of the field relative to
110 * the local pattern characters.
112 public int getField()
114 return field;
118 * Retrieves the size of the character sequence.
120 public int getSize()
122 return size;
126 * Retrieves the character used in the sequence.
128 public char getCharacter()
130 return character;
134 * Returns a <code>String</code> representation
135 * of the compiled field, primarily for debugging
136 * purposes.
138 * @return a <code>String</code> representation.
140 public String toString()
142 StringBuilder builder;
144 builder = new StringBuilder(getClass().getName());
145 builder.append("[field=");
146 builder.append(field);
147 builder.append(", size=");
148 builder.append(size);
149 builder.append(", character=");
150 builder.append(character);
151 builder.append("]");
153 return builder.toString();
158 * A list of <code>CompiledField</code>s,
159 * representing the compiled version of the pattern.
161 * @see CompiledField
162 * @serial Ignored.
164 private transient ArrayList tokens;
167 * The localised data used in formatting,
168 * such as the day and month names in the local
169 * language, and the localized pattern characters.
171 * @see DateFormatSymbols
172 * @serial The localisation data. May not be null.
174 private DateFormatSymbols formatData;
177 * The date representing the start of the century
178 * used for interpreting two digit years. For
179 * example, 24/10/2004 would cause two digit
180 * years to be interpreted as representing
181 * the years between 2004 and 2104.
183 * @see #get2DigitYearStart()
184 * @see #set2DigitYearStart(java.util.Date)
185 * @see Date
186 * @serial The start date of the century for parsing two digit years.
187 * May not be null.
189 private Date defaultCenturyStart;
192 * The year at which interpretation of two
193 * digit years starts.
195 * @see #get2DigitYearStart()
196 * @see #set2DigitYearStart(java.util.Date)
197 * @serial Ignored.
199 private transient int defaultCentury;
202 * The non-localized pattern string. This
203 * only ever contains the pattern characters
204 * stored in standardChars. Localized patterns
205 * are translated to this form.
207 * @see #applyPattern(String)
208 * @see #applyLocalizedPattern(String)
209 * @see #toPattern()
210 * @see #toLocalizedPattern()
211 * @serial The non-localized pattern string. May not be null.
213 private String pattern;
216 * The version of serialized data used by this class.
217 * Version 0 only includes the pattern and formatting
218 * data. Version 1 adds the start date for interpreting
219 * two digit years.
221 * @serial This specifies the version of the data being serialized.
222 * Version 0 (or no version) specifies just <code>pattern</code>
223 * and <code>formatData</code>. Version 1 adds
224 * the <code>defaultCenturyStart</code>. This implementation
225 * always writes out version 1 data.
227 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
230 * For compatability.
232 private static final long serialVersionUID = 4774881970558875024L;
234 // This string is specified in the root of the CLDR. We set it here
235 // rather than doing a DateFormatSymbols(Locale.US).getLocalPatternChars()
236 // since someone could theoretically change those values (though unlikely).
237 private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZ";
240 * Reads the serialized version of this object.
241 * If the serialized data is only version 0,
242 * then the date for the start of the century
243 * for interpreting two digit years is computed.
244 * The pattern is parsed and compiled following the process
245 * of reading in the serialized data.
247 * @param stream the object stream to read the data from.
248 * @throws IOException if an I/O error occurs.
249 * @throws ClassNotFoundException if the class of the serialized data
250 * could not be found.
251 * @throws InvalidObjectException if the pattern is invalid.
253 private void readObject(ObjectInputStream stream)
254 throws IOException, ClassNotFoundException
256 stream.defaultReadObject();
257 if (serialVersionOnStream < 1)
259 computeCenturyStart ();
260 serialVersionOnStream = 1;
262 else
263 // Ensure that defaultCentury gets set.
264 set2DigitYearStart(defaultCenturyStart);
266 // Set up items normally taken care of by the constructor.
267 tokens = new ArrayList();
270 compileFormat(pattern);
272 catch (IllegalArgumentException e)
274 throw new InvalidObjectException("The stream pattern was invalid.");
279 * Compiles the supplied non-localized pattern into a form
280 * from which formatting and parsing can be performed.
281 * This also detects errors in the pattern, which will
282 * be raised on later use of the compiled data.
284 * @param pattern the non-localized pattern to compile.
285 * @throws IllegalArgumentException if the pattern is invalid.
287 private void compileFormat(String pattern)
289 // Any alphabetical characters are treated as pattern characters
290 // unless enclosed in single quotes.
292 char thisChar;
293 int pos;
294 int field;
295 CompiledField current = null;
297 for (int i = 0; i < pattern.length(); i++)
299 thisChar = pattern.charAt(i);
300 field = standardChars.indexOf(thisChar);
301 if (field == -1)
303 current = null;
304 if ((thisChar >= 'A' && thisChar <= 'Z')
305 || (thisChar >= 'a' && thisChar <= 'z'))
307 // Not a valid letter
308 throw new IllegalArgumentException("Invalid letter "
309 + thisChar +
310 " encountered at character "
311 + i + ".");
313 else if (thisChar == '\'')
315 // Quoted text section; skip to next single quote
316 pos = pattern.indexOf('\'', i + 1);
317 // First look for '' -- meaning a single quote.
318 if (pos == i + 1)
319 tokens.add("'");
320 else
322 // Look for the terminating quote. However, if we
323 // see a '', that represents a literal quote and
324 // we must iterate.
325 StringBuilder buf = new StringBuilder();
326 int oldPos = i + 1;
329 if (pos == -1)
330 throw new IllegalArgumentException("Quotes starting at character "
331 + i +
332 " not closed.");
333 buf.append(pattern.substring(oldPos, pos));
334 if (pos + 1 >= pattern.length()
335 || pattern.charAt(pos + 1) != '\'')
336 break;
337 buf.append('\'');
338 oldPos = pos + 2;
339 pos = pattern.indexOf('\'', pos + 2);
341 while (true);
342 tokens.add(buf.toString());
344 i = pos;
346 else
348 // A special character
349 tokens.add(Character.valueOf(thisChar));
352 else
354 // A valid field
355 if ((current != null) && (field == current.field))
356 current.size++;
357 else
359 current = new CompiledField(field, 1, thisChar);
360 tokens.add(current);
367 * Returns a string representation of this
368 * class.
370 * @return a string representation of the <code>SimpleDateFormat</code>
371 * instance.
373 public String toString()
375 StringBuilder output = new StringBuilder(getClass().getName());
376 output.append("[tokens=");
377 output.append(tokens);
378 output.append(", formatData=");
379 output.append(formatData);
380 output.append(", defaultCenturyStart=");
381 output.append(defaultCenturyStart);
382 output.append(", defaultCentury=");
383 output.append(defaultCentury);
384 output.append(", pattern=");
385 output.append(pattern);
386 output.append(", serialVersionOnStream=");
387 output.append(serialVersionOnStream);
388 output.append(", standardChars=");
389 output.append(standardChars);
390 output.append("]");
391 return output.toString();
395 * Constructs a SimpleDateFormat using the default pattern for
396 * the default locale.
398 public SimpleDateFormat()
401 * There does not appear to be a standard API for determining
402 * what the default pattern for a locale is, so use package-scope
403 * variables in DateFormatSymbols to encapsulate this.
405 super();
406 Locale locale = Locale.getDefault();
407 calendar = new GregorianCalendar(locale);
408 computeCenturyStart();
409 tokens = new ArrayList();
410 formatData = new DateFormatSymbols(locale);
411 pattern = (formatData.dateFormats[DEFAULT] + ' '
412 + formatData.timeFormats[DEFAULT]);
413 compileFormat(pattern);
414 numberFormat = NumberFormat.getInstance(locale);
415 numberFormat.setGroupingUsed (false);
416 numberFormat.setParseIntegerOnly (true);
417 numberFormat.setMaximumFractionDigits (0);
421 * Creates a date formatter using the specified non-localized pattern,
422 * with the default DateFormatSymbols for the default locale.
424 * @param pattern the pattern to use.
425 * @throws NullPointerException if the pattern is null.
426 * @throws IllegalArgumentException if the pattern is invalid.
428 public SimpleDateFormat(String pattern)
430 this(pattern, Locale.getDefault());
434 * Creates a date formatter using the specified non-localized pattern,
435 * with the default DateFormatSymbols for the given locale.
437 * @param pattern the non-localized pattern to use.
438 * @param locale the locale to use for the formatting symbols.
439 * @throws NullPointerException if the pattern is null.
440 * @throws IllegalArgumentException if the pattern is invalid.
442 public SimpleDateFormat(String pattern, Locale locale)
444 super();
445 calendar = new GregorianCalendar(locale);
446 computeCenturyStart();
447 tokens = new ArrayList();
448 formatData = new DateFormatSymbols(locale);
449 compileFormat(pattern);
450 this.pattern = pattern;
451 numberFormat = NumberFormat.getInstance(locale);
452 numberFormat.setGroupingUsed (false);
453 numberFormat.setParseIntegerOnly (true);
454 numberFormat.setMaximumFractionDigits (0);
458 * Creates a date formatter using the specified non-localized
459 * pattern. The specified DateFormatSymbols will be used when
460 * formatting.
462 * @param pattern the non-localized pattern to use.
463 * @param formatData the formatting symbols to use.
464 * @throws NullPointerException if the pattern or formatData is null.
465 * @throws IllegalArgumentException if the pattern is invalid.
467 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
469 super();
470 calendar = new GregorianCalendar();
471 computeCenturyStart ();
472 tokens = new ArrayList();
473 if (formatData == null)
474 throw new NullPointerException("formatData");
475 this.formatData = formatData;
476 compileFormat(pattern);
477 this.pattern = pattern;
478 numberFormat = NumberFormat.getInstance();
479 numberFormat.setGroupingUsed (false);
480 numberFormat.setParseIntegerOnly (true);
481 numberFormat.setMaximumFractionDigits (0);
485 * This method returns a string with the formatting pattern being used
486 * by this object. This string is unlocalized.
488 * @return The format string.
490 public String toPattern()
492 return pattern;
496 * This method returns a string with the formatting pattern being used
497 * by this object. This string is localized.
499 * @return The format string.
501 public String toLocalizedPattern()
503 String localChars = formatData.getLocalPatternChars();
504 return translateLocalizedPattern(pattern, standardChars, localChars);
508 * This method sets the formatting pattern that should be used by this
509 * object. This string is not localized.
511 * @param pattern The new format pattern.
512 * @throws NullPointerException if the pattern is null.
513 * @throws IllegalArgumentException if the pattern is invalid.
515 public void applyPattern(String pattern)
517 tokens = new ArrayList();
518 compileFormat(pattern);
519 this.pattern = pattern;
523 * This method sets the formatting pattern that should be used by this
524 * object. This string is localized.
526 * @param pattern The new format pattern.
527 * @throws NullPointerException if the pattern is null.
528 * @throws IllegalArgumentException if the pattern is invalid.
530 public void applyLocalizedPattern(String pattern)
532 String localChars = formatData.getLocalPatternChars();
533 pattern = translateLocalizedPattern(pattern, localChars, standardChars);
534 applyPattern(pattern);
538 * Translates either from or to a localized variant of the pattern
539 * string. For example, in the German locale, 't' (for 'tag') is
540 * used instead of 'd' (for 'date'). This method translates
541 * a localized pattern (such as 'ttt') to a non-localized pattern
542 * (such as 'ddd'), or vice versa. Non-localized patterns use
543 * a standard set of characters, which match those of the U.S. English
544 * locale.
546 * @param pattern the pattern to translate.
547 * @param oldChars the old set of characters (used in the pattern).
548 * @param newChars the new set of characters (which will be used in the
549 * pattern).
550 * @return a version of the pattern using the characters in
551 * <code>newChars</code>.
553 private String translateLocalizedPattern(String pattern,
554 String oldChars, String newChars)
556 int len = pattern.length();
557 StringBuilder buf = new StringBuilder(len);
558 boolean quoted = false;
559 for (int i = 0; i < len; i++)
561 char ch = pattern.charAt(i);
562 if (ch == '\'')
563 quoted = ! quoted;
564 if (! quoted)
566 int j = oldChars.indexOf(ch);
567 if (j >= 0)
568 ch = newChars.charAt(j);
570 buf.append(ch);
572 return buf.toString();
575 /**
576 * Returns the start of the century used for two digit years.
578 * @return A <code>Date</code> representing the start of the century
579 * for two digit years.
581 public Date get2DigitYearStart()
583 return defaultCenturyStart;
587 * Sets the start of the century used for two digit years.
589 * @param date A <code>Date</code> representing the start of the century for
590 * two digit years.
592 public void set2DigitYearStart(Date date)
594 defaultCenturyStart = date;
595 calendar.clear();
596 calendar.setTime(date);
597 int year = calendar.get(Calendar.YEAR);
598 defaultCentury = year - (year % 100);
602 * This method returns a copy of the format symbol information used
603 * for parsing and formatting dates.
605 * @return a copy of the date format symbols.
607 public DateFormatSymbols getDateFormatSymbols()
609 return (DateFormatSymbols) formatData.clone();
613 * This method sets the format symbols information used for parsing
614 * and formatting dates.
616 * @param formatData The date format symbols.
617 * @throws NullPointerException if <code>formatData</code> is null.
619 public void setDateFormatSymbols(DateFormatSymbols formatData)
621 if (formatData == null)
623 throw new
624 NullPointerException("The supplied format data was null.");
626 this.formatData = formatData;
630 * This methods tests whether the specified object is equal to this
631 * object. This will be true if and only if the specified object:
632 * <p>
633 * <ul>
634 * <li>Is not <code>null</code>.</li>
635 * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
636 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
637 * level.</li>
638 * <li>Has the same formatting pattern.</li>
639 * <li>Is using the same formatting symbols.</li>
640 * <li>Is using the same century for two digit years.</li>
641 * </ul>
643 * @param o The object to compare for equality against.
645 * @return <code>true</code> if the specified object is equal to this object,
646 * <code>false</code> otherwise.
648 public boolean equals(Object o)
650 if (!super.equals(o))
651 return false;
653 if (!(o instanceof SimpleDateFormat))
654 return false;
656 SimpleDateFormat sdf = (SimpleDateFormat)o;
658 if (defaultCentury != sdf.defaultCentury)
659 return false;
661 if (!toPattern().equals(sdf.toPattern()))
662 return false;
664 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
665 return false;
667 return true;
671 * This method returns a hash value for this object.
673 * @return A hash value for this object.
675 public int hashCode()
677 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
678 getDateFormatSymbols().hashCode();
683 * Formats the date input according to the format string in use,
684 * appending to the specified StringBuffer. The input StringBuffer
685 * is returned as output for convenience.
687 private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
689 String temp;
690 AttributedCharacterIterator.Attribute attribute;
691 calendar.setTime(date);
693 // go through vector, filling in fields where applicable, else toString
694 Iterator iter = tokens.iterator();
695 while (iter.hasNext())
697 Object o = iter.next();
698 if (o instanceof CompiledField)
700 CompiledField cf = (CompiledField) o;
701 int beginIndex = buffer.length();
703 switch (cf.getField())
705 case ERA_FIELD:
706 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
707 break;
708 case YEAR_FIELD:
709 // If we have two digits, then we truncate. Otherwise, we
710 // use the size of the pattern, and zero pad.
711 buffer.setDefaultAttribute (DateFormat.Field.YEAR);
712 if (cf.getSize() == 2)
714 temp = "00"+String.valueOf (calendar.get (Calendar.YEAR));
715 buffer.append (temp.substring (temp.length() - 2));
717 else
718 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
719 break;
720 case MONTH_FIELD:
721 buffer.setDefaultAttribute (DateFormat.Field.MONTH);
722 if (cf.getSize() < 3)
723 withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer);
724 else if (cf.getSize() < 4)
725 buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]);
726 else
727 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
728 break;
729 case DATE_FIELD:
730 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
731 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
732 break;
733 case HOUR_OF_DAY1_FIELD: // 1-24
734 buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1);
735 withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1,
736 cf.getSize(), buffer);
737 break;
738 case HOUR_OF_DAY0_FIELD: // 0-23
739 buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0);
740 withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer);
741 break;
742 case MINUTE_FIELD:
743 buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
744 withLeadingZeros (calendar.get (Calendar.MINUTE),
745 cf.getSize(), buffer);
746 break;
747 case SECOND_FIELD:
748 buffer.setDefaultAttribute (DateFormat.Field.SECOND);
749 withLeadingZeros(calendar.get (Calendar.SECOND),
750 cf.getSize(), buffer);
751 break;
752 case MILLISECOND_FIELD:
753 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
754 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
755 break;
756 case DAY_OF_WEEK_FIELD:
757 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK);
758 if (cf.getSize() < 4)
759 buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
760 else
761 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
762 break;
763 case DAY_OF_YEAR_FIELD:
764 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR);
765 withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer);
766 break;
767 case DAY_OF_WEEK_IN_MONTH_FIELD:
768 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH);
769 withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH),
770 cf.getSize(), buffer);
771 break;
772 case WEEK_OF_YEAR_FIELD:
773 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR);
774 withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR),
775 cf.getSize(), buffer);
776 break;
777 case WEEK_OF_MONTH_FIELD:
778 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH);
779 withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH),
780 cf.getSize(), buffer);
781 break;
782 case AM_PM_FIELD:
783 buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
784 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
785 break;
786 case HOUR1_FIELD: // 1-12
787 buffer.setDefaultAttribute (DateFormat.Field.HOUR1);
788 withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1,
789 cf.getSize(), buffer);
790 break;
791 case HOUR0_FIELD: // 0-11
792 buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
793 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
794 break;
795 case TIMEZONE_FIELD:
796 buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE);
797 TimeZone zone = calendar.getTimeZone();
798 boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0;
799 // FIXME: XXX: This should be a localized time zone.
800 String zoneID = zone.getDisplayName
801 (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT);
802 buffer.append (zoneID);
803 break;
804 case RFC822_TIMEZONE_FIELD:
805 buffer.setDefaultAttribute(DateFormat.Field.RFC822_TIME_ZONE);
806 int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) +
807 calendar.get(Calendar.DST_OFFSET)) / (1000 * 60);
808 String sign = (pureMinutes < 0) ? "-" : "+";
809 pureMinutes = Math.abs(pureMinutes);
810 int hours = pureMinutes / 60;
811 int minutes = pureMinutes % 60;
812 buffer.append(sign);
813 withLeadingZeros(hours, 2, buffer);
814 withLeadingZeros(minutes, 2, buffer);
815 break;
816 default:
817 throw new IllegalArgumentException ("Illegal pattern character " +
818 cf.getCharacter());
820 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
821 || cf.getField() == pos.getField()))
823 pos.setBeginIndex(beginIndex);
824 pos.setEndIndex(buffer.length());
827 else
829 buffer.append(o.toString(), null);
834 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
836 formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
838 return buffer;
841 public AttributedCharacterIterator formatToCharacterIterator(Object date)
842 throws IllegalArgumentException
844 if (date == null)
845 throw new NullPointerException("null argument");
846 if (!(date instanceof Date))
847 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
849 AttributedFormatBuffer buf = new AttributedFormatBuffer();
850 formatWithAttribute((Date)date, buf,
851 null);
852 buf.sync();
854 return new FormatCharacterIterator(buf.getBuffer().toString(),
855 buf.getRanges(),
856 buf.getAttributes());
859 private void withLeadingZeros(int value, int length, FormatBuffer buffer)
861 String valStr = String.valueOf(value);
862 for (length -= valStr.length(); length > 0; length--)
863 buffer.append('0');
864 buffer.append(valStr);
867 private boolean expect(String source, ParsePosition pos, char ch)
869 int x = pos.getIndex();
870 boolean r = x < source.length() && source.charAt(x) == ch;
871 if (r)
872 pos.setIndex(x + 1);
873 else
874 pos.setErrorIndex(x);
875 return r;
879 * This method parses the specified string into a date.
881 * @param dateStr The date string to parse.
882 * @param pos The input and output parse position
884 * @return The parsed date, or <code>null</code> if the string cannot be
885 * parsed.
887 public Date parse (String dateStr, ParsePosition pos)
889 int fmt_index = 0;
890 int fmt_max = pattern.length();
892 calendar.clear();
893 boolean saw_timezone = false;
894 int quote_start = -1;
895 boolean is2DigitYear = false;
898 for (; fmt_index < fmt_max; ++fmt_index)
900 char ch = pattern.charAt(fmt_index);
901 if (ch == '\'')
903 int index = pos.getIndex();
904 if (fmt_index < fmt_max - 1
905 && pattern.charAt(fmt_index + 1) == '\'')
907 if (! expect (dateStr, pos, ch))
908 return null;
909 ++fmt_index;
911 else
912 quote_start = quote_start < 0 ? fmt_index : -1;
913 continue;
916 if (quote_start != -1
917 || ((ch < 'a' || ch > 'z')
918 && (ch < 'A' || ch > 'Z')))
920 if (quote_start == -1 && ch == ' ')
922 // A single unquoted space in the pattern may match
923 // any number of spaces in the input.
924 int index = pos.getIndex();
925 int save = index;
926 while (index < dateStr.length()
927 && Character.isWhitespace(dateStr.charAt(index)))
928 ++index;
929 if (index > save)
930 pos.setIndex(index);
931 else
933 // Didn't see any whitespace.
934 pos.setErrorIndex(index);
935 return null;
938 else if (! expect (dateStr, pos, ch))
939 return null;
940 continue;
943 // We've arrived at a potential pattern character in the
944 // pattern.
945 int fmt_count = 1;
946 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
948 ++fmt_count;
951 // We might need to limit the number of digits to parse in
952 // some cases. We look to the next pattern character to
953 // decide.
954 boolean limit_digits = false;
955 if (fmt_index < fmt_max
956 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
957 limit_digits = true;
958 --fmt_index;
960 // We can handle most fields automatically: most either are
961 // numeric or are looked up in a string vector. In some cases
962 // we need an offset. When numeric, `offset' is added to the
963 // resulting value. When doing a string lookup, offset is the
964 // initial index into the string array.
965 int calendar_field;
966 boolean is_numeric = true;
967 int offset = 0;
968 boolean maybe2DigitYear = false;
969 boolean oneBasedHour = false;
970 boolean oneBasedHourOfDay = false;
971 Integer simpleOffset;
972 String[] set1 = null;
973 String[] set2 = null;
974 switch (ch)
976 case 'd':
977 calendar_field = Calendar.DATE;
978 break;
979 case 'D':
980 calendar_field = Calendar.DAY_OF_YEAR;
981 break;
982 case 'F':
983 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
984 break;
985 case 'E':
986 is_numeric = false;
987 offset = 1;
988 calendar_field = Calendar.DAY_OF_WEEK;
989 set1 = formatData.getWeekdays();
990 set2 = formatData.getShortWeekdays();
991 break;
992 case 'w':
993 calendar_field = Calendar.WEEK_OF_YEAR;
994 break;
995 case 'W':
996 calendar_field = Calendar.WEEK_OF_MONTH;
997 break;
998 case 'M':
999 calendar_field = Calendar.MONTH;
1000 if (fmt_count <= 2)
1001 offset = -1;
1002 else
1004 is_numeric = false;
1005 set1 = formatData.getMonths();
1006 set2 = formatData.getShortMonths();
1008 break;
1009 case 'y':
1010 calendar_field = Calendar.YEAR;
1011 if (fmt_count <= 2)
1012 maybe2DigitYear = true;
1013 break;
1014 case 'K':
1015 calendar_field = Calendar.HOUR;
1016 break;
1017 case 'h':
1018 calendar_field = Calendar.HOUR;
1019 oneBasedHour = true;
1020 break;
1021 case 'H':
1022 calendar_field = Calendar.HOUR_OF_DAY;
1023 break;
1024 case 'k':
1025 calendar_field = Calendar.HOUR_OF_DAY;
1026 oneBasedHourOfDay = true;
1027 break;
1028 case 'm':
1029 calendar_field = Calendar.MINUTE;
1030 break;
1031 case 's':
1032 calendar_field = Calendar.SECOND;
1033 break;
1034 case 'S':
1035 calendar_field = Calendar.MILLISECOND;
1036 break;
1037 case 'a':
1038 is_numeric = false;
1039 calendar_field = Calendar.AM_PM;
1040 set1 = formatData.getAmPmStrings();
1041 break;
1042 case 'z':
1043 case 'Z':
1044 // We need a special case for the timezone, because it
1045 // uses a different data structure than the other cases.
1046 is_numeric = false;
1047 calendar_field = Calendar.ZONE_OFFSET;
1048 String[][] zoneStrings = formatData.getZoneStrings();
1049 int zoneCount = zoneStrings.length;
1050 int index = pos.getIndex();
1051 boolean found_zone = false;
1052 simpleOffset = computeOffset(dateStr.substring(index), pos);
1053 if (simpleOffset != null)
1055 found_zone = true;
1056 saw_timezone = true;
1057 calendar.set(Calendar.DST_OFFSET, 0);
1058 offset = simpleOffset.intValue();
1060 else
1062 for (int j = 0; j < zoneCount; j++)
1064 String[] strings = zoneStrings[j];
1065 int k;
1066 for (k = 0; k < strings.length; ++k)
1068 if (dateStr.startsWith(strings[k], index))
1069 break;
1071 if (k != strings.length)
1073 found_zone = true;
1074 saw_timezone = true;
1075 TimeZone tz = TimeZone.getTimeZone (strings[0]);
1076 // Check if it's a DST zone or ordinary
1077 if(k == 3 || k == 4)
1078 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1079 else
1080 calendar.set (Calendar.DST_OFFSET, 0);
1081 offset = tz.getRawOffset ();
1082 pos.setIndex(index + strings[k].length());
1083 break;
1087 if (! found_zone)
1089 pos.setErrorIndex(pos.getIndex());
1090 return null;
1092 break;
1093 default:
1094 pos.setErrorIndex(pos.getIndex());
1095 return null;
1098 // Compute the value we should assign to the field.
1099 int value;
1100 int index = -1;
1101 if (is_numeric)
1103 numberFormat.setMinimumIntegerDigits(fmt_count);
1104 if (maybe2DigitYear)
1105 index = pos.getIndex();
1106 Number n = null;
1107 if (limit_digits)
1109 // numberFormat.setMaximumIntegerDigits(fmt_count) may
1110 // not work as expected. So we explicitly use substring
1111 // of dateStr.
1112 int origPos = pos.getIndex();
1113 pos.setIndex(0);
1114 n = numberFormat.parse(dateStr.substring(origPos, origPos + fmt_count), pos);
1115 pos.setIndex(origPos + pos.getIndex());
1117 else
1118 n = numberFormat.parse(dateStr, pos);
1119 if (pos == null || ! (n instanceof Long))
1120 return null;
1121 value = n.intValue() + offset;
1123 else if (set1 != null)
1125 index = pos.getIndex();
1126 int i;
1127 boolean found = false;
1128 for (i = offset; i < set1.length; ++i)
1130 if (set1[i] != null)
1131 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1132 index))
1134 found = true;
1135 pos.setIndex(index + set1[i].length());
1136 break;
1139 if (!found && set2 != null)
1141 for (i = offset; i < set2.length; ++i)
1143 if (set2[i] != null)
1144 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1145 index))
1147 found = true;
1148 pos.setIndex(index + set2[i].length());
1149 break;
1153 if (!found)
1155 pos.setErrorIndex(index);
1156 return null;
1158 value = i;
1160 else
1161 value = offset;
1163 if (maybe2DigitYear)
1165 // Parse into default century if the numeric year string has
1166 // exactly 2 digits.
1167 int digit_count = pos.getIndex() - index;
1168 if (digit_count == 2)
1170 is2DigitYear = true;
1171 value += defaultCentury;
1175 // Calendar uses 0-based hours.
1176 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1177 if (oneBasedHour && value == 12)
1178 value = 0;
1180 if (oneBasedHourOfDay && value == 24)
1181 value = 0;
1183 // Assign the value and move on.
1184 calendar.set(calendar_field, value);
1187 if (is2DigitYear)
1189 // Apply the 80-20 heuristic to dermine the full year based on
1190 // defaultCenturyStart.
1191 int year = calendar.get(Calendar.YEAR);
1192 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1193 calendar.set(Calendar.YEAR, year + 100);
1195 if (! saw_timezone)
1197 // Use the real rules to determine whether or not this
1198 // particular time is in daylight savings.
1199 calendar.clear (Calendar.DST_OFFSET);
1200 calendar.clear (Calendar.ZONE_OFFSET);
1202 return calendar.getTime();
1204 catch (IllegalArgumentException x)
1206 pos.setErrorIndex(pos.getIndex());
1207 return null;
1212 * <p>
1213 * Computes the time zone offset in milliseconds
1214 * relative to GMT, based on the supplied
1215 * <code>String</code> representation.
1216 * </p>
1217 * <p>
1218 * The supplied <code>String</code> must be a three
1219 * or four digit signed number, with an optional 'GMT'
1220 * prefix. The first one or two digits represents the hours,
1221 * while the last two represent the minutes. The
1222 * two sets of digits can optionally be separated by
1223 * ':'. The mandatory sign prefix (either '+' or '-')
1224 * indicates the direction of the offset from GMT.
1225 * </p>
1226 * <p>
1227 * For example, 'GMT+0200' specifies 2 hours after
1228 * GMT, while '-05:00' specifies 5 hours prior to
1229 * GMT. The special case of 'GMT' alone can be used
1230 * to represent the offset, 0.
1231 * </p>
1232 * <p>
1233 * If the <code>String</code> can not be parsed,
1234 * the result will be null. The resulting offset
1235 * is wrapped in an <code>Integer</code> object, in
1236 * order to allow such failure to be represented.
1237 * </p>
1239 * @param zoneString a string in the form
1240 * (GMT)? sign hours : minutes
1241 * where sign = '+' or '-', hours
1242 * is a one or two digits representing
1243 * a number between 0 and 23, and
1244 * minutes is two digits representing
1245 * a number between 0 and 59.
1246 * @return the parsed offset, or null if parsing
1247 * failed.
1249 private Integer computeOffset(String zoneString, ParsePosition pos)
1251 Pattern pattern =
1252 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1253 Matcher matcher = pattern.matcher(zoneString);
1255 // Match from start, but ignore trailing parts
1256 boolean hasAll = matcher.lookingAt();
1259 // Do we have at least the sign, hour and minute?
1260 matcher.group(2);
1261 matcher.group(4);
1262 matcher.group(5);
1264 catch (IllegalStateException ise)
1266 hasAll = false;
1268 if (hasAll)
1270 int sign = matcher.group(2).equals("+") ? 1 : -1;
1271 int hour = Integer.parseInt(matcher.group(4));
1272 if (!matcher.group(3).equals(""))
1273 hour += (Integer.parseInt(matcher.group(3)) * 10);
1274 int minutes = Integer.parseInt(matcher.group(5));
1276 if (hour > 23)
1277 return null;
1278 int offset = sign * ((hour * 60) + minutes) * 60000;
1280 // advance the index
1281 pos.setIndex(pos.getIndex() + matcher.end());
1282 return Integer.valueOf(offset);
1284 else if (zoneString.startsWith("GMT"))
1286 pos.setIndex(pos.getIndex() + 3);
1287 return Integer.valueOf(0);
1289 return null;
1292 // Compute the start of the current century as defined by
1293 // get2DigitYearStart.
1294 private void computeCenturyStart()
1296 int year = calendar.get(Calendar.YEAR);
1297 calendar.set(Calendar.YEAR, year - 80);
1298 set2DigitYearStart(calendar.getTime());
1302 * Returns a copy of this instance of
1303 * <code>SimpleDateFormat</code>. The copy contains
1304 * clones of the formatting symbols and the 2-digit
1305 * year century start date.
1307 public Object clone()
1309 SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1310 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1311 clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1312 return clone;