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