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