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.lang.CPStringBuilder;
044    
045    import gnu.java.text.AttributedFormatBuffer;
046    import gnu.java.text.FormatBuffer;
047    import gnu.java.text.FormatCharacterIterator;
048    import gnu.java.text.StringFormatBuffer;
049    
050    import java.io.IOException;
051    import java.io.InvalidObjectException;
052    import java.io.ObjectInputStream;
053    import java.util.ArrayList;
054    import java.util.Calendar;
055    import java.util.Date;
056    import java.util.GregorianCalendar;
057    import java.util.Iterator;
058    import java.util.Locale;
059    import java.util.TimeZone;
060    import java.util.regex.Matcher;
061    import 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     */
069    public 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    }