• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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