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