xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/IsoCurrencyParser.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import com.google.common.collect.ImmutableMap;
4 import com.ibm.icu.impl.Relation;
5 import com.ibm.icu.impl.Utility;
6 import java.util.Arrays;
7 import java.util.LinkedHashSet;
8 import java.util.Locale;
9 import java.util.Map;
10 import java.util.Set;
11 import java.util.TreeMap;
12 import java.util.TreeSet;
13 
14 public class IsoCurrencyParser {
15 
16     /** Note: path is relative to CldrUtility, {@link CldrUtility#getInputStream(String)} */
17     private static final String ISO_CURRENT_CODES_XML = "dl_iso_table_a1.xml";
18 
19     /*
20      * IsoCurrencyParser doesn't currently use the historic codes list, but it could easily be modified/extended to do
21      * so if we need to at some point. (JCE)
22      * private static final String ISO_HISTORIC_CODES_XML = "dl_iso_tables_a3.xml";
23      */
24 
25     /*
26      * CLDR_EXTENSIONS_XML is stuff that would/should be in ISO, but that we KNOW for a fact to be correct.
27      * Some subterritory designations that we use in CLDR, like Ascension Island or Tristan da Cunha aren't
28      * used in ISO4217, so we use an extensions data file to allow our tests to validate the CLDR data properly.
29      */
30     private static final String CLDR_EXTENSIONS_XML = "dl_cldr_extensions.xml";
31 
32     /*
33      * These corrections are country descriptions that are in the ISO4217 tables but carry a different spelling
34      * in the language subtag registry.
35      */
36     private static final ImmutableMap<String, String> COUNTRY_CORRECTIONS =
37             new ImmutableMap.Builder<String, String>()
38                     .put("UNITED ARAB EMIRATES (THE)", "AE")
39                     .put(Utility.unescape("\u00C5LAND ISLANDS"), "AX")
40                     .put("SAINT BARTH\u00C9LEMY", "BL")
41                     .put("BOLIVIA (PLURINATIONAL STATE OF)", "BO")
42                     .put("BAHAMAS (THE)", "BS")
43                     .put("COCOS (KEELING) ISLANDS (THE)", "CC")
44                     .put("CONGO (THE DEMOCRATIC REPUBLIC OF THE)", "CD")
45                     .put("CENTRAL AFRICAN REPUBLIC (THE)", "CF")
46                     .put("CONGO (THE)", "CG")
47                     .put(Utility.unescape("C\u00D4TE D\u2019IVOIRE"), "CI")
48                     .put("COOK ISLANDS (THE)", "CK")
49                     .put("CABO VERDE", "CV")
50                     .put(Utility.unescape("CURA\u00C7AO"), "CW")
51                     .put("CZECHIA", "CZ")
52                     .put("DOMINICAN REPUBLIC (THE)", "DO")
53                     .put("FALKLAND ISLANDS (THE) [MALVINAS]", "FK")
54                     .put("MICRONESIA (FEDERATED STATES OF)", "FM")
55                     .put("FAROE ISLANDS (THE)", "FO")
56                     .put("UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE)", "GB")
57                     .put("GAMBIA (THE)", "GM")
58                     .put("HEARD ISLAND AND McDONALD ISLANDS", "HM")
59                     .put("BRITISH INDIAN OCEAN TERRITORY (THE)", "IO")
60                     .put("IRAN (ISLAMIC REPUBLIC OF)", "IR")
61                     .put("COMOROS (THE)", "KM")
62                     .put(Utility.unescape("KOREA (THE DEMOCRATIC PEOPLE\u2019S REPUBLIC OF)"), "KP")
63                     .put("KOREA (THE REPUBLIC OF)", "KR")
64                     .put("CAYMAN ISLANDS (THE)", "KY")
65                     .put(Utility.unescape("LAO PEOPLE\u2019S DEMOCRATIC REPUBLIC (THE)"), "LA")
66                     .put("MOLDOVA (THE REPUBLIC OF)", "MD")
67                     .put("SAINT MARTIN", "MF")
68                     .put("MARSHALL ISLANDS (THE)", "MH")
69                     .put("MACEDONIA (THE FORMER YUGOSLAV REPUBLIC OF)", "MK")
70                     .put("NORTHERN MARIANA ISLANDS (THE)", "MP")
71                     .put("NETHERLANDS (THE)", "NL")
72                     .put("NIGER (THE)", "NE")
73                     .put("PHILIPPINES (THE)", "PH")
74                     .put("PALESTINE, STATE OF", "PS")
75                     .put(Utility.unescape("R\u00C9UNION"), "RE")
76                     .put("RUSSIAN FEDERATION (THE)", "RU")
77                     .put("SUDAN (THE)", "SD")
78                     .put("ESWATINI", "SZ")
79                     .put("TURKS AND CAICOS ISLANDS (THE)", "TC")
80                     .put("FRENCH SOUTHERN TERRITORIES (THE)", "TF")
81                     .put("TAIWAN (PROVINCE OF CHINA)", "TW")
82                     .put("TANZANIA, UNITED REPUBLIC OF", "TZ")
83                     .put("UNITED STATES MINOR OUTLYING ISLANDS (THE)", "UM")
84                     .put("UNITED STATES OF AMERICA (THE)", "US")
85                     .put("HOLY SEE (THE)", "VA")
86                     .put("VENEZUELA (BOLIVARIAN REPUBLIC OF)", "VE")
87                     .put("VIRGIN ISLANDS (BRITISH)", "VG")
88                     .put("VIRGIN ISLANDS (U.S.)", "VI")
89                     .put(Utility.unescape("INTERNATIONAL MONETARY FUND (IMF)\u00A0"), "ZZ")
90                     .put("MEMBER COUNTRIES OF THE AFRICAN DEVELOPMENT BANK GROUP", "ZZ")
91                     .put("SISTEMA UNITARIO DE COMPENSACION REGIONAL DE PAGOS \"SUCRE\"", "ZZ")
92                     .put("EUROPEAN MONETARY CO-OPERATION FUND (EMCF)", "ZZ")
93                     .put("TÜRKİYE", "TR")
94                     .build();
95 
96     static Map<String, String> iso4217CountryToCountryCode = new TreeMap<>();
97     static Set<String> exceptionList = new LinkedHashSet<>();
98 
99     static {
100         StandardCodes sc = StandardCodes.make();
101         Set<String> countries = sc.getAvailableCodes("territory");
102         for (String country : countries) {
103             String name = sc.getData("territory", country);
name.toUpperCase(Locale.ENGLISH)104             iso4217CountryToCountryCode.put(name.toUpperCase(Locale.ENGLISH), country);
105         }
106         iso4217CountryToCountryCode.putAll(COUNTRY_CORRECTIONS);
107     }
108 
109     private Relation<String, Data> codeList =
110             Relation.of(new TreeMap<String, Set<Data>>(), TreeSet.class, null);
111     private Relation<String, String> countryToCodes =
112             Relation.of(new TreeMap<String, Set<String>>(), TreeSet.class, null);
113 
114     public static class Data implements Comparable<Object> {
115         private String name;
116         private String countryCode;
117         private int numericCode;
118         private int minor_unit;
119 
Data(String countryCode, String name, int numericCode, int minor_unit)120         public Data(String countryCode, String name, int numericCode, int minor_unit) {
121             this.countryCode = countryCode;
122             this.name = name;
123             this.numericCode = numericCode;
124             this.minor_unit = minor_unit;
125         }
126 
getCountryCode()127         public String getCountryCode() {
128             return countryCode;
129         }
130 
getName()131         public String getName() {
132             return name;
133         }
134 
getNumericCode()135         public int getNumericCode() {
136             return numericCode;
137         }
138 
getMinorUnit()139         public int getMinorUnit() {
140             return minor_unit;
141         }
142 
143         @Override
toString()144         public String toString() {
145             return String.format(
146                     "[%s,\t%s [%s],\t%d]",
147                     name,
148                     countryCode,
149                     StandardCodes.make().getData("territory", countryCode),
150                     numericCode);
151         }
152 
153         @Override
compareTo(Object o)154         public int compareTo(Object o) {
155             Data other = (Data) o;
156             int result;
157             if (0 != (result = countryCode.compareTo(other.countryCode))) return result;
158             if (0 != (result = name.compareTo(other.name))) return result;
159             return numericCode - other.numericCode;
160         }
161     }
162 
163     private static IsoCurrencyParser INSTANCE_WITHOUT_EXTENSIONS = new IsoCurrencyParser(false);
164     private static IsoCurrencyParser INSTANCE_WITH_EXTENSIONS = new IsoCurrencyParser(true);
165 
getInstance(boolean useCLDRExtensions)166     public static IsoCurrencyParser getInstance(boolean useCLDRExtensions) {
167         return useCLDRExtensions ? INSTANCE_WITH_EXTENSIONS : INSTANCE_WITHOUT_EXTENSIONS;
168     }
169 
getInstance()170     public static IsoCurrencyParser getInstance() {
171         return getInstance(true);
172     }
173 
getCodeList()174     public Relation<String, Data> getCodeList() {
175         return codeList;
176     }
177 
IsoCurrencyParser(boolean useCLDRExtensions)178     private IsoCurrencyParser(boolean useCLDRExtensions) {
179 
180         ISOCurrencyHandler isoCurrentHandler = new ISOCurrencyHandler();
181         XMLFileReader xfr = new XMLFileReader().setHandler(isoCurrentHandler);
182         xfr.readCLDRResource(ISO_CURRENT_CODES_XML, -1, false);
183         if (useCLDRExtensions) {
184             xfr.readCLDRResource(CLDR_EXTENSIONS_XML, -1, false);
185         }
186         if (exceptionList.size() != 0) {
187             throw new IllegalArgumentException(exceptionList.toString());
188         }
189         codeList.freeze();
190         countryToCodes.freeze();
191     }
192 
193     /*
194      * private Relation<String,Data> codeList = new Relation(new TreeMap(), TreeSet.class, null);
195      * private String version;
196      */
197 
getCountryToCodes()198     public Relation<String, String> getCountryToCodes() {
199         return countryToCodes;
200     }
201 
getCountryCode(String iso4217Country)202     public static String getCountryCode(String iso4217Country) {
203         iso4217Country = iso4217Country.trim();
204         if (iso4217Country.startsWith("\"")) {
205             iso4217Country = iso4217Country.substring(1, iso4217Country.length() - 1);
206         }
207         String name = iso4217CountryToCountryCode.get(iso4217Country);
208         if (name != null) return name;
209         if (iso4217Country.startsWith("ZZ")) {
210             return "ZZ";
211         }
212         exceptionList.add(
213                 String.format(
214                         CldrUtility.LINE_SEPARATOR
215                                 + "\t\t.put(\"%s\", \"XXX\") // fix XXX and add to COUNTRY_CORRECTIONS in "
216                                 + StackTracker.currentElement(0).getFileName(),
217                         iso4217Country));
218         return "ZZ";
219     }
220 
221     public class ISOCurrencyHandler extends XMLFileReader.SimpleHandler {
222 
223         // This Set represents the entries in ISO4217 which we know to be bad. I have sent e-mail
224         // to the ISO 4217 Maintenance agency attempting to get them removed. Once that happens,
225         // we can remove these as well.
226         // SVC - El Salvador Colon - not used anymore ( uses USD instead )
227         // ZWL - Last Zimbabwe Dollar - abandoned due to hyper-inflation.
228         Set<String> KNOWN_BAD_ISO_DATA_CODES = new TreeSet<>(Arrays.asList("SVC", "ZWL"));
229         String country_code;
230         String currency_name;
231         String alphabetic_code;
232         int numeric_code;
233         int minor_unit;
234 
235         /** Finish processing anything left hanging in the file. */
cleanup()236         public void cleanup() {}
237 
238         @Override
handlePathValue(String path, String value)239         public void handlePathValue(String path, String value) {
240             try {
241                 XPathParts parts = XPathParts.getFrozenInstance(path);
242                 String type = parts.getElement(-1);
243                 if (type.equals("CtryNm")) {
244                     value = value.replaceAll("\n", "");
245                     country_code = getCountryCode(value);
246                     if (country_code == null) {
247                         country_code = "ZZ";
248                     }
249                     alphabetic_code = "XXX";
250                     numeric_code = -1;
251                     minor_unit = 0;
252                 } else if (type.equals("CcyNm")) {
253                     currency_name = value;
254                 } else if (type.equals("Ccy")) {
255                     alphabetic_code = value;
256                 } else if (type.equals("CcyNbr")) {
257                     try {
258                         numeric_code = Integer.parseInt(value);
259                     } catch (NumberFormatException ex) {
260                         numeric_code = -1;
261                     }
262                 } else if (type.equals("CcyMnrUnts")) {
263                     try {
264                         minor_unit = Integer.parseInt(value);
265                     } catch (NumberFormatException ex) {
266                         minor_unit = 2;
267                     }
268                 }
269 
270                 if (type.equals("CcyMnrUnts")
271                         && alphabetic_code.length() > 0
272                         && !KNOWN_BAD_ISO_DATA_CODES.contains(alphabetic_code)) {
273                     Data data = new Data(country_code, currency_name, numeric_code, minor_unit);
274                     codeList.put(alphabetic_code, data);
275                     countryToCodes.put(data.getCountryCode(), alphabetic_code);
276                 }
277 
278             } catch (Exception e) {
279                 throw (IllegalArgumentException)
280                         new IllegalArgumentException("path: " + path + ",\tvalue: " + value)
281                                 .initCause(e);
282             }
283         }
284     }
285 }
286