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