| /* 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; | |
| } | |
| } |