/* SimpleDateFormat.java -- A class for parsing/formating simple 
   date constructs
   Copyright (C) 1998, 1999, 2000 Free Software Foundation, Inc.

This file is part of GNU Classpath.

GNU Classpath is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
 
GNU Classpath is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with GNU Classpath; see the file COPYING.  If not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
02111-1307 USA.

As a special exception, if you link this library with other files to
produce an executable, this library does not by itself cause the
resulting executable to be covered by the GNU General Public License.
This exception does not however invalidate any other reasons why the
executable file might be covered by the GNU General Public License. */


package java.text;

import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.SimpleTimeZone;
import java.util.Vector;
import java.io.ObjectInputStream;
import java.io.IOException;

/**
 * SimpleDateFormat provides convenient methods for parsing and formatting
 * dates using Gregorian calendars (see java.util.GregorianCalendar). 
 */
public class SimpleDateFormat extends DateFormat 
{
  /** A pair class used by SimpleDateFormat as a compiled representation
   *  of a format string.
   */
  private class FieldSizePair 
  {
    public int field;
    public int size;

    /** Constructs a pair with the given field and size values */
    public FieldSizePair(int f, int s) {
      field = f;
      size = s;
    }
  }

  private transient Vector tokens;
  private DateFormatSymbols formatData;  // formatData
  private Date defaultCenturyStart = 
    new Date(System.currentTimeMillis() - (80*365*24*60*60*1000));
  private String pattern;
  private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
  private static final long serialVersionUID = 4774881970558875024L;

  // This string is specified in the JCL.  We set it here rather than
  // do a DateFormatSymbols(Locale.US).getLocalPatternChars() since
  // someone could theoretically change those values (though unlikely).
  private static final String standardChars = "GyMdkHmsSEDFwWahKz";

  private void readObject(ObjectInputStream stream)
    throws IOException, ClassNotFoundException
  {
    stream.defaultReadObject();
    if (serialVersionOnStream < 1)
      {
        defaultCenturyStart =
	  new Date(System.currentTimeMillis() - (80*365*24*60*60*1000));
	serialVersionOnStream = 1;
      }

    // Set up items normally taken care of by the constructor.
    tokens = new Vector();
    compileFormat(pattern);
  }

  private void compileFormat(String pattern) 
  {
    // Any alphabetical characters are treated as pattern characters
    // unless enclosed in single quotes.

    char thisChar;
    int pos;
    int field;
    FieldSizePair current = null;

    for (int i=0; i<pattern.length(); i++) {
      thisChar = pattern.charAt(i);
      field = formatData.getLocalPatternChars().indexOf(thisChar);
      if (field == -1) {
	current = null;
	if (Character.isLetter(thisChar)) {
	  // Not a valid letter
	  tokens.addElement(new FieldSizePair(-1,0));
	} else if (thisChar == '\'') {
	  // Quoted text section; skip to next single quote
	  pos = pattern.indexOf('\'',i+1);
	  if (pos == -1) {
	    // This ought to be an exception, but spec does not
	    // let us throw one.
	    tokens.addElement(new FieldSizePair(-1,0));
	  }
	  if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
	    tokens.addElement(pattern.substring(i+1,pos+1));
	  } else {
	    tokens.addElement(pattern.substring(i+1,pos));
	  }
	  i = pos;
	} else {
	  // A special character
	  tokens.addElement(new Character(thisChar));
	}
      } else {
	// A valid field
	if ((current != null) && (field == current.field)) {
	  current.size++;
	} else {
	  current = new FieldSizePair(field,1);
	  tokens.addElement(current);
	}
      }
    }
  }
    
  public String toString() 
  {
    StringBuffer output = new StringBuffer();
    Enumeration e = tokens.elements();
    while (e.hasMoreElements()) {
      output.append(e.nextElement().toString());
    }
    return output.toString();
  }
      
  /**
   * Constructs a SimpleDateFormat using the default pattern for
   * the default locale.
   */
  public SimpleDateFormat() 
  {
    /*
     * There does not appear to be a standard API for determining 
     * what the default pattern for a locale is, so use package-scope
     * variables in DateFormatSymbols to encapsulate this.
     */
    super();
    Locale locale = Locale.getDefault();
    calendar = new GregorianCalendar(locale);
    tokens = new Vector();
    formatData = new DateFormatSymbols(locale);
    pattern = formatData.dateFormats[DEFAULT]+' '+formatData.timeFormats[DEFAULT];
    compileFormat(pattern);
    numberFormat = NumberFormat.getInstance(locale);
  }
  
  /**
   * Creates a date formatter using the specified pattern, with the default
   * DateFormatSymbols for the default locale.
   */
  public SimpleDateFormat(String pattern) 
  {
    this(pattern, Locale.getDefault());
  }

  /**
   * Creates a date formatter using the specified pattern, with the default
   * DateFormatSymbols for the given locale.
   */
  public SimpleDateFormat(String pattern, Locale locale) 
  {
    super();
    calendar = new GregorianCalendar(locale);
    tokens = new Vector();
    formatData = new DateFormatSymbols(locale);
    compileFormat(pattern);
    this.pattern = pattern;
    numberFormat = NumberFormat.getInstance(locale);
  }

  /**
   * Creates a date formatter using the specified pattern. The
   * specified DateFormatSymbols will be used when formatting.
   */
  public SimpleDateFormat(String pattern, DateFormatSymbols formatData) {
    super();
    calendar = new GregorianCalendar();
    // FIXME: XXX: Is it really necessary to set the timezone?
    // The Calendar constructor is supposed to take care of this.
    calendar.setTimeZone(TimeZone.getDefault());
    tokens = new Vector();
    this.formatData = formatData;
    compileFormat(pattern);
    this.pattern = pattern;
    numberFormat = NumberFormat.getInstance();
  }

  // What is the difference between localized and unlocalized?  The
  // docs don't say.

  /**
   * This method returns a string with the formatting pattern being used
   * by this object.  This string is unlocalized.
   *
   * @return The format string.
   */
  public String toPattern()
  {
    return pattern;
  }

  /**
   * This method returns a string with the formatting pattern being used
   * by this object.  This string is localized.
   *
   * @return The format string.
   */
  public String toLocalizedPattern()
  {
    String localChars = formatData.getLocalPatternChars();
    return applyLocalizedPattern (pattern, standardChars, localChars);
  }

  /**
   * This method sets the formatting pattern that should be used by this
   * object.  This string is not localized.
   *
   * @param pattern The new format pattern.
   */
  public void applyPattern(String pattern)
  {
    tokens = new Vector();
    compileFormat(pattern);
    this.pattern = pattern;
  }

  /**
   * This method sets the formatting pattern that should be used by this
   * object.  This string is localized.
   *
   * @param pattern The new format pattern.
   */
  public void applyLocalizedPattern(String pattern)
  {
    String localChars = formatData.getLocalPatternChars();
    pattern = applyLocalizedPattern (pattern, localChars, standardChars);
    applyPattern(pattern);
  }

  private String applyLocalizedPattern(String pattern,
				       String oldChars, String newChars)
  {
    int len = pattern.length();
    StringBuffer buf = new StringBuffer(len);
    boolean quoted = false;
    for (int i = 0;  i < len;  i++)
      {
	char ch = pattern.charAt(i);
	if (ch == '\'')
	  quoted = ! quoted;
	if (! quoted)
	  {
	    int j = oldChars.indexOf(ch);
	    if (j >= 0)
	      ch = newChars.charAt(j);
	  }
	buf.append(ch);
      }
    return buf.toString();
  }

  /** 
   * Returns the start of the century used for two digit years.
   *
   * @return A <code>Date</code> representing the start of the century
   * for two digit years.
   */
  public Date get2DigitYearStart()
  {
    return defaultCenturyStart;
  }

  /**
   * Sets the start of the century used for two digit years.
   *
   * @param date A <code>Date</code> representing the start of the century for
   * two digit years.
   */
  public void set2DigitYearStart(Date date)
  {
    defaultCenturyStart = date;
  }

  /**
   * This method returns the format symbol information used for parsing
   * and formatting dates.
   *
   * @return The date format symbols.
   */
  public DateFormatSymbols getDateFormatSymbols()
  {
    return formatData;
  }

  /**
   * This method sets the format symbols information used for parsing
   * and formatting dates.
   *
   * @param formatData The date format symbols.
   */
   public void setDateFormatSymbols(DateFormatSymbols formatData)
   {
     this.formatData = formatData;
   }

  /**
   * This methods tests whether the specified object is equal to this
   * object.  This will be true if and only if the specified object:
   * <p>
   * <ul>
   * <li>Is not <code>null</code>.
   * <li>Is an instance of <code>SimpleDateFormat</code>.
   * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
   *     level.
   * <li>Has the same formatting pattern.
   * <li>Is using the same formatting symbols.
   * <li>Is using the same century for two digit years.
   * </ul>
   *
   * @param obj The object to compare for equality against.
   *
   * @return <code>true</code> if the specified object is equal to this object,
   * <code>false</code> otherwise.
   */
  public boolean equals(Object o)
  {
    if (o == null)
      return false;

    if (!super.equals(o))
      return false;

    if (!(o instanceof SimpleDateFormat))
      return false;

    SimpleDateFormat sdf = (SimpleDateFormat)o;

    if (!toPattern().equals(sdf.toPattern()))
      return false;

    if (!get2DigitYearStart().equals(sdf.get2DigitYearStart()))
      return false;

    if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
      return false;

    return true;
  }


  /**
   * Formats the date input according to the format string in use,
   * appending to the specified StringBuffer.  The input StringBuffer
   * is returned as output for convenience.
   */
  public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos) {
    String temp;
    Calendar theCalendar = (Calendar) calendar.clone();
    theCalendar.setTime(date);
    
    // go through vector, filling in fields where applicable, else toString
    Enumeration e = tokens.elements();
    while (e.hasMoreElements()) {
      Object o = e.nextElement();
      if (o instanceof FieldSizePair) {
	FieldSizePair p = (FieldSizePair) o;
	int beginIndex = buffer.length();
	switch (p.field) {
	case ERA_FIELD:
	  buffer.append(formatData.eras[theCalendar.get(Calendar.ERA)]);
	  break;
	case YEAR_FIELD:
	  temp = String.valueOf(theCalendar.get(Calendar.YEAR));
	  if (p.size < 4)
	    buffer.append(temp.substring(temp.length()-2));
	  else
	    buffer.append(temp);
	  break;
	case MONTH_FIELD:
	  if (p.size < 3)
	    withLeadingZeros(theCalendar.get(Calendar.MONTH)+1,p.size,buffer);
	  else if (p.size < 4)
	    buffer.append(formatData.shortMonths[theCalendar.get(Calendar.MONTH)]);
	  else
	    buffer.append(formatData.months[theCalendar.get(Calendar.MONTH)]);
	  break;
	case DATE_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.DATE),p.size,buffer);
	  break;
	case HOUR_OF_DAY1_FIELD:  // 1-12
	  withLeadingZeros(theCalendar.get(Calendar.HOUR),p.size,buffer);
	  break;
	case HOUR_OF_DAY0_FIELD: // 0-23
	  withLeadingZeros(theCalendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
	  break;
	case MINUTE_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.MINUTE),p.size,buffer);
	  break;
	case SECOND_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.SECOND),p.size,buffer);
	  break;
	case MILLISECOND_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.MILLISECOND),p.size,buffer);
	  break;
	case DAY_OF_WEEK_FIELD:
	  if (p.size < 4)
	    buffer.append(formatData.shortWeekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
	  else
	    buffer.append(formatData.weekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
	  break;
	case DAY_OF_YEAR_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
	  break;
	case DAY_OF_WEEK_IN_MONTH_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
	  break;
	case WEEK_OF_YEAR_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
	  break;
	case WEEK_OF_MONTH_FIELD:
	  withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
	  break;
	case AM_PM_FIELD:
	  buffer.append(formatData.ampms[theCalendar.get(Calendar.AM_PM)]);
	  break;
	case HOUR1_FIELD: // 1-24
	  withLeadingZeros(theCalendar.get(Calendar.HOUR_OF_DAY)+1,p.size,buffer);
	  break;
	case HOUR0_FIELD: // 0-11
	  withLeadingZeros(theCalendar.get(Calendar.HOUR)-1,p.size,buffer);
	  break;
	case TIMEZONE_FIELD:
	  TimeZone zone = theCalendar.getTimeZone();
	  boolean isDST = theCalendar.get(Calendar.DST_OFFSET) != 0;
	  // FIXME: XXX: This should be a localized time zone.
	  String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
	  buffer.append(zoneID);
	  break;
	default:
	  throw new IllegalArgumentException("Illegal pattern character");
	}
	if (pos != null && p.field == pos.getField())
	  {
	    pos.setBeginIndex(beginIndex);
	    pos.setEndIndex(buffer.length());
	  }
      } else {
	buffer.append(o.toString());
      }
    }
    return buffer;
  }

  private void withLeadingZeros(int value, int length, StringBuffer buffer) {
    String valStr = String.valueOf(value);
    for (length -= valStr.length(); length > 0; length--)
      buffer.append('0');
    buffer.append(valStr);
  }

  private int indexInArray(String dateStr, int index, String[] values) {
    int l1 = dateStr.length()-index;
    int l2;

    for (int i=0; i < values.length; i++) {
      if (values[i] == null)
        continue;

      l2 = values[i].length();
      //System.err.println(values[i] + " " + dateStr.substring(index,index+l2));
      if ((l1 >= l2) && (dateStr.substring(index,index+l2).equals(values[i])))
	return i;
    }
    return -1;
  }

  /*
   * Get the actual year value, converting two digit years if necessary.
   */
  private int processYear(int val)
  {
    if (val > 100)
      return val;

    Date d = get2DigitYearStart();
    Calendar c = Calendar.getInstance();
    c.setTime(d);
    int y = c.get(YEAR_FIELD);

    return ((y / 100) * 100) + val;
  }

  /*
   * Ok, we ignore the format string and just try to parse what we can
   * out of the string.  We need, month, day, year at a minimum. The real
   * killer is stuff like XX/XX/XX.  How do we interpret that?  Is is the
   * US style MM/DD/YY or the European style DD/MM/YY. Or is it YYYY/MM/DD?
   * I'm an American, so I guess you know which one I'm choosing....
   */
  private Date parseLenient(String dateStr, ParsePosition pos)
  {
    int month = -1;
    int day = -1;
    int year = -1;
    int era = -1;
    int hour = -1;
    int hour24 = -1;
    int minute = -1;
    int second = -1;
    int millis = -1;
    int ampm = -1;
    int last = -1;
    TimeZone tz = null;
    char lastsep = ' ';
    char nextchar = ' ';

    Calendar cal = (Calendar)calendar.clone();
    cal.clear();
    cal.setTime(new Date(0));

    int index = pos.getIndex();
    String buf = dateStr.substring(index, dateStr.length());

    top:
    for(;;)
      {

        // Are we at the end of the string?  If so, make sure we have
        // enough data and return. // FIXME: Also detect sufficient data
        // and return by setting buf to "" on an unparsible string.
        if (buf.equals(""))
          {
            pos.setIndex(index);

            // This is the minimum we need
            if ((month == -1) || (day == -1) || (year == -1))
              {
                pos.setErrorIndex(index);
                return null;
              }

             if (tz != null)
               cal.setTimeZone(tz);

             cal.set(Calendar.YEAR, year);
             cal.set(Calendar.MONTH, month - 1);
             cal.set(Calendar.DATE, day);           

             if (ampm == 0)
               cal.set(Calendar.AM_PM, Calendar.AM);
             else if (ampm == 1)
               cal.set(Calendar.AM_PM, Calendar.PM);

             // If am/pm not set, we assume 24 hour day
             if (hour != -1)
               {
                 if (ampm == -1)
                   cal.set(Calendar.HOUR_OF_DAY, hour);
                 else
                   {
                     if (ampm == 0)
                       {
                         if (hour == 12)
                           hour = 0;
                       }
                     else
                       {
                         if (hour != 12)
                           hour += 12;
                       }
    
                     cal.set(Calendar.HOUR_OF_DAY, hour);
                   }
               }

             if (minute != -1)
               cal.set(Calendar.MINUTE, minute);

             if (second != -1)
               cal.set(Calendar.SECOND, second);

             if (millis != -1)
               cal.set(Calendar.MILLISECOND, millis);

             if (era == 0)
               cal.set(Calendar.ERA, GregorianCalendar.BC);
             else if (era == 1)
               cal.set(Calendar.ERA, GregorianCalendar.AD);

             return cal.getTime();
          }

        // Skip over whitespace and expected punctuation
        char c = buf.charAt(0);
        boolean comma_found = false;
        while(Character.isWhitespace(c) || (c == ':') || 
              (c == ',') || (c == '.') || (c == '/'))
          {
            lastsep = c;
            if (c == ',') // This is a total and utter crock
              comma_found = true;
            buf = buf.substring(1);
            if (buf.equals(""))
              continue;
            c = buf.charAt(0);
          }

        if (comma_found == true)
          lastsep = ',';

        // Is it a month name?
        for (int i = 0; i < formatData.months.length; i++)
          if ((formatData.months[i] != null) 
              && buf.startsWith(formatData.months[i]))
            {
              month = i + 1;
              buf = buf.substring(formatData.months[i].length());
              index += formatData.months[i].length();
              last = MONTH_FIELD;
              continue top;
            }

        // Is it a short month name?
        for (int i = 0; i < formatData.shortMonths.length; i++)
          if ((formatData.shortMonths[i] != null) 
              && buf.startsWith(formatData.shortMonths[i]))
            {
              month = i + 1;
              buf = buf.substring(formatData.shortMonths[i].length());
              index += formatData.shortMonths[i].length();
              last = MONTH_FIELD;
              continue top;
            }

        // Is it a weekday name?
        for (int i = 0; i < formatData.weekdays.length; i++)
          if ((formatData.weekdays[i] != null)
              && buf.startsWith(formatData.weekdays[i]))
            {
              buf = buf.substring(formatData.weekdays[i].length());
              index += formatData.weekdays[i].length();
              last = DAY_OF_WEEK_FIELD;
              continue top;
            }

        // Is it a short weekday name?
        for (int i = 0; i < formatData.shortWeekdays.length; i++)
          if ((formatData.shortWeekdays[i] != null)
              && buf.startsWith(formatData.shortWeekdays[i]))
            {
              buf = buf.substring(formatData.shortWeekdays[i].length());
              index += formatData.shortWeekdays[i].length();
              last = DAY_OF_WEEK_FIELD;
              continue top;
            }

        // Is this an am/pm string?
        for (int i = 0; i < formatData.ampms.length; i++) {
          if ((formatData.ampms[i] != null)
              && buf.toLowerCase().startsWith(formatData.ampms[i].toLowerCase()))
            {
              ampm = i;
              buf = buf.substring(formatData.ampms[i].length());
              index += formatData.ampms[i].length();
              last = AM_PM_FIELD;
              continue top;
            }
        }

        // See if we have a number
        c = buf.charAt(0);
        String nbrstr = "";
        while (Character.isDigit(c))
          {
            nbrstr = nbrstr + c;
            buf = buf.substring(1);
            if (buf.equals(""))
              break;
            c = buf.charAt(0);
          }

        // If we didn't get a number, try for a timezone, otherwise set buf
        // to "" and loop to see if we are done.
        if (nbrstr.equals(""))
          {
            // Ok, try for a timezone name
            while(!Character.isWhitespace(c) && (c != ',') && (c != '.') &&
                  (c != ':') && (c != '/'))
              {
                nbrstr = nbrstr + c;
                buf = buf.substring(1);
                if (buf.equals(""))
                  break;
                c = buf.charAt(0);
              }
            TimeZone tmptz = TimeZone.getTimeZone(nbrstr);
             
            // We get GMT on failure, so be sure we asked for it.
            if (tmptz.getID().equals("GMT"))
              {
                if (!nbrstr.equals("GMT"))
                  {
                    buf = "";
                    continue top;
                  }
              }

            tz = tmptz;
            last = TIMEZONE_FIELD;
            index += nbrstr.length();
            continue top;
          }

        // Convert to integer
        int val = 0;
        try
          {
            val = Integer.parseInt(nbrstr);
          }
        catch(Exception e)
          {
            return null; // Shouldn't happen
          }

        if (!buf.equals(""))
          nextchar = buf.charAt(0);
        else
          nextchar = ' ';

        // Figure out which value to assign to
        // I make bad US assumptions about MM/DD/YYYY
        if (last == DAY_OF_WEEK_FIELD)
          {
            day = val;
            last = DATE_FIELD;
          }
        else if ((last == MONTH_FIELD) && (day != -1))
          {
            year = processYear(val);
            last = YEAR_FIELD;
          }
        else if (last == MONTH_FIELD)
          {
            day = val;
            last = DATE_FIELD;
          }
        else if (last == -1)
          {
            // Assume month
            if ((val < 13) && (val > 0))
              {
                month = val;
                last = MONTH_FIELD;
              }
            // Assume year. This only works for two digit years that aren't
            // between 01 and 12
            else
              {
                year = processYear(val);
                last = YEAR_FIELD;
              } 
          }
        else if ((last == YEAR_FIELD) && ((nextchar == '/') ||
                 (nextchar == '.')))
          {
            month = val;
            last = MONTH_FIELD;
          }
        else if (last == YEAR_FIELD)
          {
            hour = val;
            last = HOUR0_FIELD;
          }
        else if ((last == DATE_FIELD) && ((nextchar == '/') ||
                 (nextchar == '.') || buf.equals("")))
          {
            year = processYear(val);
            last = YEAR_FIELD;
          }
        else if ((last == DATE_FIELD) && ((lastsep == '/') ||
                 (lastsep == '.') || (lastsep == ',')))
          {
            year = processYear(val);
            last = YEAR_FIELD;
          }
        else if (last == DATE_FIELD)
          {
            hour = val;
            last = HOUR0_FIELD;
          }
        else if (last == HOUR0_FIELD)
          {
            minute = val;
            last = MINUTE_FIELD;
          }
        else if (last == MINUTE_FIELD)
          {
            second = val;
            last = SECOND_FIELD;
          }
        else if (lastsep == '.') 
          {
            ; // This is milliseconds or something.  Ignore it
            last = WEEK_OF_YEAR_FIELD; // Just a random value
          }
        else // It is year. I have spoken!
          {
            year = processYear(val);
            last = YEAR_FIELD;
          }
      }
  }

  private int parseLeadingZeros(String dateStr, ParsePosition pos,
                                FieldSizePair p)
  {
    int value;
    int index = pos.getIndex();
    String buf = null;

    if (p.size == 1)
      {
        char c = dateStr.charAt(index+1);
        if ((dateStr.charAt(index) == '1') && 
             Character.isDigit(dateStr.charAt(index+1)))
          buf = dateStr.substring(index, index+2);
        else
          buf = dateStr.substring(index, index+1);
        pos.setIndex(index + buf.length());
      }
    else if (p.size == 2)
      {
        buf = dateStr.substring(index, index+2);
        pos.setIndex(index+2);
      }
    else if (p.size == 3)
      {
        buf = dateStr.substring(index, index+3);
        pos.setIndex(index+3);
      }
    else
      {
        buf = dateStr.substring(index, index+4);
        pos.setIndex(index+4);
      }
    try
      {
        value = Integer.parseInt(buf);
      }
    catch(NumberFormatException nfe)
      {
        pos.setIndex(index);
        pos.setErrorIndex(index);
        return -1;
      } 

    return value;
  }

  /*
   * Note that this method doesn't properly protect against
   * StringIndexOutOfBoundsException.  FIXME
   */
  private Date parseStrict(String dateStr, ParsePosition pos)
  {
    // start looking at position pos.index
    Enumeration e = tokens.elements();
    Calendar theCalendar = (Calendar) calendar.clone();
    theCalendar.clear();
    theCalendar.setTime(new Date(0));

    int value, index, hour = -1;
    String buf;
    while (pos.getIndex() < dateStr.length()) {
      Object o = e.nextElement();
      if (o instanceof FieldSizePair) {
	FieldSizePair p = (FieldSizePair) o;
	switch (p.field) {

	case ERA_FIELD:
	  value = indexInArray(dateStr,pos.getIndex(),formatData.eras);
	  if (value == -1) {
	    pos.setErrorIndex(pos.getIndex());
	    return null;
	  }
	  pos.setIndex(pos.getIndex() + formatData.eras[value].length());
	  theCalendar.set(Calendar.ERA,value);
	  break;

	case YEAR_FIELD:
          String y;
	  if (p.size < 4)
            y = dateStr.substring(pos.getIndex(), pos.getIndex() + 2);
          else
            y = dateStr.substring(pos.getIndex(), pos.getIndex() + 4);
            
          int year;
          try
            {
              year = Integer.parseInt(y);
            }
          catch(NumberFormatException nfe)
            {
              pos.setErrorIndex(pos.getIndex());
              return null;
            }

	  if (p.size < 4)
            year += get2DigitYearStart().getYear();

          theCalendar.set(Calendar.YEAR, year);
	  if (p.size < 4)
            pos.setIndex(pos.getIndex()+2);
          else
            pos.setIndex(pos.getIndex()+4);
	  break;

	case MONTH_FIELD:
          if (p.size > 2)
            {
              index = pos.getIndex();

	      value = indexInArray(dateStr,pos.getIndex(),
                 (p.size == 3) ? formatData.shortMonths : formatData.months);
	      if (value == -1) 
                {
	          pos.setErrorIndex(pos.getIndex());
	          return null;
	        }
              if (p.size == 3)
                pos.setIndex(index + formatData.shortMonths[value].length());
              else
                pos.setIndex(index + formatData.months[value].length());
              theCalendar.set(Calendar.MONTH, value);
              break;
            }

          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;

          theCalendar.set(Calendar.MONTH, value);
          break;

	case DATE_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;

          theCalendar.set(Calendar.DATE, value);
	  break;

	case HOUR_OF_DAY1_FIELD:
	case HOUR_OF_DAY0_FIELD:
          index = pos.getIndex();
          buf = dateStr.substring(index, index+2);
          try
            {
              value = Integer.parseInt(buf);
            }
          catch(NumberFormatException nfe)
            {
              return null;
            }
          if (p.field == HOUR_OF_DAY0_FIELD)
           // theCalendar.set(Calendar.HOUR_OF_DAY, value);
            hour = value + 1;
          else
           // theCalendar.set(Calendar.HOUR_OF_DAY, value-1);
            hour = value;
          pos.setIndex(index+2);

	  break;

	case MINUTE_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;

          theCalendar.set(Calendar.MINUTE, value);
	  break;

	case SECOND_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;

          theCalendar.set(Calendar.SECOND, value);
	  break;

	case MILLISECOND_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;
         
          theCalendar.set(Calendar.MILLISECOND, value);
	  break;

	case DAY_OF_WEEK_FIELD:
	  value = indexInArray(dateStr,pos.getIndex(),(p.size < 4) ? formatData.shortWeekdays : formatData.weekdays);
	  if (value == -1) {
	    pos.setErrorIndex(pos.getIndex());
	    return null;
	  }
	  pos.setIndex(pos.getIndex() + ((p.size < 4) ? formatData.shortWeekdays[value].length()
	    : formatData.weekdays[value].length()));
	  // Note: Calendar.set(Calendar.DAY_OF_WEEK,value) does not work
	  // as implemented in jdk1.1.5 (possibly DAY_OF_WEEK is meant to
	  // be read-only). Instead, calculate number of days offset.
	  theCalendar.add(Calendar.DATE,value 
			  - theCalendar.get(Calendar.DAY_OF_WEEK));
	  // in JDK, this seems to clear the hours, so we'll do the same.
	  theCalendar.set(Calendar.HOUR_OF_DAY,0);
	  break;

	case DAY_OF_YEAR_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;
         
	  theCalendar.set(Calendar.DAY_OF_YEAR, value);
	  break;

        // Just parse and ignore
	case DAY_OF_WEEK_IN_MONTH_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;
         
	  break;

        // Just parse and ignore
	case WEEK_OF_YEAR_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;
         
	  break;

        // Just parse and ignore
	case WEEK_OF_MONTH_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;

	  break;

	case AM_PM_FIELD:
	  value = indexInArray(dateStr,pos.getIndex(),formatData.ampms);
	  if (value == -1) {
	    pos.setErrorIndex(pos.getIndex());
	    return null;
	  }
	  pos.setIndex(pos.getIndex() + formatData.ampms[value].length());
	  theCalendar.set(Calendar.AM_PM,value);
	  break;

	case HOUR1_FIELD:
	case HOUR0_FIELD:
          value = parseLeadingZeros(dateStr, pos, p);
          if (value == -1)
            return null;
          if (p.field == HOUR1_FIELD)
            theCalendar.set(Calendar.HOUR, value);
          if (p.field == HOUR0_FIELD)
            theCalendar.set(Calendar.HOUR, value+1);
	  break;

	  /*
	case TIMEZONE_FIELD:
	  // TODO: FIXME: XXX
	  break;
	  */

	default:
	  throw new IllegalArgumentException("Illegal pattern character: " +
             p.field);
	} // end switch
      } else if (o instanceof String) {
	String ostr = (String) o;
	if (dateStr.substring(pos.getIndex(),pos.getIndex()+ostr.length()).equals(ostr)) {
	  pos.setIndex(pos.getIndex() + ostr.length());
	} else {
	  pos.setErrorIndex(pos.getIndex());
	  return null;
	}
      } else if (o instanceof Character) {
	Character ochar = (Character) o;
	if (dateStr.charAt(pos.getIndex()) == ochar.charValue()) {
	  pos.setIndex(pos.getIndex() + 1);
	} else {
	  pos.setErrorIndex(pos.getIndex());
	  return null;
	}
      }
    }

    if (hour != -1)
      {
        if (theCalendar.get(Calendar.AM_PM) == Calendar.PM)
          {
            if (hour == 12)
              theCalendar.set(Calendar.HOUR_OF_DAY, 12);
            else
              theCalendar.set(Calendar.HOUR_OF_DAY, hour + 12);
          }
        else
          {
            if (hour == 12)
              theCalendar.set(Calendar.HOUR_OF_DAY, 0);
            else
              theCalendar.set(Calendar.HOUR_OF_DAY, hour);
          }
      }

    return theCalendar.getTime();
  }

  /**
   * This method parses the specified string into a date.
   * 
   * @param dateStr The date string to parse.
   * @param pos The input and output parse position
   *
   * @return The parsed date, or <code>null</code> if the string cannot be
   * parsed.
   */
  public Date parse(String dateStr, ParsePosition pos) {
    if (isLenient())
       return parseLenient(dateStr, pos);
    else
       return parseStrict(dateStr, pos);

  }
}

