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