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