1 | /* |
2 | * Copyright 2001-2005 Stephen Colebourne |
3 | * |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | * you may not use this file except in compliance with the License. |
6 | * You may obtain a copy of the License at |
7 | * |
8 | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | * |
10 | * Unless required by applicable law or agreed to in writing, software |
11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. |
15 | */ |
16 | package org.joda.time.format; |
17 | |
18 | import java.io.IOException; |
19 | import java.io.Writer; |
20 | import java.text.DateFormat; |
21 | import java.text.SimpleDateFormat; |
22 | import java.util.HashMap; |
23 | import java.util.Locale; |
24 | import java.util.Map; |
25 | |
26 | import org.joda.time.Chronology; |
27 | import org.joda.time.DateTime; |
28 | import org.joda.time.DateTimeZone; |
29 | import org.joda.time.ReadablePartial; |
30 | |
31 | /** |
32 | * Factory that creates instances of DateTimeFormatter from patterns and styles. |
33 | * <p> |
34 | * Datetime formatting is performed by the {@link DateTimeFormatter} class. |
35 | * Three classes provide factory methods to create formatters, and this is one. |
36 | * The others are {@link ISODateTimeFormat} and {@link DateTimeFormatterBuilder}. |
37 | * <p> |
38 | * This class provides two types of factory: |
39 | * <ul> |
40 | * <li>{@link #forPattern(String) Pattern} provides a DateTimeFormatter based on |
41 | * a pattern string that is mostly compatible with the JDK date patterns. |
42 | * <li>{@link #forStyle(String) Style} provides a DateTimeFormatter based on a |
43 | * two character style, representing short, medium, long and full. |
44 | * </ul> |
45 | * <p> |
46 | * For example, to use a patterm: |
47 | * <pre> |
48 | * DateTime dt = new DateTime(); |
49 | * DateTimeFormatter fmt = DateTimeFormat.forPattern("MMMM, yyyy"); |
50 | * String str = fmt.print(dt); |
51 | * </pre> |
52 | * |
53 | * The pattern syntax is mostly compatible with java.text.SimpleDateFormat - |
54 | * time zone names cannot be parsed and a few more symbols are supported. |
55 | * All ASCII letters are reserved as pattern letters, which are defined as follows: |
56 | * <blockquote> |
57 | * <pre> |
58 | * Symbol Meaning Presentation Examples |
59 | * ------ ------- ------------ ------- |
60 | * G era text AD |
61 | * C century of era (>=0) number 20 |
62 | * Y year of era (>=0) year 1996 |
63 | * |
64 | * x weekyear year 1996 |
65 | * w week of weekyear number 27 |
66 | * e day of week number 2 |
67 | * E day of week text Tuesday; Tue |
68 | * |
69 | * y year year 1996 |
70 | * D day of year number 189 |
71 | * M month of year month July; Jul; 07 |
72 | * d day of month number 10 |
73 | * |
74 | * a halfday of day text PM |
75 | * K hour of halfday (0~11) number 0 |
76 | * h clockhour of halfday (1~12) number 12 |
77 | * |
78 | * H hour of day (0~23) number 0 |
79 | * k clockhour of day (1~24) number 24 |
80 | * m minute of hour number 30 |
81 | * s second of minute number 55 |
82 | * S fraction of second number 978 |
83 | * |
84 | * z time zone text Pacific Standard Time; PST |
85 | * Z time zone offset/id zone -0800; -08:00; America/Los_Angeles |
86 | * |
87 | * ' escape for text delimiter |
88 | * '' single quote literal ' |
89 | * </pre> |
90 | * </blockquote> |
91 | * The count of pattern letters determine the format. |
92 | * <p> |
93 | * <strong>Text</strong>: If the number of pattern letters is 4 or more, |
94 | * the full form is used; otherwise a short or abbreviated form is used if |
95 | * available. |
96 | * <p> |
97 | * <strong>Number</strong>: The minimum number of digits. Shorter numbers |
98 | * are zero-padded to this amount. |
99 | * <p> |
100 | * <strong>Year</strong>: Numeric presentation for year and weekyear fields |
101 | * are handled specially. For example, if the count of 'y' is 2, the year |
102 | * will be displayed as the zero-based year of the century, which is two |
103 | * digits. |
104 | * <p> |
105 | * <strong>Month</strong>: 3 or over, use text, otherwise use number. |
106 | * <p> |
107 | * <strong>Zone</strong>: 'Z' outputs offset without a colon, 'ZZ' outputs |
108 | * the offset with a colon, 'ZZZ' or more outputs the zone id. |
109 | * <p> |
110 | * <strong>Zone names</strong>: Time zone names ('z') cannot be parsed. |
111 | * <p> |
112 | * Any characters in the pattern that are not in the ranges of ['a'..'z'] |
113 | * and ['A'..'Z'] will be treated as quoted text. For instance, characters |
114 | * like ':', '.', ' ', '#' and '?' will appear in the resulting time text |
115 | * even they are not embraced within single quotes. |
116 | * <p> |
117 | * DateTimeFormat is thread-safe and immutable, and the formatters it returns |
118 | * are as well. |
119 | * |
120 | * @author Brian S O'Neill |
121 | * @author Maxim Zhao |
122 | * @since 1.0 |
123 | * @see ISODateTimeFormat |
124 | * @see DateTimeFormatterBuilder |
125 | */ |
126 | public class DateTimeFormat { |
127 | |
128 | /** Style constant for FULL. */ |
129 | static final int FULL = 0; // DateFormat.FULL |
130 | /** Style constant for LONG. */ |
131 | static final int LONG = 1; // DateFormat.LONG |
132 | /** Style constant for MEDIUM. */ |
133 | static final int MEDIUM = 2; // DateFormat.MEDIUM |
134 | /** Style constant for SHORT. */ |
135 | static final int SHORT = 3; // DateFormat.SHORT |
136 | /** Style constant for NONE. */ |
137 | static final int NONE = 4; |
138 | |
139 | /** Type constant for DATE only. */ |
140 | static final int DATE = 0; |
141 | /** Type constant for TIME only. */ |
142 | static final int TIME = 1; |
143 | /** Type constant for DATETIME. */ |
144 | static final int DATETIME = 2; |
145 | |
146 | /** Maps patterns to formatters, patterns don't vary by locale. */ |
147 | private static final Map cPatternedCache = new HashMap(7); |
148 | /** Maps patterns to formatters, patterns don't vary by locale. */ |
149 | private static final DateTimeFormatter[] cStyleCache = new DateTimeFormatter[25]; |
150 | |
151 | //----------------------------------------------------------------------- |
152 | /** |
153 | * Factory to create a formatter from a pattern string. |
154 | * The pattern string is described above in the class level javadoc. |
155 | * It is very similar to SimpleDateFormat patterns. |
156 | * <p> |
157 | * The format may contain locale specific output, and this will change as |
158 | * you change the locale of the formatter. |
159 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
160 | * For example: |
161 | * <pre> |
162 | * DateTimeFormat.forPattern(pattern).withLocale(Locale.FRANCE).print(dt); |
163 | * </pre> |
164 | * |
165 | * @param pattern pattern specification |
166 | * @return the formatter |
167 | * @throws IllegalArgumentException if the pattern is invalid |
168 | */ |
169 | public static DateTimeFormatter forPattern(String pattern) { |
170 | return createFormatterForPattern(pattern); |
171 | } |
172 | |
173 | /** |
174 | * Factory to create a format from a two character style pattern. |
175 | * <p> |
176 | * The first character is the date style, and the second character is the |
177 | * time style. Specify a character of 'S' for short style, 'M' for medium, |
178 | * 'L' for long, and 'F' for full. |
179 | * A date or time may be ommitted by specifying a style character '-'. |
180 | * <p> |
181 | * The returned formatter will dynamically adjust to the locale that |
182 | * the print/parse takes place in. Thus you just call |
183 | * {@link DateTimeFormatter#withLocale(Locale)} and the Short/Medium/Long/Full |
184 | * style for that locale will be output. For example: |
185 | * <pre> |
186 | * DateTimeFormat.forStyle(style).withLocale(Locale.FRANCE).print(dt); |
187 | * </pre> |
188 | * |
189 | * @param style two characters from the set {"S", "M", "L", "F", "-"} |
190 | * @return the formatter |
191 | * @throws IllegalArgumentException if the style is invalid |
192 | */ |
193 | public static DateTimeFormatter forStyle(String style) { |
194 | return createFormatterForStyle(style); |
195 | } |
196 | |
197 | /** |
198 | * Returns the pattern used by a particular style and locale. |
199 | * <p> |
200 | * The first character is the date style, and the second character is the |
201 | * time style. Specify a character of 'S' for short style, 'M' for medium, |
202 | * 'L' for long, and 'F' for full. |
203 | * A date or time may be ommitted by specifying a style character '-'. |
204 | * |
205 | * @param style two characters from the set {"S", "M", "L", "F", "-"} |
206 | * @param locale locale to use, null means default |
207 | * @return the formatter |
208 | * @throws IllegalArgumentException if the style is invalid |
209 | * @since 1.3 |
210 | */ |
211 | public static String patternForStyle(String style, Locale locale) { |
212 | DateTimeFormatter formatter = createFormatterForStyle(style); |
213 | if (locale == null) { |
214 | locale = Locale.getDefault(); |
215 | } |
216 | // Not pretty, but it works. |
217 | return ((StyleFormatter) formatter.getPrinter()).getPattern(locale); |
218 | } |
219 | |
220 | //----------------------------------------------------------------------- |
221 | /** |
222 | * Creates a format that outputs a short date format. |
223 | * <p> |
224 | * The format will change as you change the locale of the formatter. |
225 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
226 | * |
227 | * @return the formatter |
228 | */ |
229 | public static DateTimeFormatter shortDate() { |
230 | return createFormatterForStyleIndex(SHORT, NONE); |
231 | } |
232 | |
233 | /** |
234 | * Creates a format that outputs a short time format. |
235 | * <p> |
236 | * The format will change as you change the locale of the formatter. |
237 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
238 | * |
239 | * @return the formatter |
240 | */ |
241 | public static DateTimeFormatter shortTime() { |
242 | return createFormatterForStyleIndex(NONE, SHORT); |
243 | } |
244 | |
245 | /** |
246 | * Creates a format that outputs a short datetime format. |
247 | * <p> |
248 | * The format will change as you change the locale of the formatter. |
249 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
250 | * |
251 | * @return the formatter |
252 | */ |
253 | public static DateTimeFormatter shortDateTime() { |
254 | return createFormatterForStyleIndex(SHORT, SHORT); |
255 | } |
256 | |
257 | //----------------------------------------------------------------------- |
258 | /** |
259 | * Creates a format that outputs a medium date format. |
260 | * <p> |
261 | * The format will change as you change the locale of the formatter. |
262 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
263 | * |
264 | * @return the formatter |
265 | */ |
266 | public static DateTimeFormatter mediumDate() { |
267 | return createFormatterForStyleIndex(MEDIUM, NONE); |
268 | } |
269 | |
270 | /** |
271 | * Creates a format that outputs a medium time format. |
272 | * <p> |
273 | * The format will change as you change the locale of the formatter. |
274 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
275 | * |
276 | * @return the formatter |
277 | */ |
278 | public static DateTimeFormatter mediumTime() { |
279 | return createFormatterForStyleIndex(NONE, MEDIUM); |
280 | } |
281 | |
282 | /** |
283 | * Creates a format that outputs a medium datetime format. |
284 | * <p> |
285 | * The format will change as you change the locale of the formatter. |
286 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
287 | * |
288 | * @return the formatter |
289 | */ |
290 | public static DateTimeFormatter mediumDateTime() { |
291 | return createFormatterForStyleIndex(MEDIUM, MEDIUM); |
292 | } |
293 | |
294 | //----------------------------------------------------------------------- |
295 | /** |
296 | * Creates a format that outputs a long date format. |
297 | * <p> |
298 | * The format will change as you change the locale of the formatter. |
299 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
300 | * |
301 | * @return the formatter |
302 | */ |
303 | public static DateTimeFormatter longDate() { |
304 | return createFormatterForStyleIndex(LONG, NONE); |
305 | } |
306 | |
307 | /** |
308 | * Creates a format that outputs a long time format. |
309 | * <p> |
310 | * The format will change as you change the locale of the formatter. |
311 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
312 | * |
313 | * @return the formatter |
314 | */ |
315 | public static DateTimeFormatter longTime() { |
316 | return createFormatterForStyleIndex(NONE, LONG); |
317 | } |
318 | |
319 | /** |
320 | * Creates a format that outputs a long datetime format. |
321 | * <p> |
322 | * The format will change as you change the locale of the formatter. |
323 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
324 | * |
325 | * @return the formatter |
326 | */ |
327 | public static DateTimeFormatter longDateTime() { |
328 | return createFormatterForStyleIndex(LONG, LONG); |
329 | } |
330 | |
331 | //----------------------------------------------------------------------- |
332 | /** |
333 | * Creates a format that outputs a full date format. |
334 | * <p> |
335 | * The format will change as you change the locale of the formatter. |
336 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
337 | * |
338 | * @return the formatter |
339 | */ |
340 | public static DateTimeFormatter fullDate() { |
341 | return createFormatterForStyleIndex(FULL, NONE); |
342 | } |
343 | |
344 | /** |
345 | * Creates a format that outputs a full time format. |
346 | * <p> |
347 | * The format will change as you change the locale of the formatter. |
348 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
349 | * |
350 | * @return the formatter |
351 | */ |
352 | public static DateTimeFormatter fullTime() { |
353 | return createFormatterForStyleIndex(NONE, FULL); |
354 | } |
355 | |
356 | /** |
357 | * Creates a format that outputs a full datetime format. |
358 | * <p> |
359 | * The format will change as you change the locale of the formatter. |
360 | * Call {@link DateTimeFormatter#withLocale(Locale)} to switch the locale. |
361 | * |
362 | * @return the formatter |
363 | */ |
364 | public static DateTimeFormatter fullDateTime() { |
365 | return createFormatterForStyleIndex(FULL, FULL); |
366 | } |
367 | |
368 | //----------------------------------------------------------------------- |
369 | /** |
370 | * Parses the given pattern and appends the rules to the given |
371 | * DateTimeFormatterBuilder. |
372 | * |
373 | * @param pattern pattern specification |
374 | * @throws IllegalArgumentException if the pattern is invalid |
375 | */ |
376 | static void appendPatternTo(DateTimeFormatterBuilder builder, String pattern) { |
377 | parsePatternTo(builder, pattern); |
378 | } |
379 | |
380 | //----------------------------------------------------------------------- |
381 | /** |
382 | * Constructor. |
383 | * |
384 | * @since 1.1 (previously private) |
385 | */ |
386 | protected DateTimeFormat() { |
387 | super(); |
388 | } |
389 | |
390 | //----------------------------------------------------------------------- |
391 | /** |
392 | * Parses the given pattern and appends the rules to the given |
393 | * DateTimeFormatterBuilder. |
394 | * |
395 | * @param pattern pattern specification |
396 | * @throws IllegalArgumentException if the pattern is invalid |
397 | * @see #forPattern |
398 | */ |
399 | private static void parsePatternTo(DateTimeFormatterBuilder builder, String pattern) { |
400 | int length = pattern.length(); |
401 | int[] indexRef = new int[1]; |
402 | |
403 | for (int i=0; i<length; i++) { |
404 | indexRef[0] = i; |
405 | String token = parseToken(pattern, indexRef); |
406 | i = indexRef[0]; |
407 | |
408 | int tokenLen = token.length(); |
409 | if (tokenLen == 0) { |
410 | break; |
411 | } |
412 | char c = token.charAt(0); |
413 | |
414 | switch (c) { |
415 | case 'G': // era designator (text) |
416 | builder.appendEraText(); |
417 | break; |
418 | case 'C': // century of era (number) |
419 | builder.appendCenturyOfEra(tokenLen, tokenLen); |
420 | break; |
421 | case 'x': // weekyear (number) |
422 | case 'y': // year (number) |
423 | case 'Y': // year of era (number) |
424 | if (tokenLen == 2) { |
425 | boolean lenientParse = true; |
426 | |
427 | // Peek ahead to next token. |
428 | if (i + 1 < length) { |
429 | indexRef[0]++; |
430 | if (isNumericToken(parseToken(pattern, indexRef))) { |
431 | // If next token is a number, cannot support |
432 | // lenient parse, because it will consume digits |
433 | // that it should not. |
434 | lenientParse = false; |
435 | } |
436 | indexRef[0]--; |
437 | } |
438 | |
439 | // Use pivots which are compatible with SimpleDateFormat. |
440 | switch (c) { |
441 | case 'x': |
442 | builder.appendTwoDigitWeekyear |
443 | (new DateTime().getWeekyear() - 30, lenientParse); |
444 | break; |
445 | case 'y': |
446 | case 'Y': |
447 | default: |
448 | builder.appendTwoDigitYear(new DateTime().getYear() - 30, lenientParse); |
449 | break; |
450 | } |
451 | } else { |
452 | // Try to support long year values. |
453 | int maxDigits = 9; |
454 | |
455 | // Peek ahead to next token. |
456 | if (i + 1 < length) { |
457 | indexRef[0]++; |
458 | if (isNumericToken(parseToken(pattern, indexRef))) { |
459 | // If next token is a number, cannot support long years. |
460 | maxDigits = tokenLen; |
461 | } |
462 | indexRef[0]--; |
463 | } |
464 | |
465 | switch (c) { |
466 | case 'x': |
467 | builder.appendWeekyear(tokenLen, maxDigits); |
468 | break; |
469 | case 'y': |
470 | builder.appendYear(tokenLen, maxDigits); |
471 | break; |
472 | case 'Y': |
473 | builder.appendYearOfEra(tokenLen, maxDigits); |
474 | break; |
475 | } |
476 | } |
477 | break; |
478 | case 'M': // month of year (text and number) |
479 | if (tokenLen >= 3) { |
480 | if (tokenLen >= 4) { |
481 | builder.appendMonthOfYearText(); |
482 | } else { |
483 | builder.appendMonthOfYearShortText(); |
484 | } |
485 | } else { |
486 | builder.appendMonthOfYear(tokenLen); |
487 | } |
488 | break; |
489 | case 'd': // day of month (number) |
490 | builder.appendDayOfMonth(tokenLen); |
491 | break; |
492 | case 'a': // am/pm marker (text) |
493 | builder.appendHalfdayOfDayText(); |
494 | break; |
495 | case 'h': // clockhour of halfday (number, 1..12) |
496 | builder.appendClockhourOfHalfday(tokenLen); |
497 | break; |
498 | case 'H': // hour of day (number, 0..23) |
499 | builder.appendHourOfDay(tokenLen); |
500 | break; |
501 | case 'k': // clockhour of day (1..24) |
502 | builder.appendClockhourOfDay(tokenLen); |
503 | break; |
504 | case 'K': // hour of halfday (0..11) |
505 | builder.appendHourOfHalfday(tokenLen); |
506 | break; |
507 | case 'm': // minute of hour (number) |
508 | builder.appendMinuteOfHour(tokenLen); |
509 | break; |
510 | case 's': // second of minute (number) |
511 | builder.appendSecondOfMinute(tokenLen); |
512 | break; |
513 | case 'S': // fraction of second (number) |
514 | builder.appendFractionOfSecond(tokenLen, tokenLen); |
515 | break; |
516 | case 'e': // day of week (number) |
517 | builder.appendDayOfWeek(tokenLen); |
518 | break; |
519 | case 'E': // dayOfWeek (text) |
520 | if (tokenLen >= 4) { |
521 | builder.appendDayOfWeekText(); |
522 | } else { |
523 | builder.appendDayOfWeekShortText(); |
524 | } |
525 | break; |
526 | case 'D': // day of year (number) |
527 | builder.appendDayOfYear(tokenLen); |
528 | break; |
529 | case 'w': // week of weekyear (number) |
530 | builder.appendWeekOfWeekyear(tokenLen); |
531 | break; |
532 | case 'z': // time zone (text) |
533 | if (tokenLen >= 4) { |
534 | builder.appendTimeZoneName(); |
535 | } else { |
536 | builder.appendTimeZoneShortName(); |
537 | } |
538 | break; |
539 | case 'Z': // time zone offset |
540 | if (tokenLen == 1) { |
541 | builder.appendTimeZoneOffset(null, false, 2, 2); |
542 | } else if (tokenLen == 2) { |
543 | builder.appendTimeZoneOffset(null, true, 2, 2); |
544 | } else { |
545 | builder.appendTimeZoneId(); |
546 | } |
547 | break; |
548 | case '\'': // literal text |
549 | String sub = token.substring(1); |
550 | if (sub.length() == 1) { |
551 | builder.appendLiteral(sub.charAt(0)); |
552 | } else { |
553 | // Create copy of sub since otherwise the temporary quoted |
554 | // string would still be referenced internally. |
555 | builder.appendLiteral(new String(sub)); |
556 | } |
557 | break; |
558 | default: |
559 | throw new IllegalArgumentException |
560 | ("Illegal pattern component: " + token); |
561 | } |
562 | } |
563 | } |
564 | |
565 | /** |
566 | * Parses an individual token. |
567 | * |
568 | * @param pattern the pattern string |
569 | * @param indexRef a single element array, where the input is the start |
570 | * location and the output is the location after parsing the token |
571 | * @return the parsed token |
572 | */ |
573 | private static String parseToken(String pattern, int[] indexRef) { |
574 | StringBuffer buf = new StringBuffer(); |
575 | |
576 | int i = indexRef[0]; |
577 | int length = pattern.length(); |
578 | |
579 | char c = pattern.charAt(i); |
580 | if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') { |
581 | // Scan a run of the same character, which indicates a time |
582 | // pattern. |
583 | buf.append(c); |
584 | |
585 | while (i + 1 < length) { |
586 | char peek = pattern.charAt(i + 1); |
587 | if (peek == c) { |
588 | buf.append(c); |
589 | i++; |
590 | } else { |
591 | break; |
592 | } |
593 | } |
594 | } else { |
595 | // This will identify token as text. |
596 | buf.append('\''); |
597 | |
598 | boolean inLiteral = false; |
599 | |
600 | for (; i < length; i++) { |
601 | c = pattern.charAt(i); |
602 | |
603 | if (c == '\'') { |
604 | if (i + 1 < length && pattern.charAt(i + 1) == '\'') { |
605 | // '' is treated as escaped ' |
606 | i++; |
607 | buf.append(c); |
608 | } else { |
609 | inLiteral = !inLiteral; |
610 | } |
611 | } else if (!inLiteral && |
612 | (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')) { |
613 | i--; |
614 | break; |
615 | } else { |
616 | buf.append(c); |
617 | } |
618 | } |
619 | } |
620 | |
621 | indexRef[0] = i; |
622 | return buf.toString(); |
623 | } |
624 | |
625 | /** |
626 | * Returns true if token should be parsed as a numeric field. |
627 | * |
628 | * @param token the token to parse |
629 | * @return true if numeric field |
630 | */ |
631 | private static boolean isNumericToken(String token) { |
632 | int tokenLen = token.length(); |
633 | if (tokenLen > 0) { |
634 | char c = token.charAt(0); |
635 | switch (c) { |
636 | case 'c': // century (number) |
637 | case 'C': // century of era (number) |
638 | case 'x': // weekyear (number) |
639 | case 'y': // year (number) |
640 | case 'Y': // year of era (number) |
641 | case 'd': // day of month (number) |
642 | case 'h': // hour of day (number, 1..12) |
643 | case 'H': // hour of day (number, 0..23) |
644 | case 'm': // minute of hour (number) |
645 | case 's': // second of minute (number) |
646 | case 'S': // fraction of second (number) |
647 | case 'e': // day of week (number) |
648 | case 'D': // day of year (number) |
649 | case 'F': // day of week in month (number) |
650 | case 'w': // week of year (number) |
651 | case 'W': // week of month (number) |
652 | case 'k': // hour of day (1..24) |
653 | case 'K': // hour of day (0..11) |
654 | return true; |
655 | case 'M': // month of year (text and number) |
656 | if (tokenLen <= 2) { |
657 | return true; |
658 | } |
659 | } |
660 | } |
661 | |
662 | return false; |
663 | } |
664 | |
665 | //----------------------------------------------------------------------- |
666 | /** |
667 | * Select a format from a custom pattern. |
668 | * |
669 | * @param pattern pattern specification |
670 | * @throws IllegalArgumentException if the pattern is invalid |
671 | * @see #appendPatternTo |
672 | */ |
673 | private static DateTimeFormatter createFormatterForPattern(String pattern) { |
674 | if (pattern == null || pattern.length() == 0) { |
675 | throw new IllegalArgumentException("Invalid pattern specification"); |
676 | } |
677 | DateTimeFormatter formatter = null; |
678 | synchronized (cPatternedCache) { |
679 | formatter = (DateTimeFormatter) cPatternedCache.get(pattern); |
680 | if (formatter == null) { |
681 | DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder(); |
682 | parsePatternTo(builder, pattern); |
683 | formatter = builder.toFormatter(); |
684 | |
685 | cPatternedCache.put(pattern, formatter); |
686 | } |
687 | } |
688 | return formatter; |
689 | } |
690 | |
691 | /** |
692 | * Select a format from a two character style pattern. The first character |
693 | * is the date style, and the second character is the time style. Specify a |
694 | * character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' |
695 | * for full. A date or time may be ommitted by specifying a style character '-'. |
696 | * |
697 | * @param style two characters from the set {"S", "M", "L", "F", "-"} |
698 | * @throws IllegalArgumentException if the style is invalid |
699 | */ |
700 | private static DateTimeFormatter createFormatterForStyle(String style) { |
701 | if (style == null || style.length() != 2) { |
702 | throw new IllegalArgumentException("Invalid style specification: " + style); |
703 | } |
704 | int dateStyle = selectStyle(style.charAt(0)); |
705 | int timeStyle = selectStyle(style.charAt(1)); |
706 | if (dateStyle == NONE && timeStyle == NONE) { |
707 | throw new IllegalArgumentException("Style '--' is invalid"); |
708 | } |
709 | return createFormatterForStyleIndex(dateStyle, timeStyle); |
710 | } |
711 | |
712 | /** |
713 | * Gets the formatter for the specified style. |
714 | * |
715 | * @param dateStyle the date style |
716 | * @param timeStyle the time style |
717 | * @return the formatter |
718 | */ |
719 | private static DateTimeFormatter createFormatterForStyleIndex(int dateStyle, int timeStyle) { |
720 | int index = ((dateStyle << 2) + dateStyle) + timeStyle; |
721 | DateTimeFormatter f = null; |
722 | synchronized (cStyleCache) { |
723 | f = cStyleCache[index]; |
724 | if (f == null) { |
725 | int type = DATETIME; |
726 | if (dateStyle == NONE) { |
727 | type = TIME; |
728 | } else if (timeStyle == NONE) { |
729 | type = DATE; |
730 | } |
731 | StyleFormatter llf = new StyleFormatter( |
732 | dateStyle, timeStyle, type); |
733 | f = new DateTimeFormatter(llf, llf); |
734 | cStyleCache[index] = f; |
735 | } |
736 | } |
737 | return f; |
738 | } |
739 | |
740 | /** |
741 | * Gets the JDK style code from the Joda code. |
742 | * |
743 | * @param ch the Joda style code |
744 | * @return the JDK style code |
745 | */ |
746 | private static int selectStyle(char ch) { |
747 | switch (ch) { |
748 | case 'S': |
749 | return SHORT; |
750 | case 'M': |
751 | return MEDIUM; |
752 | case 'L': |
753 | return LONG; |
754 | case 'F': |
755 | return FULL; |
756 | case '-': |
757 | return NONE; |
758 | default: |
759 | throw new IllegalArgumentException("Invalid style character: " + ch); |
760 | } |
761 | } |
762 | |
763 | //----------------------------------------------------------------------- |
764 | static class StyleFormatter |
765 | implements DateTimePrinter, DateTimeParser { |
766 | |
767 | private static final Map cCache = new HashMap(); // manual sync |
768 | |
769 | private final int iDateStyle; |
770 | private final int iTimeStyle; |
771 | private final int iType; |
772 | |
773 | StyleFormatter(int dateStyle, int timeStyle, int type) { |
774 | super(); |
775 | iDateStyle = dateStyle; |
776 | iTimeStyle = timeStyle; |
777 | iType = type; |
778 | } |
779 | |
780 | public int estimatePrintedLength() { |
781 | return 40; // guess |
782 | } |
783 | |
784 | public void printTo( |
785 | StringBuffer buf, long instant, Chronology chrono, |
786 | int displayOffset, DateTimeZone displayZone, Locale locale) { |
787 | DateTimePrinter p = getFormatter(locale).getPrinter(); |
788 | p.printTo(buf, instant, chrono, displayOffset, displayZone, locale); |
789 | } |
790 | |
791 | public void printTo( |
792 | Writer out, long instant, Chronology chrono, |
793 | int displayOffset, DateTimeZone displayZone, Locale locale) throws IOException { |
794 | DateTimePrinter p = getFormatter(locale).getPrinter(); |
795 | p.printTo(out, instant, chrono, displayOffset, displayZone, locale); |
796 | } |
797 | |
798 | public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) { |
799 | DateTimePrinter p = getFormatter(locale).getPrinter(); |
800 | p.printTo(buf, partial, locale); |
801 | } |
802 | |
803 | public void printTo(Writer out, ReadablePartial partial, Locale locale) throws IOException { |
804 | DateTimePrinter p = getFormatter(locale).getPrinter(); |
805 | p.printTo(out, partial, locale); |
806 | } |
807 | |
808 | public int estimateParsedLength() { |
809 | return 40; // guess |
810 | } |
811 | |
812 | public int parseInto(DateTimeParserBucket bucket, String text, int position) { |
813 | DateTimeParser p = getFormatter(bucket.getLocale()).getParser(); |
814 | return p.parseInto(bucket, text, position); |
815 | } |
816 | |
817 | private DateTimeFormatter getFormatter(Locale locale) { |
818 | locale = (locale == null ? Locale.getDefault() : locale); |
819 | String key = Integer.toString(iType + (iDateStyle << 4) + (iTimeStyle << 8)) + locale.toString(); |
820 | DateTimeFormatter f = null; |
821 | synchronized (cCache) { |
822 | f = (DateTimeFormatter) cCache.get(key); |
823 | if (f == null) { |
824 | String pattern = getPattern(locale); |
825 | f = DateTimeFormat.forPattern(pattern); |
826 | cCache.put(key, f); |
827 | } |
828 | } |
829 | return f; |
830 | } |
831 | |
832 | String getPattern(Locale locale) { |
833 | DateFormat f = null; |
834 | switch (iType) { |
835 | case DATE: |
836 | f = DateFormat.getDateInstance(iDateStyle, locale); |
837 | break; |
838 | case TIME: |
839 | f = DateFormat.getTimeInstance(iTimeStyle, locale); |
840 | break; |
841 | case DATETIME: |
842 | f = DateFormat.getDateTimeInstance(iDateStyle, iTimeStyle, locale); |
843 | break; |
844 | } |
845 | if (f instanceof SimpleDateFormat == false) { |
846 | throw new IllegalArgumentException("No datetime pattern for locale: " + locale); |
847 | } |
848 | return ((SimpleDateFormat) f).toPattern(); |
849 | } |
850 | } |
851 | |
852 | } |