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