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