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