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