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.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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    654     private static String zeroPad(int inValue, int inMinDigits) {
    655         return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
    656     }
    657 }
    658