DateFormat.java revision 9d68b3c839046d6a0ef505ab020fc4219ea36170
1/*
2 * Copyright (C) 2006 The Android Open Source Project
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
17package android.text.format;
18
19import android.content.Context;
20import android.provider.Settings;
21import android.text.SpannableStringBuilder;
22import android.text.Spanned;
23import android.text.SpannedString;
24
25import com.android.internal.R;
26
27import java.util.Calendar;
28import java.util.Date;
29import java.util.GregorianCalendar;
30import java.util.Locale;
31import java.util.TimeZone;
32import java.text.SimpleDateFormat;
33
34import libcore.icu.ICU;
35import libcore.icu.LocaleData;
36
37/**
38 * Utility class for producing strings with formatted date/time.
39 *
40 * <p>Most callers should avoid supplying their own format strings to this
41 * class' {@code format} methods and rely on the correctly localized ones
42 * supplied by the system. This class' factory methods return
43 * appropriately-localized {@link java.text.DateFormat} instances, suitable
44 * for both formatting and parsing dates. For the canonical documentation
45 * of format strings, see {@link java.text.SimpleDateFormat}.
46 *
47 * <p>In cases where the system does not provide a suitable pattern,
48 * this class offers the {@link #getBestDateTimePattern} method.
49 *
50 * <p>The {@code format} methods in this class implement a subset of Unicode
51 * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> patterns.
52 * The subset currently supported by this class includes the following format characters:
53 * {@code acdEHhLKkLMmsyz}. Up to API level 17, only {@code adEhkMmszy} were supported.
54 * Note that this class incorrectly implements {@code k} as if it were {@code H} for backwards
55 * compatibility.
56 *
57 * <p>See {@link java.text.SimpleDateFormat} for more documentation
58 * about patterns, or if you need a more complete or correct implementation.
59 * Note that the non-{@code format} methods in this class are implemented by
60 * {@code SimpleDateFormat}.
61 */
62public class DateFormat {
63    /**
64     * @deprecated Use a literal {@code '} instead.
65     * @removed
66     */
67    @Deprecated
68    public  static final char    QUOTE                  =    '\'';
69
70    /**
71     * @deprecated Use a literal {@code 'a'} instead.
72     * @removed
73     */
74    @Deprecated
75    public  static final char    AM_PM                  =    'a';
76
77    /**
78     * @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'.
79     * @removed
80     */
81    @Deprecated
82    public  static final char    CAPITAL_AM_PM          =    'A';
83
84    /**
85     * @deprecated Use a literal {@code 'd'} instead.
86     * @removed
87     */
88    @Deprecated
89    public  static final char    DATE                   =    'd';
90
91    /**
92     * @deprecated Use a literal {@code 'E'} instead.
93     * @removed
94     */
95    @Deprecated
96    public  static final char    DAY                    =    'E';
97
98    /**
99     * @deprecated Use a literal {@code 'h'} instead.
100     * @removed
101     */
102    @Deprecated
103    public  static final char    HOUR                   =    'h';
104
105    /**
106     * @deprecated Use a literal {@code 'H'} (for compatibility with {@link SimpleDateFormat}
107     * and Unicode) or {@code 'k'} (for compatibility with Android releases up to and including
108     * Jelly Bean MR-1) instead. Note that the two are incompatible.
109     *
110     * @removed
111     */
112    @Deprecated
113    public  static final char    HOUR_OF_DAY            =    'k';
114
115    /**
116     * @deprecated Use a literal {@code 'm'} instead.
117     * @removed
118     */
119    @Deprecated
120    public  static final char    MINUTE                 =    'm';
121
122    /**
123     * @deprecated Use a literal {@code 'M'} instead.
124     * @removed
125     */
126    @Deprecated
127    public  static final char    MONTH                  =    'M';
128
129    /**
130     * @deprecated Use a literal {@code 'L'} instead.
131     * @removed
132     */
133    @Deprecated
134    public  static final char    STANDALONE_MONTH       =    'L';
135
136    /**
137     * @deprecated Use a literal {@code 's'} instead.
138     * @removed
139     */
140    @Deprecated
141    public  static final char    SECONDS                =    's';
142
143    /**
144     * @deprecated Use a literal {@code 'z'} instead.
145     * @removed
146     */
147    @Deprecated
148    public  static final char    TIME_ZONE              =    'z';
149
150    /**
151     * @deprecated Use a literal {@code 'y'} instead.
152     * @removed
153     */
154    @Deprecated
155    public  static final char    YEAR                   =    'y';
156
157
158    private static final Object sLocaleLock = new Object();
159    private static Locale sIs24HourLocale;
160    private static boolean sIs24Hour;
161
162
163    /**
164     * Returns true if user preference is set to 24-hour format.
165     * @param context the context to use for the content resolver
166     * @return true if 24 hour time format is selected, false otherwise.
167     */
168    public static boolean is24HourFormat(Context context) {
169        String value = Settings.System.getString(context.getContentResolver(),
170                Settings.System.TIME_12_24);
171
172        if (value == null) {
173            Locale locale = context.getResources().getConfiguration().locale;
174
175            synchronized (sLocaleLock) {
176                if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) {
177                    return sIs24Hour;
178                }
179            }
180
181            java.text.DateFormat natural =
182                java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale);
183
184            if (natural instanceof SimpleDateFormat) {
185                SimpleDateFormat sdf = (SimpleDateFormat) natural;
186                String pattern = sdf.toPattern();
187
188                if (pattern.indexOf('H') >= 0) {
189                    value = "24";
190                } else {
191                    value = "12";
192                }
193            } else {
194                value = "12";
195            }
196
197            synchronized (sLocaleLock) {
198                sIs24HourLocale = locale;
199                sIs24Hour = value.equals("24");
200            }
201
202            return sIs24Hour;
203        }
204
205        return value.equals("24");
206    }
207
208    /**
209     * Returns the best possible localized form of the given skeleton for the given
210     * locale. A skeleton is similar to, and uses the same format characters as, a Unicode
211     * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a>
212     * pattern.
213     *
214     * <p>One difference is that order is irrelevant. For example, "MMMMd" will return
215     * "MMMM d" in the {@code en_US} locale, but "d. MMMM" in the {@code de_CH} locale.
216     *
217     * <p>Note also in that second example that the necessary punctuation for German was
218     * added. For the same input in {@code es_ES}, we'd have even more extra text:
219     * "d 'de' MMMM".
220     *
221     * <p>This method will automatically correct for grammatical necessity. Given the
222     * same "MMMMd" input, this method will return "d LLLL" in the {@code fa_IR} locale,
223     * where stand-alone months are necessary. Lengths are preserved where meaningful,
224     * so "Md" would give a different result to "MMMd", say, except in a locale such as
225     * {@code ja_JP} where there is only one length of month.
226     *
227     * <p>This method will only return patterns that are in CLDR, and is useful whenever
228     * you know what elements you want in your format string but don't want to make your
229     * code specific to any one locale.
230     *
231     * @param locale the locale into which the skeleton should be localized
232     * @param skeleton a skeleton as described above
233     * @return a string pattern suitable for use with {@link java.text.SimpleDateFormat}.
234     */
235    public static String getBestDateTimePattern(Locale locale, String skeleton) {
236        return ICU.getBestDateTimePattern(skeleton, locale);
237    }
238
239    /**
240     * Returns a {@link java.text.DateFormat} object that can format the time according
241     * to the current locale and the user's 12-/24-hour clock preference.
242     * @param context the application context
243     * @return the {@link java.text.DateFormat} object that properly formats the time.
244     */
245    public static java.text.DateFormat getTimeFormat(Context context) {
246        return new java.text.SimpleDateFormat(getTimeFormatString(context));
247    }
248
249    /**
250     * Returns a String pattern that can be used to format the time according
251     * to the current locale and the user's 12-/24-hour clock preference.
252     * @param context the application context
253     * @hide
254     */
255    public static String getTimeFormatString(Context context) {
256        LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
257        return is24HourFormat(context) ? d.timeFormat24 : d.timeFormat12;
258    }
259
260    /**
261     * Returns a {@link java.text.DateFormat} object that can format the date
262     * in short form (such as 12/31/1999) according
263     * to the current locale and the user's date-order preference.
264     * @param context the application context
265     * @return the {@link java.text.DateFormat} object that properly formats the date.
266     */
267    public static java.text.DateFormat getDateFormat(Context context) {
268        String value = Settings.System.getString(context.getContentResolver(),
269                Settings.System.DATE_FORMAT);
270
271        return getDateFormatForSetting(context, value);
272    }
273
274    /**
275     * Returns a {@link java.text.DateFormat} object to format the date
276     * as if the date format setting were set to <code>value</code>,
277     * including null to use the locale's default format.
278     * @param context the application context
279     * @param value the date format setting string to interpret for
280     *              the current locale
281     * @hide
282     */
283    public static java.text.DateFormat getDateFormatForSetting(Context context,
284                                                               String value) {
285        String format = getDateFormatStringForSetting(context, value);
286        return new java.text.SimpleDateFormat(format);
287    }
288
289    private static String getDateFormatStringForSetting(Context context, String value) {
290        if (value != null) {
291            int month = value.indexOf('M');
292            int day = value.indexOf('d');
293            int year = value.indexOf('y');
294
295            if (month >= 0 && day >= 0 && year >= 0) {
296                String template = context.getString(R.string.numeric_date_template);
297                if (year < month && year < day) {
298                    if (month < day) {
299                        value = String.format(template, "yyyy", "MM", "dd");
300                    } else {
301                        value = String.format(template, "yyyy", "dd", "MM");
302                    }
303                } else if (month < day) {
304                    if (day < year) {
305                        value = String.format(template, "MM", "dd", "yyyy");
306                    } else { // unlikely
307                        value = String.format(template, "MM", "yyyy", "dd");
308                    }
309                } else { // day < month
310                    if (month < year) {
311                        value = String.format(template, "dd", "MM", "yyyy");
312                    } else { // unlikely
313                        value = String.format(template, "dd", "yyyy", "MM");
314                    }
315                }
316
317                return value;
318            }
319        }
320
321        // The setting is not set; use the locale's default.
322        LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
323        return d.shortDateFormat4;
324    }
325
326    /**
327     * Returns a {@link java.text.DateFormat} object that can format the date
328     * in long form (such as {@code Monday, January 3, 2000}) for the current locale.
329     * @param context the application context
330     * @return the {@link java.text.DateFormat} object that formats the date in long form.
331     */
332    public static java.text.DateFormat getLongDateFormat(Context context) {
333        return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG);
334    }
335
336    /**
337     * Returns a {@link java.text.DateFormat} object that can format the date
338     * in medium form (such as {@code Jan 3, 2000}) for the current locale.
339     * @param context the application context
340     * @return the {@link java.text.DateFormat} object that formats the date in long form.
341     */
342    public static java.text.DateFormat getMediumDateFormat(Context context) {
343        return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM);
344    }
345
346    /**
347     * Gets the current date format stored as a char array. Returns a 3 element
348     * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'}))
349     * in the order specified by the user's format preference.  Note that this order is
350     * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG)
351     * dates will generally contain other punctuation, spaces, or words,
352     * not just the day, month, and year, and not necessarily in the same
353     * order returned here.
354     */
355    public static char[] getDateFormatOrder(Context context) {
356        return ICU.getDateFormatOrder(getDateFormatString(context));
357    }
358
359    private static String getDateFormatString(Context context) {
360        String value = Settings.System.getString(context.getContentResolver(),
361                Settings.System.DATE_FORMAT);
362
363        return getDateFormatStringForSetting(context, value);
364    }
365
366    /**
367     * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a
368     * CharSequence containing the requested date.
369     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
370     * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT
371     * @return a {@link CharSequence} containing the requested text
372     */
373    public static CharSequence format(CharSequence inFormat, long inTimeInMillis) {
374        return format(inFormat, new Date(inTimeInMillis));
375    }
376
377    /**
378     * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing
379     * the requested date.
380     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
381     * @param inDate the date to format
382     * @return a {@link CharSequence} containing the requested text
383     */
384    public static CharSequence format(CharSequence inFormat, Date inDate) {
385        Calendar c = new GregorianCalendar();
386        c.setTime(inDate);
387        return format(inFormat, c);
388    }
389
390    /**
391     * Indicates whether the specified format string contains seconds.
392     *
393     * Always returns false if the input format is null.
394     *
395     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
396     *
397     * @return true if the format string contains {@link #SECONDS}, false otherwise
398     *
399     * @hide
400     */
401    public static boolean hasSeconds(CharSequence inFormat) {
402        return hasDesignator(inFormat, SECONDS);
403    }
404
405    /**
406     * Test if a format string contains the given designator. Always returns
407     * {@code false} if the input format is {@code null}.
408     *
409     * @hide
410     */
411    public static boolean hasDesignator(CharSequence inFormat, char designator) {
412        if (inFormat == null) return false;
413
414        final int length = inFormat.length();
415
416        int c;
417        int count;
418
419        for (int i = 0; i < length; i += count) {
420            count = 1;
421            c = inFormat.charAt(i);
422
423            if (c == QUOTE) {
424                count = skipQuotedText(inFormat, i, length);
425            } else if (c == designator) {
426                return true;
427            }
428        }
429
430        return false;
431    }
432
433    private static int skipQuotedText(CharSequence s, int i, int len) {
434        if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
435            return 2;
436        }
437
438        int count = 1;
439        // skip leading quote
440        i++;
441
442        while (i < len) {
443            char c = s.charAt(i);
444
445            if (c == QUOTE) {
446                count++;
447                //  QUOTEQUOTE -> QUOTE
448                if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
449                    i++;
450                } else {
451                    break;
452                }
453            } else {
454                i++;
455                count++;
456            }
457        }
458
459        return count;
460    }
461
462    /**
463     * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
464     * containing the requested date.
465     * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
466     * @param inDate the date to format
467     * @return a {@link CharSequence} containing the requested text
468     */
469    public static CharSequence format(CharSequence inFormat, Calendar inDate) {
470        SpannableStringBuilder s = new SpannableStringBuilder(inFormat);
471        int count;
472
473        LocaleData localeData = LocaleData.get(Locale.getDefault());
474
475        int len = inFormat.length();
476
477        for (int i = 0; i < len; i += count) {
478            count = 1;
479            int c = s.charAt(i);
480
481            if (c == QUOTE) {
482                count = appendQuotedText(s, i, len);
483                len = s.length();
484                continue;
485            }
486
487            while ((i + count < len) && (s.charAt(i + count) == c)) {
488                count++;
489            }
490
491            String replacement;
492            switch (c) {
493                case 'A':
494                case 'a':
495                    replacement = localeData.amPm[inDate.get(Calendar.AM_PM) - Calendar.AM];
496                    break;
497                case 'd':
498                    replacement = zeroPad(inDate.get(Calendar.DATE), count);
499                    break;
500                case 'c':
501                case 'E':
502                    replacement = getDayOfWeekString(localeData,
503                                                     inDate.get(Calendar.DAY_OF_WEEK), count, c);
504                    break;
505                case 'K': // hour in am/pm (0-11)
506                case 'h': // hour in am/pm (1-12)
507                    {
508                        int hour = inDate.get(Calendar.HOUR);
509                        if (c == 'h' && hour == 0) {
510                            hour = 12;
511                        }
512                        replacement = zeroPad(hour, count);
513                    }
514                    break;
515                case 'H': // hour in day (0-23)
516                case 'k': // hour in day (1-24) [but see note below]
517                    {
518                        int hour = inDate.get(Calendar.HOUR_OF_DAY);
519                        // Historically on Android 'k' was interpreted as 'H', which wasn't
520                        // implemented, so pretty much all callers that want to format 24-hour
521                        // times are abusing 'k'. http://b/8359981.
522                        if (false && c == 'k' && hour == 0) {
523                            hour = 24;
524                        }
525                        replacement = zeroPad(hour, count);
526                    }
527                    break;
528                case 'L':
529                case 'M':
530                    replacement = getMonthString(localeData,
531                                                 inDate.get(Calendar.MONTH), count, c);
532                    break;
533                case 'm':
534                    replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
535                    break;
536                case 's':
537                    replacement = zeroPad(inDate.get(Calendar.SECOND), count);
538                    break;
539                case 'y':
540                    replacement = getYearString(inDate.get(Calendar.YEAR), count);
541                    break;
542                case 'z':
543                    replacement = getTimeZoneString(inDate, count);
544                    break;
545                default:
546                    replacement = null;
547                    break;
548            }
549
550            if (replacement != null) {
551                s.replace(i, i + count, replacement);
552                count = replacement.length(); // CARE: count is used in the for loop above
553                len = s.length();
554            }
555        }
556
557        if (inFormat instanceof Spanned) {
558            return new SpannedString(s);
559        } else {
560            return s.toString();
561        }
562    }
563
564    private static String getDayOfWeekString(LocaleData ld, int day, int count, int kind) {
565        boolean standalone = (kind == 'c');
566        if (count == 5) {
567            return standalone ? ld.tinyStandAloneWeekdayNames[day] : ld.tinyWeekdayNames[day];
568        } else if (count == 4) {
569            return standalone ? ld.longStandAloneWeekdayNames[day] : ld.longWeekdayNames[day];
570        } else {
571            return standalone ? ld.shortStandAloneWeekdayNames[day] : ld.shortWeekdayNames[day];
572        }
573    }
574
575    private static String getMonthString(LocaleData ld, int month, int count, int kind) {
576        boolean standalone = (kind == 'L');
577        if (count == 5) {
578            return standalone ? ld.tinyStandAloneMonthNames[month] : ld.tinyMonthNames[month];
579        } else if (count == 4) {
580            return standalone ? ld.longStandAloneMonthNames[month] : ld.longMonthNames[month];
581        } else if (count == 3) {
582            return standalone ? ld.shortStandAloneMonthNames[month] : ld.shortMonthNames[month];
583        } else {
584            // Calendar.JANUARY == 0, so add 1 to month.
585            return zeroPad(month+1, count);
586        }
587    }
588
589    private static String getTimeZoneString(Calendar inDate, int count) {
590        TimeZone tz = inDate.getTimeZone();
591        if (count < 2) { // FIXME: shouldn't this be <= 2 ?
592            return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
593                                    inDate.get(Calendar.ZONE_OFFSET),
594                                    count);
595        } else {
596            boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
597            return tz.getDisplayName(dst, TimeZone.SHORT);
598        }
599    }
600
601    private static String formatZoneOffset(int offset, int count) {
602        offset /= 1000; // milliseconds to seconds
603        StringBuilder tb = new StringBuilder();
604
605        if (offset < 0) {
606            tb.insert(0, "-");
607            offset = -offset;
608        } else {
609            tb.insert(0, "+");
610        }
611
612        int hours = offset / 3600;
613        int minutes = (offset % 3600) / 60;
614
615        tb.append(zeroPad(hours, 2));
616        tb.append(zeroPad(minutes, 2));
617        return tb.toString();
618    }
619
620    private static String getYearString(int year, int count) {
621        return (count <= 2) ? zeroPad(year % 100, 2)
622                            : String.format(Locale.getDefault(), "%d", year);
623    }
624
625    private static int appendQuotedText(SpannableStringBuilder s, int i, int len) {
626        if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
627            s.delete(i, i + 1);
628            return 1;
629        }
630
631        int count = 0;
632
633        // delete leading quote
634        s.delete(i, i + 1);
635        len--;
636
637        while (i < len) {
638            char c = s.charAt(i);
639
640            if (c == QUOTE) {
641                //  QUOTEQUOTE -> QUOTE
642                if (i + 1 < len && s.charAt(i + 1) == QUOTE) {
643
644                    s.delete(i, i + 1);
645                    len--;
646                    count++;
647                    i++;
648                } else {
649                    //  Closing QUOTE ends quoted text copying
650                    s.delete(i, i + 1);
651                    break;
652                }
653            } else {
654                i++;
655                count++;
656            }
657        }
658
659        return count;
660    }
661
662    private static String zeroPad(int inValue, int inMinDigits) {
663        return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
664    }
665}
666