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