OSDN Git Service

2001-05-10 Tom Tromey <tromey@redhat.com>
[pf3gnuchains/gcc-fork.git] / libjava / java / text / SimpleDateFormat.java
1 /* SimpleDateFormat.java -- A class for parsing/formating simple 
2    date constructs
3    Copyright (C) 1998, 1999, 2000, 2001 Free Software Foundation, Inc.
4
5 This file is part of GNU Classpath.
6
7 GNU Classpath is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2, or (at your option)
10 any later version.
11  
12 GNU Classpath is distributed in the hope that it will be useful, but
13 WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with GNU Classpath; see the file COPYING.  If not, write to the
19 Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
20 02111-1307 USA.
21
22 As a special exception, if you link this library with other files to
23 produce an executable, this library does not by itself cause the
24 resulting executable to be covered by the GNU General Public License.
25 This exception does not however invalidate any other reasons why the
26 executable file might be covered by the GNU General Public License. */
27
28
29 package java.text;
30
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.Enumeration;
34 import java.util.GregorianCalendar;
35 import java.util.Locale;
36 import java.util.TimeZone;
37 import java.util.SimpleTimeZone;
38 import java.util.Vector;
39 import java.io.ObjectInputStream;
40 import java.io.IOException;
41
42 /**
43  * SimpleDateFormat provides convenient methods for parsing and formatting
44  * dates using Gregorian calendars (see java.util.GregorianCalendar). 
45  */
46 public class SimpleDateFormat extends DateFormat 
47 {
48   /** A pair class used by SimpleDateFormat as a compiled representation
49    *  of a format string.
50    */
51   private class FieldSizePair 
52   {
53     public int field;
54     public int size;
55
56     /** Constructs a pair with the given field and size values */
57     public FieldSizePair(int f, int s) {
58       field = f;
59       size = s;
60     }
61   }
62
63   private transient Vector tokens;
64   private DateFormatSymbols formatData;  // formatData
65   private Date defaultCenturyStart = computeCenturyStart ();
66   private String pattern;
67   private int serialVersionOnStream = 1; // 0 indicates JDK1.1.3 or earlier
68   private static final long serialVersionUID = 4774881970558875024L;
69
70   // This string is specified in the JCL.  We set it here rather than
71   // do a DateFormatSymbols(Locale.US).getLocalPatternChars() since
72   // someone could theoretically change those values (though unlikely).
73   private static final String standardChars = "GyMdkHmsSEDFwWahKz";
74
75   private void readObject(ObjectInputStream stream)
76     throws IOException, ClassNotFoundException
77   {
78     stream.defaultReadObject();
79     if (serialVersionOnStream < 1)
80       {
81         defaultCenturyStart = computeCenturyStart ();
82         serialVersionOnStream = 1;
83       }
84
85     // Set up items normally taken care of by the constructor.
86     tokens = new Vector();
87     compileFormat(pattern);
88   }
89
90   private void compileFormat(String pattern) 
91   {
92     // Any alphabetical characters are treated as pattern characters
93     // unless enclosed in single quotes.
94
95     char thisChar;
96     int pos;
97     int field;
98     FieldSizePair current = null;
99
100     for (int i=0; i<pattern.length(); i++) {
101       thisChar = pattern.charAt(i);
102       field = formatData.getLocalPatternChars().indexOf(thisChar);
103       if (field == -1) {
104         current = null;
105         if (Character.isLetter(thisChar)) {
106           // Not a valid letter
107           tokens.addElement(new FieldSizePair(-1,0));
108         } else if (thisChar == '\'') {
109           // Quoted text section; skip to next single quote
110           pos = pattern.indexOf('\'',i+1);
111           if (pos == -1) {
112             // This ought to be an exception, but spec does not
113             // let us throw one.
114             tokens.addElement(new FieldSizePair(-1,0));
115           }
116           if ((pos+1 < pattern.length()) && (pattern.charAt(pos+1) == '\'')) {
117             tokens.addElement(pattern.substring(i+1,pos+1));
118           } else {
119             tokens.addElement(pattern.substring(i+1,pos));
120           }
121           i = pos;
122         } else {
123           // A special character
124           tokens.addElement(new Character(thisChar));
125         }
126       } else {
127         // A valid field
128         if ((current != null) && (field == current.field)) {
129           current.size++;
130         } else {
131           current = new FieldSizePair(field,1);
132           tokens.addElement(current);
133         }
134       }
135     }
136   }
137     
138   public String toString() 
139   {
140     StringBuffer output = new StringBuffer();
141     Enumeration e = tokens.elements();
142     while (e.hasMoreElements()) {
143       output.append(e.nextElement().toString());
144     }
145     return output.toString();
146   }
147       
148   /**
149    * Constructs a SimpleDateFormat using the default pattern for
150    * the default locale.
151    */
152   public SimpleDateFormat() 
153   {
154     /*
155      * There does not appear to be a standard API for determining 
156      * what the default pattern for a locale is, so use package-scope
157      * variables in DateFormatSymbols to encapsulate this.
158      */
159     super();
160     Locale locale = Locale.getDefault();
161     calendar = new GregorianCalendar(locale);
162     calendar.clear ();
163     tokens = new Vector();
164     formatData = new DateFormatSymbols(locale);
165     pattern = (formatData.dateFormats[DEFAULT] + ' '
166                + formatData.timeFormats[DEFAULT]);
167     compileFormat(pattern);
168     numberFormat = NumberFormat.getInstance(locale);
169     numberFormat.setGroupingUsed (false);
170   }
171   
172   /**
173    * Creates a date formatter using the specified pattern, with the default
174    * DateFormatSymbols for the default locale.
175    */
176   public SimpleDateFormat(String pattern) 
177   {
178     this(pattern, Locale.getDefault());
179   }
180
181   /**
182    * Creates a date formatter using the specified pattern, with the default
183    * DateFormatSymbols for the given locale.
184    */
185   public SimpleDateFormat(String pattern, Locale locale) 
186   {
187     super();
188     calendar = new GregorianCalendar(locale);
189     calendar.clear ();
190     tokens = new Vector();
191     formatData = new DateFormatSymbols(locale);
192     compileFormat(pattern);
193     this.pattern = pattern;
194     numberFormat = NumberFormat.getInstance(locale);
195     numberFormat.setGroupingUsed (false);
196   }
197
198   /**
199    * Creates a date formatter using the specified pattern. The
200    * specified DateFormatSymbols will be used when formatting.
201    */
202   public SimpleDateFormat(String pattern, DateFormatSymbols formatData)
203   {
204     super();
205     calendar = new GregorianCalendar();
206     calendar.clear ();
207     // FIXME: XXX: Is it really necessary to set the timezone?
208     // The Calendar constructor is supposed to take care of this.
209     calendar.setTimeZone(TimeZone.getDefault());
210     tokens = new Vector();
211     this.formatData = formatData;
212     compileFormat(pattern);
213     this.pattern = pattern;
214     numberFormat = NumberFormat.getInstance();
215     numberFormat.setGroupingUsed (false);
216   }
217
218   // What is the difference between localized and unlocalized?  The
219   // docs don't say.
220
221   /**
222    * This method returns a string with the formatting pattern being used
223    * by this object.  This string is unlocalized.
224    *
225    * @return The format string.
226    */
227   public String toPattern()
228   {
229     return pattern;
230   }
231
232   /**
233    * This method returns a string with the formatting pattern being used
234    * by this object.  This string is localized.
235    *
236    * @return The format string.
237    */
238   public String toLocalizedPattern()
239   {
240     String localChars = formatData.getLocalPatternChars();
241     return applyLocalizedPattern (pattern, standardChars, localChars);
242   }
243
244   /**
245    * This method sets the formatting pattern that should be used by this
246    * object.  This string is not localized.
247    *
248    * @param pattern The new format pattern.
249    */
250   public void applyPattern(String pattern)
251   {
252     tokens = new Vector();
253     compileFormat(pattern);
254     this.pattern = pattern;
255   }
256
257   /**
258    * This method sets the formatting pattern that should be used by this
259    * object.  This string is localized.
260    *
261    * @param pattern The new format pattern.
262    */
263   public void applyLocalizedPattern(String pattern)
264   {
265     String localChars = formatData.getLocalPatternChars();
266     pattern = applyLocalizedPattern (pattern, localChars, standardChars);
267     applyPattern(pattern);
268   }
269
270   private String applyLocalizedPattern(String pattern,
271                                        String oldChars, String newChars)
272   {
273     int len = pattern.length();
274     StringBuffer buf = new StringBuffer(len);
275     boolean quoted = false;
276     for (int i = 0;  i < len;  i++)
277       {
278         char ch = pattern.charAt(i);
279         if (ch == '\'')
280           quoted = ! quoted;
281         if (! quoted)
282           {
283             int j = oldChars.indexOf(ch);
284             if (j >= 0)
285               ch = newChars.charAt(j);
286           }
287         buf.append(ch);
288       }
289     return buf.toString();
290   }
291
292   /** 
293    * Returns the start of the century used for two digit years.
294    *
295    * @return A <code>Date</code> representing the start of the century
296    * for two digit years.
297    */
298   public Date get2DigitYearStart()
299   {
300     return defaultCenturyStart;
301   }
302
303   /**
304    * Sets the start of the century used for two digit years.
305    *
306    * @param date A <code>Date</code> representing the start of the century for
307    * two digit years.
308    */
309   public void set2DigitYearStart(Date date)
310   {
311     defaultCenturyStart = date;
312   }
313
314   /**
315    * This method returns the format symbol information used for parsing
316    * and formatting dates.
317    *
318    * @return The date format symbols.
319    */
320   public DateFormatSymbols getDateFormatSymbols()
321   {
322     return formatData;
323   }
324
325   /**
326    * This method sets the format symbols information used for parsing
327    * and formatting dates.
328    *
329    * @param formatData The date format symbols.
330    */
331    public void setDateFormatSymbols(DateFormatSymbols formatData)
332    {
333      this.formatData = formatData;
334    }
335
336   /**
337    * This methods tests whether the specified object is equal to this
338    * object.  This will be true if and only if the specified object:
339    * <p>
340    * <ul>
341    * <li>Is not <code>null</code>.
342    * <li>Is an instance of <code>SimpleDateFormat</code>.
343    * <li>Is equal to this object at the superclass (i.e., <code>DateFormat</code>)
344    *     level.
345    * <li>Has the same formatting pattern.
346    * <li>Is using the same formatting symbols.
347    * <li>Is using the same century for two digit years.
348    * </ul>
349    *
350    * @param obj The object to compare for equality against.
351    *
352    * @return <code>true</code> if the specified object is equal to this object,
353    * <code>false</code> otherwise.
354    */
355   public boolean equals(Object o)
356   {
357     if (o == null)
358       return false;
359
360     if (!super.equals(o))
361       return false;
362
363     if (!(o instanceof SimpleDateFormat))
364       return false;
365
366     SimpleDateFormat sdf = (SimpleDateFormat)o;
367
368     if (!toPattern().equals(sdf.toPattern()))
369       return false;
370
371     if (!get2DigitYearStart().equals(sdf.get2DigitYearStart()))
372       return false;
373
374     if (!getDateFormatSymbols().equals(sdf.getDateFormatSymbols()))
375       return false;
376
377     return true;
378   }
379
380
381   /**
382    * Formats the date input according to the format string in use,
383    * appending to the specified StringBuffer.  The input StringBuffer
384    * is returned as output for convenience.
385    */
386   public StringBuffer format(Date date, StringBuffer buffer, FieldPosition pos)
387   {
388     String temp;
389     Calendar theCalendar = (Calendar) calendar.clone();
390     theCalendar.setTime(date);
391     
392     // go through vector, filling in fields where applicable, else toString
393     Enumeration e = tokens.elements();
394     while (e.hasMoreElements()) {
395       Object o = e.nextElement();
396       if (o instanceof FieldSizePair) {
397         FieldSizePair p = (FieldSizePair) o;
398         int beginIndex = buffer.length();
399         switch (p.field) {
400         case ERA_FIELD:
401           buffer.append(formatData.eras[theCalendar.get(Calendar.ERA)]);
402           break;
403         case YEAR_FIELD:
404           temp = String.valueOf(theCalendar.get(Calendar.YEAR));
405           if (p.size < 4)
406             buffer.append(temp.substring(temp.length()-2));
407           else
408             buffer.append(temp);
409           break;
410         case MONTH_FIELD:
411           if (p.size < 3)
412             withLeadingZeros(theCalendar.get(Calendar.MONTH)+1,p.size,buffer);
413           else if (p.size < 4)
414             buffer.append(formatData.shortMonths[theCalendar.get(Calendar.MONTH)]);
415           else
416             buffer.append(formatData.months[theCalendar.get(Calendar.MONTH)]);
417           break;
418         case DATE_FIELD:
419           withLeadingZeros(theCalendar.get(Calendar.DATE),p.size,buffer);
420           break;
421         case HOUR_OF_DAY1_FIELD: // 1-24
422           withLeadingZeros(((theCalendar.get(Calendar.HOUR_OF_DAY)+23)%24)+1,p.size,buffer);
423           break;
424         case HOUR_OF_DAY0_FIELD: // 0-23
425           withLeadingZeros(theCalendar.get(Calendar.HOUR_OF_DAY),p.size,buffer);
426           break;
427         case MINUTE_FIELD:
428           withLeadingZeros(theCalendar.get(Calendar.MINUTE),p.size,buffer);
429           break;
430         case SECOND_FIELD:
431           withLeadingZeros(theCalendar.get(Calendar.SECOND),p.size,buffer);
432           break;
433         case MILLISECOND_FIELD:
434           withLeadingZeros(theCalendar.get(Calendar.MILLISECOND),p.size,buffer);
435           break;
436         case DAY_OF_WEEK_FIELD:
437           if (p.size < 4)
438             buffer.append(formatData.shortWeekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
439           else
440             buffer.append(formatData.weekdays[theCalendar.get(Calendar.DAY_OF_WEEK)]);
441           break;
442         case DAY_OF_YEAR_FIELD:
443           withLeadingZeros(theCalendar.get(Calendar.DAY_OF_YEAR),p.size,buffer);
444           break;
445         case DAY_OF_WEEK_IN_MONTH_FIELD:
446           withLeadingZeros(theCalendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),p.size,buffer);
447           break;
448         case WEEK_OF_YEAR_FIELD:
449           withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_YEAR),p.size,buffer);
450           break;
451         case WEEK_OF_MONTH_FIELD:
452           withLeadingZeros(theCalendar.get(Calendar.WEEK_OF_MONTH),p.size,buffer);
453           break;
454         case AM_PM_FIELD:
455           buffer.append(formatData.ampms[theCalendar.get(Calendar.AM_PM)]);
456           break;
457         case HOUR1_FIELD: // 1-12
458           withLeadingZeros(((theCalendar.get(Calendar.HOUR)+11)%12)+1,p.size,buffer);
459           break;
460         case HOUR0_FIELD: // 0-11
461           withLeadingZeros(theCalendar.get(Calendar.HOUR),p.size,buffer);
462           break;
463         case TIMEZONE_FIELD:
464           TimeZone zone = theCalendar.getTimeZone();
465           boolean isDST = theCalendar.get(Calendar.DST_OFFSET) != 0;
466           // FIXME: XXX: This should be a localized time zone.
467           String zoneID = zone.getDisplayName(isDST, p.size > 3 ? TimeZone.LONG : TimeZone.SHORT);
468           buffer.append(zoneID);
469           break;
470         default:
471           throw new IllegalArgumentException("Illegal pattern character");
472         }
473         if (pos != null && p.field == pos.getField())
474           {
475             pos.setBeginIndex(beginIndex);
476             pos.setEndIndex(buffer.length());
477           }
478       } else {
479         buffer.append(o.toString());
480       }
481     }
482     return buffer;
483   }
484
485   private void withLeadingZeros(int value, int length, StringBuffer buffer) {
486     String valStr = String.valueOf(value);
487     for (length -= valStr.length(); length > 0; length--)
488       buffer.append('0');
489     buffer.append(valStr);
490   }
491
492   private final boolean expect (String source, ParsePosition pos, char ch)
493   {
494     int x = pos.getIndex();
495     boolean r = x < source.length() && source.charAt(x) == ch;
496     if (r)
497       pos.setIndex(x + 1);
498     else
499       pos.setErrorIndex(x);
500     return r;
501   }
502
503   /**
504    * This method parses the specified string into a date.
505    * 
506    * @param dateStr The date string to parse.
507    * @param pos The input and output parse position
508    *
509    * @return The parsed date, or <code>null</code> if the string cannot be
510    * parsed.
511    */
512   public Date parse (String dateStr, ParsePosition pos)
513   {
514     int fmt_index = 0;
515     int fmt_max = pattern.length();
516
517     // We copy the Calendar because if we don't we will modify it and
518     // then this.equals() will no longer have the desired result.
519     Calendar theCalendar = (Calendar) calendar.clone ();
520     theCalendar.clear();
521     int quote_start = -1;
522     for (; fmt_index < fmt_max; ++fmt_index)
523       {
524         char ch = pattern.charAt(fmt_index);
525         if (ch == '\'')
526           {
527             int index = pos.getIndex();
528             if (fmt_index < fmt_max - 1
529                 && pattern.charAt(fmt_index + 1) == '\'')
530               {
531                 if (! expect (dateStr, pos, ch))
532                   return null;
533                 ++fmt_index;
534               }
535             else
536               quote_start = quote_start < 0 ? fmt_index : -1;
537             continue;
538           }
539
540         if (quote_start != -1
541             || ((ch < 'a' || ch > 'z')
542                 && (ch < 'A' || ch > 'Z')))
543           {
544             if (! expect (dateStr, pos, ch))
545               return null;
546             continue;
547           }
548
549         // We've arrived at a potential pattern character in the
550         // pattern.
551         int first = fmt_index;
552         while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
553           ;
554         int count = fmt_index - first;
555         --fmt_index;
556
557         // We can handle most fields automatically: most either are
558         // numeric or are looked up in a string vector.  In some cases
559         // we need an offset.  When numeric, `offset' is added to the
560         // resulting value.  When doing a string lookup, offset is the
561         // initial index into the string array.
562         int calendar_field;
563         boolean is_numeric = true;
564         String[] match = null;
565         int offset = 0;
566         switch (ch)
567           {
568           case 'd':
569             calendar_field = Calendar.DATE;
570             break;
571           case 'D':
572             calendar_field = Calendar.DAY_OF_YEAR;
573             break;
574           case 'F':
575             calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
576             break;
577           case 'E':
578             is_numeric = false;
579             offset = 1;
580             calendar_field = Calendar.DAY_OF_WEEK;
581             match = (count <= 3
582                      ? formatData.getShortWeekdays()
583                      : formatData.getWeekdays());
584             break;
585           case 'w':
586             calendar_field = Calendar.WEEK_OF_YEAR;
587             break;
588           case 'W':
589             calendar_field = Calendar.WEEK_OF_MONTH;
590             break;
591           case 'M':
592             calendar_field = Calendar.MONTH;
593             if (count <= 2)
594               offset = -1;
595             else
596               {
597                 is_numeric = false;
598                 match = (count <= 3
599                          ? formatData.getShortMonths()
600                          : formatData.getMonths());
601               }
602             break;
603           case 'y':
604             calendar_field = Calendar.YEAR;
605             if (count <= 2)
606               offset = 1900;
607             break;
608           case 'K':
609             calendar_field = Calendar.HOUR;
610             break;
611           case 'h':
612             calendar_field = Calendar.HOUR;
613             break;
614           case 'H':
615             calendar_field = Calendar.HOUR_OF_DAY;
616             break;
617           case 'k':
618             calendar_field = Calendar.HOUR_OF_DAY;
619             break;
620           case 'm':
621             calendar_field = Calendar.MINUTE;
622             break;
623           case 's':
624             calendar_field = Calendar.SECOND;
625             break;
626           case 'S':
627             calendar_field = Calendar.MILLISECOND;
628             break;
629           case 'a':
630             is_numeric = false;
631             calendar_field = Calendar.AM_PM;
632             match = formatData.getAmPmStrings();
633             break;
634           case 'z':
635             // We need a special case for the timezone, because it
636             // uses a different data structure than the other cases.
637             is_numeric = false;
638             // We don't actually use this; see below.
639             calendar_field = Calendar.DST_OFFSET;
640             String[][] zoneStrings = formatData.getZoneStrings();
641             int zoneCount = zoneStrings.length;
642             int index = pos.getIndex();
643             boolean found_zone = false;
644             for (int j = 0;  j < zoneCount;  j++)
645               {
646                 String[] strings = zoneStrings[j];
647                 int k;
648                 for (k = 1; k < strings.length; ++k)
649                   {
650                     if (dateStr.startsWith(strings[k], index))
651                       break;
652                   }
653                 if (k != strings.length)
654                   {
655                     found_zone = true;
656                     TimeZone tz = TimeZone.getTimeZone (strings[0]);
657                     theCalendar.setTimeZone (tz);
658                     theCalendar.clear (Calendar.DST_OFFSET);
659                     theCalendar.clear (Calendar.ZONE_OFFSET);
660                     pos.setIndex(index + strings[k].length());
661                     break;
662                   }
663               }
664             if (! found_zone)
665               {
666                 pos.setErrorIndex(pos.getIndex());
667                 return null;
668               }
669             break;
670           default:
671             pos.setErrorIndex(pos.getIndex());
672             return null;
673           }
674
675         // Compute the value we should assign to the field.
676         int value;
677         if (is_numeric)
678           {
679             numberFormat.setMinimumIntegerDigits(count);
680             Number n = numberFormat.parse(dateStr, pos);
681             if (pos == null || ! (n instanceof Long))
682               return null;
683             value = n.intValue() + offset;
684           }
685         else if (match != null)
686           {
687             int index = pos.getIndex();
688             int i;
689             for (i = offset; i < match.length; ++i)
690               {
691                 if (dateStr.startsWith(match[i], index))
692                   break;
693               }
694             if (i == match.length)
695               {
696                 pos.setErrorIndex(index);
697                 return null;
698               }
699             pos.setIndex(index + match[i].length());
700             value = i;
701           }
702         else
703           value = 0;
704
705         // Assign the value and move on.
706         if (calendar_field != Calendar.DST_OFFSET)
707           theCalendar.set(calendar_field, value);
708       }
709
710     try
711       {
712         return theCalendar.getTime();
713       }
714     catch (IllegalArgumentException x)
715       {
716         pos.setErrorIndex(pos.getIndex());
717         return null;
718       }
719   }
720
721   // Compute the start of the current century as defined by
722   // get2DigitYearStart.
723   private Date computeCenturyStart ()
724   {
725     // Compute the current year.  We assume a year has 365 days.  Then
726     // compute 80 years ago, and finally reconstruct the number of
727     // milliseconds.  We do this computation in this strange way
728     // because it lets us easily truncate the milliseconds, seconds,
729     // etc, which don't matter and which confuse
730     // SimpleDateFormat.equals().
731     long now = System.currentTimeMillis ();
732     now /= 365L * 24L * 60L * 60L * 1000L;
733     now -= 80;
734     now *= 365L * 24L * 60L * 60L * 1000L;
735     return new Date (now);
736   }
737 }