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