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 /** 35 Utility class for producing strings with formatted date/time. 36 37 <p> 38 Most callers should avoid supplying their own format strings to this 39 class' {@code format} methods and rely on the correctly localized ones 40 supplied by the system. This class' factory methods return 41 appropriately-localized {@link java.text.DateFormat} instances, suitable 42 for both formatting and parsing dates. For the canonical documentation 43 of format strings, see {@link java.text.SimpleDateFormat}. 44 </p> 45 <p> 46 The format methods in this class takes as inputs a format string and a representation of a date/time. 47 The format string controls how the output is generated. 48 This class only supports a subset of the full Unicode specification. 49 Use {@link java.text.SimpleDateFormat} if you need more. 50 Formatting characters may be repeated in order to get more detailed representations 51 of that field. For instance, the format character 'M' is used to 52 represent the month. Depending on how many times that character is repeated 53 you get a different representation. 54 </p> 55 <p> 56 For the month of September:<br/> 57 M -> 9<br/> 58 MM -> 09<br/> 59 MMM -> Sep<br/> 60 MMMM -> September 61 </p> 62 <p> 63 The effects of the duplication vary depending on the nature of the field. 64 See the notes on the individual field formatters for details. For purely numeric 65 fields such as <code>HOUR</code> adding more copies of the designator will 66 zero-pad the value to that number of characters. 67 </p> 68 <p> 69 For 7 minutes past the hour:<br/> 70 m -> 7<br/> 71 mm -> 07<br/> 72 mmm -> 007<br/> 73 mmmm -> 0007 74 </p> 75 <p> 76 Examples for April 6, 1970 at 3:23am:<br/> 77 "MM/dd/yy h:mmaa" -> "04/06/70 3:23am"<br/> 78 "MMM dd, yyyy h:mmaa" -> "Apr 6, 1970 3:23am"<br/> 79 "MMMM dd, yyyy h:mmaa" -> "April 6, 1970 3:23am"<br/> 80 "E, MMMM dd, yyyy h:mmaa" -> "Mon, April 6, 1970 3:23am&<br/> 81 "EEEE, MMMM dd, yyyy h:mmaa" -> "Monday, April 6, 1970 3:23am"<br/> 82 "'Noteworthy day: 'M/d/yy" -> "Noteworthy day: 4/6/70" 83 */ 84 85 public class DateFormat { 86 /** 87 Text in the format string that should be copied verbatim rather that 88 interpreted as formatting codes must be surrounded by the <code>QUOTE</code> 89 character. If you need to embed a literal <code>QUOTE</code> character in 90 the output text then use two in a row. 91 */ 92 public static final char QUOTE = '\''; 93 94 /** 95 This designator indicates whether the <code>HOUR</code> field is before 96 or after noon. The output is lower-case. 97 98 Examples: 99 a -> a or p 100 aa -> am or pm 101 */ 102 public static final char AM_PM = 'a'; 103 104 /** 105 This designator indicates whether the <code>HOUR</code> field is before 106 or after noon. The output is capitalized. 107 108 Examples: 109 A -> A or P 110 AA -> AM or PM 111 */ 112 public static final char CAPITAL_AM_PM = 'A'; 113 114 /** 115 This designator indicates the day of the month. 116 117 Examples for the 9th of the month: 118 d -> 9 119 dd -> 09 120 */ 121 public static final char DATE = 'd'; 122 123 /** 124 This designator indicates the name of the day of the week. 125 126 Examples for Sunday: 127 E -> Sun 128 EEEE -> Sunday 129 */ 130 public static final char DAY = 'E'; 131 132 /** 133 This designator indicates the hour of the day in 12 hour format. 134 135 Examples for 3pm: 136 h -> 3 137 hh -> 03 138 */ 139 public static final char HOUR = 'h'; 140 141 /** 142 This designator indicates the hour of the day in 24 hour format. 143 144 Example for 3pm: 145 k -> 15 146 147 Examples for midnight: 148 k -> 0 149 kk -> 00 150 */ 151 public static final char HOUR_OF_DAY = 'k'; 152 153 /** 154 This designator indicates the minute of the hour. 155 156 Examples for 7 minutes past the hour: 157 m -> 7 158 mm -> 07 159 */ 160 public static final char MINUTE = 'm'; 161 162 /** 163 This designator indicates the month of the year. See also 164 {@link #STANDALONE_MONTH}. 165 166 Examples for September: 167 M -> 9 168 MM -> 09 169 MMM -> Sep 170 MMMM -> September 171 */ 172 public static final char MONTH = 'M'; 173 174 /** 175 This designator indicates the standalone month of the year, 176 necessary in some format strings in some languages. For 177 example, Russian distinguishes between the "June" in 178 "June" and that in "June 2010". 179 */ 180 public static final char STANDALONE_MONTH = 'L'; 181 182 /** 183 This designator indicates the seconds of the minute. 184 185 Examples for 7 seconds past the minute: 186 s -> 7 187 ss -> 07 188 */ 189 public static final char SECONDS = 's'; 190 191 /** 192 This designator indicates the offset of the timezone from GMT. 193 194 Example for US/Pacific timezone: 195 z -> -0800 196 zz -> PST 197 */ 198 public static final char TIME_ZONE = 'z'; 199 200 /** 201 This designator indicates the year. 202 203 Examples for 2006 204 y -> 06 205 yyyy -> 2006 206 */ 207 public static final char YEAR = 'y'; 208 209 210 private static final Object sLocaleLock = new Object(); 211 private static Locale sIs24HourLocale; 212 private static boolean sIs24Hour; 213 214 215 /** 216 * Returns true if user preference is set to 24-hour format. 217 * @param context the context to use for the content resolver 218 * @return true if 24 hour time format is selected, false otherwise. 219 */ is24HourFormat(Context context)220 public static boolean is24HourFormat(Context context) { 221 String value = Settings.System.getString(context.getContentResolver(), 222 Settings.System.TIME_12_24); 223 224 if (value == null) { 225 Locale locale = context.getResources().getConfiguration().locale; 226 227 synchronized (sLocaleLock) { 228 if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) { 229 return sIs24Hour; 230 } 231 } 232 233 java.text.DateFormat natural = 234 java.text.DateFormat.getTimeInstance( 235 java.text.DateFormat.LONG, locale); 236 237 if (natural instanceof SimpleDateFormat) { 238 SimpleDateFormat sdf = (SimpleDateFormat) natural; 239 String pattern = sdf.toPattern(); 240 241 if (pattern.indexOf('H') >= 0) { 242 value = "24"; 243 } else { 244 value = "12"; 245 } 246 } else { 247 value = "12"; 248 } 249 250 synchronized (sLocaleLock) { 251 sIs24HourLocale = locale; 252 sIs24Hour = value.equals("24"); 253 } 254 255 return sIs24Hour; 256 } 257 258 return value.equals("24"); 259 } 260 261 /** 262 * Returns a {@link java.text.DateFormat} object that can format the time according 263 * to the current locale and the user's 12-/24-hour clock preference. 264 * @param context the application context 265 * @return the {@link java.text.DateFormat} object that properly formats the time. 266 */ getTimeFormat(Context context)267 public static java.text.DateFormat getTimeFormat(Context context) { 268 boolean b24 = is24HourFormat(context); 269 int res; 270 271 if (b24) { 272 res = R.string.twenty_four_hour_time_format; 273 } else { 274 res = R.string.twelve_hour_time_format; 275 } 276 277 return new java.text.SimpleDateFormat(context.getString(res)); 278 } 279 280 /** 281 * Returns a {@link java.text.DateFormat} object that can format the date 282 * in short form (such as 12/31/1999) according 283 * to the current locale and the user's date-order preference. 284 * @param context the application context 285 * @return the {@link java.text.DateFormat} object that properly formats the date. 286 */ getDateFormat(Context context)287 public static java.text.DateFormat getDateFormat(Context context) { 288 String value = Settings.System.getString(context.getContentResolver(), 289 Settings.System.DATE_FORMAT); 290 291 return getDateFormatForSetting(context, value); 292 } 293 294 /** 295 * Returns a {@link java.text.DateFormat} object to format the date 296 * as if the date format setting were set to <code>value</code>, 297 * including null to use the locale's default format. 298 * @param context the application context 299 * @param value the date format setting string to interpret for 300 * the current locale 301 * @hide 302 */ getDateFormatForSetting(Context context, String value)303 public static java.text.DateFormat getDateFormatForSetting(Context context, 304 String value) { 305 String format = getDateFormatStringForSetting(context, value); 306 307 return new java.text.SimpleDateFormat(format); 308 } 309 getDateFormatStringForSetting(Context context, String value)310 private static String getDateFormatStringForSetting(Context context, String value) { 311 if (value != null) { 312 int month = value.indexOf('M'); 313 int day = value.indexOf('d'); 314 int year = value.indexOf('y'); 315 316 if (month >= 0 && day >= 0 && year >= 0) { 317 String template = context.getString(R.string.numeric_date_template); 318 if (year < month && year < day) { 319 if (month < day) { 320 value = String.format(template, "yyyy", "MM", "dd"); 321 } else { 322 value = String.format(template, "yyyy", "dd", "MM"); 323 } 324 } else if (month < day) { 325 if (day < year) { 326 value = String.format(template, "MM", "dd", "yyyy"); 327 } else { // unlikely 328 value = String.format(template, "MM", "yyyy", "dd"); 329 } 330 } else { // day < month 331 if (month < year) { 332 value = String.format(template, "dd", "MM", "yyyy"); 333 } else { // unlikely 334 value = String.format(template, "dd", "yyyy", "MM"); 335 } 336 } 337 338 return value; 339 } 340 } 341 342 /* 343 * The setting is not set; use the default. 344 * We use a resource string here instead of just DateFormat.SHORT 345 * so that we get a four-digit year instead a two-digit year. 346 */ 347 value = context.getString(R.string.numeric_date_format); 348 return value; 349 } 350 351 /** 352 * Returns a {@link java.text.DateFormat} object that can format the date 353 * in long form (such as December 31, 1999) for the current locale. 354 * @param context the application context 355 * @return the {@link java.text.DateFormat} object that formats the date in long form. 356 */ getLongDateFormat(Context context)357 public static java.text.DateFormat getLongDateFormat(Context context) { 358 return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG); 359 } 360 361 /** 362 * Returns a {@link java.text.DateFormat} object that can format the date 363 * in medium form (such as Dec. 31, 1999) for the current locale. 364 * @param context the application context 365 * @return the {@link java.text.DateFormat} object that formats the date in long form. 366 */ getMediumDateFormat(Context context)367 public static java.text.DateFormat getMediumDateFormat(Context context) { 368 return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM); 369 } 370 371 /** 372 * Gets the current date format stored as a char array. The array will contain 373 * 3 elements ({@link #DATE}, {@link #MONTH}, and {@link #YEAR}) in the order 374 * specified by the user's format preference. Note that this order is 375 * only appropriate for all-numeric dates; spelled-out (MEDIUM and LONG) 376 * dates will generally contain other punctuation, spaces, or words, 377 * not just the day, month, and year, and not necessarily in the same 378 * order returned here. 379 */ getDateFormatOrder(Context context)380 public static char[] getDateFormatOrder(Context context) { 381 char[] order = new char[] {DATE, MONTH, YEAR}; 382 String value = getDateFormatString(context); 383 int index = 0; 384 boolean foundDate = false; 385 boolean foundMonth = false; 386 boolean foundYear = false; 387 388 for (char c : value.toCharArray()) { 389 if (!foundDate && (c == DATE)) { 390 foundDate = true; 391 order[index] = DATE; 392 index++; 393 } 394 395 if (!foundMonth && (c == MONTH || c == STANDALONE_MONTH)) { 396 foundMonth = true; 397 order[index] = MONTH; 398 index++; 399 } 400 401 if (!foundYear && (c == YEAR)) { 402 foundYear = true; 403 order[index] = YEAR; 404 index++; 405 } 406 } 407 return order; 408 } 409 getDateFormatString(Context context)410 private static String getDateFormatString(Context context) { 411 String value = Settings.System.getString(context.getContentResolver(), 412 Settings.System.DATE_FORMAT); 413 414 return getDateFormatStringForSetting(context, value); 415 } 416 417 /** 418 * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a 419 * CharSequence containing the requested date. 420 * @param inFormat the format string, as described in {@link android.text.format.DateFormat} 421 * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT 422 * @return a {@link CharSequence} containing the requested text 423 */ format(CharSequence inFormat, long inTimeInMillis)424 public static CharSequence format(CharSequence inFormat, long inTimeInMillis) { 425 return format(inFormat, new Date(inTimeInMillis)); 426 } 427 428 /** 429 * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing 430 * the requested date. 431 * @param inFormat the format string, as described in {@link android.text.format.DateFormat} 432 * @param inDate the date to format 433 * @return a {@link CharSequence} containing the requested text 434 */ format(CharSequence inFormat, Date inDate)435 public static CharSequence format(CharSequence inFormat, Date inDate) { 436 Calendar c = new GregorianCalendar(); 437 438 c.setTime(inDate); 439 440 return format(inFormat, c); 441 } 442 443 /** 444 * Indicates whether the specified format string contains seconds. 445 * 446 * Always returns false if the input format is null. 447 * 448 * @param inFormat the format string, as described in {@link android.text.format.DateFormat} 449 * 450 * @return true if the format string contains {@link #SECONDS}, false otherwise 451 * 452 * @hide 453 */ hasSeconds(CharSequence inFormat)454 public static boolean hasSeconds(CharSequence inFormat) { 455 if (inFormat == null) return false; 456 457 final int length = inFormat.length(); 458 459 int c; 460 int count; 461 462 for (int i = 0; i < length; i += count) { 463 count = 1; 464 c = inFormat.charAt(i); 465 466 if (c == QUOTE) { 467 count = skipQuotedText(inFormat, i, length); 468 } else if (c == SECONDS) { 469 return true; 470 } 471 } 472 473 return false; 474 } 475 skipQuotedText(CharSequence s, int i, int len)476 private static int skipQuotedText(CharSequence s, int i, int len) { 477 if (i + 1 < len && s.charAt(i + 1) == QUOTE) { 478 return 2; 479 } 480 481 int count = 1; 482 // skip leading quote 483 i++; 484 485 while (i < len) { 486 char c = s.charAt(i); 487 488 if (c == QUOTE) { 489 count++; 490 // QUOTEQUOTE -> QUOTE 491 if (i + 1 < len && s.charAt(i + 1) == QUOTE) { 492 i++; 493 } else { 494 break; 495 } 496 } else { 497 i++; 498 count++; 499 } 500 } 501 502 return count; 503 } 504 505 /** 506 * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence 507 * containing the requested date. 508 * @param inFormat the format string, as described in {@link android.text.format.DateFormat} 509 * @param inDate the date to format 510 * @return a {@link CharSequence} containing the requested text 511 */ format(CharSequence inFormat, Calendar inDate)512 public static CharSequence format(CharSequence inFormat, Calendar inDate) { 513 SpannableStringBuilder s = new SpannableStringBuilder(inFormat); 514 int c; 515 int count; 516 517 int len = inFormat.length(); 518 519 for (int i = 0; i < len; i += count) { 520 int temp; 521 522 count = 1; 523 c = s.charAt(i); 524 525 if (c == QUOTE) { 526 count = appendQuotedText(s, i, len); 527 len = s.length(); 528 continue; 529 } 530 531 while ((i + count < len) && (s.charAt(i + count) == c)) { 532 count++; 533 } 534 535 String replacement; 536 537 switch (c) { 538 case AM_PM: 539 replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM)); 540 break; 541 542 case CAPITAL_AM_PM: 543 //FIXME: this is the same as AM_PM? no capital? 544 replacement = DateUtils.getAMPMString(inDate.get(Calendar.AM_PM)); 545 break; 546 547 case DATE: 548 replacement = zeroPad(inDate.get(Calendar.DATE), count); 549 break; 550 551 case DAY: 552 temp = inDate.get(Calendar.DAY_OF_WEEK); 553 replacement = DateUtils.getDayOfWeekString(temp, 554 count < 4 ? 555 DateUtils.LENGTH_MEDIUM : 556 DateUtils.LENGTH_LONG); 557 break; 558 559 case HOUR: 560 temp = inDate.get(Calendar.HOUR); 561 562 if (0 == temp) 563 temp = 12; 564 565 replacement = zeroPad(temp, count); 566 break; 567 568 case HOUR_OF_DAY: 569 replacement = zeroPad(inDate.get(Calendar.HOUR_OF_DAY), count); 570 break; 571 572 case MINUTE: 573 replacement = zeroPad(inDate.get(Calendar.MINUTE), count); 574 break; 575 576 case MONTH: 577 case STANDALONE_MONTH: 578 replacement = getMonthString(inDate, count, c); 579 break; 580 581 case SECONDS: 582 replacement = zeroPad(inDate.get(Calendar.SECOND), count); 583 break; 584 585 case TIME_ZONE: 586 replacement = getTimeZoneString(inDate, count); 587 break; 588 589 case YEAR: 590 replacement = getYearString(inDate, count); 591 break; 592 593 default: 594 replacement = null; 595 break; 596 } 597 598 if (replacement != null) { 599 s.replace(i, i + count, replacement); 600 count = replacement.length(); // CARE: count is used in the for loop above 601 len = s.length(); 602 } 603 } 604 605 if (inFormat instanceof Spanned) 606 return new SpannedString(s); 607 else 608 return s.toString(); 609 } 610 getMonthString(Calendar inDate, int count, int kind)611 private static String getMonthString(Calendar inDate, int count, int kind) { 612 boolean standalone = (kind == STANDALONE_MONTH); 613 int month = inDate.get(Calendar.MONTH); 614 615 if (count >= 4) { 616 return standalone 617 ? DateUtils.getStandaloneMonthString(month, DateUtils.LENGTH_LONG) 618 : DateUtils.getMonthString(month, DateUtils.LENGTH_LONG); 619 } else if (count == 3) { 620 return standalone 621 ? DateUtils.getStandaloneMonthString(month, DateUtils.LENGTH_MEDIUM) 622 : DateUtils.getMonthString(month, DateUtils.LENGTH_MEDIUM); 623 } else { 624 // Calendar.JANUARY == 0, so add 1 to month. 625 return zeroPad(month+1, count); 626 } 627 } 628 getTimeZoneString(Calendar inDate, int count)629 private static String getTimeZoneString(Calendar inDate, int count) { 630 TimeZone tz = inDate.getTimeZone(); 631 632 if (count < 2) { // FIXME: shouldn't this be <= 2 ? 633 return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) + 634 inDate.get(Calendar.ZONE_OFFSET), 635 count); 636 } else { 637 boolean dst = inDate.get(Calendar.DST_OFFSET) != 0; 638 return tz.getDisplayName(dst, TimeZone.SHORT); 639 } 640 } 641 formatZoneOffset(int offset, int count)642 private static String formatZoneOffset(int offset, int count) { 643 offset /= 1000; // milliseconds to seconds 644 StringBuilder tb = new StringBuilder(); 645 646 if (offset < 0) { 647 tb.insert(0, "-"); 648 offset = -offset; 649 } else { 650 tb.insert(0, "+"); 651 } 652 653 int hours = offset / 3600; 654 int minutes = (offset % 3600) / 60; 655 656 tb.append(zeroPad(hours, 2)); 657 tb.append(zeroPad(minutes, 2)); 658 return tb.toString(); 659 } 660 getYearString(Calendar inDate, int count)661 private static String getYearString(Calendar inDate, int count) { 662 int year = inDate.get(Calendar.YEAR); 663 return (count <= 2) ? zeroPad(year % 100, 2) 664 : String.format(Locale.getDefault(), "%d", year); 665 } 666 appendQuotedText(SpannableStringBuilder s, int i, int len)667 private static int appendQuotedText(SpannableStringBuilder s, int i, int len) { 668 if (i + 1 < len && s.charAt(i + 1) == QUOTE) { 669 s.delete(i, i + 1); 670 return 1; 671 } 672 673 int count = 0; 674 675 // delete leading quote 676 s.delete(i, i + 1); 677 len--; 678 679 while (i < len) { 680 char c = s.charAt(i); 681 682 if (c == QUOTE) { 683 // QUOTEQUOTE -> QUOTE 684 if (i + 1 < len && s.charAt(i + 1) == QUOTE) { 685 686 s.delete(i, i + 1); 687 len--; 688 count++; 689 i++; 690 } else { 691 // Closing QUOTE ends quoted text copying 692 s.delete(i, i + 1); 693 break; 694 } 695 } else { 696 i++; 697 count++; 698 } 699 } 700 701 return count; 702 } 703 zeroPad(int inValue, int inMinDigits)704 private static String zeroPad(int inValue, int inMinDigits) { 705 return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue); 706 } 707 } 708