Initial revision
[gcc.git] / libjava / java / text / SimpleDateFormat.java
1 /* Copyright (C) 1998, 1999 Cygnus Solutions
2
3 This file is part of libgcj.
4
5 This software is copyrighted work licensed under the terms of the
6 Libgcj License. Please consult the file "LIBGCJ_LICENSE" for
7 details. */
8
9 package java.text;
10
11 import java.util.*;
12
13 /**
14 * @author Per Bothner <bothner@cygnus.com>
15 * @date October 25, 1998.
16 */
17 /* Written using "Java Class Libraries", 2nd edition, plus online
18 * API docs for JDK 1.2 beta from http://www.javasoft.com.
19 * Status: parse is not implemented.
20 */
21
22 public class SimpleDateFormat extends DateFormat
23 {
24 private Date defaultCenturyStart;
25 private DateFormatSymbols formatData;
26 private String pattern;
27
28 public SimpleDateFormat ()
29 {
30 this("dd/MM/yy HH:mm", Locale.getDefault());
31 }
32
33 public SimpleDateFormat (String pattern)
34 {
35 this(pattern, Locale.getDefault());
36 }
37
38 public SimpleDateFormat (String pattern, Locale locale)
39 {
40 this.pattern = pattern;
41 this.calendar = Calendar.getInstance(locale);
42 this.numberFormat = NumberFormat.getInstance(locale);
43 numberFormat.setGroupingUsed(false);
44 this.formatData = new DateFormatSymbols (locale);
45 }
46
47 public SimpleDateFormat (String pattern, DateFormatSymbols formatData)
48 {
49 this.pattern = pattern;
50 this.formatData = formatData;
51 this.calendar = Calendar.getInstance();
52 this.numberFormat = NumberFormat.getInstance();
53 numberFormat.setGroupingUsed(false);
54 }
55
56 public Date get2DigitYearStart()
57 {
58 return defaultCenturyStart;
59 }
60
61 public void set2DigitYearStart(Date startDate)
62 {
63 defaultCenturyStart = startDate;
64 }
65
66 public DateFormatSymbols getDateFormatSymbols ()
67 {
68 return formatData;
69 }
70
71 public void setDateFormatSymbols (DateFormatSymbols value)
72 {
73 formatData = value;
74 }
75
76 public String toPattern ()
77 {
78 return pattern;
79 }
80
81 public void applyPattern (String pattern)
82 {
83 this.pattern = pattern;
84 }
85
86 private String applyLocalizedPattern (String pattern,
87 String oldChars, String newChars)
88 {
89 int len = pattern.length();
90 StringBuffer buf = new StringBuffer(len);
91 boolean quoted = false;
92 for (int i = 0; i < len; i++)
93 {
94 char ch = pattern.charAt(i);
95 if (ch == '\'')
96 quoted = ! quoted;
97 if (! quoted)
98 {
99 int j = oldChars.indexOf(ch);
100 if (j >= 0)
101 ch = newChars.charAt(j);
102 }
103 buf.append(ch);
104 }
105 return buf.toString();
106 }
107
108 public void applyLocalizedPattern (String pattern)
109 {
110 String localChars = formatData.getLocalPatternChars();
111 String standardChars = DateFormatSymbols.localPatternCharsDefault;
112 pattern = applyLocalizedPattern (pattern, localChars, standardChars);
113 applyPattern(pattern);
114 }
115
116 public String toLocalizedPattern ()
117 {
118 String localChars = formatData.getLocalPatternChars();
119 String standardChars = DateFormatSymbols.localPatternCharsDefault;
120 return applyLocalizedPattern (pattern, standardChars, localChars);
121 }
122
123 private final void append (StringBuffer buf, int value, int numDigits)
124 {
125 numberFormat.setMinimumIntegerDigits(numDigits);
126 numberFormat.format(value, buf, null);
127 }
128
129 public StringBuffer format (Date date, StringBuffer buf, FieldPosition pos)
130 {
131 Calendar calendar = (Calendar) this.calendar.clone();
132 calendar.setTime(date);
133 int len = pattern.length();
134 int quoteStart = -1;
135 for (int i = 0; i < len; i++)
136 {
137 char ch = pattern.charAt(i);
138 if (ch == '\'')
139 {
140 // We must do a little lookahead to see if we have two
141 // single quotes embedded in quoted text.
142 if (i < len - 1 && pattern.charAt(i + 1) == '\'')
143 {
144 ++i;
145 buf.append(ch);
146 }
147 else
148 quoteStart = quoteStart < 0 ? i : -1;
149 }
150 // From JCL: any characters in the pattern that are not in
151 // the ranges of [a..z] and [A..Z] are treated as quoted
152 // text.
153 else if (quoteStart != -1
154 || ((ch < 'a' || ch > 'z')
155 && (ch < 'A' || ch > 'Z')))
156 buf.append(ch);
157 else
158 {
159 int first = i;
160 int value;
161 while (++i < len && pattern.charAt(i) == ch) ;
162 int count = i - first; // Number of repetions of ch in pattern.
163 int beginIndex = buf.length();
164 int field;
165 i--; // Skip all but last instance of ch in pattern.
166 switch (ch)
167 {
168 case 'd':
169 append(buf, calendar.get(Calendar.DATE), count);
170 field = DateFormat.DATE_FIELD;
171 break;
172 case 'D':
173 append(buf, calendar.get(Calendar.DAY_OF_YEAR), count);
174 field = DateFormat.DAY_OF_YEAR_FIELD;
175 break;
176 case 'F':
177 append(buf, calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH),count);
178 field = DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD;
179 break;
180 case 'E':
181 value = calendar.get(calendar.DAY_OF_WEEK);
182 buf.append(count <= 3 ? formatData.getShortWeekdays()[value]
183 : formatData.getWeekdays()[value]);
184 field = DateFormat.DAY_OF_WEEK_FIELD;
185 break;
186 case 'w':
187 append(buf, calendar.get(Calendar.WEEK_OF_YEAR), count);
188 field = DateFormat.WEEK_OF_YEAR_FIELD;
189 break;
190 case 'W':
191 append(buf, calendar.get(Calendar.WEEK_OF_MONTH), count);
192 field = DateFormat.WEEK_OF_MONTH_FIELD;
193 break;
194 case 'M':
195 value = calendar.get(Calendar.MONTH);
196 if (count <= 2)
197 append(buf, value + 1, count);
198 else
199 buf.append(count <= 3 ? formatData.getShortMonths()[value]
200 : formatData.getMonths()[value]);
201 field = DateFormat.MONTH_FIELD;
202 break;
203 case 'y':
204 value = calendar.get(Calendar.YEAR);
205 append(buf, count <= 2 ? value % 100 : value, count);
206 field = DateFormat.YEAR_FIELD;
207 break;
208 case 'K':
209 append(buf, calendar.get(Calendar.HOUR), count);
210 field = DateFormat.HOUR0_FIELD;
211 break;
212 case 'h':
213 value = ((calendar.get(Calendar.HOUR) + 11) % 12) + 1;
214 append(buf, value, count);
215 field = DateFormat.HOUR1_FIELD;
216 break;
217 case 'H':
218 append(buf, calendar.get(Calendar.HOUR_OF_DAY), count);
219 field = DateFormat.HOUR_OF_DAY0_FIELD;
220 break;
221 case 'k':
222 value = ((calendar.get(Calendar.HOUR_OF_DAY) + 23) % 24) + 1;
223 append(buf, value, count);
224 field = DateFormat.HOUR_OF_DAY1_FIELD;
225 break;
226 case 'm':
227 append(buf, calendar.get(Calendar.MINUTE), count);
228 field = DateFormat.MINUTE_FIELD;
229 break;
230 case 's':
231 append(buf, calendar.get(Calendar.SECOND), count);
232 field = DateFormat.SECOND_FIELD;
233 break;
234 case 'S':
235 append(buf, calendar.get(Calendar.MILLISECOND), count);
236 field = DateFormat.MILLISECOND_FIELD;
237 break;
238 case 'a':
239 value = calendar.get(calendar.AM_PM);
240 buf.append(formatData.getAmPmStrings()[value]);
241 field = DateFormat.AM_PM_FIELD;
242 break;
243 case 'z':
244 String zoneID = calendar.getTimeZone().getID();
245 String[][] zoneStrings = formatData.getZoneStrings();
246 int zoneCount = zoneStrings.length;
247 for (int j = 0; j < zoneCount; j++)
248 {
249 String[] strings = zoneStrings[j];
250 if (zoneID.equals(strings[0]))
251 {
252 j = count > 3 ? 2 : 1;
253 if (calendar.get(Calendar.DST_OFFSET) != 0)
254 j+=2;
255 zoneID = strings[j];
256 break;
257 }
258 }
259 buf.append(zoneID);
260 field = DateFormat.TIMEZONE_FIELD;
261 break;
262 default:
263 // Note that the JCL is actually somewhat
264 // contradictory here. It defines the pattern letters
265 // to be a particular list, but also says that a
266 // pattern containing an invalid pattern letter must
267 // throw an exception. It doesn't describe what an
268 // invalid pattern letter might be, so we just assume
269 // it is any letter in [a-zA-Z] not explicitly covered
270 // above.
271 throw new RuntimeException("bad format string");
272 }
273 if (pos != null && field == pos.getField())
274 {
275 pos.setBeginIndex(beginIndex);
276 pos.setEndIndex(buf.length());
277 }
278 }
279 }
280 return buf;
281 }
282
283 private final boolean expect (String source, ParsePosition pos,
284 char ch)
285 {
286 int x = pos.getIndex();
287 boolean r = x < source.length() && source.charAt(x) == ch;
288 if (r)
289 pos.setIndex(x + 1);
290 else
291 pos.setErrorIndex(x);
292 return r;
293 }
294
295 public Date parse (String source, ParsePosition pos)
296 {
297 int fmt_index = 0;
298 int fmt_max = pattern.length();
299
300 calendar.clear();
301 int quote_start = -1;
302 for (; fmt_index < fmt_max; ++fmt_index)
303 {
304 char ch = pattern.charAt(fmt_index);
305 if (ch == '\'')
306 {
307 int index = pos.getIndex();
308 if (fmt_index < fmt_max - 1
309 && pattern.charAt(fmt_index + 1) == '\'')
310 {
311 if (! expect (source, pos, ch))
312 return null;
313 ++fmt_index;
314 }
315 else
316 quote_start = quote_start < 0 ? fmt_index : -1;
317 continue;
318 }
319
320 if (quote_start != -1
321 || ((ch < 'a' || ch > 'z')
322 && (ch < 'A' || ch > 'Z')))
323 {
324 if (! expect (source, pos, ch))
325 return null;
326 continue;
327 }
328
329 // We've arrived at a potential pattern character in the
330 // pattern.
331 int first = fmt_index;
332 while (++fmt_index < fmt_max && pattern.charAt(fmt_index) == ch)
333 ;
334 int count = fmt_index - first;
335 --fmt_index;
336
337 // We can handle most fields automatically: most either are
338 // numeric or are looked up in a string vector. In some cases
339 // we need an offset. When numeric, `offset' is added to the
340 // resulting value. When doing a string lookup, offset is the
341 // initial index into the string array.
342 int calendar_field;
343 boolean is_numeric = true;
344 String[] match = null;
345 int offset = 0;
346 int zone_number = 0;
347 switch (ch)
348 {
349 case 'd':
350 calendar_field = Calendar.DATE;
351 break;
352 case 'D':
353 calendar_field = Calendar.DAY_OF_YEAR;
354 break;
355 case 'F':
356 calendar_field = Calendar.DAY_OF_WEEK_IN_MONTH;
357 break;
358 case 'E':
359 is_numeric = false;
360 offset = 1;
361 calendar_field = Calendar.DAY_OF_WEEK;
362 match = (count <= 3
363 ? formatData.getShortWeekdays()
364 : formatData.getWeekdays());
365 break;
366 case 'w':
367 calendar_field = Calendar.WEEK_OF_YEAR;
368 break;
369 case 'W':
370 calendar_field = Calendar.WEEK_OF_MONTH;
371 break;
372 case 'M':
373 calendar_field = Calendar.MONTH;
374 if (count <= 2)
375 ;
376 else
377 {
378 is_numeric = false;
379 match = (count <= 3
380 ? formatData.getShortMonths()
381 : formatData.getMonths());
382 }
383 break;
384 case 'y':
385 calendar_field = Calendar.YEAR;
386 if (count <= 2)
387 offset = 1900;
388 break;
389 case 'K':
390 calendar_field = Calendar.HOUR;
391 break;
392 case 'h':
393 calendar_field = Calendar.HOUR;
394 offset = -1;
395 break;
396 case 'H':
397 calendar_field = Calendar.HOUR_OF_DAY;
398 break;
399 case 'k':
400 calendar_field = Calendar.HOUR_OF_DAY;
401 offset = -1;
402 break;
403 case 'm':
404 calendar_field = Calendar.MINUTE;
405 break;
406 case 's':
407 calendar_field = Calendar.SECOND;
408 break;
409 case 'S':
410 calendar_field = Calendar.MILLISECOND;
411 break;
412 case 'a':
413 is_numeric = false;
414 calendar_field = Calendar.AM_PM;
415 match = formatData.getAmPmStrings();
416 break;
417 case 'z':
418 // We need a special case for the timezone, because it
419 // uses a different data structure than the other cases.
420 is_numeric = false;
421 calendar_field = Calendar.DST_OFFSET;
422 String[][] zoneStrings = formatData.getZoneStrings();
423 int zoneCount = zoneStrings.length;
424 int index = pos.getIndex();
425 boolean found_zone = false;
426 for (int j = 0; j < zoneCount; j++)
427 {
428 String[] strings = zoneStrings[j];
429 int k;
430 for (k = 1; k < strings.length; ++k)
431 {
432 if (source.startsWith(strings[k], index))
433 break;
434 }
435 if (k != strings.length)
436 {
437 if (k > 2)
438 ; // FIXME: dst.
439 zone_number = 0; // FIXME: dst.
440 // FIXME: raw offset to SimpleTimeZone const.
441 calendar.setTimeZone(new SimpleTimeZone (1, strings[0]));
442 pos.setIndex(index + strings[k].length());
443 break;
444 }
445 }
446 if (! found_zone)
447 {
448 pos.setErrorIndex(pos.getIndex());
449 return null;
450 }
451 break;
452 default:
453 pos.setErrorIndex(pos.getIndex());
454 return null;
455 }
456
457 // Compute the value we should assign to the field.
458 int value;
459 if (is_numeric)
460 {
461 numberFormat.setMinimumIntegerDigits(count);
462 Number n = numberFormat.parse(source, pos);
463 if (pos == null || ! (n instanceof Long))
464 return null;
465 value = n.intValue() + offset;
466 }
467 else if (match != null)
468 {
469 int index = pos.getIndex();
470 int i;
471 for (i = offset; i < match.length; ++i)
472 {
473 if (source.startsWith(match[i], index))
474 break;
475 }
476 if (i == match.length)
477 {
478 pos.setErrorIndex(index);
479 return null;
480 }
481 pos.setIndex(index + match[i].length());
482 value = i;
483 }
484 else
485 value = zone_number;
486
487 // Assign the value and move on.
488 try
489 {
490 calendar.set(calendar_field, value);
491 }
492 // FIXME: what exception is thrown on an invalid
493 // non-lenient set?
494 catch (IllegalArgumentException x)
495 {
496 pos.setErrorIndex(pos.getIndex());
497 return null;
498 }
499 }
500
501 return calendar.getTime();
502 }
503
504 public boolean equals (Object obj)
505 {
506 if (! (obj instanceof SimpleDateFormat) || ! super.equals(obj) )
507 return false;
508 SimpleDateFormat other = (SimpleDateFormat) obj;
509 return (DateFormatSymbols.equals(pattern, other.pattern)
510 && DateFormatSymbols.equals(formatData, other.formatData)
511 && DateFormatSymbols.equals(defaultCenturyStart,
512 other.defaultCenturyStart));
513 }
514
515 public int hashCode ()
516 {
517 int hash = super.hashCode();
518 if (pattern != null)
519 hash ^= pattern.hashCode();
520 return hash;
521 }
522 }