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