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    
006    This file is part of GNU Classpath.
007    
008    GNU Classpath is free software; you can redistribute it and/or modify
009    it under the terms of the GNU General Public License as published by
010    the Free Software Foundation; either version 2, or (at your option)
011    any later version.
012     
013    GNU Classpath is distributed in the hope that it will be useful, but
014    WITHOUT ANY WARRANTY; without even the implied warranty of
015    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
016    General Public License for more details.
017    
018    You should have received a copy of the GNU General Public License
019    along with GNU Classpath; see the file COPYING.  If not, write to the
020    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
021    02110-1301 USA.
022    
023    Linking this library statically or dynamically with other modules is
024    making a combined work based on this library.  Thus, the terms and
025    conditions of the GNU General Public License cover the whole
026    combination.
027    
028    As a special exception, the copyright holders of this library give you
029    permission to link this library with independent modules to produce an
030    executable, regardless of the license terms of these independent
031    modules, and to copy and distribute the resulting executable under
032    terms of your choice, provided that you also meet, for each linked
033    independent module, the terms and conditions of the license of that
034    module.  An independent module is a module which is not derived from
035    or based on this library.  If you modify this library, you may extend
036    this exception to your version of the library, but you are not
037    obligated to do so.  If you do not wish to do so, delete this
038    exception statement from your version. */
039    
040    
041    package java.text;
042    
043    import gnu.java.text.AttributedFormatBuffer;
044    import gnu.java.text.FormatBuffer;
045    import gnu.java.text.FormatCharacterIterator;
046    import gnu.java.text.StringFormatBuffer;
047    
048    import java.io.IOException;
049    import java.io.InvalidObjectException;
050    import java.io.ObjectInputStream;
051    import java.util.ArrayList;
052    import java.util.Calendar;
053    import java.util.Date;
054    import java.util.GregorianCalendar;
055    import java.util.Iterator;
056    import java.util.Locale;
057    import java.util.TimeZone;
058    import java.util.regex.Matcher;
059    import java.util.regex.Pattern;
060    
061    /**
062     * SimpleDateFormat provides convenient methods for parsing and formatting
063     * dates using Gregorian calendars (see java.util.GregorianCalendar). 
064     */
065    public class SimpleDateFormat extends DateFormat 
066    {
067      /** 
068       * This class is used by <code>SimpleDateFormat</code> as a
069       * compiled representation of a format string.  The field
070       * ID, size, and character used are stored for each sequence
071       * of pattern characters.
072       */
073      private class CompiledField
074      {
075        /**
076         * The ID of the field within the local pattern characters.
077         * Package private for use in out class.
078         */
079        int field;
080    
081        /**
082         * The size of the character sequence.
083         * Package private for use in out class.
084         */
085        int size;
086    
087        /**
088         * The character used.
089         */
090        private char character;
091    
092        /** 
093         * Constructs a compiled field using the
094         * the given field ID, size and character
095         * values.
096         *
097         * @param f the field ID.
098         * @param s the size of the field.
099         * @param c the character used.
100         */
101        public CompiledField(int f, int s, char c)
102        {
103          field = f;
104          size = s;
105          character = c;
106        }
107    
108        /**
109         * Retrieves the ID of the field relative to
110         * the local pattern characters.
111         */
112        public int getField()
113        {
114          return field;
115        }
116    
117        /**
118         * Retrieves the size of the character sequence.
119         */
120        public int getSize()
121        {
122          return size;
123        }
124    
125        /**
126         * Retrieves the character used in the sequence.
127         */
128        public char getCharacter()
129        {
130          return character;
131        }
132    
133        /**
134         * Returns a <code>String</code> representation
135         * of the compiled field, primarily for debugging
136         * purposes.
137         *
138         * @return a <code>String</code> representation.
139         */
140        public String toString()
141        {
142          StringBuffer builder;
143    
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("]");
152    
153          return builder.toString();
154        }
155      }
156    
157      /**
158       * A list of <code>CompiledField</code>s,
159       * representing the compiled version of the pattern.
160       *
161       * @see CompiledField
162       * @serial Ignored.
163       */
164      private transient ArrayList tokens;
165    
166      /**
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.
170       *
171       * @see DateFormatSymbols
172       * @serial The localisation data.  May not be null.
173       */
174      private DateFormatSymbols formatData;
175    
176      /**
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.
182       *
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.
188       */
189      private Date defaultCenturyStart;
190    
191      /**
192       * The year at which interpretation of two
193       * digit years starts.
194       *
195       * @see #get2DigitYearStart()
196       * @see #set2DigitYearStart(java.util.Date)
197       * @serial Ignored.
198       */
199      private transient int defaultCentury;
200    
201      /**
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.
206       *
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.
212       */
213      private String pattern;
214    
215      /**
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.
220       *
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.
226       */
227      private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
228    
229      /**
230       * For compatability.
231       */
232      private static final long serialVersionUID = 4774881970558875024L;
233    
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";
238    
239      /**
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.
246       *
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.
252       */ 
253      private void readObject(ObjectInputStream stream)
254        throws IOException, ClassNotFoundException
255      {
256        stream.defaultReadObject();
257        if (serialVersionOnStream < 1)
258          {
259            computeCenturyStart ();
260            serialVersionOnStream = 1;
261          }
262        else
263          // Ensure that defaultCentury gets set.
264          set2DigitYearStart(defaultCenturyStart);
265    
266        // Set up items normally taken care of by the constructor.
267        tokens = new ArrayList();
268        try
269          {
270            compileFormat(pattern);
271          }
272        catch (IllegalArgumentException e)
273          {
274            throw new InvalidObjectException("The stream pattern was invalid.");
275          }
276      }
277    
278      /**
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.
283       *
284       * @param pattern the non-localized pattern to compile.
285       * @throws IllegalArgumentException if the pattern is invalid.
286       */
287      private void compileFormat(String pattern) 
288      {
289        // Any alphabetical characters are treated as pattern characters
290        // unless enclosed in single quotes.
291    
292        char thisChar;
293        int pos;
294        int field;
295        CompiledField current = null;
296    
297        for (int i = 0; i < pattern.length(); i++)
298          {
299            thisChar = pattern.charAt(i);
300            field = standardChars.indexOf(thisChar);
301            if (field == -1)
302              {
303                current = null;
304                if ((thisChar >= 'A' && thisChar <= 'Z')
305                    || (thisChar >= 'a' && thisChar <= 'z'))
306                  {
307                    // Not a valid letter
308                    throw new IllegalArgumentException("Invalid letter "
309                                                       + thisChar +
310                                                       " encountered at character "
311                                                       + i + ".");
312                  }
313                else if (thisChar == '\'')
314                  {
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
321                      {
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;
327                        do
328                          {
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);
340                          }
341                        while (true);
342                        tokens.add(buf.toString());
343                      }
344                    i = pos;
345                  }
346                else
347                  {
348                    // A special character
349                    tokens.add(new Character(thisChar));
350                  }
351              }
352            else
353              {
354                // A valid field
355                if ((current != null) && (field == current.field))
356                  current.size++;
357                else
358                  {
359                    current = new CompiledField(field, 1, thisChar);
360                    tokens.add(current);
361                  }
362              }
363          }
364      }
365    
366      /**
367       * Returns a string representation of this
368       * class.
369       *
370       * @return a string representation of the <code>SimpleDateFormat</code>
371       *         instance.
372       */
373      public String toString() 
374      {
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();
392      }
393    
394      /**
395       * Constructs a SimpleDateFormat using the default pattern for
396       * the default locale.
397       */
398      public SimpleDateFormat() 
399      {
400        /*
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.
404         */
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);
418      }
419      
420      /**
421       * Creates a date formatter using the specified non-localized pattern,
422       * with the default DateFormatSymbols for the default locale.
423       *
424       * @param pattern the pattern to use.
425       * @throws NullPointerException if the pattern is null.
426       * @throws IllegalArgumentException if the pattern is invalid.
427       */
428      public SimpleDateFormat(String pattern) 
429      {
430        this(pattern, Locale.getDefault());
431      }
432    
433      /**
434       * Creates a date formatter using the specified non-localized pattern,
435       * with the default DateFormatSymbols for the given locale.
436       *
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.
441       */
442      public SimpleDateFormat(String pattern, Locale locale) 
443      {
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);
455      }
456    
457      /**
458       * Creates a date formatter using the specified non-localized
459       * pattern. The specified DateFormatSymbols will be used when
460       * formatting.
461       *
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.
466       */
467      public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
468      {
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);
482      }
483    
484      /**
485       * This method returns a string with the formatting pattern being used
486       * by this object.  This string is unlocalized.
487       *
488       * @return The format string.
489       */
490      public String toPattern()
491      {
492        return pattern;
493      }
494    
495      /**
496       * This method returns a string with the formatting pattern being used
497       * by this object.  This string is localized.
498       *
499       * @return The format string.
500       */
501      public String toLocalizedPattern()
502      {
503        String localChars = formatData.getLocalPatternChars();
504        return translateLocalizedPattern(pattern, standardChars, localChars);
505      }
506    
507      /**
508       * This method sets the formatting pattern that should be used by this
509       * object.  This string is not localized.
510       *
511       * @param pattern The new format pattern.
512       * @throws NullPointerException if the pattern is null.
513       * @throws IllegalArgumentException if the pattern is invalid.
514       */
515      public void applyPattern(String pattern)
516      {
517        tokens = new ArrayList();
518        compileFormat(pattern);
519        this.pattern = pattern;
520      }
521    
522      /**
523       * This method sets the formatting pattern that should be used by this
524       * object.  This string is localized.
525       *
526       * @param pattern The new format pattern.
527       * @throws NullPointerException if the pattern is null.
528       * @throws IllegalArgumentException if the pattern is invalid.
529       */
530      public void applyLocalizedPattern(String pattern)
531      {
532        String localChars = formatData.getLocalPatternChars();
533        pattern = translateLocalizedPattern(pattern, localChars, standardChars);
534        applyPattern(pattern);
535      }
536    
537      /**
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.
545       *
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>.
552       */
553      private String translateLocalizedPattern(String pattern,
554                                               String oldChars, String newChars)
555      {
556        int len = pattern.length();
557        StringBuffer buf = new StringBuffer(len);
558        boolean quoted = false;
559        for (int i = 0;  i < len;  i++)
560          {
561            char ch = pattern.charAt(i);
562            if (ch == '\'')
563              quoted = ! quoted;
564            if (! quoted)
565              {
566                int j = oldChars.indexOf(ch);
567                if (j >= 0)
568                  ch = newChars.charAt(j);
569              }
570            buf.append(ch);
571          }
572        return buf.toString();
573      }
574    
575      /** 
576       * Returns the start of the century used for two digit years.
577       *
578       * @return A <code>Date</code> representing the start of the century
579       * for two digit years.
580       */
581      public Date get2DigitYearStart()
582      {
583        return defaultCenturyStart;
584      }
585    
586      /**
587       * Sets the start of the century used for two digit years.
588       *
589       * @param date A <code>Date</code> representing the start of the century for
590       * two digit years.
591       */
592      public void set2DigitYearStart(Date date)
593      {
594        defaultCenturyStart = date;
595        calendar.clear();
596        calendar.setTime(date);
597        int year = calendar.get(Calendar.YEAR);
598        defaultCentury = year - (year % 100);
599      }
600    
601      /**
602       * This method returns a copy of the format symbol information used
603       * for parsing and formatting dates.
604       *
605       * @return a copy of the date format symbols.
606       */
607      public DateFormatSymbols getDateFormatSymbols()
608      {
609        return (DateFormatSymbols) formatData.clone();
610      }
611    
612      /**
613       * This method sets the format symbols information used for parsing
614       * and formatting dates.
615       *
616       * @param formatData The date format symbols.
617       * @throws NullPointerException if <code>formatData</code> is null.
618       */
619       public void setDateFormatSymbols(DateFormatSymbols formatData)
620       {
621         if (formatData == null)
622           {
623             throw new
624               NullPointerException("The supplied format data was null.");
625           }
626         this.formatData = formatData;
627       }
628    
629      /**
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>
642       *
643       * @param o The object to compare for equality against.
644       *
645       * @return <code>true</code> if the specified object is equal to this object,
646       * <code>false</code> otherwise.
647       */
648      public boolean equals(Object o)
649      {
650        if (!super.equals(o))
651          return false;
652    
653        if (!(o instanceof SimpleDateFormat))
654          return false;
655    
656        SimpleDateFormat sdf = (SimpleDateFormat)o;
657    
658        if (defaultCentury != sdf.defaultCentury)
659          return false;
660    
661        if (!toPattern().equals(sdf.toPattern()))
662          return false;
663    
664        if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
665          return false;
666    
667        return true;
668      }
669    
670      /**
671       * This method returns a hash value for this object.
672       *
673       * @return A hash value for this object.
674       */
675      public int hashCode()
676      {
677        return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
678          getDateFormatSymbols().hashCode();
679      }
680    
681    
682      /**
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.
686       */
687      private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
688      {
689        String temp;
690        AttributedCharacterIterator.Attribute attribute;
691        calendar.setTime(date);
692    
693        // go through vector, filling in fields where applicable, else toString
694        Iterator iter = tokens.iterator();
695        while (iter.hasNext())
696          {
697            Object o = iter.next();
698            if (o instanceof CompiledField)
699              {
700                CompiledField cf = (CompiledField) o;
701                int beginIndex = buffer.length();
702                
703                switch (cf.getField())
704                  {
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)
713                      {
714                        temp = "00"+String.valueOf (calendar.get (Calendar.YEAR));
715                        buffer.append (temp.substring (temp.length() - 2));
716                      }
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());
819                  }
820                if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
821                                    || cf.getField() == pos.getField()))
822                  {
823                    pos.setBeginIndex(beginIndex);
824                    pos.setEndIndex(buffer.length());
825                  }
826              } 
827          else
828            {  
829              buffer.append(o.toString(), null);
830            }
831          }
832      }
833      
834      public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
835      {
836        formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
837    
838        return buffer;
839      }
840    
841      public AttributedCharacterIterator formatToCharacterIterator(Object date)
842        throws IllegalArgumentException
843      {
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");
848    
849        AttributedFormatBuffer buf = new AttributedFormatBuffer();
850        formatWithAttribute((Date)date, buf,
851                            null);
852        buf.sync();
853            
854        return new FormatCharacterIterator(buf.getBuffer().toString(),
855                                           buf.getRanges(),
856                                           buf.getAttributes());
857      }
858    
859      private void withLeadingZeros(int value, int length, FormatBuffer buffer) 
860      {
861        String valStr = String.valueOf(value);
862        for (length -= valStr.length(); length > 0; length--)
863          buffer.append('0');
864        buffer.append(valStr);
865      }
866    
867      private boolean expect(String source, ParsePosition pos, char ch)
868      {
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;
876      }
877    
878      /**
879       * This method parses the specified string into a date.
880       * 
881       * @param dateStr The date string to parse.
882       * @param pos The input and output parse position
883       *
884       * @return The parsed date, or <code>null</code> if the string cannot be
885       * parsed.
886       */
887      public Date parse (String dateStr, ParsePosition pos)
888      {
889        int fmt_index = 0;
890        int fmt_max = pattern.length();
891    
892        calendar.clear();
893        boolean saw_timezone = false;
894        int quote_start = -1;
895        boolean is2DigitYear = false;
896        try
897          {
898            for (; fmt_index < fmt_max; ++fmt_index)
899              {
900                char ch = pattern.charAt(fmt_index);
901                if (ch == '\'')
902                  {
903                    int index = pos.getIndex();
904                    if (fmt_index < fmt_max - 1
905                        && pattern.charAt(fmt_index + 1) == '\'')
906                      {
907                        if (! expect (dateStr, pos, ch))
908                          return null;
909                        ++fmt_index;
910                      }
911                    else
912                      quote_start = quote_start < 0 ? fmt_index : -1;
913                    continue;
914                  }
915                
916                if (quote_start != -1
917                    || ((ch < 'a' || ch > 'z')
918                        && (ch < 'A' || ch > 'Z')))
919                  {
920                    if (quote_start == -1 && ch == ' ')
921                      {
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
932                          {
933                            // Didn't see any whitespace.
934                            pos.setErrorIndex(index);
935                            return null;
936                          }
937                      }
938                    else if (! expect (dateStr, pos, ch))
939                      return null;
940                    continue;
941                  }
942                
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)
947                  {
948                    ++fmt_count;
949                  }
950                
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;
959                
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)
975                  {
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
1003                      {
1004                        is_numeric = false;
1005                        set1 = formatData.getMonths();
1006                        set2 = formatData.getShortMonths();
1007                      }
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)
1054                      {
1055                        found_zone = true;
1056                        saw_timezone = true;
1057                        calendar.set(Calendar.DST_OFFSET, 0);
1058                        offset = simpleOffset.intValue();
1059                      }
1060                    else
1061                      {
1062                        for (int j = 0;  j < zoneCount;  j++)
1063                          {
1064                            String[] strings = zoneStrings[j];
1065                            int k;
1066                            for (k = 0; k < strings.length; ++k)
1067                              {
1068                                if (dateStr.startsWith(strings[k], index))
1069                                  break;
1070                              }
1071                            if (k != strings.length)
1072                              {
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;
1084                              }
1085                          }
1086                      }
1087                    if (! found_zone)
1088                      {
1089                            pos.setErrorIndex(pos.getIndex());
1090                            return null;
1091                      }
1092                    break;
1093                  default:
1094                    pos.setErrorIndex(pos.getIndex());
1095                    return null;
1096                  }
1097          
1098                // Compute the value we should assign to the field.
1099                int value;
1100                int index = -1;
1101                if (is_numeric)
1102                  {
1103                    numberFormat.setMinimumIntegerDigits(fmt_count);
1104                    if (maybe2DigitYear)
1105                      index = pos.getIndex();
1106                    Number n = null;
1107                    if (limit_digits)
1108                      {
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());
1116                      }
1117                    else
1118                      n = numberFormat.parse(dateStr, pos);
1119                    if (pos == null || ! (n instanceof Long))
1120                      return null;
1121                    value = n.intValue() + offset;
1122                  }
1123                else if (set1 != null)
1124                  {
1125                    index = pos.getIndex();
1126                    int i;
1127                    boolean found = false;
1128                    for (i = offset; i < set1.length; ++i)
1129                      {
1130                        if (set1[i] != null)
1131                          if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1132                                                               index))
1133                            {
1134                              found = true;
1135                              pos.setIndex(index + set1[i].length());
1136                              break;
1137                            }
1138                      }
1139                    if (!found && set2 != null)
1140                      {
1141                        for (i = offset; i < set2.length; ++i)
1142                          {
1143                            if (set2[i] != null)
1144                              if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1145                                                                   index))
1146                                {
1147                                  found = true;
1148                                  pos.setIndex(index + set2[i].length());
1149                                  break;
1150                                }
1151                          }
1152                      }
1153                    if (!found)
1154                      {
1155                        pos.setErrorIndex(index);
1156                        return null;
1157                      }
1158                    value = i;
1159                  }
1160                else
1161                  value = offset;
1162              
1163                if (maybe2DigitYear)
1164                  {
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)
1169                      {
1170                        is2DigitYear = true;
1171                        value += defaultCentury;
1172                      }
1173                  }
1174                
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;
1179    
1180                if (oneBasedHourOfDay && value == 24)
1181                  value = 0;
1182                
1183                // Assign the value and move on.
1184                calendar.set(calendar_field, value);
1185              }
1186        
1187            if (is2DigitYear)
1188              {
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);      
1194              }
1195            if (! saw_timezone)
1196              {
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);
1201              }
1202            return calendar.getTime();
1203          }
1204        catch (IllegalArgumentException x)
1205          {
1206            pos.setErrorIndex(pos.getIndex());
1207            return null;
1208          }
1209          }
1210    
1211      /**
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>
1238       *
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.
1248       */
1249      private Integer computeOffset(String zoneString, ParsePosition pos)
1250      {
1251        Pattern pattern = 
1252          Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1253        Matcher matcher = pattern.matcher(zoneString);
1254    
1255        // Match from start, but ignore trailing parts
1256        boolean hasAll = matcher.lookingAt();
1257        try
1258          {
1259            // Do we have at least the sign, hour and minute?
1260            matcher.group(2);
1261            matcher.group(4);
1262            matcher.group(5);
1263          }
1264        catch (IllegalStateException ise)
1265          {
1266            hasAll = false;
1267          }
1268        if (hasAll)
1269          {
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));
1275    
1276            if (hour > 23)
1277              return null;
1278            int offset = sign * ((hour * 60) + minutes) * 60000;
1279    
1280            // advance the index
1281            pos.setIndex(pos.getIndex() + matcher.end());
1282            return new Integer(offset);
1283          }
1284        else if (zoneString.startsWith("GMT"))
1285          {
1286            pos.setIndex(pos.getIndex() + 3);
1287            return new Integer(0);
1288          }
1289        return null;
1290      }
1291    
1292      // Compute the start of the current century as defined by
1293      // get2DigitYearStart.
1294      private void computeCenturyStart()
1295      {
1296        int year = calendar.get(Calendar.YEAR);
1297        calendar.set(Calendar.YEAR, year - 80);
1298        set2DigitYearStart(calendar.getTime());
1299      }
1300    
1301      /**
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.
1306       */
1307      public Object clone()
1308      {
1309        SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1310        clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1311        clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1312        return clone;
1313      }
1314    
1315    }