Imported GNU Classpath 0.90
[official-gcc.git] / libjava / classpath / java / text / SimpleDateFormat.java
blob00c08507e811f69b206fab720d36446ba263df5a
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 StringBuffer builder;
144 builder = new StringBuffer(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 StringBuffer buf = new StringBuffer();
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(new Character(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 StringBuffer output = new StringBuffer(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 StringBuffer buf = new StringBuffer(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 (! expect (dateStr, pos, ch))
921 return null;
922 continue;
925 // We've arrived at a potential pattern character in the
926 // pattern.
927 int fmt_count = 1;
928 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
930 ++fmt_count;
933 // We might need to limit the number of digits to parse in
934 // some cases. We look to the next pattern character to
935 // decide.
936 boolean limit_digits = false;
937 if (fmt_index < fmt_max
938 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
939 limit_digits = true;
940 --fmt_index;
942 // We can handle most fields automatically: most either are
943 // numeric or are looked up in a string vector. In some cases
944 // we need an offset. When numeric, `offset' is added to the
945 // resulting value. When doing a string lookup, offset is the
946 // initial index into the string array.
947 int calendar_field;
948 boolean is_numeric = true;
949 int offset = 0;
950 boolean maybe2DigitYear = false;
951 boolean oneBasedHour = false;
952 boolean oneBasedHourOfDay = false;
953 Integer simpleOffset;
954 String[] set1 = null;
955 String[] set2 = null;
956 switch (ch)
958 case 'd':
959 calendar_field = Calendar.DATE;
960 break;
961 case 'D':
962 calendar_field = Calendar.DAY_OF_YEAR;
963 break;
964 case 'F':
965 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
966 break;
967 case 'E':
968 is_numeric = false;
969 offset = 1;
970 calendar_field = Calendar.DAY_OF_WEEK;
971 set1 = formatData.getWeekdays();
972 set2 = formatData.getShortWeekdays();
973 break;
974 case 'w':
975 calendar_field = Calendar.WEEK_OF_YEAR;
976 break;
977 case 'W':
978 calendar_field = Calendar.WEEK_OF_MONTH;
979 break;
980 case 'M':
981 calendar_field = Calendar.MONTH;
982 if (fmt_count <= 2)
983 offset = -1;
984 else
986 is_numeric = false;
987 set1 = formatData.getMonths();
988 set2 = formatData.getShortMonths();
990 break;
991 case 'y':
992 calendar_field = Calendar.YEAR;
993 if (fmt_count <= 2)
994 maybe2DigitYear = true;
995 break;
996 case 'K':
997 calendar_field = Calendar.HOUR;
998 break;
999 case 'h':
1000 calendar_field = Calendar.HOUR;
1001 oneBasedHour = true;
1002 break;
1003 case 'H':
1004 calendar_field = Calendar.HOUR_OF_DAY;
1005 break;
1006 case 'k':
1007 calendar_field = Calendar.HOUR_OF_DAY;
1008 oneBasedHourOfDay = true;
1009 break;
1010 case 'm':
1011 calendar_field = Calendar.MINUTE;
1012 break;
1013 case 's':
1014 calendar_field = Calendar.SECOND;
1015 break;
1016 case 'S':
1017 calendar_field = Calendar.MILLISECOND;
1018 break;
1019 case 'a':
1020 is_numeric = false;
1021 calendar_field = Calendar.AM_PM;
1022 set1 = formatData.getAmPmStrings();
1023 break;
1024 case 'z':
1025 case 'Z':
1026 // We need a special case for the timezone, because it
1027 // uses a different data structure than the other cases.
1028 is_numeric = false;
1029 calendar_field = Calendar.ZONE_OFFSET;
1030 String[][] zoneStrings = formatData.getZoneStrings();
1031 int zoneCount = zoneStrings.length;
1032 int index = pos.getIndex();
1033 boolean found_zone = false;
1034 simpleOffset = computeOffset(dateStr.substring(index), pos);
1035 if (simpleOffset != null)
1037 found_zone = true;
1038 saw_timezone = true;
1039 calendar.set(Calendar.DST_OFFSET, 0);
1040 offset = simpleOffset.intValue();
1042 else
1044 for (int j = 0; j < zoneCount; j++)
1046 String[] strings = zoneStrings[j];
1047 int k;
1048 for (k = 0; k < strings.length; ++k)
1050 if (dateStr.startsWith(strings[k], index))
1051 break;
1053 if (k != strings.length)
1055 found_zone = true;
1056 saw_timezone = true;
1057 TimeZone tz = TimeZone.getTimeZone (strings[0]);
1058 // Check if it's a DST zone or ordinary
1059 if(k == 3 || k == 4)
1060 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1061 else
1062 calendar.set (Calendar.DST_OFFSET, 0);
1063 offset = tz.getRawOffset ();
1064 pos.setIndex(index + strings[k].length());
1065 break;
1069 if (! found_zone)
1071 pos.setErrorIndex(pos.getIndex());
1072 return null;
1074 break;
1075 default:
1076 pos.setErrorIndex(pos.getIndex());
1077 return null;
1080 // Compute the value we should assign to the field.
1081 int value;
1082 int index = -1;
1083 if (is_numeric)
1085 numberFormat.setMinimumIntegerDigits(fmt_count);
1086 if (limit_digits)
1087 numberFormat.setMaximumIntegerDigits(fmt_count);
1088 if (maybe2DigitYear)
1089 index = pos.getIndex();
1090 Number n = numberFormat.parse(dateStr, pos);
1091 if (pos == null || ! (n instanceof Long))
1092 return null;
1093 value = n.intValue() + offset;
1095 else if (set1 != null)
1097 index = pos.getIndex();
1098 int i;
1099 boolean found = false;
1100 for (i = offset; i < set1.length; ++i)
1102 if (set1[i] != null)
1103 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1104 index))
1106 found = true;
1107 pos.setIndex(index + set1[i].length());
1108 break;
1111 if (!found && set2 != null)
1113 for (i = offset; i < set2.length; ++i)
1115 if (set2[i] != null)
1116 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1117 index))
1119 found = true;
1120 pos.setIndex(index + set2[i].length());
1121 break;
1125 if (!found)
1127 pos.setErrorIndex(index);
1128 return null;
1130 value = i;
1132 else
1133 value = offset;
1135 if (maybe2DigitYear)
1137 // Parse into default century if the numeric year string has
1138 // exactly 2 digits.
1139 int digit_count = pos.getIndex() - index;
1140 if (digit_count == 2)
1142 is2DigitYear = true;
1143 value += defaultCentury;
1147 // Calendar uses 0-based hours.
1148 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1149 if (oneBasedHour && value == 12)
1150 value = 0;
1152 if (oneBasedHourOfDay && value == 24)
1153 value = 0;
1155 // Assign the value and move on.
1156 calendar.set(calendar_field, value);
1159 if (is2DigitYear)
1161 // Apply the 80-20 heuristic to dermine the full year based on
1162 // defaultCenturyStart.
1163 int year = calendar.get(Calendar.YEAR);
1164 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1165 calendar.set(Calendar.YEAR, year + 100);
1167 if (! saw_timezone)
1169 // Use the real rules to determine whether or not this
1170 // particular time is in daylight savings.
1171 calendar.clear (Calendar.DST_OFFSET);
1172 calendar.clear (Calendar.ZONE_OFFSET);
1174 return calendar.getTime();
1176 catch (IllegalArgumentException x)
1178 pos.setErrorIndex(pos.getIndex());
1179 return null;
1184 * <p>
1185 * Computes the time zone offset in milliseconds
1186 * relative to GMT, based on the supplied
1187 * <code>String</code> representation.
1188 * </p>
1189 * <p>
1190 * The supplied <code>String</code> must be a three
1191 * or four digit signed number, with an optional 'GMT'
1192 * prefix. The first one or two digits represents the hours,
1193 * while the last two represent the minutes. The
1194 * two sets of digits can optionally be separated by
1195 * ':'. The mandatory sign prefix (either '+' or '-')
1196 * indicates the direction of the offset from GMT.
1197 * </p>
1198 * <p>
1199 * For example, 'GMT+0200' specifies 2 hours after
1200 * GMT, while '-05:00' specifies 5 hours prior to
1201 * GMT. The special case of 'GMT' alone can be used
1202 * to represent the offset, 0.
1203 * </p>
1204 * <p>
1205 * If the <code>String</code> can not be parsed,
1206 * the result will be null. The resulting offset
1207 * is wrapped in an <code>Integer</code> object, in
1208 * order to allow such failure to be represented.
1209 * </p>
1211 * @param zoneString a string in the form
1212 * (GMT)? sign hours : minutes
1213 * where sign = '+' or '-', hours
1214 * is a one or two digits representing
1215 * a number between 0 and 23, and
1216 * minutes is two digits representing
1217 * a number between 0 and 59.
1218 * @return the parsed offset, or null if parsing
1219 * failed.
1221 private Integer computeOffset(String zoneString, ParsePosition pos)
1223 Pattern pattern =
1224 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1225 Matcher matcher = pattern.matcher(zoneString);
1227 // Match from start, but ignore trailing parts
1228 boolean hasAll = matcher.lookingAt();
1231 // Do we have at least the sign, hour and minute?
1232 matcher.group(2);
1233 matcher.group(4);
1234 matcher.group(5);
1236 catch (IllegalStateException ise)
1238 hasAll = false;
1240 if (hasAll)
1242 int sign = matcher.group(2).equals("+") ? 1 : -1;
1243 int hour = Integer.parseInt(matcher.group(4));
1244 if (!matcher.group(3).equals(""))
1245 hour += (Integer.parseInt(matcher.group(3)) * 10);
1246 int minutes = Integer.parseInt(matcher.group(5));
1248 if (hour > 23)
1249 return null;
1250 int offset = sign * ((hour * 60) + minutes) * 60000;
1252 // advance the index
1253 pos.setIndex(pos.getIndex() + matcher.end());
1254 return new Integer(offset);
1256 else if (zoneString.startsWith("GMT"))
1258 pos.setIndex(pos.getIndex() + 3);
1259 return new Integer(0);
1261 return null;
1264 // Compute the start of the current century as defined by
1265 // get2DigitYearStart.
1266 private void computeCenturyStart()
1268 int year = calendar.get(Calendar.YEAR);
1269 calendar.set(Calendar.YEAR, year - 80);
1270 set2DigitYearStart(calendar.getTime());
1274 * Returns a copy of this instance of
1275 * <code>SimpleDateFormat</code>. The copy contains
1276 * clones of the formatting symbols and the 2-digit
1277 * year century start date.
1279 public Object clone()
1281 SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1282 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1283 clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1284 return clone;