1 /* 2 * Copyright (C) 2008 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 libcore.icu; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.icu.lang.UCharacter; 21 import android.icu.text.DateTimePatternGenerator; 22 import android.icu.text.TimeZoneFormat; 23 import android.icu.util.Currency; 24 import android.icu.util.IllformedLocaleException; 25 import android.icu.util.ULocale; 26 27 import com.android.icu.util.ExtendedCalendar; 28 import com.android.icu.util.LocaleNative; 29 30 import java.util.Arrays; 31 import java.util.Collections; 32 import java.util.Date; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.LinkedHashSet; 36 import java.util.Locale; 37 import java.util.Map; 38 import java.util.Map.Entry; 39 import java.util.Set; 40 import java.util.stream.Stream; 41 42 import libcore.util.BasicLruCache; 43 44 /** 45 * Makes ICU data accessible to Java. 46 * @hide 47 */ 48 public final class ICU { 49 50 @UnsupportedAppUsage 51 private static final BasicLruCache<String, String> CACHED_PATTERNS = 52 new BasicLruCache<String, String>(8); 53 54 private static volatile Locale[] availableLocalesCache; 55 56 private static volatile String[] isoCountries; 57 private static volatile Set<String> isoCountriesSet; 58 59 private static volatile String[] isoLanguages; 60 61 /** 62 * Avoid initialization with many dependencies here, because when this is called, 63 * lower-level classes, e.g. java.lang.System, are not initialized and java.lang.System 64 * relies on getIcuVersion(). 65 */ 66 static { 67 68 } 69 ICU()70 private ICU() { 71 } 72 initializeCacheInZygote()73 public static void initializeCacheInZygote() { 74 // Fill CACHED_PATTERNS with the patterns from default locale and en-US initially. 75 // This should be called in Zygote pre-fork process and the initial values in the cache 76 // can be shared among app. The cache was filled by LocaleData in the older Android platform, 77 // but moved here, due to an performance issue http://b/161846393. 78 // It initializes 2 x 4 = 8 values in the CACHED_PATTERNS whose max size should be >= 8. 79 for (Locale locale : new Locale[] {Locale.US, Locale.getDefault()}) { 80 getTimePattern(locale, false, false); 81 getTimePattern(locale, false, true); 82 getTimePattern(locale, true, false); 83 getTimePattern(locale, true, true); 84 } 85 } 86 87 /** 88 * Returns an array of two-letter ISO 639-1 language codes, either from ICU or our cache. 89 */ getISOLanguages()90 public static String[] getISOLanguages() { 91 if (isoLanguages == null) { 92 synchronized (ICU.class) { 93 if (isoLanguages == null) { 94 isoLanguages = getISOLanguagesNative(); 95 } 96 } 97 } 98 return isoLanguages.clone(); 99 } 100 101 /** 102 * Returns an array of two-letter ISO 3166 country codes, either from ICU or our cache. 103 */ getISOCountries()104 public static String[] getISOCountries() { 105 return getISOCountriesInternal().clone(); 106 } 107 108 /** 109 * Returns true if the string is a 2-letter ISO 3166 country code. 110 */ isIsoCountry(String country)111 public static boolean isIsoCountry(String country) { 112 if (isoCountriesSet == null) { 113 synchronized (ICU.class) { 114 if (isoCountriesSet == null) { 115 String[] isoCountries = getISOCountriesInternal(); 116 Set<String> newSet = new HashSet<>(isoCountries.length); 117 for (String isoCountry : isoCountries) { 118 newSet.add(isoCountry); 119 } 120 isoCountriesSet = newSet; 121 } 122 } 123 } 124 return country != null && isoCountriesSet.contains(country); 125 } 126 getISOCountriesInternal()127 private static String[] getISOCountriesInternal() { 128 if (isoCountries == null) { 129 synchronized (ICU.class) { 130 if (isoCountries == null) { 131 isoCountries = getISOCountriesNative(); 132 } 133 } 134 } 135 return isoCountries; 136 } 137 138 139 140 private static final int IDX_LANGUAGE = 0; 141 private static final int IDX_SCRIPT = 1; 142 private static final int IDX_REGION = 2; 143 private static final int IDX_VARIANT = 3; 144 145 /* 146 * Parse the {Language, Script, Region, Variant*} section of the ICU locale 147 * ID. This is the bit that appears before the keyword separate "@". The general 148 * structure is a series of ASCII alphanumeric strings (subtags) 149 * separated by underscores. 150 * 151 * Each subtag is interpreted according to its position in the list of subtags 152 * AND its length (groan...). The various cases are explained in comments 153 * below. 154 */ parseLangScriptRegionAndVariants(String string, String[] outputArray)155 private static void parseLangScriptRegionAndVariants(String string, 156 String[] outputArray) { 157 final int first = string.indexOf('_'); 158 final int second = string.indexOf('_', first + 1); 159 final int third = string.indexOf('_', second + 1); 160 161 if (first == -1) { 162 outputArray[IDX_LANGUAGE] = string; 163 } else if (second == -1) { 164 // Language and country ("ja_JP") OR 165 // Language and script ("en_Latn") OR 166 // Language and variant ("en_POSIX"). 167 168 outputArray[IDX_LANGUAGE] = string.substring(0, first); 169 final String secondString = string.substring(first + 1); 170 171 if (secondString.length() == 4) { 172 // 4 Letter ISO script code. 173 outputArray[IDX_SCRIPT] = secondString; 174 } else if (secondString.length() == 2 || secondString.length() == 3) { 175 // 2 or 3 Letter region code. 176 outputArray[IDX_REGION] = secondString; 177 } else { 178 // If we're here, the length of the second half is either 1 or greater 179 // than 5. Assume that ICU won't hand us malformed tags, and therefore 180 // assume the rest of the string is a series of variant tags. 181 outputArray[IDX_VARIANT] = secondString; 182 } 183 } else if (third == -1) { 184 // Language and country and variant ("ja_JP_TRADITIONAL") OR 185 // Language and script and variant ("en_Latn_POSIX") OR 186 // Language and script and region ("en_Latn_US"). OR 187 // Language and variant with multiple subtags ("en_POSIX_XISOP") 188 189 outputArray[IDX_LANGUAGE] = string.substring(0, first); 190 final String secondString = string.substring(first + 1, second); 191 final String thirdString = string.substring(second + 1); 192 193 if (secondString.length() == 4) { 194 // The second subtag is a script. 195 outputArray[IDX_SCRIPT] = secondString; 196 197 // The third subtag can be either a region or a variant, depending 198 // on its length. 199 if (thirdString.length() == 2 || thirdString.length() == 3 || 200 thirdString.isEmpty()) { 201 outputArray[IDX_REGION] = thirdString; 202 } else { 203 outputArray[IDX_VARIANT] = thirdString; 204 } 205 } else if (secondString.isEmpty() || 206 secondString.length() == 2 || secondString.length() == 3) { 207 // The second string is a region, and the third a variant. 208 outputArray[IDX_REGION] = secondString; 209 outputArray[IDX_VARIANT] = thirdString; 210 } else { 211 // Variant with multiple subtags. 212 outputArray[IDX_VARIANT] = string.substring(first + 1); 213 } 214 } else { 215 // Language, script, region and variant with 1 or more subtags 216 // ("en_Latn_US_POSIX") OR 217 // Language, region and variant with 2 or more subtags 218 // (en_US_POSIX_VARIANT). 219 outputArray[IDX_LANGUAGE] = string.substring(0, first); 220 final String secondString = string.substring(first + 1, second); 221 if (secondString.length() == 4) { 222 outputArray[IDX_SCRIPT] = secondString; 223 outputArray[IDX_REGION] = string.substring(second + 1, third); 224 outputArray[IDX_VARIANT] = string.substring(third + 1); 225 } else { 226 outputArray[IDX_REGION] = secondString; 227 outputArray[IDX_VARIANT] = string.substring(second + 1); 228 } 229 } 230 } 231 232 /** 233 * Returns the appropriate {@code Locale} given a {@code String} of the form returned 234 * by {@code toString}. This is very lenient, and doesn't care what's between the underscores: 235 * this method can parse strings that {@code Locale.toString} won't produce. 236 * Used to remove duplication. 237 */ localeFromIcuLocaleId(String localeId)238 public static Locale localeFromIcuLocaleId(String localeId) { 239 // @ == ULOC_KEYWORD_SEPARATOR_UNICODE (uloc.h). 240 final int extensionsIndex = localeId.indexOf('@'); 241 242 Map<Character, String> extensionsMap = Collections.EMPTY_MAP; 243 Map<String, String> unicodeKeywordsMap = Collections.EMPTY_MAP; 244 Set<String> unicodeAttributeSet = Collections.EMPTY_SET; 245 246 if (extensionsIndex != -1) { 247 extensionsMap = new HashMap<Character, String>(); 248 unicodeKeywordsMap = new HashMap<String, String>(); 249 unicodeAttributeSet = new HashSet<String>(); 250 251 // ICU sends us a semi-colon (ULOC_KEYWORD_ITEM_SEPARATOR) delimited string 252 // containing all "keywords" it could parse. An ICU keyword is a key-value pair 253 // separated by an "=" (ULOC_KEYWORD_ASSIGN). 254 // 255 // Each keyword item can be one of three things : 256 // - A unicode extension attribute list: In this case the item key is "attribute" 257 // and the value is a hyphen separated list of unicode attributes. 258 // - A unicode extension keyword: In this case, the item key will be larger than 259 // 1 char in length, and the value will be the unicode extension value. 260 // - A BCP-47 extension subtag: In this case, the item key will be exactly one 261 // char in length, and the value will be a sequence of unparsed subtags that 262 // represent the extension. 263 // 264 // Note that this implies that unicode extension keywords are "promoted" to 265 // to the same namespace as the top level extension subtags and their values. 266 // There can't be any collisions in practice because the BCP-47 spec imposes 267 // restrictions on their lengths. 268 final String extensionsString = localeId.substring(extensionsIndex + 1); 269 final String[] extensions = extensionsString.split(";"); 270 for (String extension : extensions) { 271 // This is the special key for the unicode attributes 272 if (extension.startsWith("attribute=")) { 273 String unicodeAttributeValues = extension.substring("attribute=".length()); 274 for (String unicodeAttribute : unicodeAttributeValues.split("-")) { 275 unicodeAttributeSet.add(unicodeAttribute); 276 } 277 } else { 278 final int separatorIndex = extension.indexOf('='); 279 280 if (separatorIndex == 1) { 281 // This is a BCP-47 extension subtag. 282 final String value = extension.substring(2); 283 final char extensionId = extension.charAt(0); 284 285 extensionsMap.put(extensionId, value); 286 } else { 287 // This is a unicode extension keyword. 288 unicodeKeywordsMap.put(extension.substring(0, separatorIndex), 289 extension.substring(separatorIndex + 1)); 290 } 291 } 292 } 293 } 294 295 final String[] outputArray = new String[] { "", "", "", "" }; 296 if (extensionsIndex == -1) { 297 parseLangScriptRegionAndVariants(localeId, outputArray); 298 } else { 299 parseLangScriptRegionAndVariants(localeId.substring(0, extensionsIndex), 300 outputArray); 301 } 302 Locale.Builder builder = new Locale.Builder(); 303 builder.setLanguage(outputArray[IDX_LANGUAGE]); 304 builder.setRegion(outputArray[IDX_REGION]); 305 builder.setVariant(outputArray[IDX_VARIANT]); 306 builder.setScript(outputArray[IDX_SCRIPT]); 307 for (String attribute : unicodeAttributeSet) { 308 builder.addUnicodeLocaleAttribute(attribute); 309 } 310 for (Entry<String, String> keyword : unicodeKeywordsMap.entrySet()) { 311 builder.setUnicodeLocaleKeyword(keyword.getKey(), keyword.getValue()); 312 } 313 314 for (Entry<Character, String> extension : extensionsMap.entrySet()) { 315 builder.setExtension(extension.getKey(), extension.getValue()); 316 } 317 318 return builder.build(); 319 } 320 localesFromStrings(String[] localeNames)321 public static Locale[] localesFromStrings(String[] localeNames) { 322 // We need to remove duplicates caused by the conversion of "he" to "iw", et cetera. 323 // Java needs the obsolete code, ICU needs the modern code, but we let ICU know about 324 // both so that we never need to convert back when talking to it. 325 LinkedHashSet<Locale> set = new LinkedHashSet<Locale>(); 326 for (String localeName : localeNames) { 327 set.add(localeFromIcuLocaleId(localeName)); 328 } 329 return set.toArray(new Locale[set.size()]); 330 } 331 332 // This method returns availableLocalesCache array as-it-is. Do not leak it. getAvailableLocalesInternal()333 private static Locale[] getAvailableLocalesInternal() { 334 if (availableLocalesCache == null) { 335 synchronized (ICU.class) { 336 if (availableLocalesCache == null) { 337 availableLocalesCache = localesFromStrings(getAvailableLocalesNative()); 338 } 339 } 340 } 341 return availableLocalesCache; 342 } 343 getAvailableLocales()344 public static Locale[] getAvailableLocales() { 345 return getAvailableLocalesInternal().clone(); 346 } 347 streamAvailableLocales()348 public static Stream<Locale> streamAvailableLocales() { 349 return Arrays.stream(getAvailableLocalesInternal()); 350 } 351 352 /** 353 * Content of {@link #availableLocalesCache} depends on the USE_NEW_ISO_LOCALE_CODES flag value. 354 * Resetting it so a following {@link #getAvailableLocales()} call will fill it with the right 355 * values. 356 */ 357 // VisibleForTesting clearAvailableLocales()358 public static void clearAvailableLocales() { 359 availableLocalesCache = null; 360 } 361 362 /** 363 * DO NOT USE this method directly. 364 * Please use {@link SimpleDateFormatData.DateTimeFormatStringGenerator#getTimePattern} 365 */ getTimePattern(Locale locale, boolean is24Hour, boolean withSecond)366 /* package */ static String getTimePattern(Locale locale, boolean is24Hour, boolean withSecond) { 367 final String skeleton; 368 if (withSecond) { 369 skeleton = is24Hour ? "Hms" : "hms"; 370 } else { 371 skeleton = is24Hour ? "Hm" : "hm"; 372 } 373 return getBestDateTimePattern(skeleton, locale); 374 } 375 /** 376 * DO NOT USE this method directly. 377 * Please use {@link SimpleDateFormatData.DateTimeFormatStringGenerator#getTimePattern} 378 */ 379 @UnsupportedAppUsage getBestDateTimePattern(String skeleton, Locale locale)380 public static String getBestDateTimePattern(String skeleton, Locale locale) { 381 String languageTag = locale.toLanguageTag(); 382 String key = skeleton + "\t" + languageTag; 383 synchronized (CACHED_PATTERNS) { 384 String pattern = CACHED_PATTERNS.get(key); 385 if (pattern == null) { 386 pattern = getBestDateTimePattern0(skeleton, locale); 387 CACHED_PATTERNS.put(key, pattern); 388 } 389 return pattern; 390 } 391 } 392 getBestDateTimePattern0(String skeleton, Locale locale)393 private static String getBestDateTimePattern0(String skeleton, Locale locale) { 394 DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(locale); 395 return dtpg.getBestPattern(skeleton); 396 } 397 398 @UnsupportedAppUsage getBestDateTimePatternNative(String skeleton, String languageTag)399 private static String getBestDateTimePatternNative(String skeleton, String languageTag) { 400 return getBestDateTimePattern0(skeleton, Locale.forLanguageTag(languageTag)); 401 } 402 403 @UnsupportedAppUsage getDateFormatOrder(String pattern)404 public static char[] getDateFormatOrder(String pattern) { 405 char[] result = new char[3]; 406 int resultIndex = 0; 407 boolean sawDay = false; 408 boolean sawMonth = false; 409 boolean sawYear = false; 410 411 for (int i = 0; i < pattern.length(); ++i) { 412 char ch = pattern.charAt(i); 413 if (ch == 'd' || ch == 'L' || ch == 'M' || ch == 'y') { 414 if (ch == 'd' && !sawDay) { 415 result[resultIndex++] = 'd'; 416 sawDay = true; 417 } else if ((ch == 'L' || ch == 'M') && !sawMonth) { 418 result[resultIndex++] = 'M'; 419 sawMonth = true; 420 } else if ((ch == 'y') && !sawYear) { 421 result[resultIndex++] = 'y'; 422 sawYear = true; 423 } 424 } else if (ch == 'G') { 425 // Ignore the era specifier, if present. 426 } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { 427 throw new IllegalArgumentException("Bad pattern character '" + ch + "' in " + pattern); 428 } else if (ch == '\'') { 429 if (i < pattern.length() - 1 && pattern.charAt(i + 1) == '\'') { 430 ++i; 431 } else { 432 i = pattern.indexOf('\'', i + 1); 433 if (i == -1) { 434 throw new IllegalArgumentException("Bad quoting in " + pattern); 435 } 436 ++i; 437 } 438 } else { 439 // Ignore spaces and punctuation. 440 } 441 } 442 return result; 443 } 444 445 /** 446 * {@link java.time.format.DateTimeFormatter} does not handle some date symbols, e.g. 'B' / 'b', 447 * and thus we use a heuristic algorithm to remove the symbol. See http://b/174804526. 448 * See {@link #transformIcuDateTimePattern(String)} for documentation about the implementation. 449 */ transformIcuDateTimePattern_forJavaTime(String pattern)450 public static String transformIcuDateTimePattern_forJavaTime(String pattern) { 451 return transformIcuDateTimePattern(pattern, /* isJavaTime= */ true); 452 } 453 454 /** 455 * {@link java.text.SimpleDateFormat} does not handle some date symbols, e.g. 'B' / 'b', 456 * and simply ignore the symbol in formatting. Instead, we should avoid exposing the symbol 457 * entirely in all public APIs, e.g. {@link java.text.SimpleDateFormat#toPattern()}, 458 * and thus we use a heuristic algorithm to remove the symbol. See http://b/174804526. 459 * See {@link #transformIcuDateTimePattern(String)} for documentation about the implementation. 460 */ transformIcuDateTimePattern_forJavaText(String pattern)461 public static String transformIcuDateTimePattern_forJavaText(String pattern) { 462 return transformIcuDateTimePattern(pattern, /* isJavaTime= */ false); 463 } 464 465 /** 466 * Rewrite the date/time pattern coming ICU to be consumed by libcore classes. 467 * It's an ideal place to rewrite the pattern entirely when multiple symbols not digested 468 * by libcore need to be removed/processed. Rewriting in single place could be more efficient 469 * in a small or constant number of scans instead of scanning for every symbol. 470 * 471 * {@link LocaleData#initLocaleData(Locale)} also rewrites time format, but only a subset of 472 * patterns. In the future, that should migrate to this function in order to handle the symbols 473 * in one place, but now separate because java.text and java.time handles different sets of 474 * symbols. 475 */ transformIcuDateTimePattern(String pattern, boolean isJavaTime)476 private static String transformIcuDateTimePattern(String pattern, boolean isJavaTime) { 477 if (pattern == null) { 478 return null; 479 } 480 481 pattern = transformSymbolB(pattern); 482 483 if (isJavaTime) { 484 // '#' is reserved for the future use in java.time, but it's treated as literal in CLDR. 485 // It needs to be quoted for the usage in java.time. 486 pattern = transformHashSign(pattern); 487 } 488 489 return pattern; 490 } 491 transformHashSign(String pattern)492 private static String transformHashSign(String pattern) { 493 if (pattern.indexOf('#') == -1) { 494 return pattern; 495 } 496 497 StringBuilder sb = new StringBuilder(pattern.length()); 498 boolean isInQuote = false; 499 for (int i = 0; i < pattern.length(); i++) { 500 char curr = pattern.charAt(i); 501 if (isInQuote) { 502 if (curr == '\'') { 503 // e.g. '' represents a single quote literal or 'xyz' represents literal text. 504 // This applies to both java.time and java.text date / time patterns. 505 isInQuote = false; 506 } 507 sb.append(curr); 508 } else if (curr == '#') { 509 sb.append("'#'"); 510 } else { 511 if (curr == '\'') { 512 isInQuote = true; 513 } 514 sb.append(curr); 515 } 516 } 517 return sb.toString(); 518 519 } 520 transformSymbolB(String pattern)521 private static String transformSymbolB(String pattern) { 522 // For details about the different symbols, see 523 // http://cldr.unicode.org/translation/date-time-1/date-time-patterns#TOC-Day-period-patterns 524 // The symbols B means "Day periods with locale-specific ranges". 525 // English example: 2:00 at night, 10:00 in the morning, 12:00 in the afternoon. 526 boolean contains_B = pattern.indexOf('B') != -1; 527 // AM, PM, noon and midnight. English example: 10:00 AM, 12:00 noon, 7:00 PM 528 boolean contains_b = pattern.indexOf('b') != -1; 529 530 if (!contains_B && !contains_b) { 531 return pattern; 532 } 533 534 // Simply remove the symbol 'B' and 'b' if 24-hour 'H' exists because the 24-hour format 535 // provides enough information and the day periods are optional. See http://b/174804526. 536 // Don't handle symbol 'B'/'b' with 12-hour 'h' because it's much more complicated because 537 // we likely need to replace 'B'/'b' with 'a' inserted into a new right position or use other 538 // ways. 539 if (pattern.indexOf('H') != -1) { 540 return removeBFromDateTimePattern(pattern); 541 } 542 543 // Non-ideal workaround until http://b/68139386 is implemented. 544 // This workaround may create a pattern that isn't usual / common for the language users. 545 if (pattern.indexOf('h') != -1) { 546 if (contains_b) { 547 pattern = replaceSymbolInDatePattern(pattern, 'b', 'a'); 548 } 549 if (contains_B) { 550 pattern = replaceSymbolInDatePattern(pattern, 'B', 'a'); 551 } 552 } // else { } // not sure what to do as we assume that B is only useful when the hour is given. 553 554 return pattern; 555 } 556 557 /** 558 * Remove 'b' and 'B' from simple patterns, e.g. "B H:mm" and "dd-MM-yy B HH:mm:ss" only. 559 */ removeBFromDateTimePattern(String pattern)560 private static String removeBFromDateTimePattern(String pattern) { 561 // The below implementation can likely be replaced by a regular expression via 562 // String.replaceAll(). However, it's known that libcore's regex implementation is more 563 // memory-intensive, and the below implementation is likely cheaper, but it's not yet measured. 564 StringBuilder sb = new StringBuilder(pattern.length()); 565 char prev = ' '; // the initial value is not used. 566 boolean isInQuote = false; 567 for (int i = 0; i < pattern.length(); i++) { 568 char curr = pattern.charAt(i); 569 if (isInQuote) { 570 if (curr == '\'') { 571 // e.g. '' represents a single quote literal or 'xyz' represents literal text. 572 // This applies to both java.time and java.text date / time patterns. 573 isInQuote = false; 574 } 575 sb.append(curr); 576 continue; 577 } 578 switch(curr) { 579 case 'B': 580 case 'b': 581 // Ignore 'B' and 'b' 582 break; 583 case ' ': // Ascii whitespace 584 // caveat: Ideally it's a case for all Unicode whitespaces by UCharacter.isUWhiteSpace(c) 585 // but checking ascii whitespace only is enough for the CLDR data when this is written. 586 if (i != 0 && (prev == 'B' || prev == 'b')) { 587 // Ignore the whitespace behind the symbol 'B'/'b' because it's likely a whitespace to 588 // separate the day period with the next text. 589 } else { 590 sb.append(curr); 591 } 592 break; 593 case '\'': 594 isInQuote = true; 595 sb.append(curr); 596 break; 597 default: 598 sb.append(curr); 599 break; 600 } 601 prev = curr; 602 } 603 604 // Remove the trailing whitespace which is likely following the symbol 'B'/'b' in the original 605 // pattern, e.g. "hh:mm B" (12:00 in the afternoon). 606 int lastIndex = sb.length() - 1; 607 if (lastIndex >= 0 && sb.charAt(lastIndex) == ' ') { 608 sb.deleteCharAt(lastIndex); 609 } 610 return sb.toString(); 611 } 612 613 replaceSymbolInDatePattern(String pattern, char existingSymbol, char newSymbol)614 private static String replaceSymbolInDatePattern(String pattern, char existingSymbol, 615 char newSymbol) { 616 if (pattern.indexOf('\'') == -1) { 617 // Fast path if the pattern contains no quoted literals. 618 return pattern.replace(existingSymbol, newSymbol); 619 } 620 621 StringBuilder sb = new StringBuilder(pattern.length()); 622 boolean isInQuote = false; 623 for (int i = 0; i < pattern.length(); i++) { 624 char curr = pattern.charAt(i); 625 char modified; 626 if (isInQuote) { 627 if (curr == '\'') { 628 // e.g. '' represents a single quote literal or 'xyz' represents literal text. 629 // This applies to both java.time and java.text date / time patterns. 630 isInQuote = false; 631 } 632 modified = curr; 633 } else if (curr == '\'') { 634 isInQuote = true; 635 modified = curr; 636 } else if (curr == existingSymbol) { 637 modified = newSymbol; 638 } else { 639 modified = curr; 640 } 641 sb.append(modified); 642 } 643 return sb.toString(); 644 } 645 646 /** 647 * Returns the version of the CLDR data in use, such as "22.1.1". 648 * 649 */ getCldrVersion()650 public static native String getCldrVersion(); 651 652 /** 653 * Returns the icu4c version in use, such as "50.1.1". 654 */ getIcuVersion()655 public static native String getIcuVersion(); 656 657 /** 658 * Returns the Unicode version our ICU supports, such as "6.2". 659 */ getUnicodeVersion()660 public static native String getUnicodeVersion(); 661 662 // --- Errors. 663 664 // --- Native methods accessing ICU's database. 665 getAvailableLocalesNative()666 private static native String[] getAvailableLocalesNative(); 667 668 /** 669 * Query ICU for the currency being used in the country right now. 670 * @param countryCode ISO 3166 two-letter country code 671 * @return ISO 4217 3-letter currency code if found, otherwise null. 672 */ getCurrencyCode(String countryCode)673 public static String getCurrencyCode(String countryCode) { 674 // Fail fast when country code is not valid. 675 if (countryCode == null || countryCode.length() == 0) { 676 return null; 677 } 678 final ULocale countryLocale; 679 try { 680 countryLocale = new ULocale.Builder().setRegion(countryCode).build(); 681 } catch (IllformedLocaleException e) { 682 return null; // Return null on invalid country code. 683 } 684 String[] isoCodes = Currency.getAvailableCurrencyCodes(countryLocale, new Date()); 685 if (isoCodes == null || isoCodes.length == 0) { 686 return null; 687 } 688 return isoCodes[0]; 689 } 690 691 getISO3Country(String languageTag)692 public static native String getISO3Country(String languageTag); 693 getISO3Language(String languageTag)694 public static native String getISO3Language(String languageTag); 695 696 /** 697 * @deprecated Use {@link android.icu.util.ULocale#addLikelySubtags(ULocale)} instead. 698 * The method is only kept for @UnsupportedAppUsage. 699 */ 700 @UnsupportedAppUsage 701 @Deprecated addLikelySubtags(Locale locale)702 public static Locale addLikelySubtags(Locale locale) { 703 return ULocale.addLikelySubtags(ULocale.forLocale(locale)).toLocale(); 704 } 705 706 /** 707 * @return ICU localeID 708 * @deprecated Use {@link android.icu.util.ULocale#addLikelySubtags(ULocale)} instead. 709 * The method is only kept for @UnsupportedAppUsage. 710 */ 711 @UnsupportedAppUsage 712 @Deprecated addLikelySubtags(String locale)713 public static String addLikelySubtags(String locale) { 714 return ULocale.addLikelySubtags(new ULocale(locale)).getName(); 715 } 716 717 /** 718 * @deprecated use {@link java.util.Locale#getScript()} instead. This has been kept 719 * around only for the support library. 720 */ 721 @UnsupportedAppUsage 722 @Deprecated getScript(String locale)723 public static native String getScript(String locale); 724 getISOLanguagesNative()725 private static native String[] getISOLanguagesNative(); getISOCountriesNative()726 private static native String[] getISOCountriesNative(); 727 728 /** 729 * Takes a BCP-47 language tag (Locale.toLanguageTag()). e.g. en-US, not en_US 730 */ setDefaultLocale(String languageTag)731 public static void setDefaultLocale(String languageTag) { 732 LocaleNative.setDefault(languageTag); 733 } 734 735 /** 736 * Returns a locale name, not a BCP-47 language tag. e.g. en_US not en-US. 737 */ getDefaultLocale()738 public static native String getDefaultLocale(); 739 740 741 /** 742 * @param calendarType LDML-defined legacy calendar type. See keyTypeData.txt in ICU. 743 */ getExtendedCalendar(Locale locale, String calendarType)744 public static ExtendedCalendar getExtendedCalendar(Locale locale, String calendarType) { 745 ULocale uLocale = ULocale.forLocale(locale) 746 .setKeywordValue("calendar", calendarType); 747 return ExtendedCalendar.getInstance(uLocale); 748 } 749 750 /** 751 * Converts CLDR LDML short time zone id to an ID that can be recognized by 752 * {@link java.util.TimeZone#getTimeZone(String)}. 753 * @param cldrShortTzId 754 * @return null if no tz id can be matched to the short id. 755 */ convertToTzId(String cldrShortTzId)756 public static String convertToTzId(String cldrShortTzId) { 757 if (cldrShortTzId == null) { 758 return null; 759 } 760 String tzid = ULocale.toLegacyType("tz", cldrShortTzId); 761 // ULocale.toLegacyType() returns the lower case of the input ID if it matches the spec, but 762 // it's not a valid tz id. 763 if (tzid == null || tzid.equals(cldrShortTzId.toLowerCase(Locale.ROOT))) { 764 return null; 765 } 766 return tzid; 767 } 768 getGMTZeroFormatString(Locale locale)769 public static String getGMTZeroFormatString(Locale locale) { 770 return TimeZoneFormat.getInstance(locale).getGMTZeroFormat(); 771 } 772 773 /** 774 * If {@link java.lang.Character} calls {@link UCharacter#hasBinaryProperty(int, int)} directly, 775 * Dex2oatImageTest.TestExtension gtest fails. dex2oat fails to initialize the class because 776 * class verification fails and returns kAccessChecksFailure error when creating 777 * a boot image extension. 778 * This method is created to avoid the class initialization and verification failure. 779 * If this method creates any actual runtime circular dependency between {@link Character} 780 * and {@link UCharacter#hasBinaryProperty(int, int)}, consider use the ICU4C API instead. 781 * https://developer.android.com/ndk/reference/group/icu4c#u_hasbinaryproperty 782 */ hasBinaryProperty(int ch, int property)783 public static boolean hasBinaryProperty(int ch, int property) { 784 return UCharacter.hasBinaryProperty(ch, property); 785 } 786 787 } 788