| /* SimpleDateFormat.java -- A class for parsing/formating simple |
| date constructs |
| Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005 |
| 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., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| |
| package java.text; |
| |
| import gnu.java.text.AttributedFormatBuffer; |
| import gnu.java.text.FormatBuffer; |
| import gnu.java.text.FormatCharacterIterator; |
| import gnu.java.text.StringFormatBuffer; |
| |
| import java.io.IOException; |
| import java.io.InvalidObjectException; |
| import java.io.ObjectInputStream; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.GregorianCalendar; |
| import java.util.Iterator; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * SimpleDateFormat provides convenient methods for parsing and formatting |
| * dates using Gregorian calendars (see java.util.GregorianCalendar). |
| */ |
| public class SimpleDateFormat extends DateFormat |
| { |
| /** |
| * This class is used by <code>SimpleDateFormat</code> as a |
| * compiled representation of a format string. The field |
| * ID, size, and character used are stored for each sequence |
| * of pattern characters. |
| */ |
| private class CompiledField |
| { |
| /** |
| * The ID of the field within the local pattern characters. |
| * Package private for use in out class. |
| */ |
| int field; |
| |
| /** |
| * The size of the character sequence. |
| * Package private for use in out class. |
| */ |
| int size; |
| |
| /** |
| * The character used. |
| */ |
| private char character; |
| |
| /** |
| * Constructs a compiled field using the |
| * the given field ID, size and character |
| * values. |
| * |
| * @param f the field ID. |
| * @param s the size of the field. |
| * @param c the character used. |
| */ |
| public CompiledField(int f, int s, char c) |
| { |
| field = f; |
| size = s; |
| character = c; |
| } |
| |
| /** |
| * Retrieves the ID of the field relative to |
| * the local pattern characters. |
| */ |
| public int getField() |
| { |
| return field; |
| } |
| |
| /** |
| * Retrieves the size of the character sequence. |
| */ |
| public int getSize() |
| { |
| return size; |
| } |
| |
| /** |
| * Retrieves the character used in the sequence. |
| */ |
| public char getCharacter() |
| { |
| return character; |
| } |
| |
| /** |
| * Returns a <code>String</code> representation |
| * of the compiled field, primarily for debugging |
| * purposes. |
| * |
| * @return a <code>String</code> representation. |
| */ |
| public String toString() |
| { |
| StringBuffer builder; |
| |
| builder = new StringBuffer(getClass().getName()); |
| builder.append("[field="); |
| builder.append(field); |
| builder.append(", size="); |
| builder.append(size); |
| builder.append(", character="); |
| builder.append(character); |
| builder.append("]"); |
| |
| return builder.toString(); |
| } |
| } |
| |
| /** |
| * A list of <code>CompiledField</code>s, |
| * representing the compiled version of the pattern. |
| * |
| * @see CompiledField |
| * @serial Ignored. |
| */ |
| private transient ArrayList tokens; |
| |
| /** |
| * The localised data used in formatting, |
| * such as the day and month names in the local |
| * language, and the localized pattern characters. |
| * |
| * @see DateFormatSymbols |
| * @serial The localisation data. May not be null. |
| */ |
| private DateFormatSymbols formatData; |
| |
| /** |
| * The date representing the start of the century |
| * used for interpreting two digit years. For |
| * example, 24/10/2004 would cause two digit |
| * years to be interpreted as representing |
| * the years between 2004 and 2104. |
| * |
| * @see #get2DigitYearStart() |
| * @see #set2DigitYearStart(java.util.Date) |
| * @see Date |
| * @serial The start date of the century for parsing two digit years. |
| * May not be null. |
| */ |
| private Date defaultCenturyStart; |
| |
| /** |
| * The year at which interpretation of two |
| * digit years starts. |
| * |
| * @see #get2DigitYearStart() |
| * @see #set2DigitYearStart(java.util.Date) |
| * @serial Ignored. |
| */ |
| private transient int defaultCentury; |
| |
| /** |
| * The non-localized pattern string. This |
| * only ever contains the pattern characters |
| * stored in standardChars. Localized patterns |
| * are translated to this form. |
| * |
| * @see #applyPattern(String) |
| * @see #applyLocalizedPattern(String) |
| * @see #toPattern() |
| * @see #toLocalizedPattern() |
| * @serial The non-localized pattern string. May not be null. |
| */ |
| private String pattern; |
| |
| /** |
| * The version of serialized data used by this class. |
| * Version 0 only includes the pattern and formatting |
| * data. Version 1 adds the start date for interpreting |
| * two digit years. |
| * |
| * @serial This specifies the version of the data being serialized. |
| * Version 0 (or no version) specifies just <code>pattern</code> |
| * and <code>formatData</code>. Version 1 adds |
| * the <code>defaultCenturyStart</code>. This implementation |
| * always writes out version 1 data. |
| */ |
| private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier |
| |
| /** |
| * For compatability. |
| */ |
| private static final long serialVersionUID = 4774881970558875024L; |
| |
| // This string is specified in the root of the CLDR. We set it here |
| // rather than doing a DateFormatSymbols(Locale.US).getLocalPatternChars() |
| // since someone could theoretically change those values (though unlikely). |
| private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZ"; |
| |
| /** |
| * Reads the serialized version of this object. |
| * If the serialized data is only version 0, |
| * then the date for the start of the century |
| * for interpreting two digit years is computed. |
| * The pattern is parsed and compiled following the process |
| * of reading in the serialized data. |
| * |
| * @param stream the object stream to read the data from. |
| * @throws IOException if an I/O error occurs. |
| * @throws ClassNotFoundException if the class of the serialized data |
| * could not be found. |
| * @throws InvalidObjectException if the pattern is invalid. |
| */ |
| private void readObject(ObjectInputStream stream) |
| throws IOException, ClassNotFoundException |
| { |
| stream.defaultReadObject(); |
| if (serialVersionOnStream < 1) |
| { |
| computeCenturyStart (); |
| serialVersionOnStream = 1; |
| } |
| else |
| // Ensure that defaultCentury gets set. |
| set2DigitYearStart(defaultCenturyStart); |
| |
| // Set up items normally taken care of by the constructor. |
| tokens = new ArrayList(); |
| try |
| { |
| compileFormat(pattern); |
| } |
| catch (IllegalArgumentException e) |
| { |
| throw new InvalidObjectException("The stream pattern was invalid."); |
| } |
| } |
| |
| /** |
| * Compiles the supplied non-localized pattern into a form |
| * from which formatting and parsing can be performed. |
| * This also detects errors in the pattern, which will |
| * be raised on later use of the compiled data. |
| * |
| * @param pattern the non-localized pattern to compile. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| private void compileFormat(String pattern) |
| { |
| // Any alphabetical characters are treated as pattern characters |
| // unless enclosed in single quotes. |
| |
| char thisChar; |
| int pos; |
| int field; |
| CompiledField current = null; |
| |
| for (int i = 0; i < pattern.length(); i++) |
| { |
| thisChar = pattern.charAt(i); |
| field = standardChars.indexOf(thisChar); |
| if (field == -1) |
| { |
| current = null; |
| if ((thisChar >= 'A' && thisChar <= 'Z') |
| || (thisChar >= 'a' && thisChar <= 'z')) |
| { |
| // Not a valid letter |
| throw new IllegalArgumentException("Invalid letter " |
| + thisChar + |
| " encountered at character " |
| + i + "."); |
| } |
| else if (thisChar == '\'') |
| { |
| // Quoted text section; skip to next single quote |
| pos = pattern.indexOf('\'', i + 1); |
| // First look for '' -- meaning a single quote. |
| if (pos == i + 1) |
| tokens.add("'"); |
| else |
| { |
| // Look for the terminating quote. However, if we |
| // see a '', that represents a literal quote and |
| // we must iterate. |
| StringBuffer buf = new StringBuffer(); |
| int oldPos = i + 1; |
| do |
| { |
| if (pos == -1) |
| throw new IllegalArgumentException("Quotes starting at character " |
| + i + |
| " not closed."); |
| buf.append(pattern.substring(oldPos, pos)); |
| if (pos + 1 >= pattern.length() |
| || pattern.charAt(pos + 1) != '\'') |
| break; |
| buf.append('\''); |
| oldPos = pos + 2; |
| pos = pattern.indexOf('\'', pos + 2); |
| } |
| while (true); |
| tokens.add(buf.toString()); |
| } |
| i = pos; |
| } |
| else |
| { |
| // A special character |
| tokens.add(new Character(thisChar)); |
| } |
| } |
| else |
| { |
| // A valid field |
| if ((current != null) && (field == current.field)) |
| current.size++; |
| else |
| { |
| current = new CompiledField(field, 1, thisChar); |
| tokens.add(current); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns a string representation of this |
| * class. |
| * |
| * @return a string representation of the <code>SimpleDateFormat</code> |
| * instance. |
| */ |
| public String toString() |
| { |
| StringBuffer output = new StringBuffer(getClass().getName()); |
| output.append("[tokens="); |
| output.append(tokens); |
| output.append(", formatData="); |
| output.append(formatData); |
| output.append(", defaultCenturyStart="); |
| output.append(defaultCenturyStart); |
| output.append(", defaultCentury="); |
| output.append(defaultCentury); |
| output.append(", pattern="); |
| output.append(pattern); |
| output.append(", serialVersionOnStream="); |
| output.append(serialVersionOnStream); |
| output.append(", standardChars="); |
| output.append(standardChars); |
| output.append("]"); |
| 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); |
| computeCenturyStart(); |
| tokens = new ArrayList(); |
| formatData = new DateFormatSymbols(locale); |
| pattern = (formatData.dateFormats[DEFAULT] + ' ' |
| + formatData.timeFormats[DEFAULT]); |
| compileFormat(pattern); |
| numberFormat = NumberFormat.getInstance(locale); |
| numberFormat.setGroupingUsed (false); |
| numberFormat.setParseIntegerOnly (true); |
| numberFormat.setMaximumFractionDigits (0); |
| } |
| |
| /** |
| * Creates a date formatter using the specified non-localized pattern, |
| * with the default DateFormatSymbols for the default locale. |
| * |
| * @param pattern the pattern to use. |
| * @throws NullPointerException if the pattern is null. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| public SimpleDateFormat(String pattern) |
| { |
| this(pattern, Locale.getDefault()); |
| } |
| |
| /** |
| * Creates a date formatter using the specified non-localized pattern, |
| * with the default DateFormatSymbols for the given locale. |
| * |
| * @param pattern the non-localized pattern to use. |
| * @param locale the locale to use for the formatting symbols. |
| * @throws NullPointerException if the pattern is null. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| public SimpleDateFormat(String pattern, Locale locale) |
| { |
| super(); |
| calendar = new GregorianCalendar(locale); |
| computeCenturyStart(); |
| tokens = new ArrayList(); |
| formatData = new DateFormatSymbols(locale); |
| compileFormat(pattern); |
| this.pattern = pattern; |
| numberFormat = NumberFormat.getInstance(locale); |
| numberFormat.setGroupingUsed (false); |
| numberFormat.setParseIntegerOnly (true); |
| numberFormat.setMaximumFractionDigits (0); |
| } |
| |
| /** |
| * Creates a date formatter using the specified non-localized |
| * pattern. The specified DateFormatSymbols will be used when |
| * formatting. |
| * |
| * @param pattern the non-localized pattern to use. |
| * @param formatData the formatting symbols to use. |
| * @throws NullPointerException if the pattern or formatData is null. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| public SimpleDateFormat(String pattern, DateFormatSymbols formatData) |
| { |
| super(); |
| calendar = new GregorianCalendar(); |
| computeCenturyStart (); |
| tokens = new ArrayList(); |
| if (formatData == null) |
| throw new NullPointerException("formatData"); |
| this.formatData = formatData; |
| compileFormat(pattern); |
| this.pattern = pattern; |
| numberFormat = NumberFormat.getInstance(); |
| numberFormat.setGroupingUsed (false); |
| numberFormat.setParseIntegerOnly (true); |
| numberFormat.setMaximumFractionDigits (0); |
| } |
| |
| /** |
| * 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 translateLocalizedPattern(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. |
| * @throws NullPointerException if the pattern is null. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| public void applyPattern(String pattern) |
| { |
| tokens = new ArrayList(); |
| 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. |
| * @throws NullPointerException if the pattern is null. |
| * @throws IllegalArgumentException if the pattern is invalid. |
| */ |
| public void applyLocalizedPattern(String pattern) |
| { |
| String localChars = formatData.getLocalPatternChars(); |
| pattern = translateLocalizedPattern(pattern, localChars, standardChars); |
| applyPattern(pattern); |
| } |
| |
| /** |
| * Translates either from or to a localized variant of the pattern |
| * string. For example, in the German locale, 't' (for 'tag') is |
| * used instead of 'd' (for 'date'). This method translates |
| * a localized pattern (such as 'ttt') to a non-localized pattern |
| * (such as 'ddd'), or vice versa. Non-localized patterns use |
| * a standard set of characters, which match those of the U.S. English |
| * locale. |
| * |
| * @param pattern the pattern to translate. |
| * @param oldChars the old set of characters (used in the pattern). |
| * @param newChars the new set of characters (which will be used in the |
| * pattern). |
| * @return a version of the pattern using the characters in |
| * <code>newChars</code>. |
| */ |
| private String translateLocalizedPattern(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; |
| calendar.clear(); |
| calendar.setTime(date); |
| int year = calendar.get(Calendar.YEAR); |
| defaultCentury = year - (year % 100); |
| } |
| |
| /** |
| * This method returns a copy of the format symbol information used |
| * for parsing and formatting dates. |
| * |
| * @return a copy of the date format symbols. |
| */ |
| public DateFormatSymbols getDateFormatSymbols() |
| { |
| return (DateFormatSymbols) formatData.clone(); |
| } |
| |
| /** |
| * This method sets the format symbols information used for parsing |
| * and formatting dates. |
| * |
| * @param formatData The date format symbols. |
| * @throws NullPointerException if <code>formatData</code> is null. |
| */ |
| public void setDateFormatSymbols(DateFormatSymbols formatData) |
| { |
| if (formatData == null) |
| { |
| throw new |
| NullPointerException("The supplied format data was null."); |
| } |
| 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> |
| * <li>Is an instance of <code>SimpleDateFormat</code>.</li> |
| * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>) |
| * level.</li> |
| * <li>Has the same formatting pattern.</li> |
| * <li>Is using the same formatting symbols.</li> |
| * <li>Is using the same century for two digit years.</li> |
| * </ul> |
| * |
| * @param o 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 (!super.equals(o)) |
| return false; |
| |
| if (!(o instanceof SimpleDateFormat)) |
| return false; |
| |
| SimpleDateFormat sdf = (SimpleDateFormat)o; |
| |
| if (defaultCentury != sdf.defaultCentury) |
| return false; |
| |
| if (!toPattern().equals(sdf.toPattern())) |
| return false; |
| |
| if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols())) |
| return false; |
| |
| return true; |
| } |
| |
| /** |
| * This method returns a hash value for this object. |
| * |
| * @return A hash value for this object. |
| */ |
| public int hashCode() |
| { |
| return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^ |
| getDateFormatSymbols().hashCode(); |
| } |
| |
| |
| /** |
| * 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. |
| */ |
| private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos) |
| { |
| String temp; |
| AttributedCharacterIterator.Attribute attribute; |
| calendar.setTime(date); |
| |
| // go through vector, filling in fields where applicable, else toString |
| Iterator iter = tokens.iterator(); |
| while (iter.hasNext()) |
| { |
| Object o = iter.next(); |
| if (o instanceof CompiledField) |
| { |
| CompiledField cf = (CompiledField) o; |
| int beginIndex = buffer.length(); |
| |
| switch (cf.getField()) |
| { |
| case ERA_FIELD: |
| buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA); |
| break; |
| case YEAR_FIELD: |
| // If we have two digits, then we truncate. Otherwise, we |
| // use the size of the pattern, and zero pad. |
| buffer.setDefaultAttribute (DateFormat.Field.YEAR); |
| if (cf.getSize() == 2) |
| { |
| temp = "00"+String.valueOf (calendar.get (Calendar.YEAR)); |
| buffer.append (temp.substring (temp.length() - 2)); |
| } |
| else |
| withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer); |
| break; |
| case MONTH_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.MONTH); |
| if (cf.getSize() < 3) |
| withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer); |
| else if (cf.getSize() < 4) |
| buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]); |
| else |
| buffer.append (formatData.months[calendar.get (Calendar.MONTH)]); |
| break; |
| case DATE_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH); |
| withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer); |
| break; |
| case HOUR_OF_DAY1_FIELD: // 1-24 |
| buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1); |
| withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1, |
| cf.getSize(), buffer); |
| break; |
| case HOUR_OF_DAY0_FIELD: // 0-23 |
| buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0); |
| withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer); |
| break; |
| case MINUTE_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.MINUTE); |
| withLeadingZeros (calendar.get (Calendar.MINUTE), |
| cf.getSize(), buffer); |
| break; |
| case SECOND_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.SECOND); |
| withLeadingZeros(calendar.get (Calendar.SECOND), |
| cf.getSize(), buffer); |
| break; |
| case MILLISECOND_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND); |
| withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer); |
| break; |
| case DAY_OF_WEEK_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK); |
| if (cf.getSize() < 4) |
| buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]); |
| else |
| buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]); |
| break; |
| case DAY_OF_YEAR_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR); |
| withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer); |
| break; |
| case DAY_OF_WEEK_IN_MONTH_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH); |
| withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH), |
| cf.getSize(), buffer); |
| break; |
| case WEEK_OF_YEAR_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR); |
| withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR), |
| cf.getSize(), buffer); |
| break; |
| case WEEK_OF_MONTH_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH); |
| withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH), |
| cf.getSize(), buffer); |
| break; |
| case AM_PM_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.AM_PM); |
| buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]); |
| break; |
| case HOUR1_FIELD: // 1-12 |
| buffer.setDefaultAttribute (DateFormat.Field.HOUR1); |
| withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1, |
| cf.getSize(), buffer); |
| break; |
| case HOUR0_FIELD: // 0-11 |
| buffer.setDefaultAttribute (DateFormat.Field.HOUR0); |
| withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer); |
| break; |
| case TIMEZONE_FIELD: |
| buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE); |
| TimeZone zone = calendar.getTimeZone(); |
| boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0; |
| // FIXME: XXX: This should be a localized time zone. |
| String zoneID = zone.getDisplayName |
| (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT); |
| buffer.append (zoneID); |
| break; |
| case RFC822_TIMEZONE_FIELD: |
| buffer.setDefaultAttribute(DateFormat.Field.RFC822_TIME_ZONE); |
| int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) + |
| calendar.get(Calendar.DST_OFFSET)) / (1000 * 60); |
| String sign = (pureMinutes < 0) ? "-" : "+"; |
| pureMinutes = Math.abs(pureMinutes); |
| int hours = pureMinutes / 60; |
| int minutes = pureMinutes % 60; |
| buffer.append(sign); |
| withLeadingZeros(hours, 2, buffer); |
| withLeadingZeros(minutes, 2, buffer); |
| break; |
| default: |
| throw new IllegalArgumentException ("Illegal pattern character " + |
| cf.getCharacter()); |
| } |
| if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute() |
| || cf.getField() == pos.getField())) |
| { |
| pos.setBeginIndex(beginIndex); |
| pos.setEndIndex(buffer.length()); |
| } |
| } |
| else |
| { |
| buffer.append(o.toString(), null); |
| } |
| } |
| } |
| |
| public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos) |
| { |
| formatWithAttribute(date, new StringFormatBuffer (buffer), pos); |
| |
| return buffer; |
| } |
| |
| public AttributedCharacterIterator formatToCharacterIterator(Object date) |
| throws IllegalArgumentException |
| { |
| if (date == null) |
| throw new NullPointerException("null argument"); |
| if (!(date instanceof Date)) |
| throw new IllegalArgumentException("argument should be an instance of java.util.Date"); |
| |
| AttributedFormatBuffer buf = new AttributedFormatBuffer(); |
| formatWithAttribute((Date)date, buf, |
| null); |
| buf.sync(); |
| |
| return new FormatCharacterIterator(buf.getBuffer().toString(), |
| buf.getRanges(), |
| buf.getAttributes()); |
| } |
| |
| private void withLeadingZeros(int value, int length, FormatBuffer buffer) |
| { |
| String valStr = String.valueOf(value); |
| for (length -= valStr.length(); length > 0; length--) |
| buffer.append('0'); |
| buffer.append(valStr); |
| } |
| |
| private boolean expect(String source, ParsePosition pos, char ch) |
| { |
| int x = pos.getIndex(); |
| boolean r = x < source.length() && source.charAt(x) == ch; |
| if (r) |
| pos.setIndex(x + 1); |
| else |
| pos.setErrorIndex(x); |
| return r; |
| } |
| |
| /** |
| * 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) |
| { |
| int fmt_index = 0; |
| int fmt_max = pattern.length(); |
| |
| calendar.clear(); |
| boolean saw_timezone = false; |
| int quote_start = -1; |
| boolean is2DigitYear = false; |
| try |
| { |
| for (; fmt_index < fmt_max; ++fmt_index) |
| { |
| char ch = pattern.charAt(fmt_index); |
| if (ch == '\'') |
| { |
| int index = pos.getIndex(); |
| if (fmt_index < fmt_max - 1 |
| && pattern.charAt(fmt_index + 1) == '\'') |
| { |
| if (! expect (dateStr, pos, ch)) |
| return null; |
| ++fmt_index; |
| } |
| else |
| quote_start = quote_start < 0 ? fmt_index : -1; |
| continue; |
| } |
| |
| if (quote_start != -1 |
| || ((ch < 'a' || ch > 'z') |
| && (ch < 'A' || ch > 'Z'))) |
| { |
| if (! expect (dateStr, pos, ch)) |
| return null; |
| continue; |
| } |
| |
| // We've arrived at a potential pattern character in the |
| // pattern. |
| int fmt_count = 1; |
| while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch) |
| { |
| ++fmt_count; |
| } |
| |
| // We might need to limit the number of digits to parse in |
| // some cases. We look to the next pattern character to |
| // decide. |
| boolean limit_digits = false; |
| if (fmt_index < fmt_max |
| && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0) |
| limit_digits = true; |
| --fmt_index; |
| |
| // We can handle most fields automatically: most either are |
| // numeric or are looked up in a string vector. In some cases |
| // we need an offset. When numeric, `offset' is added to the |
| // resulting value. When doing a string lookup, offset is the |
| // initial index into the string array. |
| int calendar_field; |
| boolean is_numeric = true; |
| int offset = 0; |
| boolean maybe2DigitYear = false; |
| boolean oneBasedHour = false; |
| boolean oneBasedHourOfDay = false; |
| Integer simpleOffset; |
| String[] set1 = null; |
| String[] set2 = null; |
| switch (ch) |
| { |
| case 'd': |
| calendar_field = Calendar.DATE; |
| break; |
| case 'D': |
| calendar_field = Calendar.DAY_OF_YEAR; |
| break; |
| case 'F': |
| calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH; |
| break; |
| case 'E': |
| is_numeric = false; |
| offset = 1; |
| calendar_field = Calendar.DAY_OF_WEEK; |
| set1 = formatData.getWeekdays(); |
| set2 = formatData.getShortWeekdays(); |
| break; |
| case 'w': |
| calendar_field = Calendar.WEEK_OF_YEAR; |
| break; |
| case 'W': |
| calendar_field = Calendar.WEEK_OF_MONTH; |
| break; |
| case 'M': |
| calendar_field = Calendar.MONTH; |
| if (fmt_count <= 2) |
| offset = -1; |
| else |
| { |
| is_numeric = false; |
| set1 = formatData.getMonths(); |
| set2 = formatData.getShortMonths(); |
| } |
| break; |
| case 'y': |
| calendar_field = Calendar.YEAR; |
| if (fmt_count <= 2) |
| maybe2DigitYear = true; |
| break; |
| case 'K': |
| calendar_field = Calendar.HOUR; |
| break; |
| case 'h': |
| calendar_field = Calendar.HOUR; |
| oneBasedHour = true; |
| break; |
| case 'H': |
| calendar_field = Calendar.HOUR_OF_DAY; |
| break; |
| case 'k': |
| calendar_field = Calendar.HOUR_OF_DAY; |
| oneBasedHourOfDay = true; |
| break; |
| case 'm': |
| calendar_field = Calendar.MINUTE; |
| break; |
| case 's': |
| calendar_field = Calendar.SECOND; |
| break; |
| case 'S': |
| calendar_field = Calendar.MILLISECOND; |
| break; |
| case 'a': |
| is_numeric = false; |
| calendar_field = Calendar.AM_PM; |
| set1 = formatData.getAmPmStrings(); |
| break; |
| case 'z': |
| case 'Z': |
| // We need a special case for the timezone, because it |
| // uses a different data structure than the other cases. |
| is_numeric = false; |
| calendar_field = Calendar.ZONE_OFFSET; |
| String[][] zoneStrings = formatData.getZoneStrings(); |
| int zoneCount = zoneStrings.length; |
| int index = pos.getIndex(); |
| boolean found_zone = false; |
| simpleOffset = computeOffset(dateStr.substring(index), pos); |
| if (simpleOffset != null) |
| { |
| found_zone = true; |
| saw_timezone = true; |
| calendar.set(Calendar.DST_OFFSET, 0); |
| offset = simpleOffset.intValue(); |
| } |
| else |
| { |
| for (int j = 0; j < zoneCount; j++) |
| { |
| String[] strings = zoneStrings[j]; |
| int k; |
| for (k = 0; k < strings.length; ++k) |
| { |
| if (dateStr.startsWith(strings[k], index)) |
| break; |
| } |
| if (k != strings.length) |
| { |
| found_zone = true; |
| saw_timezone = true; |
| TimeZone tz = TimeZone.getTimeZone (strings[0]); |
| // Check if it's a DST zone or ordinary |
| if(k == 3 || k == 4) |
| calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings()); |
| else |
| calendar.set (Calendar.DST_OFFSET, 0); |
| offset = tz.getRawOffset (); |
| pos.setIndex(index + strings[k].length()); |
| break; |
| } |
| } |
| } |
| if (! found_zone) |
| { |
| pos.setErrorIndex(pos.getIndex()); |
| return null; |
| } |
| break; |
| default: |
| pos.setErrorIndex(pos.getIndex()); |
| return null; |
| } |
| |
| // Compute the value we should assign to the field. |
| int value; |
| int index = -1; |
| if (is_numeric) |
| { |
| numberFormat.setMinimumIntegerDigits(fmt_count); |
| if (limit_digits) |
| numberFormat.setMaximumIntegerDigits(fmt_count); |
| if (maybe2DigitYear) |
| index = pos.getIndex(); |
| Number n = numberFormat.parse(dateStr, pos); |
| if (pos == null || ! (n instanceof Long)) |
| return null; |
| value = n.intValue() + offset; |
| } |
| else if (set1 != null) |
| { |
| index = pos.getIndex(); |
| int i; |
| boolean found = false; |
| for (i = offset; i < set1.length; ++i) |
| { |
| if (set1[i] != null) |
| if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(), |
| index)) |
| { |
| found = true; |
| pos.setIndex(index + set1[i].length()); |
| break; |
| } |
| } |
| if (!found && set2 != null) |
| { |
| for (i = offset; i < set2.length; ++i) |
| { |
| if (set2[i] != null) |
| if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(), |
| index)) |
| { |
| found = true; |
| pos.setIndex(index + set2[i].length()); |
| break; |
| } |
| } |
| } |
| if (!found) |
| { |
| pos.setErrorIndex(index); |
| return null; |
| } |
| value = i; |
| } |
| else |
| value = offset; |
| |
| if (maybe2DigitYear) |
| { |
| // Parse into default century if the numeric year string has |
| // exactly 2 digits. |
| int digit_count = pos.getIndex() - index; |
| if (digit_count == 2) |
| { |
| is2DigitYear = true; |
| value += defaultCentury; |
| } |
| } |
| |
| // Calendar uses 0-based hours. |
| // I.e. 00:00 AM is midnight, not 12 AM or 24:00 |
| if (oneBasedHour && value == 12) |
| value = 0; |
| |
| if (oneBasedHourOfDay && value == 24) |
| value = 0; |
| |
| // Assign the value and move on. |
| calendar.set(calendar_field, value); |
| } |
| |
| if (is2DigitYear) |
| { |
| // Apply the 80-20 heuristic to dermine the full year based on |
| // defaultCenturyStart. |
| int year = calendar.get(Calendar.YEAR); |
| if (calendar.getTime().compareTo(defaultCenturyStart) < 0) |
| calendar.set(Calendar.YEAR, year + 100); |
| } |
| if (! saw_timezone) |
| { |
| // Use the real rules to determine whether or not this |
| // particular time is in daylight savings. |
| calendar.clear (Calendar.DST_OFFSET); |
| calendar.clear (Calendar.ZONE_OFFSET); |
| } |
| return calendar.getTime(); |
| } |
| catch (IllegalArgumentException x) |
| { |
| pos.setErrorIndex(pos.getIndex()); |
| return null; |
| } |
| } |
| |
| /** |
| * <p> |
| * Computes the time zone offset in milliseconds |
| * relative to GMT, based on the supplied |
| * <code>String</code> representation. |
| * </p> |
| * <p> |
| * The supplied <code>String</code> must be a three |
| * or four digit signed number, with an optional 'GMT' |
| * prefix. The first one or two digits represents the hours, |
| * while the last two represent the minutes. The |
| * two sets of digits can optionally be separated by |
| * ':'. The mandatory sign prefix (either '+' or '-') |
| * indicates the direction of the offset from GMT. |
| * </p> |
| * <p> |
| * For example, 'GMT+0200' specifies 2 hours after |
| * GMT, while '-05:00' specifies 5 hours prior to |
| * GMT. The special case of 'GMT' alone can be used |
| * to represent the offset, 0. |
| * </p> |
| * <p> |
| * If the <code>String</code> can not be parsed, |
| * the result will be null. The resulting offset |
| * is wrapped in an <code>Integer</code> object, in |
| * order to allow such failure to be represented. |
| * </p> |
| * |
| * @param zoneString a string in the form |
| * (GMT)? sign hours : minutes |
| * where sign = '+' or '-', hours |
| * is a one or two digits representing |
| * a number between 0 and 23, and |
| * minutes is two digits representing |
| * a number between 0 and 59. |
| * @return the parsed offset, or null if parsing |
| * failed. |
| */ |
| private Integer computeOffset(String zoneString, ParsePosition pos) |
| { |
| Pattern pattern = |
| Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})"); |
| Matcher matcher = pattern.matcher(zoneString); |
| |
| // Match from start, but ignore trailing parts |
| boolean hasAll = matcher.lookingAt(); |
| try |
| { |
| // Do we have at least the sign, hour and minute? |
| matcher.group(2); |
| matcher.group(4); |
| matcher.group(5); |
| } |
| catch (IllegalStateException ise) |
| { |
| hasAll = false; |
| } |
| if (hasAll) |
| { |
| int sign = matcher.group(2).equals("+") ? 1 : -1; |
| int hour = Integer.parseInt(matcher.group(4)); |
| if (!matcher.group(3).equals("")) |
| hour += (Integer.parseInt(matcher.group(3)) * 10); |
| int minutes = Integer.parseInt(matcher.group(5)); |
| |
| if (hour > 23) |
| return null; |
| int offset = sign * ((hour * 60) + minutes) * 60000; |
| |
| // advance the index |
| pos.setIndex(pos.getIndex() + matcher.end()); |
| return new Integer(offset); |
| } |
| else if (zoneString.startsWith("GMT")) |
| { |
| pos.setIndex(pos.getIndex() + 3); |
| return new Integer(0); |
| } |
| return null; |
| } |
| |
| // Compute the start of the current century as defined by |
| // get2DigitYearStart. |
| private void computeCenturyStart() |
| { |
| int year = calendar.get(Calendar.YEAR); |
| calendar.set(Calendar.YEAR, year - 80); |
| set2DigitYearStart(calendar.getTime()); |
| } |
| |
| /** |
| * Returns a copy of this instance of |
| * <code>SimpleDateFormat</code>. The copy contains |
| * clones of the formatting symbols and the 2-digit |
| * year century start date. |
| */ |
| public Object clone() |
| { |
| SimpleDateFormat clone = (SimpleDateFormat) super.clone(); |
| clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone()); |
| clone.set2DigitYearStart((Date) defaultCenturyStart.clone()); |
| return clone; |
| } |
| |
| } |