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 Set<String> KNOWN_BAD_ISO_DATA_CODES = new TreeSet<>(Arrays.asList("SVC")); 228 String country_code; 229 String currency_name; 230 String alphabetic_code; 231 int numeric_code; 232 int minor_unit; 233 234 /** Finish processing anything left hanging in the file. */ cleanup()235 public void cleanup() {} 236 237 @Override handlePathValue(String path, String value)238 public void handlePathValue(String path, String value) { 239 try { 240 XPathParts parts = XPathParts.getFrozenInstance(path); 241 String type = parts.getElement(-1); 242 if (type.equals("CtryNm")) { 243 value = value.replaceAll("\n", ""); 244 country_code = getCountryCode(value); 245 if (country_code == null) { 246 country_code = "ZZ"; 247 } 248 alphabetic_code = "XXX"; 249 numeric_code = -1; 250 minor_unit = 0; 251 } else if (type.equals("CcyNm")) { 252 currency_name = value; 253 } else if (type.equals("Ccy")) { 254 alphabetic_code = value; 255 } else if (type.equals("CcyNbr")) { 256 try { 257 numeric_code = Integer.parseInt(value); 258 } catch (NumberFormatException ex) { 259 numeric_code = -1; 260 } 261 } else if (type.equals("CcyMnrUnts")) { 262 try { 263 minor_unit = Integer.parseInt(value); 264 } catch (NumberFormatException ex) { 265 minor_unit = 2; 266 } 267 } 268 269 if (type.equals("CcyMnrUnts") 270 && alphabetic_code.length() > 0 271 && !KNOWN_BAD_ISO_DATA_CODES.contains(alphabetic_code)) { 272 Data data = new Data(country_code, currency_name, numeric_code, minor_unit); 273 codeList.put(alphabetic_code, data); 274 countryToCodes.put(data.getCountryCode(), alphabetic_code); 275 } 276 277 } catch (Exception e) { 278 throw (IllegalArgumentException) 279 new IllegalArgumentException("path: " + path + ",\tvalue: " + value) 280 .initCause(e); 281 } 282 } 283 } 284 } 285