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