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 
17 package android.text.format;
18 
19 import android.annotation.NonNull;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.content.Context;
22 import android.provider.Settings;
23 import android.text.SpannableStringBuilder;
24 import android.text.Spanned;
25 import android.text.SpannedString;
26 
27 import libcore.icu.ICU;
28 import libcore.icu.LocaleData;
29 
30 import java.text.SimpleDateFormat;
31 import java.util.Calendar;
32 import java.util.Date;
33 import java.util.GregorianCalendar;
34 import java.util.Locale;
35 import java.util.TimeZone;
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  */
62 public 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      * Returns true if times should be formatted as 24 hour times, false if times should be
164      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
165      * @param context the context to use for the content resolver
166      * @return true if 24 hour time format is selected, false otherwise.
167      */
is24HourFormat(Context context)168     public static boolean is24HourFormat(Context context) {
169         return is24HourFormat(context, context.getUserId());
170     }
171 
172     /**
173      * Returns true if times should be formatted as 24 hour times, false if times should be
174      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
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     @UnsupportedAppUsage
is24HourFormat(Context context, int userHandle)182     public static boolean is24HourFormat(Context context, int userHandle) {
183         final String value = Settings.System.getStringForUser(context.getContentResolver(),
184                 Settings.System.TIME_12_24, userHandle);
185         if (value != null) {
186             return value.equals("24");
187         }
188 
189         return is24HourLocale(context.getResources().getConfiguration().locale);
190     }
191 
192     /**
193      * Returns true if the specified locale uses a 24-hour time format by default, ignoring user
194      * settings.
195      * @param locale the locale to check
196      * @return true if the locale uses a 24 hour time format by default, false otherwise
197      * @hide
198      */
is24HourLocale(@onNull Locale locale)199     public static boolean is24HourLocale(@NonNull Locale locale) {
200         synchronized (sLocaleLock) {
201             if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) {
202                 return sIs24Hour;
203             }
204         }
205 
206         final java.text.DateFormat natural =
207                 java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale);
208 
209         final boolean is24Hour;
210         if (natural instanceof SimpleDateFormat) {
211             final SimpleDateFormat sdf = (SimpleDateFormat) natural;
212             final String pattern = sdf.toPattern();
213             is24Hour = hasDesignator(pattern, 'H');
214         } else {
215             is24Hour = false;
216         }
217 
218         synchronized (sLocaleLock) {
219             sIs24HourLocale = locale;
220             sIs24Hour = is24Hour;
221         }
222 
223         return is24Hour;
224     }
225 
226     /**
227      * Returns the best possible localized form of the given skeleton for the given
228      * locale. A skeleton is similar to, and uses the same format characters as, a Unicode
229      * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a>
230      * pattern.
231      *
232      * <p>One difference is that order is irrelevant. For example, "MMMMd" will return
233      * "MMMM d" in the {@code en_US} locale, but "d. MMMM" in the {@code de_CH} locale.
234      *
235      * <p>Note also in that second example that the necessary punctuation for German was
236      * added. For the same input in {@code es_ES}, we'd have even more extra text:
237      * "d 'de' MMMM".
238      *
239      * <p>This method will automatically correct for grammatical necessity. Given the
240      * same "MMMMd" input, this method will return "d LLLL" in the {@code fa_IR} locale,
241      * where stand-alone months are necessary. Lengths are preserved where meaningful,
242      * so "Md" would give a different result to "MMMd", say, except in a locale such as
243      * {@code ja_JP} where there is only one length of month.
244      *
245      * <p>This method will only return patterns that are in CLDR, and is useful whenever
246      * you know what elements you want in your format string but don't want to make your
247      * code specific to any one locale.
248      *
249      * @param locale the locale into which the skeleton should be localized
250      * @param skeleton a skeleton as described above
251      * @return a string pattern suitable for use with {@link java.text.SimpleDateFormat}.
252      */
getBestDateTimePattern(Locale locale, String skeleton)253     public static String getBestDateTimePattern(Locale locale, String skeleton) {
254         return ICU.getBestDateTimePattern(skeleton, locale);
255     }
256 
257     /**
258      * Returns a {@link java.text.DateFormat} object that can format the time according
259      * to the context's locale and the user's 12-/24-hour clock preference.
260      * @param context the application context
261      * @return the {@link java.text.DateFormat} object that properly formats the time.
262      */
getTimeFormat(Context context)263     public static java.text.DateFormat getTimeFormat(Context context) {
264         final Locale locale = context.getResources().getConfiguration().locale;
265         return new java.text.SimpleDateFormat(getTimeFormatString(context), locale);
266     }
267 
268     /**
269      * Returns a String pattern that can be used to format the time according
270      * to the context's locale and the user's 12-/24-hour clock preference.
271      * @param context the application context
272      * @hide
273      */
274     @UnsupportedAppUsage
getTimeFormatString(Context context)275     public static String getTimeFormatString(Context context) {
276         return getTimeFormatString(context, context.getUserId());
277     }
278 
279     /**
280      * Returns a String pattern that can be used to format the time according
281      * to the context's locale and the user's 12-/24-hour clock preference.
282      * @param context the application context
283      * @param userHandle the user handle of the user to query the format for
284      * @hide
285      */
286     @UnsupportedAppUsage
getTimeFormatString(Context context, int userHandle)287     public static String getTimeFormatString(Context context, int userHandle) {
288         final LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
289         return is24HourFormat(context, userHandle) ? d.timeFormat_Hm : d.timeFormat_hm;
290     }
291 
292     /**
293      * Returns a {@link java.text.DateFormat} object that can format the date
294      * in short form according to the context's locale.
295      *
296      * @param context the application context
297      * @return the {@link java.text.DateFormat} object that properly formats the date.
298      */
getDateFormat(Context context)299     public static java.text.DateFormat getDateFormat(Context context) {
300         final Locale locale = context.getResources().getConfiguration().locale;
301         return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT, locale);
302     }
303 
304     /**
305      * Returns a {@link java.text.DateFormat} object that can format the date
306      * in long form (such as {@code Monday, January 3, 2000}) for the context's locale.
307      * @param context the application context
308      * @return the {@link java.text.DateFormat} object that formats the date in long form.
309      */
getLongDateFormat(Context context)310     public static java.text.DateFormat getLongDateFormat(Context context) {
311         final Locale locale = context.getResources().getConfiguration().locale;
312         return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG, locale);
313     }
314 
315     /**
316      * Returns a {@link java.text.DateFormat} object that can format the date
317      * in medium form (such as {@code Jan 3, 2000}) for the context's locale.
318      * @param context the application context
319      * @return the {@link java.text.DateFormat} object that formats the date in long form.
320      */
getMediumDateFormat(Context context)321     public static java.text.DateFormat getMediumDateFormat(Context context) {
322         final Locale locale = context.getResources().getConfiguration().locale;
323         return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM, locale);
324     }
325 
326     /**
327      * Gets the current date format stored as a char array. Returns a 3 element
328      * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'}))
329      * in the order specified by the user's format preference.  Note that this order is
330      * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG)
331      * dates will generally contain other punctuation, spaces, or words,
332      * not just the day, month, and year, and not necessarily in the same
333      * order returned here.
334      */
getDateFormatOrder(Context context)335     public static char[] getDateFormatOrder(Context context) {
336         return ICU.getDateFormatOrder(getDateFormatString(context));
337     }
338 
getDateFormatString(Context context)339     private static String getDateFormatString(Context context) {
340         final Locale locale = context.getResources().getConfiguration().locale;
341         java.text.DateFormat df = java.text.DateFormat.getDateInstance(
342                 java.text.DateFormat.SHORT, locale);
343         if (df instanceof SimpleDateFormat) {
344             return ((SimpleDateFormat) df).toPattern();
345         }
346 
347         throw new AssertionError("!(df instanceof SimpleDateFormat)");
348     }
349 
350     /**
351      * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a
352      * CharSequence containing the requested date.
353      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
354      * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT
355      * @return a {@link CharSequence} containing the requested text
356      */
format(CharSequence inFormat, long inTimeInMillis)357     public static CharSequence format(CharSequence inFormat, long inTimeInMillis) {
358         return format(inFormat, new Date(inTimeInMillis));
359     }
360 
361     /**
362      * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing
363      * the requested date.
364      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
365      * @param inDate the date to format
366      * @return a {@link CharSequence} containing the requested text
367      */
format(CharSequence inFormat, Date inDate)368     public static CharSequence format(CharSequence inFormat, Date inDate) {
369         Calendar c = new GregorianCalendar();
370         c.setTime(inDate);
371         return format(inFormat, c);
372     }
373 
374     /**
375      * Indicates whether the specified format string contains seconds.
376      *
377      * Always returns false if the input format is null.
378      *
379      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
380      *
381      * @return true if the format string contains {@link #SECONDS}, false otherwise
382      *
383      * @hide
384      */
385     @UnsupportedAppUsage
hasSeconds(CharSequence inFormat)386     public static boolean hasSeconds(CharSequence inFormat) {
387         return hasDesignator(inFormat, SECONDS);
388     }
389 
390     /**
391      * Test if a format string contains the given designator. Always returns
392      * {@code false} if the input format is {@code null}.
393      *
394      * Note that this is intended for searching for designators, not arbitrary
395      * characters. So searching for a literal single quote would not work correctly.
396      *
397      * @hide
398      */
399     @UnsupportedAppUsage
hasDesignator(CharSequence inFormat, char designator)400     public static boolean hasDesignator(CharSequence inFormat, char designator) {
401         if (inFormat == null) return false;
402 
403         final int length = inFormat.length();
404 
405         boolean insideQuote = false;
406         for (int i = 0; i < length; i++) {
407             final char c = inFormat.charAt(i);
408             if (c == QUOTE) {
409                 insideQuote = !insideQuote;
410             } else if (!insideQuote) {
411                 if (c == designator) {
412                     return true;
413                 }
414             }
415         }
416 
417         return false;
418     }
419 
420     /**
421      * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
422      * containing the requested date.
423      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
424      * @param inDate the date to format
425      * @return a {@link CharSequence} containing the requested text
426      */
format(CharSequence inFormat, Calendar inDate)427     public static CharSequence format(CharSequence inFormat, Calendar inDate) {
428         SpannableStringBuilder s = new SpannableStringBuilder(inFormat);
429         int count;
430 
431         LocaleData localeData = LocaleData.get(Locale.getDefault());
432 
433         int len = inFormat.length();
434 
435         for (int i = 0; i < len; i += count) {
436             count = 1;
437             int c = s.charAt(i);
438 
439             if (c == QUOTE) {
440                 count = appendQuotedText(s, i);
441                 len = s.length();
442                 continue;
443             }
444 
445             while ((i + count < len) && (s.charAt(i + count) == c)) {
446                 count++;
447             }
448 
449             String replacement;
450             switch (c) {
451                 case 'A':
452                 case 'a':
453                     replacement = localeData.amPm[inDate.get(Calendar.AM_PM) - Calendar.AM];
454                     break;
455                 case 'd':
456                     replacement = zeroPad(inDate.get(Calendar.DATE), count);
457                     break;
458                 case 'c':
459                 case 'E':
460                     replacement = getDayOfWeekString(localeData,
461                                                      inDate.get(Calendar.DAY_OF_WEEK), count, c);
462                     break;
463                 case 'K': // hour in am/pm (0-11)
464                 case 'h': // hour in am/pm (1-12)
465                     {
466                         int hour = inDate.get(Calendar.HOUR);
467                         if (c == 'h' && hour == 0) {
468                             hour = 12;
469                         }
470                         replacement = zeroPad(hour, count);
471                     }
472                     break;
473                 case 'H': // hour in day (0-23)
474                 case 'k': // hour in day (1-24) [but see note below]
475                     {
476                         int hour = inDate.get(Calendar.HOUR_OF_DAY);
477                         // Historically on Android 'k' was interpreted as 'H', which wasn't
478                         // implemented, so pretty much all callers that want to format 24-hour
479                         // times are abusing 'k'. http://b/8359981.
480                         if (false && c == 'k' && hour == 0) {
481                             hour = 24;
482                         }
483                         replacement = zeroPad(hour, count);
484                     }
485                     break;
486                 case 'L':
487                 case 'M':
488                     replacement = getMonthString(localeData,
489                                                  inDate.get(Calendar.MONTH), count, c);
490                     break;
491                 case 'm':
492                     replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
493                     break;
494                 case 's':
495                     replacement = zeroPad(inDate.get(Calendar.SECOND), count);
496                     break;
497                 case 'y':
498                     replacement = getYearString(inDate.get(Calendar.YEAR), count);
499                     break;
500                 case 'z':
501                     replacement = getTimeZoneString(inDate, count);
502                     break;
503                 default:
504                     replacement = null;
505                     break;
506             }
507 
508             if (replacement != null) {
509                 s.replace(i, i + count, replacement);
510                 count = replacement.length(); // CARE: count is used in the for loop above
511                 len = s.length();
512             }
513         }
514 
515         if (inFormat instanceof Spanned) {
516             return new SpannedString(s);
517         } else {
518             return s.toString();
519         }
520     }
521 
getDayOfWeekString(LocaleData ld, int day, int count, int kind)522     private static String getDayOfWeekString(LocaleData ld, int day, int count, int kind) {
523         boolean standalone = (kind == 'c');
524         if (count == 5) {
525             return standalone ? ld.tinyStandAloneWeekdayNames[day] : ld.tinyWeekdayNames[day];
526         } else if (count == 4) {
527             return standalone ? ld.longStandAloneWeekdayNames[day] : ld.longWeekdayNames[day];
528         } else {
529             return standalone ? ld.shortStandAloneWeekdayNames[day] : ld.shortWeekdayNames[day];
530         }
531     }
532 
getMonthString(LocaleData ld, int month, int count, int kind)533     private static String getMonthString(LocaleData ld, int month, int count, int kind) {
534         boolean standalone = (kind == 'L');
535         if (count == 5) {
536             return standalone ? ld.tinyStandAloneMonthNames[month] : ld.tinyMonthNames[month];
537         } else if (count == 4) {
538             return standalone ? ld.longStandAloneMonthNames[month] : ld.longMonthNames[month];
539         } else if (count == 3) {
540             return standalone ? ld.shortStandAloneMonthNames[month] : ld.shortMonthNames[month];
541         } else {
542             // Calendar.JANUARY == 0, so add 1 to month.
543             return zeroPad(month+1, count);
544         }
545     }
546 
getTimeZoneString(Calendar inDate, int count)547     private static String getTimeZoneString(Calendar inDate, int count) {
548         TimeZone tz = inDate.getTimeZone();
549         if (count < 2) { // FIXME: shouldn't this be <= 2 ?
550             return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
551                                     inDate.get(Calendar.ZONE_OFFSET),
552                                     count);
553         } else {
554             boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
555             return tz.getDisplayName(dst, TimeZone.SHORT);
556         }
557     }
558 
formatZoneOffset(int offset, int count)559     private static String formatZoneOffset(int offset, int count) {
560         offset /= 1000; // milliseconds to seconds
561         StringBuilder tb = new StringBuilder();
562 
563         if (offset < 0) {
564             tb.insert(0, "-");
565             offset = -offset;
566         } else {
567             tb.insert(0, "+");
568         }
569 
570         int hours = offset / 3600;
571         int minutes = (offset % 3600) / 60;
572 
573         tb.append(zeroPad(hours, 2));
574         tb.append(zeroPad(minutes, 2));
575         return tb.toString();
576     }
577 
getYearString(int year, int count)578     private static String getYearString(int year, int count) {
579         return (count <= 2) ? zeroPad(year % 100, 2)
580                             : String.format(Locale.getDefault(), "%d", year);
581     }
582 
583 
584     /**
585      * Strips quotation marks from the {@code formatString} and appends the result back to the
586      * {@code formatString}.
587      *
588      * @param formatString the format string, as described in
589      *                     {@link android.text.format.DateFormat}, to be modified
590      * @param index        index of the first quote
591      * @return the length of the quoted text that was appended.
592      * @hide
593      */
appendQuotedText(SpannableStringBuilder formatString, int index)594     public static int appendQuotedText(SpannableStringBuilder formatString, int index) {
595         int length = formatString.length();
596         if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
597             formatString.delete(index, index + 1);
598             return 1;
599         }
600 
601         int count = 0;
602 
603         // delete leading quote
604         formatString.delete(index, index + 1);
605         length--;
606 
607         while (index < length) {
608             char c = formatString.charAt(index);
609 
610             if (c == QUOTE) {
611                 //  QUOTEQUOTE -> QUOTE
612                 if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
613 
614                     formatString.delete(index, index + 1);
615                     length--;
616                     count++;
617                     index++;
618                 } else {
619                     //  Closing QUOTE ends quoted text copying
620                     formatString.delete(index, index + 1);
621                     break;
622                 }
623             } else {
624                 index++;
625                 count++;
626             }
627         }
628 
629         return count;
630     }
631 
zeroPad(int inValue, int inMinDigits)632     private static String zeroPad(int inValue, int inMinDigits) {
633         return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
634     }
635 }
636