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