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.content.Context;
20 import android.os.UserHandle;
21 import android.provider.Settings;
22 import android.text.SpannableStringBuilder;
23 import android.text.Spanned;
24 import android.text.SpannedString;
25 
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.GregorianCalendar;
29 import java.util.Locale;
30 import java.util.TimeZone;
31 import java.text.SimpleDateFormat;
32 
33 import libcore.icu.ICU;
34 import 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  */
61 public 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      */
is24HourFormat(Context context)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      */
is24HourFormat(Context context, int userHandle)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      */
getBestDateTimePattern(Locale locale, String skeleton)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      */
getTimeFormat(Context context)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      */
getTimeFormatString(Context context)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      */
getTimeFormatString(Context context, int userHandle)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      */
getDateFormat(Context context)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      */
getLongDateFormat(Context context)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      */
getMediumDateFormat(Context context)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      */
getDateFormatOrder(Context context)322     public static char[] getDateFormatOrder(Context context) {
323         return ICU.getDateFormatOrder(getDateFormatString());
324     }
325 
getDateFormatString()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      */
format(CharSequence inFormat, long inTimeInMillis)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      */
format(CharSequence inFormat, Date inDate)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      */
hasSeconds(CharSequence inFormat)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      */
hasDesignator(CharSequence inFormat, char designator)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 
skipQuotedText(CharSequence s, int i, int len)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      */
format(CharSequence inFormat, Calendar inDate)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 
getDayOfWeekString(LocaleData ld, int day, int count, int kind)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 
getMonthString(LocaleData ld, int month, int count, int kind)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 
getTimeZoneString(Calendar inDate, int count)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 
formatZoneOffset(int offset, int count)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 
getYearString(int year, int count)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 
appendQuotedText(SpannableStringBuilder s, int i, int len)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 
zeroPad(int inValue, int inMinDigits)631     private static String zeroPad(int inValue, int inMinDigits) {
632         return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
633     }
634 }
635