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