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