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