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