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 .put("TÜRKİYE", "TR") 96 .build(); 97 98 static Map<String, String> iso4217CountryToCountryCode = new TreeMap<>(); 99 static Set<String> exceptionList = new LinkedHashSet<>(); 100 static { 101 StandardCodes sc = StandardCodes.make(); 102 Set<String> countries = sc.getAvailableCodes("territory"); 103 for (String country : countries) { 104 String name = sc.getData("territory", country); name.toUpperCase(Locale.ENGLISH)105 iso4217CountryToCountryCode.put(name.toUpperCase(Locale.ENGLISH), country); 106 } 107 iso4217CountryToCountryCode.putAll(COUNTRY_CORRECTIONS); 108 } 109 110 private Relation<String, Data> codeList = Relation.of(new TreeMap<String, Set<Data>>(), TreeSet.class, null); 111 private Relation<String, String> countryToCodes = Relation.of(new TreeMap<String, Set<String>>(), TreeSet.class, null); 112 113 public static class Data implements Comparable<Object> { 114 private String name; 115 private String countryCode; 116 private int numericCode; 117 private int minor_unit; 118 Data(String countryCode, String name, int numericCode, int minor_unit)119 public Data(String countryCode, String name, int numericCode, int minor_unit) { 120 this.countryCode = countryCode; 121 this.name = name; 122 this.numericCode = numericCode; 123 this.minor_unit = minor_unit; 124 } 125 getCountryCode()126 public String getCountryCode() { 127 return countryCode; 128 } 129 getName()130 public String getName() { 131 return name; 132 } 133 getNumericCode()134 public int getNumericCode() { 135 return numericCode; 136 } 137 getMinorUnit()138 public int getMinorUnit() { 139 return minor_unit; 140 } 141 142 @Override toString()143 public String toString() { 144 return String.format("[%s,\t%s [%s],\t%d]", name, countryCode, 145 StandardCodes.make().getData("territory", countryCode), numericCode); 146 } 147 148 @Override compareTo(Object o)149 public int compareTo(Object o) { 150 Data other = (Data) o; 151 int result; 152 if (0 != (result = countryCode.compareTo(other.countryCode))) return result; 153 if (0 != (result = name.compareTo(other.name))) return result; 154 return numericCode - other.numericCode; 155 } 156 } 157 158 private static IsoCurrencyParser INSTANCE_WITHOUT_EXTENSIONS = new IsoCurrencyParser(false); 159 private static IsoCurrencyParser INSTANCE_WITH_EXTENSIONS = new IsoCurrencyParser(true); 160 getInstance(boolean useCLDRExtensions)161 public static IsoCurrencyParser getInstance(boolean useCLDRExtensions) { 162 return useCLDRExtensions ? INSTANCE_WITH_EXTENSIONS : INSTANCE_WITHOUT_EXTENSIONS; 163 } 164 getInstance()165 public static IsoCurrencyParser getInstance() { 166 return getInstance(true); 167 } 168 getCodeList()169 public Relation<String, Data> getCodeList() { 170 return codeList; 171 } 172 IsoCurrencyParser(boolean useCLDRExtensions)173 private IsoCurrencyParser(boolean useCLDRExtensions) { 174 175 ISOCurrencyHandler isoCurrentHandler = new ISOCurrencyHandler(); 176 XMLFileReader xfr = new XMLFileReader().setHandler(isoCurrentHandler); 177 xfr.readCLDRResource(ISO_CURRENT_CODES_XML, -1, false); 178 if (useCLDRExtensions) { 179 xfr.readCLDRResource(CLDR_EXTENSIONS_XML, -1, false); 180 } 181 if (exceptionList.size() != 0) { 182 throw new IllegalArgumentException(exceptionList.toString()); 183 } 184 codeList.freeze(); 185 countryToCodes.freeze(); 186 } 187 188 /* 189 * private Relation<String,Data> codeList = new Relation(new TreeMap(), TreeSet.class, null); 190 * private String version; 191 */ 192 getCountryToCodes()193 public Relation<String, String> getCountryToCodes() { 194 return countryToCodes; 195 } 196 getCountryCode(String iso4217Country)197 public static String getCountryCode(String iso4217Country) { 198 iso4217Country = iso4217Country.trim(); 199 if (iso4217Country.startsWith("\"")) { 200 iso4217Country = iso4217Country.substring(1, iso4217Country.length() - 1); 201 } 202 String name = iso4217CountryToCountryCode.get(iso4217Country); 203 if (name != null) return name; 204 if (iso4217Country.startsWith("ZZ")) { 205 return "ZZ"; 206 } 207 exceptionList.add(String.format(CldrUtility.LINE_SEPARATOR + "\t\t.put(\"%s\", \"XXX\") // fix XXX and add to COUNTRY_CORRECTIONS in " 208 + StackTracker.currentElement(0).getFileName(), iso4217Country)); 209 return "ZZ"; 210 } 211 212 public class ISOCurrencyHandler extends XMLFileReader.SimpleHandler { 213 214 // This Set represents the entries in ISO4217 which we know to be bad. I have sent e-mail 215 // to the ISO 4217 Maintenance agency attempting to get them removed. Once that happens, 216 // we can remove these as well. 217 // SVC - El Salvador Colon - not used anymore ( uses USD instead ) 218 // ZWL - Last Zimbabwe Dollar - abandoned due to hyper-inflation. 219 Set<String> KNOWN_BAD_ISO_DATA_CODES = new TreeSet<>(Arrays.asList("SVC", "ZWL")); 220 String country_code; 221 String currency_name; 222 String alphabetic_code; 223 int numeric_code; 224 int minor_unit; 225 226 /** 227 * Finish processing anything left hanging in the file. 228 */ cleanup()229 public void cleanup() { 230 } 231 232 @Override handlePathValue(String path, String value)233 public void handlePathValue(String path, String value) { 234 try { 235 XPathParts parts = XPathParts.getFrozenInstance(path); 236 String type = parts.getElement(-1); 237 if (type.equals("CtryNm")) { 238 value = value.replaceAll("\n", ""); 239 country_code = getCountryCode(value); 240 if (country_code == null) { 241 country_code = "ZZ"; 242 } 243 alphabetic_code = "XXX"; 244 numeric_code = -1; 245 minor_unit = 0; 246 } else if (type.equals("CcyNm")) { 247 currency_name = value; 248 } else if (type.equals("Ccy")) { 249 alphabetic_code = value; 250 } else if (type.equals("CcyNbr")) { 251 try { 252 numeric_code = Integer.valueOf(value); 253 } catch (NumberFormatException ex) { 254 numeric_code = -1; 255 } 256 } else if (type.equals("CcyMnrUnts")) { 257 try { 258 minor_unit = Integer.valueOf(value); 259 } catch (NumberFormatException ex) { 260 minor_unit = 2; 261 } 262 } 263 264 if (type.equals("CcyMnrUnts") && alphabetic_code.length() > 0 265 && !KNOWN_BAD_ISO_DATA_CODES.contains(alphabetic_code)) { 266 Data data = new Data(country_code, currency_name, numeric_code, minor_unit); 267 codeList.put(alphabetic_code, data); 268 countryToCodes.put(data.getCountryCode(), alphabetic_code); 269 } 270 271 } catch (Exception e) { 272 throw (IllegalArgumentException) new IllegalArgumentException("path: " 273 + path + ",\tvalue: " + value).initCause(e); 274 } 275 } 276 } 277 } 278