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