001 /* SimpleDateFormat.java -- A class for parsing/formating simple
002 date constructs
003 Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005
004 Free Software Foundation, Inc.
005
006 This file is part of GNU Classpath.
007
008 GNU Classpath is free software; you can redistribute it and/or modify
009 it under the terms of the GNU General Public License as published by
010 the Free Software Foundation; either version 2, or (at your option)
011 any later version.
012
013 GNU Classpath is distributed in the hope that it will be useful, but
014 WITHOUT ANY WARRANTY; without even the implied warranty of
015 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
016 General Public License for more details.
017
018 You should have received a copy of the GNU General Public License
019 along with GNU Classpath; see the file COPYING. If not, write to the
020 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
021 02110-1301 USA.
022
023 Linking this library statically or dynamically with other modules is
024 making a combined work based on this library. Thus, the terms and
025 conditions of the GNU General Public License cover the whole
026 combination.
027
028 As a special exception, the copyright holders of this library give you
029 permission to link this library with independent modules to produce an
030 executable, regardless of the license terms of these independent
031 modules, and to copy and distribute the resulting executable under
032 terms of your choice, provided that you also meet, for each linked
033 independent module, the terms and conditions of the license of that
034 module. An independent module is a module which is not derived from
035 or based on this library. If you modify this library, you may extend
036 this exception to your version of the library, but you are not
037 obligated to do so. If you do not wish to do so, delete this
038 exception statement from your version. */
039
040
041 package java.text;
042
043 import gnu.java.lang.CPStringBuilder;
044
045 import gnu.java.text.AttributedFormatBuffer;
046 import gnu.java.text.FormatBuffer;
047 import gnu.java.text.FormatCharacterIterator;
048 import gnu.java.text.StringFormatBuffer;
049
050 import java.io.IOException;
051 import java.io.InvalidObjectException;
052 import java.io.ObjectInputStream;
053 import java.util.ArrayList;
054 import java.util.Calendar;
055 import java.util.Date;
056 import java.util.GregorianCalendar;
057 import java.util.Iterator;
058 import java.util.Locale;
059 import java.util.TimeZone;
060 import java.util.regex.Matcher;
061 import java.util.regex.Pattern;
062
063 /**
064 * SimpleDateFormat provides convenient methods for parsing and formatting
065 * dates using Gregorian calendars (see java.util.GregorianCalendar).
066 * This class is not thread-safe; external synchronisation should be applied
067 * if an instance is to be accessed from multiple threads.
068 */
069 public class SimpleDateFormat extends DateFormat
070 {
071 /**
072 * This class is used by <code>SimpleDateFormat</code> as a
073 * compiled representation of a format string. The field
074 * ID, size, and character used are stored for each sequence
075 * of pattern characters.
076 */
077 private class CompiledField
078 {
079 /**
080 * The ID of the field within the local pattern characters.
081 * Package private for use in out class.
082 */
083 int field;
084
085 /**
086 * The size of the character sequence.
087 * Package private for use in out class.
088 */
089 int size;
090
091 /**
092 * The character used.
093 */
094 private char character;
095
096 /**
097 * Constructs a compiled field using the
098 * the given field ID, size and character
099 * values.
100 *
101 * @param f the field ID.
102 * @param s the size of the field.
103 * @param c the character used.
104 */
105 public CompiledField(int f, int s, char c)
106 {
107 field = f;
108 size = s;
109 character = c;
110 }
111
112 /**
113 * Retrieves the ID of the field relative to
114 * the local pattern characters.
115 */
116 public int getField()
117 {
118 return field;
119 }
120
121 /**
122 * Retrieves the size of the character sequence.
123 */
124 public int getSize()
125 {
126 return size;
127 }
128
129 /**
130 * Retrieves the character used in the sequence.
131 */
132 public char getCharacter()
133 {
134 return character;
135 }
136
137 /**
138 * Returns a <code>String</code> representation
139 * of the compiled field, primarily for debugging
140 * purposes.
141 *
142 * @return a <code>String</code> representation.
143 */
144 public String toString()
145 {
146 CPStringBuilder builder;
147
148 builder = new CPStringBuilder(getClass().getName());
149 builder.append("[field=");
150 builder.append(field);
151 builder.append(", size=");
152 builder.append(size);
153 builder.append(", character=");
154 builder.append(character);
155 builder.append("]");
156
157 return builder.toString();
158 }
159 }
160
161 /**
162 * A list of <code>CompiledField</code>s and {@code String}s
163 * representing the compiled version of the pattern.
164 *
165 * @see CompiledField
166 * @serial Ignored.
167 */
168 private transient ArrayList<Object> tokens;
169
170 /**
171 * The localised data used in formatting,
172 * such as the day and month names in the local
173 * language, and the localized pattern characters.
174 *
175 * @see DateFormatSymbols
176 * @serial The localisation data. May not be null.
177 */
178 private DateFormatSymbols formatData;
179
180 /**
181 * The date representing the start of the century
182 * used for interpreting two digit years. For
183 * example, 24/10/2004 would cause two digit
184 * years to be interpreted as representing
185 * the years between 2004 and 2104.
186 *
187 * @see #get2DigitYearStart()
188 * @see #set2DigitYearStart(java.util.Date)
189 * @see Date
190 * @serial The start date of the century for parsing two digit years.
191 * May not be null.
192 */
193 private Date defaultCenturyStart;
194
195 /**
196 * The year at which interpretation of two
197 * digit years starts.
198 *
199 * @see #get2DigitYearStart()
200 * @see #set2DigitYearStart(java.util.Date)
201 * @serial Ignored.
202 */
203 private transient int defaultCentury;
204
205 /**
206 * The non-localized pattern string. This
207 * only ever contains the pattern characters
208 * stored in standardChars. Localized patterns
209 * are translated to this form.
210 *
211 * @see #applyPattern(String)
212 * @see #applyLocalizedPattern(String)
213 * @see #toPattern()
214 * @see #toLocalizedPattern()
215 * @serial The non-localized pattern string. May not be null.
216 */
217 private String pattern;
218
219 /**
220 * The version of serialized data used by this class.
221 * Version 0 only includes the pattern and formatting
222 * data. Version 1 adds the start date for interpreting
223 * two digit years.
224 *
225 * @serial This specifies the version of the data being serialized.
226 * Version 0 (or no version) specifies just <code>pattern</code>
227 * and <code>formatData</code>. Version 1 adds
228 * the <code>defaultCenturyStart</code>. This implementation
229 * always writes out version 1 data.
230 */
231 private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
232
233 /**
234 * For compatability.
235 */
236 private static final long serialVersionUID = 4774881970558875024L;
237
238 // This string is specified in the root of the CLDR.
239 private static final String standardChars = "GyMdkHmsSEDFwWahKzYeugAZvcL";
240
241 /**
242 * Represents the position of the RFC822 timezone pattern character
243 * in the array of localized pattern characters. In the
244 * U.S. locale, this is 'Z'. The value is the offset of the current
245 * time from GMT e.g. -0500 would be five hours prior to GMT.
246 */
247 private static final int RFC822_TIMEZONE_FIELD = 23;
248
249 /**
250 * Reads the serialized version of this object.
251 * If the serialized data is only version 0,
252 * then the date for the start of the century
253 * for interpreting two digit years is computed.
254 * The pattern is parsed and compiled following the process
255 * of reading in the serialized data.
256 *
257 * @param stream the object stream to read the data from.
258 * @throws IOException if an I/O error occurs.
259 * @throws ClassNotFoundException if the class of the serialized data
260 * could not be found.
261 * @throws InvalidObjectException if the pattern is invalid.
262 */
263 private void readObject(ObjectInputStream stream)
264 throws IOException, ClassNotFoundException
265 {
266 stream.defaultReadObject();
267 if (serialVersionOnStream < 1)
268 {
269 computeCenturyStart ();
270 serialVersionOnStream = 1;
271 }
272 else
273 // Ensure that defaultCentury gets set.
274 set2DigitYearStart(defaultCenturyStart);
275
276 // Set up items normally taken care of by the constructor.
277 tokens = new ArrayList<Object>();
278 try
279 {
280 compileFormat(pattern);
281 }
282 catch (IllegalArgumentException e)
283 {
284 throw new InvalidObjectException("The stream pattern was invalid.");
285 }
286 }
287
288 /**
289 * Compiles the supplied non-localized pattern into a form
290 * from which formatting and parsing can be performed.
291 * This also detects errors in the pattern, which will
292 * be raised on later use of the compiled data.
293 *
294 * @param pattern the non-localized pattern to compile.
295 * @throws IllegalArgumentException if the pattern is invalid.
296 */
297 private void compileFormat(String pattern)
298 {
299 // Any alphabetical characters are treated as pattern characters
300 // unless enclosed in single quotes.
301
302 char thisChar;
303 int pos;
304 int field;
305 CompiledField current = null;
306
307 for (int i = 0; i < pattern.length(); i++)
308 {
309 thisChar = pattern.charAt(i);
310 field = standardChars.indexOf(thisChar);
311 if (field == -1)
312 {
313 current = null;
314 if ((thisChar >= 'A' && thisChar <= 'Z')
315 || (thisChar >= 'a' && thisChar <= 'z'))
316 {
317 // Not a valid letter
318 throw new IllegalArgumentException("Invalid letter "
319 + thisChar +
320 " encountered at character "
321 + i + ".");
322 }
323 else if (thisChar == '\'')
324 {
325 // Quoted text section; skip to next single quote
326 pos = pattern.indexOf('\'', i + 1);
327 // First look for '' -- meaning a single quote.
328 if (pos == i + 1)
329 tokens.add("'");
330 else
331 {
332 // Look for the terminating quote. However, if we
333 // see a '', that represents a literal quote and
334 // we must iterate.
335 CPStringBuilder buf = new CPStringBuilder();
336 int oldPos = i + 1;
337 do
338 {
339 if (pos == -1)
340 throw new IllegalArgumentException("Quotes starting at character "
341 + i +
342 " not closed.");
343 buf.append(pattern.substring(oldPos, pos));
344 if (pos + 1 >= pattern.length()
345 || pattern.charAt(pos + 1) != '\'')
346 break;
347 buf.append('\'');
348 oldPos = pos + 2;
349 pos = pattern.indexOf('\'', pos + 2);
350 }
351 while (true);
352 tokens.add(buf.toString());
353 }
354 i = pos;
355 }
356 else
357 {
358 // A special character
359 tokens.add(Character.valueOf(thisChar));
360 }
361 }
362 else
363 {
364 // A valid field
365 if ((current != null) && (field == current.field))
366 current.size++;
367 else
368 {
369 current = new CompiledField(field, 1, thisChar);
370 tokens.add(current);
371 }
372 }
373 }
374 }
375
376 /**
377 * Returns a string representation of this
378 * class.
379 *
380 * @return a string representation of the <code>SimpleDateFormat</code>
381 * instance.
382 */
383 public String toString()
384 {
385 CPStringBuilder output = new CPStringBuilder(getClass().getName());
386 output.append("[tokens=");
387 output.append(tokens);
388 output.append(", formatData=");
389 output.append(formatData);
390 output.append(", defaultCenturyStart=");
391 output.append(defaultCenturyStart);
392 output.append(", defaultCentury=");
393 output.append(defaultCentury);
394 output.append(", pattern=");
395 output.append(pattern);
396 output.append(", serialVersionOnStream=");
397 output.append(serialVersionOnStream);
398 output.append(", standardChars=");
399 output.append(standardChars);
400 output.append("]");
401 return output.toString();
402 }
403
404 /**
405 * Constructs a SimpleDateFormat using the default pattern for
406 * the default locale.
407 */
408 public SimpleDateFormat()
409 {
410 /*
411 * There does not appear to be a standard API for determining
412 * what the default pattern for a locale is, so use package-scope
413 * variables in DateFormatSymbols to encapsulate this.
414 */
415 super();
416 Locale locale = Locale.getDefault();
417 calendar = new GregorianCalendar(locale);
418 computeCenturyStart();
419 tokens = new ArrayList<Object>();
420 formatData = new DateFormatSymbols(locale);
421 pattern = (formatData.dateFormats[DEFAULT] + ' '
422 + formatData.timeFormats[DEFAULT]);
423 compileFormat(pattern);
424 numberFormat = NumberFormat.getInstance(locale);
425 numberFormat.setGroupingUsed (false);
426 numberFormat.setParseIntegerOnly (true);
427 numberFormat.setMaximumFractionDigits (0);
428 }
429
430 /**
431 * Creates a date formatter using the specified non-localized pattern,
432 * with the default DateFormatSymbols for the default locale.
433 *
434 * @param pattern the pattern to use.
435 * @throws NullPointerException if the pattern is null.
436 * @throws IllegalArgumentException if the pattern is invalid.
437 */
438 public SimpleDateFormat(String pattern)
439 {
440 this(pattern, Locale.getDefault());
441 }
442
443 /**
444 * Creates a date formatter using the specified non-localized pattern,
445 * with the default DateFormatSymbols for the given locale.
446 *
447 * @param pattern the non-localized pattern to use.
448 * @param locale the locale to use for the formatting symbols.
449 * @throws NullPointerException if the pattern is null.
450 * @throws IllegalArgumentException if the pattern is invalid.
451 */
452 public SimpleDateFormat(String pattern, Locale locale)
453 {
454 super();
455 calendar = new GregorianCalendar(locale);
456 computeCenturyStart();
457 tokens = new ArrayList<Object>();
458 formatData = new DateFormatSymbols(locale);
459 compileFormat(pattern);
460 this.pattern = pattern;
461 numberFormat = NumberFormat.getInstance(locale);
462 numberFormat.setGroupingUsed (false);
463 numberFormat.setParseIntegerOnly (true);
464 numberFormat.setMaximumFractionDigits (0);
465 }
466
467 /**
468 * Creates a date formatter using the specified non-localized
469 * pattern. The specified DateFormatSymbols will be used when
470 * formatting.
471 *
472 * @param pattern the non-localized pattern to use.
473 * @param formatData the formatting symbols to use.
474 * @throws NullPointerException if the pattern or formatData is null.
475 * @throws IllegalArgumentException if the pattern is invalid.
476 */
477 public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
478 {
479 super();
480 calendar = new GregorianCalendar();
481 computeCenturyStart ();
482 tokens = new ArrayList<Object>();
483 if (formatData == null)
484 throw new NullPointerException("formatData");
485 this.formatData = formatData;
486 compileFormat(pattern);
487 this.pattern = pattern;
488 numberFormat = NumberFormat.getInstance();
489 numberFormat.setGroupingUsed (false);
490 numberFormat.setParseIntegerOnly (true);
491 numberFormat.setMaximumFractionDigits (0);
492 }
493
494 /**
495 * This method returns a string with the formatting pattern being used
496 * by this object. This string is unlocalized.
497 *
498 * @return The format string.
499 */
500 public String toPattern()
501 {
502 return pattern;
503 }
504
505 /**
506 * This method returns a string with the formatting pattern being used
507 * by this object. This string is localized.
508 *
509 * @return The format string.
510 */
511 public String toLocalizedPattern()
512 {
513 String localChars = formatData.getLocalPatternChars();
514 return translateLocalizedPattern(pattern, standardChars, localChars);
515 }
516
517 /**
518 * This method sets the formatting pattern that should be used by this
519 * object. This string is not localized.
520 *
521 * @param pattern The new format pattern.
522 * @throws NullPointerException if the pattern is null.
523 * @throws IllegalArgumentException if the pattern is invalid.
524 */
525 public void applyPattern(String pattern)
526 {
527 tokens.clear();
528 compileFormat(pattern);
529 this.pattern = pattern;
530 }
531
532 /**
533 * This method sets the formatting pattern that should be used by this
534 * object. This string is localized.
535 *
536 * @param pattern The new format pattern.
537 * @throws NullPointerException if the pattern is null.
538 * @throws IllegalArgumentException if the pattern is invalid.
539 */
540 public void applyLocalizedPattern(String pattern)
541 {
542 String localChars = formatData.getLocalPatternChars();
543 pattern = translateLocalizedPattern(pattern, localChars, standardChars);
544 applyPattern(pattern);
545 }
546
547 /**
548 * Translates either from or to a localized variant of the pattern
549 * string. For example, in the German locale, 't' (for 'tag') is
550 * used instead of 'd' (for 'date'). This method translates
551 * a localized pattern (such as 'ttt') to a non-localized pattern
552 * (such as 'ddd'), or vice versa. Non-localized patterns use
553 * a standard set of characters, which match those of the U.S. English
554 * locale.
555 *
556 * @param pattern the pattern to translate.
557 * @param oldChars the old set of characters (used in the pattern).
558 * @param newChars the new set of characters (which will be used in the
559 * pattern).
560 * @return a version of the pattern using the characters in
561 * <code>newChars</code>.
562 */
563 private String translateLocalizedPattern(String pattern,
564 String oldChars, String newChars)
565 {
566 int len = pattern.length();
567 CPStringBuilder buf = new CPStringBuilder(len);
568 boolean quoted = false;
569 for (int i = 0; i < len; i++)
570 {
571 char ch = pattern.charAt(i);
572 if (ch == '\'')
573 quoted = ! quoted;
574 if (! quoted)
575 {
576 int j = oldChars.indexOf(ch);
577 if (j >= 0)
578 ch = newChars.charAt(j);
579 }
580 buf.append(ch);
581 }
582 return buf.toString();
583 }
584
585 /**
586 * Returns the start of the century used for two digit years.
587 *
588 * @return A <code>Date</code> representing the start of the century
589 * for two digit years.
590 */
591 public Date get2DigitYearStart()
592 {
593 return defaultCenturyStart;
594 }
595
596 /**
597 * Sets the start of the century used for two digit years.
598 *
599 * @param date A <code>Date</code> representing the start of the century for
600 * two digit years.
601 */
602 public void set2DigitYearStart(Date date)
603 {
604 defaultCenturyStart = date;
605 calendar.clear();
606 calendar.setTime(date);
607 int year = calendar.get(Calendar.YEAR);
608 defaultCentury = year - (year % 100);
609 }
610
611 /**
612 * This method returns a copy of the format symbol information used
613 * for parsing and formatting dates.
614 *
615 * @return a copy of the date format symbols.
616 */
617 public DateFormatSymbols getDateFormatSymbols()
618 {
619 return (DateFormatSymbols) formatData.clone();
620 }
621
622 /**
623 * This method sets the format symbols information used for parsing
624 * and formatting dates.
625 *
626 * @param formatData The date format symbols.
627 * @throws NullPointerException if <code>formatData</code> is null.
628 */
629 public void setDateFormatSymbols(DateFormatSymbols formatData)
630 {
631 if (formatData == null)
632 {
633 throw new
634 NullPointerException("The supplied format data was null.");
635 }
636 this.formatData = formatData;
637 }
638
639 /**
640 * This methods tests whether the specified object is equal to this
641 * object. This will be true if and only if the specified object:
642 * <p>
643 * <ul>
644 * <li>Is not <code>null</code>.</li>
645 * <li>Is an instance of <code>SimpleDateFormat</code>.</li>
646 * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
647 * level.</li>
648 * <li>Has the same formatting pattern.</li>
649 * <li>Is using the same formatting symbols.</li>
650 * <li>Is using the same century for two digit years.</li>
651 * </ul>
652 *
653 * @param o The object to compare for equality against.
654 *
655 * @return <code>true</code> if the specified object is equal to this object,
656 * <code>false</code> otherwise.
657 */
658 public boolean equals(Object o)
659 {
660 if (!super.equals(o))
661 return false;
662
663 if (!(o instanceof SimpleDateFormat))
664 return false;
665
666 SimpleDateFormat sdf = (SimpleDateFormat)o;
667
668 if (defaultCentury != sdf.defaultCentury)
669 return false;
670
671 if (!toPattern().equals(sdf.toPattern()))
672 return false;
673
674 if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
675 return false;
676
677 return true;
678 }
679
680 /**
681 * This method returns a hash value for this object.
682 *
683 * @return A hash value for this object.
684 */
685 public int hashCode()
686 {
687 return super.hashCode() ^ toPattern().hashCode() ^ defaultCentury ^
688 getDateFormatSymbols().hashCode();
689 }
690
691
692 /**
693 * Formats the date input according to the format string in use,
694 * appending to the specified StringBuffer. The input StringBuffer
695 * is returned as output for convenience.
696 */
697 private void formatWithAttribute(Date date, FormatBuffer buffer, FieldPosition pos)
698 {
699 String temp;
700 calendar.setTime(date);
701
702 // go through vector, filling in fields where applicable, else toString
703 Iterator<Object> iter = tokens.iterator();
704 while (iter.hasNext())
705 {
706 Object o = iter.next();
707 if (o instanceof CompiledField)
708 {
709 CompiledField cf = (CompiledField) o;
710 int beginIndex = buffer.length();
711
712 switch (cf.getField())
713 {
714 case ERA_FIELD:
715 buffer.append (formatData.eras[calendar.get (Calendar.ERA)], DateFormat.Field.ERA);
716 break;
717 case YEAR_FIELD:
718 // If we have two digits, then we truncate. Otherwise, we
719 // use the size of the pattern, and zero pad.
720 buffer.setDefaultAttribute (DateFormat.Field.YEAR);
721 if (cf.getSize() == 2)
722 {
723 temp = "00"+String.valueOf (calendar.get (Calendar.YEAR));
724 buffer.append (temp.substring (temp.length() - 2));
725 }
726 else
727 withLeadingZeros (calendar.get (Calendar.YEAR), cf.getSize(), buffer);
728 break;
729 case MONTH_FIELD:
730 buffer.setDefaultAttribute (DateFormat.Field.MONTH);
731 if (cf.getSize() < 3)
732 withLeadingZeros (calendar.get (Calendar.MONTH) + 1, cf.getSize(), buffer);
733 else if (cf.getSize() < 4)
734 buffer.append (formatData.shortMonths[calendar.get (Calendar.MONTH)]);
735 else
736 buffer.append (formatData.months[calendar.get (Calendar.MONTH)]);
737 break;
738 case DATE_FIELD:
739 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_MONTH);
740 withLeadingZeros (calendar.get (Calendar.DATE), cf.getSize(), buffer);
741 break;
742 case HOUR_OF_DAY1_FIELD: // 1-24
743 buffer.setDefaultAttribute(DateFormat.Field.HOUR_OF_DAY1);
744 withLeadingZeros ( ((calendar.get (Calendar.HOUR_OF_DAY) + 23) % 24) + 1,
745 cf.getSize(), buffer);
746 break;
747 case HOUR_OF_DAY0_FIELD: // 0-23
748 buffer.setDefaultAttribute (DateFormat.Field.HOUR_OF_DAY0);
749 withLeadingZeros (calendar.get (Calendar.HOUR_OF_DAY), cf.getSize(), buffer);
750 break;
751 case MINUTE_FIELD:
752 buffer.setDefaultAttribute (DateFormat.Field.MINUTE);
753 withLeadingZeros (calendar.get (Calendar.MINUTE),
754 cf.getSize(), buffer);
755 break;
756 case SECOND_FIELD:
757 buffer.setDefaultAttribute (DateFormat.Field.SECOND);
758 withLeadingZeros(calendar.get (Calendar.SECOND),
759 cf.getSize(), buffer);
760 break;
761 case MILLISECOND_FIELD:
762 buffer.setDefaultAttribute (DateFormat.Field.MILLISECOND);
763 withLeadingZeros (calendar.get (Calendar.MILLISECOND), cf.getSize(), buffer);
764 break;
765 case DAY_OF_WEEK_FIELD:
766 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK);
767 if (cf.getSize() < 4)
768 buffer.append (formatData.shortWeekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
769 else
770 buffer.append (formatData.weekdays[calendar.get (Calendar.DAY_OF_WEEK)]);
771 break;
772 case DAY_OF_YEAR_FIELD:
773 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_YEAR);
774 withLeadingZeros (calendar.get (Calendar.DAY_OF_YEAR), cf.getSize(), buffer);
775 break;
776 case DAY_OF_WEEK_IN_MONTH_FIELD:
777 buffer.setDefaultAttribute (DateFormat.Field.DAY_OF_WEEK_IN_MONTH);
778 withLeadingZeros (calendar.get (Calendar.DAY_OF_WEEK_IN_MONTH),
779 cf.getSize(), buffer);
780 break;
781 case WEEK_OF_YEAR_FIELD:
782 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_YEAR);
783 withLeadingZeros (calendar.get (Calendar.WEEK_OF_YEAR),
784 cf.getSize(), buffer);
785 break;
786 case WEEK_OF_MONTH_FIELD:
787 buffer.setDefaultAttribute (DateFormat.Field.WEEK_OF_MONTH);
788 withLeadingZeros (calendar.get (Calendar.WEEK_OF_MONTH),
789 cf.getSize(), buffer);
790 break;
791 case AM_PM_FIELD:
792 buffer.setDefaultAttribute (DateFormat.Field.AM_PM);
793 buffer.append (formatData.ampms[calendar.get (Calendar.AM_PM)]);
794 break;
795 case HOUR1_FIELD: // 1-12
796 buffer.setDefaultAttribute (DateFormat.Field.HOUR1);
797 withLeadingZeros (((calendar.get (Calendar.HOUR) + 11) % 12) + 1,
798 cf.getSize(), buffer);
799 break;
800 case HOUR0_FIELD: // 0-11
801 buffer.setDefaultAttribute (DateFormat.Field.HOUR0);
802 withLeadingZeros (calendar.get (Calendar.HOUR), cf.getSize(), buffer);
803 break;
804 case TIMEZONE_FIELD:
805 buffer.setDefaultAttribute (DateFormat.Field.TIME_ZONE);
806 TimeZone zone = calendar.getTimeZone();
807 boolean isDST = calendar.get (Calendar.DST_OFFSET) != 0;
808 // FIXME: XXX: This should be a localized time zone.
809 String zoneID = zone.getDisplayName
810 (isDST, cf.getSize() > 3 ? TimeZone.LONG : TimeZone.SHORT);
811 buffer.append (zoneID);
812 break;
813 case RFC822_TIMEZONE_FIELD:
814 buffer.setDefaultAttribute(DateFormat.Field.TIME_ZONE);
815 int pureMinutes = (calendar.get(Calendar.ZONE_OFFSET) +
816 calendar.get(Calendar.DST_OFFSET)) / (1000 * 60);
817 String sign = (pureMinutes < 0) ? "-" : "+";
818 pureMinutes = Math.abs(pureMinutes);
819 int hours = pureMinutes / 60;
820 int minutes = pureMinutes % 60;
821 buffer.append(sign);
822 withLeadingZeros(hours, 2, buffer);
823 withLeadingZeros(minutes, 2, buffer);
824 break;
825 default:
826 throw new IllegalArgumentException ("Illegal pattern character " +
827 cf.getCharacter());
828 }
829 if (pos != null && (buffer.getDefaultAttribute() == pos.getFieldAttribute()
830 || cf.getField() == pos.getField()))
831 {
832 pos.setBeginIndex(beginIndex);
833 pos.setEndIndex(buffer.length());
834 }
835 }
836 else
837 {
838 buffer.append(o.toString(), null);
839 }
840 }
841 }
842
843 public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
844 {
845 formatWithAttribute(date, new StringFormatBuffer (buffer), pos);
846
847 return buffer;
848 }
849
850 public AttributedCharacterIterator formatToCharacterIterator(Object date)
851 throws IllegalArgumentException
852 {
853 if (date == null)
854 throw new NullPointerException("null argument");
855 if (!(date instanceof Date))
856 throw new IllegalArgumentException("argument should be an instance of java.util.Date");
857
858 AttributedFormatBuffer buf = new AttributedFormatBuffer();
859 formatWithAttribute((Date)date, buf,
860 null);
861 buf.sync();
862
863 return new FormatCharacterIterator(buf.getBuffer().toString(),
864 buf.getRanges(),
865 buf.getAttributes());
866 }
867
868 private void withLeadingZeros(int value, int length, FormatBuffer buffer)
869 {
870 String valStr = String.valueOf(value);
871 for (length -= valStr.length(); length > 0; length--)
872 buffer.append('0');
873 buffer.append(valStr);
874 }
875
876 private boolean expect(String source, ParsePosition pos, char ch)
877 {
878 int x = pos.getIndex();
879 boolean r = x < source.length() && source.charAt(x) == ch;
880 if (r)
881 pos.setIndex(x + 1);
882 else
883 pos.setErrorIndex(x);
884 return r;
885 }
886
887 /**
888 * This method parses the specified string into a date.
889 *
890 * @param dateStr The date string to parse.
891 * @param pos The input and output parse position
892 *
893 * @return The parsed date, or <code>null</code> if the string cannot be
894 * parsed.
895 */
896 public Date parse (String dateStr, ParsePosition pos)
897 {
898 int fmt_index = 0;
899 int fmt_max = pattern.length();
900
901 calendar.clear();
902 boolean saw_timezone = false;
903 int quote_start = -1;
904 boolean is2DigitYear = false;
905 try
906 {
907 for (; fmt_index < fmt_max; ++fmt_index)
908 {
909 char ch = pattern.charAt(fmt_index);
910 if (ch == '\'')
911 {
912 if (fmt_index < fmt_max - 1
913 && pattern.charAt(fmt_index + 1) == '\'')
914 {
915 if (! expect (dateStr, pos, ch))
916 return null;
917 ++fmt_index;
918 }
919 else
920 quote_start = quote_start < 0 ? fmt_index : -1;
921 continue;
922 }
923
924 if (quote_start != -1
925 || ((ch < 'a' || ch > 'z')
926 && (ch < 'A' || ch > 'Z')))
927 {
928 if (quote_start == -1 && ch == ' ')
929 {
930 // A single unquoted space in the pattern may match
931 // any number of spaces in the input.
932 int index = pos.getIndex();
933 int save = index;
934 while (index < dateStr.length()
935 && Character.isWhitespace(dateStr.charAt(index)))
936 ++index;
937 if (index > save)
938 pos.setIndex(index);
939 else
940 {
941 // Didn't see any whitespace.
942 pos.setErrorIndex(index);
943 return null;
944 }
945 }
946 else if (! expect (dateStr, pos, ch))
947 return null;
948 continue;
949 }
950
951 // We've arrived at a potential pattern character in the
952 // pattern.
953 int fmt_count = 1;
954 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
955 {
956 ++fmt_count;
957 }
958
959 // We might need to limit the number of digits to parse in
960 // some cases. We look to the next pattern character to
961 // decide.
962 boolean limit_digits = false;
963 if (fmt_index < fmt_max
964 && standardChars.indexOf(pattern.charAt(fmt_index)) >= 0)
965 limit_digits = true;
966 --fmt_index;
967
968 // We can handle most fields automatically: most either are
969 // numeric or are looked up in a string vector. In some cases
970 // we need an offset. When numeric, `offset' is added to the
971 // resulting value. When doing a string lookup, offset is the
972 // initial index into the string array.
973 int calendar_field;
974 boolean is_numeric = true;
975 int offset = 0;
976 boolean maybe2DigitYear = false;
977 boolean oneBasedHour = false;
978 boolean oneBasedHourOfDay = false;
979 Integer simpleOffset;
980 String[] set1 = null;
981 String[] set2 = null;
982 switch (ch)
983 {
984 case 'd':
985 calendar_field = Calendar.DATE;
986 break;
987 case 'D':
988 calendar_field = Calendar.DAY_OF_YEAR;
989 break;
990 case 'F':
991 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
992 break;
993 case 'E':
994 is_numeric = false;
995 offset = 1;
996 calendar_field = Calendar.DAY_OF_WEEK;
997 set1 = formatData.getWeekdays();
998 set2 = formatData.getShortWeekdays();
999 break;
1000 case 'w':
1001 calendar_field = Calendar.WEEK_OF_YEAR;
1002 break;
1003 case 'W':
1004 calendar_field = Calendar.WEEK_OF_MONTH;
1005 break;
1006 case 'M':
1007 calendar_field = Calendar.MONTH;
1008 if (fmt_count <= 2)
1009 offset = -1;
1010 else
1011 {
1012 is_numeric = false;
1013 set1 = formatData.getMonths();
1014 set2 = formatData.getShortMonths();
1015 }
1016 break;
1017 case 'y':
1018 calendar_field = Calendar.YEAR;
1019 if (fmt_count <= 2)
1020 maybe2DigitYear = true;
1021 break;
1022 case 'K':
1023 calendar_field = Calendar.HOUR;
1024 break;
1025 case 'h':
1026 calendar_field = Calendar.HOUR;
1027 oneBasedHour = true;
1028 break;
1029 case 'H':
1030 calendar_field = Calendar.HOUR_OF_DAY;
1031 break;
1032 case 'k':
1033 calendar_field = Calendar.HOUR_OF_DAY;
1034 oneBasedHourOfDay = true;
1035 break;
1036 case 'm':
1037 calendar_field = Calendar.MINUTE;
1038 break;
1039 case 's':
1040 calendar_field = Calendar.SECOND;
1041 break;
1042 case 'S':
1043 calendar_field = Calendar.MILLISECOND;
1044 break;
1045 case 'a':
1046 is_numeric = false;
1047 calendar_field = Calendar.AM_PM;
1048 set1 = formatData.getAmPmStrings();
1049 break;
1050 case 'z':
1051 case 'Z':
1052 // We need a special case for the timezone, because it
1053 // uses a different data structure than the other cases.
1054 is_numeric = false;
1055 calendar_field = Calendar.ZONE_OFFSET;
1056 String[][] zoneStrings = formatData.getZoneStrings();
1057 int zoneCount = zoneStrings.length;
1058 int index = pos.getIndex();
1059 boolean found_zone = false;
1060 simpleOffset = computeOffset(dateStr.substring(index), pos);
1061 if (simpleOffset != null)
1062 {
1063 found_zone = true;
1064 saw_timezone = true;
1065 calendar.set(Calendar.DST_OFFSET, 0);
1066 offset = simpleOffset.intValue();
1067 }
1068 else
1069 {
1070 for (int j = 0; j < zoneCount; j++)
1071 {
1072 String[] strings = zoneStrings[j];
1073 int k;
1074 for (k = 0; k < strings.length; ++k)
1075 {
1076 if (dateStr.startsWith(strings[k], index))
1077 break;
1078 }
1079 if (k != strings.length)
1080 {
1081 found_zone = true;
1082 saw_timezone = true;
1083 TimeZone tz = TimeZone.getTimeZone (strings[0]);
1084 // Check if it's a DST zone or ordinary
1085 if(k == 3 || k == 4)
1086 calendar.set (Calendar.DST_OFFSET, tz.getDSTSavings());
1087 else
1088 calendar.set (Calendar.DST_OFFSET, 0);
1089 offset = tz.getRawOffset ();
1090 pos.setIndex(index + strings[k].length());
1091 break;
1092 }
1093 }
1094 }
1095 if (! found_zone)
1096 {
1097 pos.setErrorIndex(pos.getIndex());
1098 return null;
1099 }
1100 break;
1101 default:
1102 pos.setErrorIndex(pos.getIndex());
1103 return null;
1104 }
1105
1106 // Compute the value we should assign to the field.
1107 int value;
1108 int index = -1;
1109 if (is_numeric)
1110 {
1111 numberFormat.setMinimumIntegerDigits(fmt_count);
1112 if (maybe2DigitYear)
1113 index = pos.getIndex();
1114 Number n = null;
1115 if (limit_digits)
1116 {
1117 // numberFormat.setMaximumIntegerDigits(fmt_count) may
1118 // not work as expected. So we explicitly use substring
1119 // of dateStr.
1120 int origPos = pos.getIndex();
1121 pos.setIndex(0);
1122 n = numberFormat.parse(dateStr.substring(origPos, origPos + fmt_count), pos);
1123 pos.setIndex(origPos + pos.getIndex());
1124 }
1125 else
1126 n = numberFormat.parse(dateStr, pos);
1127 if (pos == null || ! (n instanceof Long))
1128 return null;
1129 value = n.intValue() + offset;
1130 }
1131 else if (set1 != null)
1132 {
1133 index = pos.getIndex();
1134 int i;
1135 boolean found = false;
1136 for (i = offset; i < set1.length; ++i)
1137 {
1138 if (set1[i] != null)
1139 if (dateStr.toUpperCase().startsWith(set1[i].toUpperCase(),
1140 index))
1141 {
1142 found = true;
1143 pos.setIndex(index + set1[i].length());
1144 break;
1145 }
1146 }
1147 if (!found && set2 != null)
1148 {
1149 for (i = offset; i < set2.length; ++i)
1150 {
1151 if (set2[i] != null)
1152 if (dateStr.toUpperCase().startsWith(set2[i].toUpperCase(),
1153 index))
1154 {
1155 found = true;
1156 pos.setIndex(index + set2[i].length());
1157 break;
1158 }
1159 }
1160 }
1161 if (!found)
1162 {
1163 pos.setErrorIndex(index);
1164 return null;
1165 }
1166 value = i;
1167 }
1168 else
1169 value = offset;
1170
1171 if (maybe2DigitYear)
1172 {
1173 // Parse into default century if the numeric year string has
1174 // exactly 2 digits.
1175 int digit_count = pos.getIndex() - index;
1176 if (digit_count == 2)
1177 {
1178 is2DigitYear = true;
1179 value += defaultCentury;
1180 }
1181 }
1182
1183 // Calendar uses 0-based hours.
1184 // I.e. 00:00 AM is midnight, not 12 AM or 24:00
1185 if (oneBasedHour && value == 12)
1186 value = 0;
1187
1188 if (oneBasedHourOfDay && value == 24)
1189 value = 0;
1190
1191 // Assign the value and move on.
1192 calendar.set(calendar_field, value);
1193 }
1194
1195 if (is2DigitYear)
1196 {
1197 // Apply the 80-20 heuristic to dermine the full year based on
1198 // defaultCenturyStart.
1199 int year = calendar.get(Calendar.YEAR);
1200 if (calendar.getTime().compareTo(defaultCenturyStart) < 0)
1201 calendar.set(Calendar.YEAR, year + 100);
1202 }
1203 if (! saw_timezone)
1204 {
1205 // Use the real rules to determine whether or not this
1206 // particular time is in daylight savings.
1207 calendar.clear (Calendar.DST_OFFSET);
1208 calendar.clear (Calendar.ZONE_OFFSET);
1209 }
1210 return calendar.getTime();
1211 }
1212 catch (IllegalArgumentException x)
1213 {
1214 pos.setErrorIndex(pos.getIndex());
1215 return null;
1216 }
1217 }
1218
1219 /**
1220 * <p>
1221 * Computes the time zone offset in milliseconds
1222 * relative to GMT, based on the supplied
1223 * <code>String</code> representation.
1224 * </p>
1225 * <p>
1226 * The supplied <code>String</code> must be a three
1227 * or four digit signed number, with an optional 'GMT'
1228 * prefix. The first one or two digits represents the hours,
1229 * while the last two represent the minutes. The
1230 * two sets of digits can optionally be separated by
1231 * ':'. The mandatory sign prefix (either '+' or '-')
1232 * indicates the direction of the offset from GMT.
1233 * </p>
1234 * <p>
1235 * For example, 'GMT+0200' specifies 2 hours after
1236 * GMT, while '-05:00' specifies 5 hours prior to
1237 * GMT. The special case of 'GMT' alone can be used
1238 * to represent the offset, 0.
1239 * </p>
1240 * <p>
1241 * If the <code>String</code> can not be parsed,
1242 * the result will be null. The resulting offset
1243 * is wrapped in an <code>Integer</code> object, in
1244 * order to allow such failure to be represented.
1245 * </p>
1246 *
1247 * @param zoneString a string in the form
1248 * (GMT)? sign hours : minutes
1249 * where sign = '+' or '-', hours
1250 * is a one or two digits representing
1251 * a number between 0 and 23, and
1252 * minutes is two digits representing
1253 * a number between 0 and 59.
1254 * @return the parsed offset, or null if parsing
1255 * failed.
1256 */
1257 private Integer computeOffset(String zoneString, ParsePosition pos)
1258 {
1259 Pattern pattern =
1260 Pattern.compile("(GMT)?([+-])([012])?([0-9]):?([0-9]{2})");
1261 Matcher matcher = pattern.matcher(zoneString);
1262
1263 // Match from start, but ignore trailing parts
1264 boolean hasAll = matcher.lookingAt();
1265 try
1266 {
1267 // Do we have at least the sign, hour and minute?
1268 matcher.group(2);
1269 matcher.group(4);
1270 matcher.group(5);
1271 }
1272 catch (IllegalStateException ise)
1273 {
1274 hasAll = false;
1275 }
1276 if (hasAll)
1277 {
1278 int sign = matcher.group(2).equals("+") ? 1 : -1;
1279 int hour = Integer.parseInt(matcher.group(4));
1280 if (!matcher.group(3).equals(""))
1281 hour += (Integer.parseInt(matcher.group(3)) * 10);
1282 int minutes = Integer.parseInt(matcher.group(5));
1283
1284 if (hour > 23)
1285 return null;
1286 int offset = sign * ((hour * 60) + minutes) * 60000;
1287
1288 // advance the index
1289 pos.setIndex(pos.getIndex() + matcher.end());
1290 return Integer.valueOf(offset);
1291 }
1292 else if (zoneString.startsWith("GMT"))
1293 {
1294 pos.setIndex(pos.getIndex() + 3);
1295 return Integer.valueOf(0);
1296 }
1297 return null;
1298 }
1299
1300 // Compute the start of the current century as defined by
1301 // get2DigitYearStart.
1302 private void computeCenturyStart()
1303 {
1304 int year = calendar.get(Calendar.YEAR);
1305 calendar.set(Calendar.YEAR, year - 80);
1306 set2DigitYearStart(calendar.getTime());
1307 }
1308
1309 /**
1310 * Returns a copy of this instance of
1311 * <code>SimpleDateFormat</code>. The copy contains
1312 * clones of the formatting symbols and the 2-digit
1313 * year century start date.
1314 */
1315 public Object clone()
1316 {
1317 SimpleDateFormat clone = (SimpleDateFormat) super.clone();
1318 clone.setDateFormatSymbols((DateFormatSymbols) formatData.clone());
1319 clone.set2DigitYearStart((Date) defaultCenturyStart.clone());
1320 return clone;
1321 }
1322
1323 }