001/* SimpleDateFormat.java -- A class for parsing/formating simple 
002   date constructs
003   Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005
004   Free Software Foundation, Inc.
005
006This file is part of GNU Classpath.
007
008GNU Classpath is free software; you can redistribute it and/or modify
009it under the terms of the GNU General Public License as published by
010the Free Software Foundation; either version 2, or (at your option)
011any later version.
012 
013GNU Classpath is distributed in the hope that it will be useful, but
014WITHOUT ANY WARRANTY; without even the implied warranty of
015MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
016General Public License for more details.
017
018You should have received a copy of the GNU General Public License
019along with GNU Classpath; see the file COPYING.  If not, write to the
020Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02102110-1301 USA.
022
023Linking this library statically or dynamically with other modules is
024making a combined work based on this library.  Thus, the terms and
025conditions of the GNU General Public License cover the whole
026combination.
027
028As a special exception, the copyright holders of this library give you
029permission to link this library with independent modules to produce an
030executable, regardless of the license terms of these independent
031modules, and to copy and distribute the resulting executable under
032terms of your choice, provided that you also meet, for each linked
033independent module, the terms and conditions of the license of that
034module.  An independent module is a module which is not derived from
035or based on this library.  If you modify this library, you may extend
036this exception to your version of the library, but you are not
037obligated to do so.  If you do not wish to do so, delete this
038exception statement from your version. */
039
040
041package java.text;
042
043import gnu.java.lang.CPStringBuilder;
044
045import gnu.java.text.AttributedFormatBuffer;
046import gnu.java.text.FormatBuffer;
047import gnu.java.text.FormatCharacterIterator;
048import gnu.java.text.StringFormatBuffer;
049
050import java.io.IOException;
051import java.io.InvalidObjectException;
052import java.io.ObjectInputStream;
053import java.util.ArrayList;
054import java.util.Calendar;
055import java.util.Date;
056import java.util.GregorianCalendar;
057import java.util.Iterator;
058import java.util.Locale;
059import java.util.TimeZone;
060import java.util.regex.Matcher;
061import java.util.regex.Pattern;
062
063/**
064 * SimpleDateFormat provides convenient methods for parsing and formatting
065 * dates using Gregorian calendars (see java.util.GregorianCalendar). 
066 * This class is not thread-safe; external synchronisation should be applied
067 * if an instance is to be accessed from multiple threads.
068 */
069public class SimpleDateFormat extends DateFormat 
070{
071  /** 
072   * This class is used by <code>SimpleDateFormat</code> as a
073   * compiled representation of a format string.  The field
074   * ID, size, and character used are stored for each sequence
075   * of pattern characters.
076   */
077  private class CompiledField
078  {
079    /**
080     * The ID of the field within the local pattern characters.
081     * Package private for use in out class.
082     */
083    int field;
084
085    /**
086     * The size of the character sequence.
087     * Package private for use in out class.
088     */
089    int size;
090
091    /**
092     * The character used.
093     */
094    private char character;
095
096    /** 
097     * Constructs a compiled field using the
098     * the given field ID, size and character
099     * values.
100     *
101     * @param f the field ID.
102     * @param s the size of the field.
103     * @param c the character used.
104     */
105    public CompiledField(int f, int s, char c)
106    {
107      field = f;
108      size = s;
109      character = c;
110    }
111
112    /**
113     * Retrieves the ID of the field relative to
114     * the local pattern characters.
115     */
116    public int getField()
117    {
118      return field;
119    }
120
121    /**
122     * Retrieves the size of the character sequence.
123     */
124    public int getSize()
125    {
126      return size;
127    }
128
129    /**
130     * Retrieves the character used in the sequence.
131     */
132    public char getCharacter()
133    {
134      return character;
135    }
136
137    /**
138     * Returns a <code>String</code> representation
139     * of the compiled field, primarily for debugging
140     * purposes.
141     *
142     * @return a <code>String</code> representation.
143     */
144    public String toString()
145    {
146      CPStringBuilder builder;
147
148      builder = new CPStringBuilder(getClass().getName());
149      builder.append("[field=");
150      builder.append(field);
151      builder.append(", size=");
152      builder.append(size);
153      builder.append(", character=");
154      builder.append(character);
155      builder.append("]");
156
157      return builder.toString();
158    }
159  }
160
161  /**
162   * A list of <code>CompiledField</code>s and {@code String}s
163   * representing the compiled version of the pattern.
164   *
165   * @see CompiledField
166   * @serial Ignored.
167   */
168  private transient ArrayList<Object> tokens;
169
170  /**
171   * The localised data used in formatting,
172   * such as the day and month names in the local
173   * language, and the localized pattern characters.
174   *
175   * @see DateFormatSymbols
176   * @serial The localisation data.  May not be null.
177   */
178  private DateFormatSymbols formatData;
179
180  /**
181   * The date representing the start of the century
182   * used for interpreting two digit years.  For
183   * example, 24/10/2004 would cause two digit
184   * years to be interpreted as representing
185   * the years between 2004 and 2104.
186   *
187   * @see #get2DigitYearStart()
188   * @see #set2DigitYearStart(java.util.Date)
189   * @see Date
190   * @serial The start date of the century for parsing two digit years.
191   *         May not be null.
192   */
193  private Date defaultCenturyStart;
194
195  /**
196   * The year at which interpretation of two
197   * digit years starts.
198   *
199   * @see #get2DigitYearStart()
200   * @see #set2DigitYearStart(java.util.Date)
201   * @serial Ignored.
202   */
203  private transient int defaultCentury;
204
205  /**
206   * The non-localized pattern string.  This
207   * only ever contains the pattern characters
208   * stored in standardChars.  Localized patterns
209   * are translated to this form.
210   *
211   * @see #applyPattern(String)
212   * @see #applyLocalizedPattern(String)
213   * @see #toPattern()
214   * @see #toLocalizedPattern()
215   * @serial The non-localized pattern string.  May not be null.
216   */
217  private String pattern;
218
219  /**
220   * The version of serialized data used by this class.
221   * Version 0 only includes the pattern and formatting
222   * data.  Version 1 adds the start date for interpreting
223   * two digit years.
224   *
225   * @serial This specifies the version of the data being serialized.
226   *         Version 0 (or no version) specifies just <code>pattern</code>
227   *         and <code>formatData</code>.  Version 1 adds
228   *         the <code>defaultCenturyStart</code>.  This implementation
229   *         always writes out version 1 data.
230   */
231  private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
232
233  /**
234   * For compatability.
235   */
236  private static final long serialVersionUID = 4774881970558875024L;
237
238  // This string is specified in the root of the CLDR.
239  private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZvcL";
240
241  /**  
242   * Represents the position of the RFC822 timezone pattern character
243   * in the array of localized pattern characters.  In the
244   * U.S. locale, this is 'Z'.  The value is the offset of the current
245   * time from GMT e.g. -0500 would be five hours prior to GMT.
246   */  
247  private static final int RFC822_TIMEZONE_FIELD = 23;
248
249  /**
250   * Reads the serialized version of this object.
251   * If the serialized data is only version 0,
252   * then the date for the start of the century
253   * for interpreting two digit years is computed.
254   * The pattern is parsed and compiled following the process
255   * of reading in the serialized data.
256   *
257   * @param stream the object stream to read the data from.
258   * @throws IOException if an I/O error occurs.
259   * @throws ClassNotFoundException if the class of the serialized data
260   *         could not be found.
261   * @throws InvalidObjectException if the pattern is invalid.
262   */ 
263  private void readObject(ObjectInputStream stream)
264    throws IOException, ClassNotFoundException
265  {
266    stream.defaultReadObject();
267    if (serialVersionOnStream < 1)
268      {
269        computeCenturyStart ();
270        serialVersionOnStream = 1;
271      }
272    else
273      // Ensure that defaultCentury gets set.
274      set2DigitYearStart(defaultCenturyStart);
275
276    // Set up items normally taken care of by the constructor.
277    tokens = new ArrayList<Object>();
278    try
279      {
280        compileFormat(pattern);
281      }
282    catch (IllegalArgumentException e)
283      {
284        throw new InvalidObjectException("The stream pattern was invalid.");
285      }
286  }
287
288  /**
289   * Compiles the supplied non-localized pattern into a form
290   * from which formatting and parsing can be performed.
291   * This also detects errors in the pattern, which will
292   * be raised on later use of the compiled data.
293   *
294   * @param pattern the non-localized pattern to compile.
295   * @throws IllegalArgumentException if the pattern is invalid.
296   */
297  private void compileFormat(String pattern) 
298  {
299    // Any alphabetical characters are treated as pattern characters
300    // unless enclosed in single quotes.
301
302    char thisChar;
303    int pos;
304    int field;
305    CompiledField current = null;
306
307    for (int i = 0; i < pattern.length(); i++)
308      {
309        thisChar = pattern.charAt(i);
310        field = standardChars.indexOf(thisChar);
311        if (field == -1)
312          {
313            current = null;
314            if ((thisChar >= 'A' && thisChar <= 'Z')
315                || (thisChar >= 'a' && thisChar <= 'z'))
316              {
317                // Not a valid letter
318                throw new IllegalArgumentException("Invalid letter "
319                                                   + thisChar +
320                                                   " encountered at character "
321                                                   + i + ".");
322              }
323            else if (thisChar == '\'')
324              {
325                // Quoted text section; skip to next single quote
326                pos = pattern.indexOf('\'', i + 1);
327                // First look for '' -- meaning a single quote.
328                if (pos == i + 1)
329                  tokens.add("'");
330                else
331                  {
332                    // Look for the terminating quote.  However, if we
333                    // see a '', that represents a literal quote and
334                    // we must iterate.
335                    CPStringBuilder buf = new CPStringBuilder();
336                    int oldPos = i + 1;
337                    do
338                      {
339                        if (pos == -1)
340                          throw new IllegalArgumentException("Quotes starting at character "
341                                                             + i +
342                                                             " not closed.");
343                        buf.append(pattern.substring(oldPos, pos));
344                        if (pos + 1 >= pattern.length()
345                            || pattern.charAt(pos + 1) != '\'')
346                          break;
347                        buf.append('\'');
348                        oldPos = pos + 2;
349                        pos = pattern.indexOf('\'', pos + 2);
350                      }
351                    while (true);
352                    tokens.add(buf.toString());
353                  }
354                i = pos;
355              }
356            else
357              {
358                // A special character
359                tokens.add(Character.valueOf(thisChar));
360              }
361          }
362        else
363          {
364            // A valid field
365            if ((current != null) && (field == current.field))
366              current.size++;
367            else
368              {
369                current = new CompiledField(field, 1, thisChar);
370                tokens.add(current);
371              }
372          }
373      }
374  }
375
376  /**
377   * Returns a string representation of this
378   * class.
379   *
380   * @return a string representation of the <code>SimpleDateFormat</code>
381   *         instance.
382   */
383  public String toString() 
384  {
385    CPStringBuilder output = new CPStringBuilder(getClass().getName());
386    output.append("[tokens=");
387    output.append(tokens);
388    output.append(", formatData=");
389    output.append(formatData);
390    output.append(", defaultCenturyStart=");
391    output.append(defaultCenturyStart);
392    output.append(", defaultCentury=");
393    output.append(defaultCentury);
394    output.append(", pattern=");
395    output.append(pattern);
396    output.append(", serialVersionOnStream=");
397    output.append(serialVersionOnStream);
398    output.append(", standardChars=");
399    output.append(standardChars);
400    output.append("]");
401    return output.toString();
402  }
403
404  /**
405   * Constructs a SimpleDateFormat using the default pattern for
406   * the default locale.
407   */
408  public SimpleDateFormat() 
409  {
410    /*
411     * There does not appear to be a standard API for determining 
412     * what the default pattern for a locale is, so use package-scope
413     * variables in DateFormatSymbols to encapsulate this.
414     */
415    super();
416    Locale locale = Locale.getDefault();
417    calendar = new GregorianCalendar(locale);
418    computeCenturyStart();
419    tokens = new ArrayList<Object>();
420    formatData = new DateFormatSymbols(locale);
421    pattern = (formatData.dateFormats[DEFAULT] + ' '
422               + formatData.timeFormats[DEFAULT]);
423    compileFormat(pattern);
424    numberFormat = NumberFormat.getInstance(locale);
425    numberFormat.setGroupingUsed (false);
426    numberFormat.setParseIntegerOnly (true);
427    numberFormat.setMaximumFractionDigits (0);
428  }
429  
430  /**
431   * Creates a date formatter using the specified non-localized pattern,
432   * with the default DateFormatSymbols for the default locale.
433   *
434   * @param pattern the pattern to use.
435   * @throws NullPointerException if the pattern is null.
436   * @throws IllegalArgumentException if the pattern is invalid.
437   */
438  public SimpleDateFormat(String pattern) 
439  {
440    this(pattern, Locale.getDefault());
441  }
442
443  /**
444   * Creates a date formatter using the specified non-localized pattern,
445   * with the default DateFormatSymbols for the given locale.
446   *
447   * @param pattern the non-localized pattern to use.
448   * @param locale the locale to use for the formatting symbols.
449   * @throws NullPointerException if the pattern is null.
450   * @throws IllegalArgumentException if the pattern is invalid.
451   */
452  public SimpleDateFormat(String pattern, Locale locale) 
453  {
454    super();
455    calendar = new GregorianCalendar(locale);
456    computeCenturyStart();
457    tokens = new ArrayList<Object>();
458    formatData = new DateFormatSymbols(locale);
459    compileFormat(pattern);
460    this.pattern = pattern;
461    numberFormat = NumberFormat.getInstance(locale);
462    numberFormat.setGroupingUsed (false);
463    numberFormat.setParseIntegerOnly (true);
464    numberFormat.setMaximumFractionDigits (0);
465  }
466
467  /**
468   * Creates a date formatter using the specified non-localized
469   * pattern. The specified DateFormatSymbols will be used when
470   * formatting.
471   *
472   * @param pattern the non-localized pattern to use.
473   * @param formatData the formatting symbols to use.
474   * @throws NullPointerException if the pattern or formatData is null.
475   * @throws IllegalArgumentException if the pattern is invalid.
476   */
477  public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
478  {
479    super();
480    calendar = new GregorianCalendar();
481    computeCenturyStart ();
482    tokens = new ArrayList<Object>();
483    if (formatData == null)
484      throw new NullPointerException("formatData");
485    this.formatData = formatData;
486    compileFormat(pattern);
487    this.pattern = pattern;
488    numberFormat = NumberFormat.getInstance();
489    numberFormat.setGroupingUsed (false);
490    numberFormat.setParseIntegerOnly (true);
491    numberFormat.setMaximumFractionDigits (0);
492  }
493
494  /**
495   * This method returns a string with the formatting pattern being used
496   * by this object.  This string is unlocalized.
497   *
498   * @return The format string.
499   */
500  public String toPattern()
501  {
502    return pattern;
503  }
504
505  /**
506   * This method returns a string with the formatting pattern being used
507   * by this object.  This string is localized.
508   *
509   * @return The format string.
510   */
511  public String toLocalizedPattern()
512  {
513    String localChars = formatData.getLocalPatternChars();
514    return translateLocalizedPattern(pattern, standardChars, localChars);
515  }
516
517  /**
518   * This method sets the formatting pattern that should be used by this
519   * object.  This string is not localized.
520   *
521   * @param pattern The new format pattern.
522   * @throws NullPointerException if the pattern is null.
523   * @throws IllegalArgumentException if the pattern is invalid.
524   */
525  public void applyPattern(String pattern)
526  {
527    tokens.clear();
528    compileFormat(pattern);
529    this.pattern = pattern;
530  }
531
532  /**
533   * This method sets the formatting pattern that should be used by this
534   * object.  This string is localized.
535   *
536   * @param pattern The new format pattern.
537   * @throws NullPointerException if the pattern is null.
538   * @throws IllegalArgumentException if the pattern is invalid.
539   */
540  public void applyLocalizedPattern(String pattern)
541  {
542    String localChars = formatData.getLocalPatternChars();
543    pattern = translateLocalizedPattern(pattern, localChars, standardChars);
544    applyPattern(pattern);
545  }
546
547  /**
548   * Translates either from or to a localized variant of the pattern
549   * string.  For example, in the German locale, 't' (for 'tag') is
550   * used instead of 'd' (for 'date').  This method translates
551   * a localized pattern (such as 'ttt') to a non-localized pattern
552   * (such as 'ddd'), or vice versa.  Non-localized patterns use
553   * a standard set of characters, which match those of the U.S. English
554   * locale.
555   *
556   * @param pattern the pattern to translate.
557   * @param oldChars the old set of characters (used in the pattern).
558   * @param newChars the new set of characters (which will be used in the
559   *                 pattern).
560   * @return a version of the pattern using the characters in
561   *         <code>newChars</code>.
562   */
563  private String translateLocalizedPattern(String pattern,
564                                           String oldChars, String newChars)
565  {
566    int len = pattern.length();
567    CPStringBuilder buf = new CPStringBuilder(len);
568    boolean quoted = false;
569    for (int i = 0;  i < len;  i++)
570      {
571        char ch = pattern.charAt(i);
572        if (ch == '\'')
573          quoted = ! quoted;
574        if (! quoted)
575          {
576            int j = oldChars.indexOf(ch);
577            if (j >= 0)
578              ch = newChars.charAt(j);
579          }
580        buf.append(ch);
581      }
582    return buf.toString();
583  }
584
585  /** 
586   * Returns the start of the century used for two digit years.
587   *
588   * @return A <code>Date</code> representing the start of the century
589   * for two digit years.
590   */
591  public Date get2DigitYearStart()
592  {
593    return defaultCenturyStart;
594  }
595
596  /**
597   * Sets the start of the century used for two digit years.
598   *
599   * @param date A <code>Date</code> representing the start of the century for
600   * two digit years.
601   */
602  public void set2DigitYearStart(Date date)
603  {
604    defaultCenturyStart = date;
605    calendar.clear();
606    calendar.setTime(date);
607    int year = calendar.get(Calendar.YEAR);
608    defaultCentury = year - (year % 100);
609  }
610
611  /**
612   * This method returns a copy of the format symbol information used
613   * for parsing and formatting dates.
614   *
615   * @return a copy of the date format symbols.
616   */
617  public DateFormatSymbols getDateFormatSymbols()
618  {
619    return (DateFormatSymbols) formatData.clone();
620  }
621
622  /**
623   * This method sets the format symbols information used for parsing
624   * and formatting dates.
625   *
626   * @param formatData The date format symbols.
627   * @throws NullPointerException if <code>formatData</code> is null.
628   */
629   public void setDateFormatSymbols(DateFormatSymbols formatData)
630   {
631     if (formatData == null)
632       {
633         throw new
634           NullPointerException("The supplied format data was null.");
635       }
636     this.formatData = formatData;
637   }
638
639  /**
640   * This methods tests whether the specified object is equal to this
641   * object.  This will be true if and only if the specified object:
642   * <p>
643   * <ul>
644   * <li>Is not <code>null</code>.</li>
645   * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
646   * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
647   *     level.</li>
648   * <li>Has the same formatting pattern.</li>
649   * <li>Is using the same formatting symbols.</li>
650   * <li>Is using the same century for two digit years.</li>
651   * </ul>
652   *
653   * @param o The object to compare for equality against.
654   *
655   * @return <code>true</code> if the specified object is equal to this object,
656   * <code>false</code> otherwise.
657   */
658  public boolean equals(Object o)
659  {
660    if (!super.equals(o))
661      return false;
662
663    if (!(o instanceof SimpleDateFormat))
664      return false;
665
666    SimpleDateFormat sdf = (SimpleDateFormat)o;
667
668    if (defaultCentury != sdf.defaultCentury)
669      return false;
670
671    if (!toPattern().equals(sdf.toPattern()))
672      return false;
673
674    if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
675      return false;
676
677    return true;
678  }
679
680  /**
681   * This method returns a hash value for this object.
682   *
683   * @return A hash value for this object.
684   */
685  public int hashCode()
686  {
687    return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
688      getDateFormatSymbols().hashCode();
689  }
690
691
692  /**
693   * Formats the date input according to the format string in use,
694   * appending to the specified StringBuffer.  The input StringBuffer
695   * is returned as output for convenience.
696   */
697  private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
698  {
699    String temp;
700    calendar.setTime(date);
701
702    // go through vector, filling in fields where applicable, else toString
703    Iterator<Object> iter = tokens.iterator();
704    while (iter.hasNext())
705      {
706        Object o = iter.next();
707        if (o instanceof CompiledField)
708          {
709            CompiledField cf = (CompiledField) o;
710            int beginIndex = buffer.length();
711            
712            switch (cf.getField())
713              {
714              case ERA_FIELD:
715                buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
716                break;
717              case YEAR_FIELD:
718                // If we have two digits, then we truncate.  Otherwise, we
719                // use the size of the pattern, and zero pad.
720                buffer.setDefaultAttribute (DateFormat.Field.YEAR);
721                if (cf.getSize() == 2)
722                  {
723                    temp = "00"+String.valueOf (calendar.get (Calendar.YEAR));
724                    buffer.append (temp.substring (temp.length() - 2));
725                  }
726                else
727                  withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
728                break;
729              case MONTH_FIELD:
730                buffer.setDefaultAttribute (DateFormat.Field.MONTH);
731                if (cf.getSize() < 3)
732                  withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer);
733                else if (cf.getSize() < 4)
734                  buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]);
735                else
736                  buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
737                break;
738              case DATE_FIELD:
739                buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
740                withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
741                break;
742              case HOUR_OF_DAY1_FIELD: // 1-24
743                buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1);
744                withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1, 
745                                   cf.getSize(), buffer);
746                break;
747              case HOUR_OF_DAY0_FIELD: // 0-23
748                buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0);
749                withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer);
750                break;
751              case MINUTE_FIELD:
752                buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
753                withLeadingZeros (calendar.get (Calendar.MINUTE),
754                                  cf.getSize(), buffer);
755                break;
756              case SECOND_FIELD:
757                buffer.setDefaultAttribute (DateFormat.Field.SECOND);
758                withLeadingZeros(calendar.get (Calendar.SECOND), 
759                                 cf.getSize(), buffer);
760                break;
761              case MILLISECOND_FIELD:
762                buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
763                withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
764                break;
765              case DAY_OF_WEEK_FIELD:
766                buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK);
767                if (cf.getSize() < 4)
768                  buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
769                else
770                  buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
771                break;
772              case DAY_OF_YEAR_FIELD:
773                buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR);
774                withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer);
775                break;
776              case DAY_OF_WEEK_IN_MONTH_FIELD:
777                buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH);
778                withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH), 
779                                 cf.getSize(), buffer);
780                break;
781              case WEEK_OF_YEAR_FIELD:
782                buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR);
783                withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR),
784                                  cf.getSize(), buffer);
785                break;
786              case WEEK_OF_MONTH_FIELD:
787                buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH);
788                withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH),
789                                  cf.getSize(), buffer);
790                break;
791              case AM_PM_FIELD:
792                buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
793                buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
794                break;
795              case HOUR1_FIELD: // 1-12
796                buffer.setDefaultAttribute (DateFormat.Field.HOUR1);
797                withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1,
798                                  cf.getSize(), buffer);
799                break;
800              case HOUR0_FIELD: // 0-11
801                buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
802                withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
803                break;
804              case TIMEZONE_FIELD:
805                buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE);
806                TimeZone zone = calendar.getTimeZone();
807                boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0;
808                // FIXME: XXX: This should be a localized time zone.
809                String zoneID = zone.getDisplayName
810                  (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT);
811                buffer.append (zoneID);
812                break;
813              case RFC822_TIMEZONE_FIELD:
814                buffer.setDefaultAttribute(DateFormat.Field.TIME_ZONE);
815                int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) +
816                                   calendar.get(Calendar.DST_OFFSET)) / (1000 * 60);
817                String sign = (pureMinutes < 0) ? "-" : "+";
818                pureMinutes = Math.abs(pureMinutes);
819                int hours = pureMinutes / 60;
820                int minutes = pureMinutes % 60;
821                buffer.append(sign);
822                withLeadingZeros(hours, 2, buffer);
823                withLeadingZeros(minutes, 2, buffer);
824                break;
825              default:
826                throw new IllegalArgumentException ("Illegal pattern character " +
827                                                    cf.getCharacter());
828              }
829            if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
830                                || cf.getField() == pos.getField()))
831              {
832                pos.setBeginIndex(beginIndex);
833                pos.setEndIndex(buffer.length());
834              }
835          } 
836      else
837        {  
838          buffer.append(o.toString(), null);
839        }
840      }
841  }
842  
843  public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
844  {
845    formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
846
847    return buffer;
848  }
849
850  public AttributedCharacterIterator formatToCharacterIterator(Object date)
851    throws IllegalArgumentException
852  {
853    if (date == null)
854      throw new NullPointerException("null argument");
855    if (!(date instanceof Date))
856      throw new IllegalArgumentException("argument should be an instance of java.util.Date");
857
858    AttributedFormatBuffer buf = new AttributedFormatBuffer();
859    formatWithAttribute((Date)date, buf,
860                        null);
861    buf.sync();
862        
863    return new FormatCharacterIterator(buf.getBuffer().toString(),
864                                       buf.getRanges(),
865                                       buf.getAttributes());
866  }
867
868  private void withLeadingZeros(int value, int length, FormatBuffer buffer) 
869  {
870    String valStr = String.valueOf(value);
871    for (length -= valStr.length(); length > 0; length--)
872      buffer.append('0');
873    buffer.append(valStr);
874  }
875
876  private boolean expect(String source, ParsePosition pos, char ch)
877  {
878    int x = pos.getIndex();
879    boolean r = x < source.length() && source.charAt(x) == ch;
880    if (r)
881      pos.setIndex(x + 1);
882    else
883      pos.setErrorIndex(x);
884    return r;
885  }
886
887  /**
888   * This method parses the specified string into a date.
889   * 
890   * @param dateStr The date string to parse.
891   * @param pos The input and output parse position
892   *
893   * @return The parsed date, or <code>null</code> if the string cannot be
894   * parsed.
895   */
896  public Date parse (String dateStr, ParsePosition pos)
897  {
898    int fmt_index = 0;
899    int fmt_max = pattern.length();
900
901    calendar.clear();
902    boolean saw_timezone = false;
903    int quote_start = -1;
904    boolean is2DigitYear = false;
905    try
906      {
907        for (; fmt_index < fmt_max; ++fmt_index)
908          {
909            char ch = pattern.charAt(fmt_index);
910            if (ch == '\'')
911              {
912                if (fmt_index < fmt_max - 1
913                    && pattern.charAt(fmt_index + 1) == '\'')
914                  {
915                    if (! expect (dateStr, pos, ch))
916                      return null;
917                    ++fmt_index;
918                  }
919                else
920                  quote_start = quote_start < 0 ? fmt_index : -1;
921                continue;
922              }
923            
924            if (quote_start != -1
925                || ((ch < 'a' || ch > 'z')
926                    && (ch < 'A' || ch > 'Z')))
927              {
928                if (quote_start == -1 && ch == ' ')
929                  {
930                    // A single unquoted space in the pattern may match
931                    // any number of spaces in the input.
932                    int index = pos.getIndex();
933                    int save = index;
934                    while (index < dateStr.length()
935                           && Character.isWhitespace(dateStr.charAt(index)))
936                      ++index;
937                    if (index > save)
938                      pos.setIndex(index);
939                    else
940                      {
941                        // Didn't see any whitespace.
942                        pos.setErrorIndex(index);
943                        return null;
944                      }
945                  }
946                else if (! expect (dateStr, pos, ch))
947                  return null;
948                continue;
949              }
950            
951            // We've arrived at a potential pattern character in the
952            // pattern.
953            int fmt_count = 1;
954            while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
955              {
956                ++fmt_count;
957              }
958            
959            // We might need to limit the number of digits to parse in
960            // some cases.  We look to the next pattern character to
961            // decide.
962            boolean limit_digits = false;
963            if (fmt_index < fmt_max
964                && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
965              limit_digits = true;
966            --fmt_index;
967            
968            // We can handle most fields automatically: most either are
969            // numeric or are looked up in a string vector.  In some cases
970            // we need an offset.  When numeric, `offset' is added to the
971            // resulting value.  When doing a string lookup, offset is the
972            // initial index into the string array.
973            int calendar_field;
974            boolean is_numeric = true;
975            int offset = 0;
976            boolean maybe2DigitYear = false;
977            boolean oneBasedHour = false;
978            boolean oneBasedHourOfDay = false;
979            Integer simpleOffset;
980            String[] set1 = null;
981            String[] set2 = null;
982            switch (ch)
983              {
984              case 'd':
985                calendar_field = Calendar.DATE;
986                break;
987              case 'D':
988                calendar_field = Calendar.DAY_OF_YEAR;
989                break;
990              case 'F':
991                calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
992                break;
993              case 'E':
994                is_numeric = false;
995                offset = 1;
996                calendar_field = Calendar.DAY_OF_WEEK;
997                set1 = formatData.getWeekdays();
998                set2 = formatData.getShortWeekdays();
999                break;
1000              case 'w':
1001                calendar_field = Calendar.WEEK_OF_YEAR;
1002                break;
1003              case 'W':
1004                calendar_field = Calendar.WEEK_OF_MONTH;
1005                break;
1006              case 'M':
1007                calendar_field = Calendar.MONTH;
1008                if (fmt_count <= 2)
1009                  offset = -1;
1010                else
1011                  {
1012                    is_numeric = false;
1013                    set1 = formatData.getMonths();
1014                    set2 = formatData.getShortMonths();
1015                  }
1016                break;
1017              case 'y':
1018                calendar_field = Calendar.YEAR;
1019                if (fmt_count <= 2)
1020                  maybe2DigitYear = true;
1021                break;
1022              case 'K':
1023                calendar_field = Calendar.HOUR;
1024                break;
1025              case 'h':
1026                calendar_field = Calendar.HOUR;
1027                oneBasedHour = true;
1028                break;
1029              case 'H':
1030                calendar_field = Calendar.HOUR_OF_DAY;
1031                break;
1032              case 'k':
1033                calendar_field = Calendar.HOUR_OF_DAY;
1034                oneBasedHourOfDay = true;
1035                break;
1036              case 'm':
1037                calendar_field = Calendar.MINUTE;
1038                break;
1039              case 's':
1040                calendar_field = Calendar.SECOND;
1041                break;
1042              case 'S':
1043                calendar_field = Calendar.MILLISECOND;
1044                break;
1045              case 'a':
1046                is_numeric = false;
1047                calendar_field = Calendar.AM_PM;
1048                set1 = formatData.getAmPmStrings();
1049                break;
1050              case 'z':
1051              case 'Z':
1052                // We need a special case for the timezone, because it
1053                // uses a different data structure than the other cases.
1054                is_numeric = false;
1055                calendar_field = Calendar.ZONE_OFFSET;
1056                String[][] zoneStrings = formatData.getZoneStrings();
1057                int zoneCount = zoneStrings.length;
1058                int index = pos.getIndex();
1059                boolean found_zone = false;
1060                simpleOffset = computeOffset(dateStr.substring(index), pos);
1061                if (simpleOffset != null)
1062                  {
1063                    found_zone = true;
1064                    saw_timezone = true;
1065                    calendar.set(Calendar.DST_OFFSET, 0);
1066                    offset = simpleOffset.intValue();
1067                  }
1068                else
1069                  {
1070                    for (int j = 0;  j < zoneCount;  j++)
1071                      {
1072                        String[] strings = zoneStrings[j];
1073                        int k;
1074                        for (k = 0; k < strings.length; ++k)
1075                          {
1076                            if (dateStr.startsWith(strings[k], index))
1077                              break;
1078                          }
1079                        if (k != strings.length)
1080                          {
1081                            found_zone = true;
1082                            saw_timezone = true;
1083                            TimeZone tz = TimeZone.getTimeZone (strings[0]);
1084                            // Check if it's a DST zone or ordinary 
1085                            if(k == 3 || k == 4)
1086                              calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1087                            else
1088                              calendar.set (Calendar.DST_OFFSET, 0);
1089                            offset = tz.getRawOffset ();
1090                            pos.setIndex(index + strings[k].length());
1091                            break;
1092                          }
1093                      }
1094                  }
1095                if (! found_zone)
1096                  {
1097                        pos.setErrorIndex(pos.getIndex());
1098                        return null;
1099                  }
1100                break;
1101              default:
1102                pos.setErrorIndex(pos.getIndex());
1103                return null;
1104              }
1105      
1106            // Compute the value we should assign to the field.
1107            int value;
1108            int index = -1;
1109            if (is_numeric)
1110              {
1111                numberFormat.setMinimumIntegerDigits(fmt_count);
1112                if (maybe2DigitYear)
1113                  index = pos.getIndex();
1114                Number n = null;
1115                if (limit_digits)
1116                  {
1117                    // numberFormat.setMaximumIntegerDigits(fmt_count) may
1118                    // not work as expected. So we explicitly use substring
1119                    // of dateStr.
1120                    int origPos = pos.getIndex();
1121                    pos.setIndex(0);
1122                    n = numberFormat.parse(dateStr.substring(origPos, origPos + fmt_count), pos);
1123                    pos.setIndex(origPos + pos.getIndex());
1124                  }
1125                else
1126                  n = numberFormat.parse(dateStr, pos);
1127                if (pos == null || ! (n instanceof Long))
1128                  return null;
1129                value = n.intValue() + offset;
1130              }
1131            else if (set1 != null)
1132              {
1133                index = pos.getIndex();
1134                int i;
1135                boolean found = false;
1136                for (i = offset; i < set1.length; ++i)
1137                  {
1138                    if (set1[i] != null)
1139                      if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1140                                                           index))
1141                        {
1142                          found = true;
1143                          pos.setIndex(index + set1[i].length());
1144                          break;
1145                        }
1146                  }
1147                if (!found && set2 != null)
1148                  {
1149                    for (i = offset; i < set2.length; ++i)
1150                      {
1151                        if (set2[i] != null)
1152                          if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1153                                                               index))
1154                            {
1155                              found = true;
1156                              pos.setIndex(index + set2[i].length());
1157                              break;
1158                            }
1159                      }
1160                  }
1161                if (!found)
1162                  {
1163                    pos.setErrorIndex(index);
1164                    return null;
1165                  }
1166                value = i;
1167              }
1168            else
1169              value = offset;
1170          
1171            if (maybe2DigitYear)
1172              {
1173                // Parse into default century if the numeric year string has 
1174                // exactly 2 digits.
1175                int digit_count = pos.getIndex() - index;
1176                if (digit_count == 2)
1177                  {
1178                    is2DigitYear = true;
1179                    value += defaultCentury;
1180                  }
1181              }
1182            
1183            // Calendar uses 0-based hours. 
1184            // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1185            if (oneBasedHour && value == 12)
1186              value = 0;
1187
1188            if (oneBasedHourOfDay && value == 24)
1189              value = 0;
1190            
1191            // Assign the value and move on.
1192            calendar.set(calendar_field, value);
1193          }
1194    
1195        if (is2DigitYear)
1196          {
1197            // Apply the 80-20 heuristic to dermine the full year based on 
1198            // defaultCenturyStart. 
1199            int year = calendar.get(Calendar.YEAR);
1200            if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1201              calendar.set(Calendar.YEAR, year + 100);      
1202          }
1203        if (! saw_timezone)
1204          {
1205            // Use the real rules to determine whether or not this
1206            // particular time is in daylight savings.
1207            calendar.clear (Calendar.DST_OFFSET);
1208            calendar.clear (Calendar.ZONE_OFFSET);
1209          }
1210        return calendar.getTime();
1211      }
1212    catch (IllegalArgumentException x)
1213      {
1214        pos.setErrorIndex(pos.getIndex());
1215        return null;
1216      }
1217      }
1218
1219  /**
1220   * <p>
1221   * Computes the time zone offset in milliseconds
1222   * relative to GMT, based on the supplied
1223   * <code>String</code> representation.
1224   * </p>
1225   * <p>
1226   * The supplied <code>String</code> must be a three
1227   * or four digit signed number, with an optional 'GMT'
1228   * prefix.  The first one or two digits represents the hours,
1229   * while the last two represent the minutes.  The
1230   * two sets of digits can optionally be separated by
1231   * ':'.  The mandatory sign prefix (either '+' or '-')
1232   * indicates the direction of the offset from GMT.
1233   * </p>
1234   * <p>
1235   * For example, 'GMT+0200' specifies 2 hours after
1236   * GMT, while '-05:00' specifies 5 hours prior to
1237   * GMT.  The special case of 'GMT' alone can be used
1238   * to represent the offset, 0.
1239   * </p>
1240   * <p>
1241   * If the <code>String</code> can not be parsed,
1242   * the result will be null.  The resulting offset
1243   * is wrapped in an <code>Integer</code> object, in
1244   * order to allow such failure to be represented.
1245   * </p>
1246   *
1247   * @param zoneString a string in the form 
1248   *        (GMT)? sign hours : minutes
1249   *        where sign = '+' or '-', hours
1250   *        is a one or two digits representing
1251   *        a number between 0 and 23, and
1252   *        minutes is two digits representing
1253   *        a number between 0 and 59.
1254   * @return the parsed offset, or null if parsing
1255   *         failed.
1256   */
1257  private Integer computeOffset(String zoneString, ParsePosition pos)
1258  {
1259    Pattern pattern = 
1260      Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1261    Matcher matcher = pattern.matcher(zoneString);
1262
1263    // Match from start, but ignore trailing parts
1264    boolean hasAll = matcher.lookingAt();
1265    try
1266      {
1267        // Do we have at least the sign, hour and minute?
1268        matcher.group(2);
1269        matcher.group(4);
1270        matcher.group(5);
1271      }
1272    catch (IllegalStateException ise)
1273      {
1274        hasAll = false;
1275      }
1276    if (hasAll)
1277      {
1278        int sign = matcher.group(2).equals("+") ? 1 : -1;
1279        int hour = Integer.parseInt(matcher.group(4));
1280        if (!matcher.group(3).equals(""))
1281          hour += (Integer.parseInt(matcher.group(3)) * 10);
1282        int minutes = Integer.parseInt(matcher.group(5));
1283
1284        if (hour > 23)
1285          return null;
1286        int offset = sign * ((hour * 60) + minutes) * 60000;
1287
1288        // advance the index
1289        pos.setIndex(pos.getIndex() + matcher.end());
1290        return Integer.valueOf(offset);
1291      }
1292    else if (zoneString.startsWith("GMT"))
1293      {
1294        pos.setIndex(pos.getIndex() + 3);
1295        return Integer.valueOf(0);
1296      }
1297    return null;
1298  }
1299
1300  // Compute the start of the current century as defined by
1301  // get2DigitYearStart.
1302  private void computeCenturyStart()
1303  {
1304    int year = calendar.get(Calendar.YEAR);
1305    calendar.set(Calendar.YEAR, year - 80);
1306    set2DigitYearStart(calendar.getTime());
1307  }
1308
1309  /**
1310   * Returns a copy of this instance of
1311   * <code>SimpleDateFormat</code>.  The copy contains
1312   * clones of the formatting symbols and the 2-digit
1313   * year century start date.
1314   */
1315  public Object clone()
1316  {
1317    SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1318    clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1319    clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1320    return clone;
1321  }
1322
1323}