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 */ is24HourFormat(Context context)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 */ is24HourFormat(Context context, int userHandle)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 */ is24HourLocale(@onNull Locale locale)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 */ getBestDateTimePattern(Locale locale, String skeleton)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 */ getTimeFormat(Context context)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 */ getTimeFormatString(Context context)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 */ getTimeFormatString(Context context, int userHandle)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 */ getDateFormat(Context context)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 */ getLongDateFormat(Context context)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 */ getMediumDateFormat(Context context)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 */ getDateFormatOrder(Context context)332 public static char[] getDateFormatOrder(Context context) { 333 return ICU.getDateFormatOrder(getDateFormatString(context)); 334 } 335 getDateFormatString(Context context)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 */ format(CharSequence inFormat, long inTimeInMillis)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 */ format(CharSequence inFormat, Date inDate)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 */ hasSeconds(CharSequence inFormat)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 */ hasDesignator(CharSequence inFormat, char designator)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 */ format(CharSequence inFormat, Calendar inDate)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 getDayOfWeekString(LocaleData ld, int day, int count, int kind)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 getMonthString(LocaleData ld, int month, int count, int kind)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 getTimeZoneString(Calendar inDate, int count)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 formatZoneOffset(int offset, int count)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 getYearString(int year, int count)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 */ appendQuotedText(SpannableStringBuilder formatString, int index)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 zeroPad(int inValue, int inMinDigits)627 private static String zeroPad(int inValue, int inMinDigits) { 628 return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue); 629 } 630 } 631