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