1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved. 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * This code is free software; you can redistribute it and/or modify it 7 * under the terms of the GNU General Public License version 2 only, as 8 * published by the Free Software Foundation. Oracle designates this 9 * particular file as subject to the "Classpath" exception as provided 10 * by Oracle in the LICENSE file that accompanied this code. 11 * 12 * This code is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 15 * version 2 for more details (a copy is included in the LICENSE file that 16 * accompanied this code). 17 * 18 * You should have received a copy of the GNU General Public License version 19 * 2 along with this work; if not, write to the Free Software Foundation, 20 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 21 * 22 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 23 * or visit www.oracle.com if you need additional information or have any 24 * questions. 25 */ 26 27 package java.util; 28 29 import java.io.BufferedInputStream; 30 import java.io.DataInputStream; 31 import java.io.File; 32 import java.io.FileReader; 33 import java.io.InputStream; 34 import java.io.IOException; 35 import java.io.Serializable; 36 import java.util.concurrent.ConcurrentHashMap; 37 import java.util.concurrent.ConcurrentMap; 38 import java.util.regex.Pattern; 39 import java.util.regex.Matcher; 40 import java.util.stream.Collectors; 41 42 import sun.util.logging.PlatformLogger; 43 44 import libcore.icu.ICU; 45 46 // BEGIN Android-changed: Removed docs about superseding runtime currency data. 47 // Doing so via a properties file is not supported on Android. 48 /** 49 * Represents a currency. Currencies are identified by their ISO 4217 currency 50 * codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm"> 51 * ISO web site</a> for more information. 52 * <p> 53 * The class is designed so that there's never more than one 54 * {@code Currency} instance for any given currency. Therefore, there's 55 * no public constructor. You obtain a {@code Currency} instance using 56 * the {@code getInstance} methods. 57 * 58 * <p> 59 * It is recommended to use {@link java.math.BigDecimal} class while dealing 60 * with {@code Currency} or monetary values as it provides better handling of floating 61 * point numbers and their operations. 62 * 63 * @spec http://www.iso.org/iso/home/standards/currency_codes.htm ISO - ISO 4217 - Currency codes 64 * @see java.math.BigDecimal 65 * @since 1.4 66 */ 67 // END Android-changed: Removed docs about superseding runtime currency data. 68 @SuppressWarnings("removal") 69 public final class Currency implements Serializable { 70 71 @java.io.Serial 72 private static final long serialVersionUID = -158308464356906721L; 73 74 /** 75 * ISO 4217 currency code for this currency. 76 * 77 * @serial 78 */ 79 private final String currencyCode; 80 81 // BEGIN Android-changed: Use ICU. 82 // We do not keep track of defaultFractionDigits and numericCode separately. 83 /* 84 /** 85 * Default fraction digits for this currency. 86 * Set from currency data tables. 87 * 88 private final transient int defaultFractionDigits; 89 */ 90 91 /* 92 * ISO 4217 numeric code for this currency. 93 * Set from currency data tables. 94 * 95 private final transient int numericCode; 96 */ 97 private transient final android.icu.util.Currency icuCurrency; 98 // END Android-changed: Use ICU. 99 100 101 // class data: instance map 102 103 private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7); 104 private static HashSet<Currency> available; 105 106 // BEGIN Android-removed: Use ICU. 107 // We don't need any of these static fields nor the static initializer. 108 /* 109 // Class data: currency data obtained from currency.data file. 110 // Purpose: 111 // - determine valid country codes 112 // - determine valid currency codes 113 // - map country codes to currency codes 114 // - obtain default fraction digits for currency codes 115 // 116 // sc = special case; dfd = default fraction digits 117 // Simple countries are those where the country code is a prefix of the 118 // currency code, and there are no known plans to change the currency. 119 // 120 // table formats: 121 // - mainTable: 122 // - maps country code to 32-bit int 123 // - 26*26 entries, corresponding to [A-Z]*[A-Z] 124 // - \u007F -> not valid country 125 // - bits 20-31: unused 126 // - bits 10-19: numeric code (0 to 1023) 127 // - bit 9: 1 - special case, bits 0-4 indicate which one 128 // 0 - simple country, bits 0-4 indicate final char of currency code 129 // - bits 5-8: fraction digits for simple countries, 0 for special cases 130 // - bits 0-4: final char for currency code for simple country, or ID of special case 131 // - special case IDs: 132 // - 0: country has no currency 133 // - other: index into specialCasesList 134 135 static int formatVersion; 136 static int dataVersion; 137 static int[] mainTable; 138 static List<SpecialCaseEntry> specialCasesList; 139 static List<OtherCurrencyEntry> otherCurrenciesList; 140 141 // handy constants - must match definitions in GenerateCurrencyData 142 // magic number 143 private static final int MAGIC_NUMBER = 0x43757244; 144 // number of characters from A to Z 145 private static final int A_TO_Z = ('Z' - 'A') + 1; 146 // entry for invalid country codes 147 private static final int INVALID_COUNTRY_ENTRY = 0x0000007F; 148 // entry for countries without currency 149 private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200; 150 // mask for simple case country entries 151 private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000; 152 // mask for simple case country entry final character 153 private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F; 154 // mask for simple case country entry default currency digits 155 private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0; 156 // shift count for simple case country entry default currency digits 157 private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5; 158 // maximum number for simple case country entry default currency digits 159 private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9; 160 // mask for special case country entries 161 private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200; 162 // mask for special case country index 163 private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F; 164 // delta from entry index component in main table to index into special case tables 165 private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1; 166 // mask for distinguishing simple and special case countries 167 private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK; 168 // mask for the numeric code of the currency 169 private static final int NUMERIC_CODE_MASK = 0x000FFC00; 170 // shift count for the numeric code of the currency 171 private static final int NUMERIC_CODE_SHIFT = 10; 172 173 // Currency data format version 174 private static final int VALID_FORMAT_VERSION = 3; 175 176 static { 177 initStatic(); 178 } 179 180 @SuppressWarnings("removal") 181 private static void initStatic() { 182 AccessController.doPrivileged(new PrivilegedAction<>() { 183 @Override 184 public Void run() { 185 try { 186 try (InputStream in = getClass().getResourceAsStream("/java/util/currency.data")) { 187 if (in == null) { 188 throw new InternalError("Currency data not found"); 189 } 190 DataInputStream dis = new DataInputStream(new BufferedInputStream(in)); 191 if (dis.readInt() != MAGIC_NUMBER) { 192 throw new InternalError("Currency data is possibly corrupted"); 193 } 194 formatVersion = dis.readInt(); 195 if (formatVersion != VALID_FORMAT_VERSION) { 196 throw new InternalError("Currency data format is incorrect"); 197 } 198 dataVersion = dis.readInt(); 199 mainTable = readIntArray(dis, A_TO_Z * A_TO_Z); 200 int scCount = dis.readInt(); 201 specialCasesList = readSpecialCases(dis, scCount); 202 int ocCount = dis.readInt(); 203 otherCurrenciesList = readOtherCurrencies(dis, ocCount); 204 } 205 } catch (IOException e) { 206 throw new InternalError(e); 207 } 208 209 // look for the properties file for overrides 210 String propsFile = System.getProperty("java.util.currency.data"); 211 if (propsFile == null) { 212 propsFile = StaticProperty.javaHome() + File.separator + "lib" + 213 File.separator + "currency.properties"; 214 } 215 try { 216 File propFile = new File(propsFile); 217 if (propFile.exists()) { 218 Properties props = new Properties(); 219 try (FileReader fr = new FileReader(propFile)) { 220 props.load(fr); 221 } 222 Pattern propertiesPattern = 223 Pattern.compile("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" + 224 "(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" + 225 "\\d{2}:\\d{2})?"); 226 List<CurrencyProperty> currencyEntries 227 = getValidCurrencyData(props, propertiesPattern); 228 currencyEntries.forEach(Currency::replaceCurrencyData); 229 } 230 } catch (IOException e) { 231 CurrencyProperty.info("currency.properties is ignored" 232 + " because of an IOException", e); 233 } 234 return null; 235 } 236 }); 237 } 238 239 /** 240 * Constants for retrieving localized names from the name providers. 241 * 242 private static final int SYMBOL = 0; 243 private static final int DISPLAYNAME = 1; 244 */ 245 // END Android-removed: Use ICU. 246 247 /** 248 * Constructs a {@code Currency} instance. The constructor is private 249 * so that we can ensure that there's never more than one instance for a 250 * given currency. 251 */ 252 // BEGIN Android-changed: Use ICU. 253 // We do not keep track of defaultFractionDigits and numericCode separately. 254 /* 255 private Currency(String currencyCode, int defaultFractionDigits, int numericCode) { 256 this.currencyCode = currencyCode; 257 this.defaultFractionDigits = defaultFractionDigits; 258 this.numericCode = numericCode; 259 } 260 */ Currency(android.icu.util.Currency icuCurrency)261 private Currency(android.icu.util.Currency icuCurrency) { 262 this.icuCurrency = icuCurrency; 263 this.currencyCode = icuCurrency.getCurrencyCode(); 264 } 265 // END Android-changed: Use ICU. 266 267 /** 268 * Returns the {@code Currency} instance for the given currency code. 269 * 270 * @param currencyCode the ISO 4217 code of the currency 271 * @return the {@code Currency} instance for the given currency code 272 * @throws NullPointerException if {@code currencyCode} is null 273 * @throws IllegalArgumentException if {@code currencyCode} is not 274 * a supported ISO 4217 code. 275 */ getInstance(String currencyCode)276 public static Currency getInstance(String currencyCode) { 277 // BEGIN Android-changed: Use ICU. 278 // Upstream uses a private static helper method, implemented differently. 279 Currency instance = instances.get(currencyCode); 280 if (instance != null) { 281 return instance; 282 } 283 android.icu.util.Currency icuInstance = 284 android.icu.util.Currency.getInstance(currencyCode); 285 if (icuInstance == null) { 286 return null; 287 } 288 /* 289 if (defaultFractionDigits == Integer.MIN_VALUE) { 290 // Currency code not internally generated, need to verify first 291 // A currency code must have 3 characters and exist in the main table 292 // or in the list of other currencies. 293 boolean found = false; 294 if (currencyCode.length() != 3) { 295 throw new IllegalArgumentException(); 296 } 297 char char1 = currencyCode.charAt(0); 298 char char2 = currencyCode.charAt(1); 299 int tableEntry = getMainTableEntry(char1, char2); 300 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 301 && tableEntry != INVALID_COUNTRY_ENTRY 302 && currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) { 303 defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 304 numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 305 found = true; 306 } else { //special case 307 int[] fractionAndNumericCode = SpecialCaseEntry.findEntry(currencyCode); 308 if (fractionAndNumericCode != null) { 309 defaultFractionDigits = fractionAndNumericCode[0]; 310 numericCode = fractionAndNumericCode[1]; 311 found = true; 312 } 313 } 314 315 if (!found) { 316 OtherCurrencyEntry ocEntry = OtherCurrencyEntry.findEntry(currencyCode); 317 if (ocEntry == null) { 318 throw new IllegalArgumentException(); 319 } 320 defaultFractionDigits = ocEntry.fraction; 321 numericCode = ocEntry.numericCode; 322 } 323 } 324 */ 325 326 Currency currencyVal = new Currency(icuInstance); 327 // END Android-changed: Use ICU. 328 instance = instances.putIfAbsent(currencyCode, currencyVal); 329 return (instance != null ? instance : currencyVal); 330 } 331 332 // Android-changed: Remove "rg" support in the javadoc. See http://b/228322300. 333 /** 334 * Returns the {@code Currency} instance for the country of the 335 * given locale. The language and variant components of the locale 336 * are ignored. The result may vary over time, as countries change their 337 * currencies. For example, for the original member countries of the 338 * European Monetary Union, the method returns the old national currencies 339 * until December 31, 2001, and the Euro from January 1, 2002, local time 340 * of the respective countries. 341 * <p> 342 * If the specified {@code locale} contains "cu" 343 * {@linkplain Locale##def_locale_extension Unicode extensions}, 344 * the instance returned from this method reflects 345 * the values specified with those extensions. 346 * <p> 347 * The method returns {@code null} for territories that don't 348 * have a currency, such as Antarctica. 349 * 350 * @param locale the locale for whose country a {@code Currency} 351 * instance is needed 352 * @return the {@code Currency} instance for the country of the given 353 * locale, or {@code null} 354 * @throws NullPointerException if {@code locale} 355 * is {@code null} 356 * @throws IllegalArgumentException if the country of the given {@code locale} 357 * is not a supported ISO 3166 country code. 358 */ getInstance(Locale locale)359 public static Currency getInstance(Locale locale) { 360 // check for locale overrides 361 String override = locale.getUnicodeLocaleType("cu"); 362 if (override != null) { 363 try { 364 return getInstance(override.toUpperCase(Locale.ROOT)); 365 } catch (IllegalArgumentException iae) { 366 // override currency is invalid. Fall through. 367 } 368 } 369 370 // BEGIN Android-changed: Use ICU. 371 /* 372 String country = CalendarDataUtility.findRegionOverride(locale).getCountry(); 373 374 if (country == null || !country.matches("^[a-zA-Z]{2}$")) { 375 throw new IllegalArgumentException(); 376 } 377 378 char char1 = country.charAt(0); 379 char char2 = country.charAt(1); 380 int tableEntry = getMainTableEntry(char1, char2); 381 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 382 && tableEntry != INVALID_COUNTRY_ENTRY) { 383 char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A'); 384 int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 385 int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 386 StringBuilder sb = new StringBuilder(country); 387 sb.append(finalChar); 388 return getInstance(sb.toString(), defaultFractionDigits, numericCode); 389 } else { 390 // special cases 391 if (tableEntry == INVALID_COUNTRY_ENTRY) { 392 throw new IllegalArgumentException(); 393 } 394 if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) { 395 return null; 396 } else { 397 int index = SpecialCaseEntry.toIndex(tableEntry); 398 SpecialCaseEntry scEntry = specialCasesList.get(index); 399 if (scEntry.cutOverTime == Long.MAX_VALUE 400 || System.currentTimeMillis() < scEntry.cutOverTime) { 401 return getInstance(scEntry.oldCurrency, 402 scEntry.oldCurrencyFraction, 403 scEntry.oldCurrencyNumericCode); 404 } else { 405 return getInstance(scEntry.newCurrency, 406 scEntry.newCurrencyFraction, 407 scEntry.newCurrencyNumericCode); 408 } 409 } 410 } 411 */ 412 String country = locale.getCountry(); 413 android.icu.util.Currency icuInstance = 414 android.icu.util.Currency.getInstance(locale); 415 // Unknown historical reason to append variant to country code. The API documentation 416 // does not mention the effect of locale variant. The actual effect here is throwing 417 // IllegalArgumentException because the code like FR_EURO is not a valid country code. 418 String variant = locale.getVariant(); 419 if (!variant.isEmpty() && (variant.equals("EURO") || variant.equals("HK") || 420 variant.equals("PREEURO"))) { 421 country = country + "_" + variant; 422 } 423 if (!ICU.isIsoCountry(country)) { 424 // Throws IllegalArgumentException as required by the API documentation. 425 throw new IllegalArgumentException("Unsupported ISO 3166 country: " + locale); 426 } 427 String currencyCode = ICU.getCurrencyCode(country); 428 if (currencyCode == null || icuInstance == null || 429 icuInstance.getCurrencyCode().equals("XXX")) { // XXX is not a real currency. 430 return null; 431 } 432 return getInstance(currencyCode); 433 // END Android-changed: Use ICU. 434 } 435 436 /** 437 * Gets the set of available currencies. The returned set of currencies 438 * contains all of the available currencies, which may include currencies 439 * that represent obsolete ISO 4217 codes. The set can be modified 440 * without affecting the available currencies in the runtime. 441 * 442 * @return the set of available currencies. If there is no currency 443 * available in the runtime, the returned set is empty. 444 * @since 1.7 445 */ getAvailableCurrencies()446 public static Set<Currency> getAvailableCurrencies() { 447 synchronized(Currency.class) { 448 if (available == null) { 449 // BEGIN Android-changed: Use ICU. 450 /* 451 available = new HashSet<>(256); 452 453 // Add simple currencies first 454 for (char c1 = 'A'; c1 <= 'Z'; c1 ++) { 455 for (char c2 = 'A'; c2 <= 'Z'; c2 ++) { 456 int tableEntry = getMainTableEntry(c1, c2); 457 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 458 && tableEntry != INVALID_COUNTRY_ENTRY) { 459 char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A'); 460 int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 461 int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT; 462 StringBuilder sb = new StringBuilder(); 463 sb.append(c1); 464 sb.append(c2); 465 sb.append(finalChar); 466 available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode)); 467 } else if ((tableEntry & COUNTRY_TYPE_MASK) == SPECIAL_CASE_COUNTRY_MASK 468 && tableEntry != INVALID_COUNTRY_ENTRY 469 && tableEntry != COUNTRY_WITHOUT_CURRENCY_ENTRY) { 470 int index = SpecialCaseEntry.toIndex(tableEntry); 471 SpecialCaseEntry scEntry = specialCasesList.get(index); 472 473 if (scEntry.cutOverTime == Long.MAX_VALUE 474 || System.currentTimeMillis() < scEntry.cutOverTime) { 475 available.add(getInstance(scEntry.oldCurrency, 476 scEntry.oldCurrencyFraction, 477 scEntry.oldCurrencyNumericCode)); 478 } else { 479 available.add(getInstance(scEntry.newCurrency, 480 scEntry.newCurrencyFraction, 481 scEntry.newCurrencyNumericCode)); 482 } 483 } 484 } 485 } 486 487 // Now add other currencies 488 for (OtherCurrencyEntry entry : otherCurrenciesList) { 489 available.add(getInstance(entry.currencyCode)); 490 } 491 */ 492 available = new HashSet<>(); 493 Set<android.icu.util.Currency> icuAvailableCurrencies 494 = android.icu.util.Currency.getAvailableCurrencies(); 495 for (android.icu.util.Currency icuCurrency : icuAvailableCurrencies) { 496 Currency currency = getInstance(icuCurrency.getCurrencyCode()); 497 if (currency == null) { 498 currency = new Currency(icuCurrency); 499 instances.put(currency.currencyCode, currency); 500 } 501 available.add(currency); 502 } 503 // END Android-changed: Use ICU. 504 } 505 } 506 507 @SuppressWarnings("unchecked") 508 Set<Currency> result = (Set<Currency>) available.clone(); 509 return result; 510 } 511 512 /** 513 * Gets the ISO 4217 currency code of this currency. 514 * 515 * @return the ISO 4217 currency code of this currency. 516 */ getCurrencyCode()517 public String getCurrencyCode() { 518 return currencyCode; 519 } 520 521 // Android-changed: Remove "rg" support in the javadoc. See http://b/228322300. 522 /** 523 * Gets the symbol of this currency for the default 524 * {@link Locale.Category#DISPLAY DISPLAY} locale. 525 * For example, for the US Dollar, the symbol is "$" if the default 526 * locale is the US, while for other locales it may be "US$". If no 527 * symbol can be determined, the ISO 4217 currency code is returned. 528 * <p> 529 * This is equivalent to calling 530 * {@link #getSymbol(Locale) 531 * getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}. 532 * 533 * @return the symbol of this currency for the default 534 * {@link Locale.Category#DISPLAY DISPLAY} locale 535 */ getSymbol()536 public String getSymbol() { 537 return getSymbol(Locale.getDefault(Locale.Category.DISPLAY)); 538 } 539 540 // Android-changed: Remove "rg" support in the javadoc. See http://b/228322300. 541 /** 542 * Gets the symbol of this currency for the specified locale. 543 * For example, for the US Dollar, the symbol is "$" if the specified 544 * locale is the US, while for other locales it may be "US$". If no 545 * symbol can be determined, the ISO 4217 currency code is returned. 546 * 547 * @param locale the locale for which a display name for this currency is 548 * needed 549 * @return the symbol of this currency for the specified locale 550 * @throws NullPointerException if {@code locale} is null 551 */ getSymbol(Locale locale)552 public String getSymbol(Locale locale) { 553 // BEGIN Android-changed: Use ICU. 554 /* 555 LocaleServiceProviderPool pool = 556 LocaleServiceProviderPool.getPool(CurrencyNameProvider.class); 557 locale = CalendarDataUtility.findRegionOverride(locale); 558 String symbol = pool.getLocalizedObject( 559 CurrencyNameGetter.INSTANCE, 560 locale, currencyCode, SYMBOL); 561 if (symbol != null) { 562 return symbol; 563 } 564 565 // use currency code as symbol of last resort 566 return currencyCode; 567 */ 568 if (locale == null) { 569 throw new NullPointerException("locale == null"); 570 } 571 return icuCurrency.getSymbol(locale); 572 // END Android-changed: Use ICU. 573 } 574 575 /** 576 * Gets the default number of fraction digits used with this currency. 577 * Note that the number of fraction digits is the same as ISO 4217's 578 * minor unit for the currency. 579 * For example, the default number of fraction digits for the Euro is 2, 580 * while for the Japanese Yen it's 0. 581 * In the case of pseudo-currencies, such as IMF Special Drawing Rights, 582 * -1 is returned. 583 * 584 * @return the default number of fraction digits used with this currency 585 */ getDefaultFractionDigits()586 public int getDefaultFractionDigits() { 587 // BEGIN Android-changed: Use ICU. 588 // return defaultFractionDigits; 589 if (icuCurrency.getCurrencyCode().equals("XXX")) { 590 return -1; 591 } 592 return icuCurrency.getDefaultFractionDigits(); 593 // END Android-changed: Use ICU. 594 } 595 596 /** 597 * Returns the ISO 4217 numeric code of this currency. 598 * 599 * @return the ISO 4217 numeric code of this currency 600 * @since 1.7 601 */ getNumericCode()602 public int getNumericCode() { 603 // Android-changed: Use ICU. 604 // return numericCode; 605 return icuCurrency.getNumericCode(); 606 } 607 608 /** 609 * Returns the 3 digit ISO 4217 numeric code of this currency as a {@code String}. 610 * Unlike {@link #getNumericCode()}, which returns the numeric code as {@code int}, 611 * this method always returns the numeric code as a 3 digit string. 612 * e.g. a numeric value of 32 would be returned as "032", 613 * and a numeric value of 6 would be returned as "006". 614 * 615 * @return the 3 digit ISO 4217 numeric code of this currency as a {@code String} 616 * @since 9 617 */ getNumericCodeAsString()618 public String getNumericCodeAsString() { 619 // Android-added: We don't store the code as a field. Call getNumericCode(). 620 int numericCode = getNumericCode(); 621 /* numeric code could be returned as a 3 digit string simply by using 622 String.format("%03d",numericCode); which uses regex to parse the format, 623 "%03d" in this case. Parsing a regex gives an extra performance overhead, 624 so String.format() approach is avoided in this scenario. 625 */ 626 if (numericCode < 100) { 627 StringBuilder sb = new StringBuilder(); 628 sb.append('0'); 629 if (numericCode < 10) { 630 sb.append('0'); 631 } 632 return sb.append(numericCode).toString(); 633 } 634 return String.valueOf(numericCode); 635 } 636 637 /** 638 * Gets the name that is suitable for displaying this currency for 639 * the default {@link Locale.Category#DISPLAY DISPLAY} locale. 640 * If there is no suitable display name found 641 * for the default locale, the ISO 4217 currency code is returned. 642 * <p> 643 * This is equivalent to calling 644 * {@link #getDisplayName(Locale) 645 * getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}. 646 * 647 * @return the display name of this currency for the default 648 * {@link Locale.Category#DISPLAY DISPLAY} locale 649 * @since 1.7 650 */ getDisplayName()651 public String getDisplayName() { 652 return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY)); 653 } 654 655 /** 656 * Gets the name that is suitable for displaying this currency for 657 * the specified locale. If there is no suitable display name found 658 * for the specified locale, the ISO 4217 currency code is returned. 659 * 660 * @param locale the locale for which a display name for this currency is 661 * needed 662 * @return the display name of this currency for the specified locale 663 * @throws NullPointerException if {@code locale} is null 664 * @since 1.7 665 */ getDisplayName(Locale locale)666 public String getDisplayName(Locale locale) { 667 // Android-changed: Use ICU. 668 /* 669 LocaleServiceProviderPool pool = 670 LocaleServiceProviderPool.getPool(CurrencyNameProvider.class); 671 String result = pool.getLocalizedObject( 672 CurrencyNameGetter.INSTANCE, 673 locale, currencyCode, DISPLAYNAME); 674 if (result != null) { 675 return result; 676 } 677 678 // use currency code as symbol of last resort 679 return currencyCode; 680 */ 681 return icuCurrency.getDisplayName(Objects.requireNonNull(locale)); 682 } 683 684 /** 685 * Returns the ISO 4217 currency code of this currency. 686 * 687 * @return the ISO 4217 currency code of this currency 688 */ 689 @Override toString()690 public String toString() { 691 // Android-changed: Use ICU. 692 // return currencyCode; 693 return icuCurrency.toString(); 694 } 695 696 /** 697 * Resolves instances being deserialized to a single instance per currency. 698 */ 699 @java.io.Serial readResolve()700 private Object readResolve() { 701 return getInstance(currencyCode); 702 } 703 704 // Android-removed: Use ICU. 705 // Removed a bunch of private helper methods that are unused on Android. 706 /** 707 * Gets the main table entry for the country whose country code consists 708 * of char1 and char2. 709 * 710 private static int getMainTableEntry(char char1, char char2) { 711 if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') { 712 throw new IllegalArgumentException(); 713 } 714 return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')]; 715 } 716 717 /** 718 * Sets the main table entry for the country whose country code consists 719 * of char1 and char2. 720 * 721 private static void setMainTableEntry(char char1, char char2, int entry) { 722 if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') { 723 throw new IllegalArgumentException(); 724 } 725 mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry; 726 } 727 728 /** 729 * Obtains a localized currency names from a CurrencyNameProvider 730 * implementation. 731 * 732 private static class CurrencyNameGetter 733 implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider, 734 String> { 735 private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter(); 736 737 @Override 738 public String getObject(CurrencyNameProvider currencyNameProvider, 739 Locale locale, 740 String key, 741 Object... params) { 742 assert params.length == 1; 743 int type = (Integer)params[0]; 744 745 switch(type) { 746 case SYMBOL: 747 return currencyNameProvider.getSymbol(key, locale); 748 case DISPLAYNAME: 749 return currencyNameProvider.getDisplayName(key, locale); 750 default: 751 assert false; // shouldn't happen 752 } 753 754 return null; 755 } 756 } 757 758 private static int[] readIntArray(DataInputStream dis, int count) throws IOException { 759 int[] ret = new int[count]; 760 for (int i = 0; i < count; i++) { 761 ret[i] = dis.readInt(); 762 } 763 764 return ret; 765 } 766 767 private static List<SpecialCaseEntry> readSpecialCases(DataInputStream dis, 768 int count) 769 throws IOException { 770 771 List<SpecialCaseEntry> list = new ArrayList<>(count); 772 long cutOverTime; 773 String oldCurrency; 774 String newCurrency; 775 int oldCurrencyFraction; 776 int newCurrencyFraction; 777 int oldCurrencyNumericCode; 778 int newCurrencyNumericCode; 779 780 for (int i = 0; i < count; i++) { 781 cutOverTime = dis.readLong(); 782 oldCurrency = dis.readUTF(); 783 newCurrency = dis.readUTF(); 784 oldCurrencyFraction = dis.readInt(); 785 newCurrencyFraction = dis.readInt(); 786 oldCurrencyNumericCode = dis.readInt(); 787 newCurrencyNumericCode = dis.readInt(); 788 SpecialCaseEntry sc = new SpecialCaseEntry(cutOverTime, 789 oldCurrency, newCurrency, 790 oldCurrencyFraction, newCurrencyFraction, 791 oldCurrencyNumericCode, newCurrencyNumericCode); 792 list.add(sc); 793 } 794 return list; 795 } 796 797 private static List<OtherCurrencyEntry> readOtherCurrencies(DataInputStream dis, 798 int count) 799 throws IOException { 800 801 List<OtherCurrencyEntry> list = new ArrayList<>(count); 802 String currencyCode; 803 int fraction; 804 int numericCode; 805 806 for (int i = 0; i < count; i++) { 807 currencyCode = dis.readUTF(); 808 fraction = dis.readInt(); 809 numericCode = dis.readInt(); 810 OtherCurrencyEntry oc = new OtherCurrencyEntry(currencyCode, 811 fraction, 812 numericCode); 813 list.add(oc); 814 } 815 return list; 816 } 817 818 /** 819 * Parse currency data found in the properties file (that 820 * java.util.currency.data designates) to a List of CurrencyProperty 821 * instances. Also, remove invalid entries and the multiple currency 822 * code inconsistencies. 823 * 824 * @param props properties containing currency data 825 * @param pattern regex pattern for the properties entry 826 * @return list of parsed property entries 827 * 828 private static List<CurrencyProperty> getValidCurrencyData(Properties props, 829 Pattern pattern) { 830 831 Set<String> keys = props.stringPropertyNames(); 832 List<CurrencyProperty> propertyEntries = new ArrayList<>(); 833 834 // remove all invalid entries and parse all valid currency properties 835 // entries to a group of CurrencyProperty, classified by currency code 836 Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream() 837 .map(k -> CurrencyProperty 838 .getValidEntry(k.toUpperCase(Locale.ROOT), 839 props.getProperty(k).toUpperCase(Locale.ROOT), 840 pattern)).flatMap(o -> o.stream()) 841 .collect(Collectors.groupingBy(entry -> entry.currencyCode)); 842 843 // check each group for inconsistencies 844 currencyCodeGroup.forEach((curCode, list) -> { 845 boolean inconsistent = CurrencyProperty 846 .containsInconsistentInstances(list); 847 if (inconsistent) { 848 list.forEach(prop -> CurrencyProperty.info("The property" 849 + " entry for " + prop.country + " is inconsistent." 850 + " Ignored.", null)); 851 } else { 852 propertyEntries.addAll(list); 853 } 854 }); 855 856 return propertyEntries; 857 } 858 859 /** 860 * Replaces currency data found in the properties file that 861 * java.util.currency.data designates. This method is invoked for 862 * each valid currency entry. 863 * 864 * @param prop CurrencyProperty instance of the valid property entry 865 * 866 private static void replaceCurrencyData(CurrencyProperty prop) { 867 868 869 String ctry = prop.country; 870 String code = prop.currencyCode; 871 int numeric = prop.numericCode; 872 int fraction = prop.fraction; 873 int entry = numeric << NUMERIC_CODE_SHIFT; 874 875 int index = SpecialCaseEntry.indexOf(code, fraction, numeric); 876 877 878 // If a new entry changes the numeric code/dfd of an existing 879 // currency code, update it in the sc list at the respective 880 // index and also change it in the other currencies list and 881 // main table (if that currency code is also used as a 882 // simple case). 883 884 // If all three components do not match with the new entry, 885 // but the currency code exists in the special case list 886 // update the sc entry with the new entry 887 int scCurrencyCodeIndex = -1; 888 if (index == -1) { 889 scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code); 890 if (scCurrencyCodeIndex != -1) { 891 //currency code exists in sc list, then update the old entry 892 specialCasesList.set(scCurrencyCodeIndex, 893 new SpecialCaseEntry(code, fraction, numeric)); 894 895 // also update the entry in other currencies list 896 OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code); 897 if (oe != null) { 898 int oIndex = otherCurrenciesList.indexOf(oe); 899 otherCurrenciesList.set(oIndex, new OtherCurrencyEntry( 900 code, fraction, numeric)); 901 } 902 } 903 } 904 905 /* If a country switches from simple case to special case or 906 * one special case to other special case which is not present 907 * in the sc arrays then insert the new entry in special case arrays. 908 * If an entry with given currency code exists, update with the new 909 * entry. 910 * 911 if (index == -1 && (ctry.charAt(0) != code.charAt(0) 912 || ctry.charAt(1) != code.charAt(1))) { 913 914 if(scCurrencyCodeIndex == -1) { 915 specialCasesList.add(new SpecialCaseEntry(code, fraction, 916 numeric)); 917 index = specialCasesList.size() - 1; 918 } else { 919 index = scCurrencyCodeIndex; 920 } 921 922 // update the entry in main table if it exists as a simple case 923 updateMainTableEntry(code, fraction, numeric); 924 } 925 926 if (index == -1) { 927 // simple case 928 entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) 929 | (code.charAt(2) - 'A'); 930 } else { 931 // special case 932 entry = SPECIAL_CASE_COUNTRY_MASK 933 | (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA); 934 } 935 setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry); 936 } 937 938 // update the entry in maintable for any simple case found, if a new 939 // entry as a special case updates the entry in sc list with 940 // existing currency code 941 private static void updateMainTableEntry(String code, int fraction, 942 int numeric) { 943 // checking the existence of currency code in mainTable 944 int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1)); 945 int entry = numeric << NUMERIC_CODE_SHIFT; 946 if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK 947 && tableEntry != INVALID_COUNTRY_ENTRY 948 && code.charAt(2) - 'A' == (tableEntry 949 & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) { 950 951 int numericCode = (tableEntry & NUMERIC_CODE_MASK) 952 >> NUMERIC_CODE_SHIFT; 953 int defaultFractionDigits = (tableEntry 954 & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) 955 >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT; 956 if (numeric != numericCode || fraction != defaultFractionDigits) { 957 // update the entry in main table 958 entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT) 959 | (code.charAt(2) - 'A'); 960 setMainTableEntry(code.charAt(0), code.charAt(1), entry); 961 } 962 } 963 } 964 965 /* Used to represent a special case currency entry 966 * - cutOverTime: cut-over time in millis as returned by 967 * System.currentTimeMillis for special case countries that are changing 968 * currencies; Long.MAX_VALUE for countries that are not changing currencies 969 * - oldCurrency: old currencies for special case countries 970 * - newCurrency: new currencies for special case countries that are 971 * changing currencies; null for others 972 * - oldCurrencyFraction: default fraction digits for old currencies 973 * - newCurrencyFraction: default fraction digits for new currencies, 0 for 974 * countries that are not changing currencies 975 * - oldCurrencyNumericCode: numeric code for old currencies 976 * - newCurrencyNumericCode: numeric code for new currencies, 0 for countries 977 * that are not changing currencies 978 * 979 private static class SpecialCaseEntry { 980 981 private final long cutOverTime; 982 private final String oldCurrency; 983 private final String newCurrency; 984 private final int oldCurrencyFraction; 985 private final int newCurrencyFraction; 986 private final int oldCurrencyNumericCode; 987 private final int newCurrencyNumericCode; 988 989 private SpecialCaseEntry(long cutOverTime, String oldCurrency, String newCurrency, 990 int oldCurrencyFraction, int newCurrencyFraction, 991 int oldCurrencyNumericCode, int newCurrencyNumericCode) { 992 this.cutOverTime = cutOverTime; 993 this.oldCurrency = oldCurrency; 994 this.newCurrency = newCurrency; 995 this.oldCurrencyFraction = oldCurrencyFraction; 996 this.newCurrencyFraction = newCurrencyFraction; 997 this.oldCurrencyNumericCode = oldCurrencyNumericCode; 998 this.newCurrencyNumericCode = newCurrencyNumericCode; 999 } 1000 1001 private SpecialCaseEntry(String currencyCode, int fraction, 1002 int numericCode) { 1003 this(Long.MAX_VALUE, currencyCode, "", fraction, 0, numericCode, 0); 1004 } 1005 1006 //get the index of the special case entry 1007 private static int indexOf(String code, int fraction, int numeric) { 1008 int size = specialCasesList.size(); 1009 for (int index = 0; index < size; index++) { 1010 SpecialCaseEntry scEntry = specialCasesList.get(index); 1011 if (scEntry.oldCurrency.equals(code) 1012 && scEntry.oldCurrencyFraction == fraction 1013 && scEntry.oldCurrencyNumericCode == numeric 1014 && scEntry.cutOverTime == Long.MAX_VALUE) { 1015 return index; 1016 } 1017 } 1018 return -1; 1019 } 1020 1021 // get the fraction and numericCode of the sc currencycode 1022 private static int[] findEntry(String code) { 1023 int[] fractionAndNumericCode = null; 1024 int size = specialCasesList.size(); 1025 for (int index = 0; index < size; index++) { 1026 SpecialCaseEntry scEntry = specialCasesList.get(index); 1027 if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE 1028 || System.currentTimeMillis() < scEntry.cutOverTime)) { 1029 //consider only when there is no new currency or cutover time is not passed 1030 fractionAndNumericCode = new int[2]; 1031 fractionAndNumericCode[0] = scEntry.oldCurrencyFraction; 1032 fractionAndNumericCode[1] = scEntry.oldCurrencyNumericCode; 1033 break; 1034 } else if (scEntry.newCurrency.equals(code) 1035 && System.currentTimeMillis() >= scEntry.cutOverTime) { 1036 //consider only if the cutover time is passed 1037 fractionAndNumericCode = new int[2]; 1038 fractionAndNumericCode[0] = scEntry.newCurrencyFraction; 1039 fractionAndNumericCode[1] = scEntry.newCurrencyNumericCode; 1040 break; 1041 } 1042 } 1043 return fractionAndNumericCode; 1044 } 1045 1046 // get the index based on currency code 1047 private static int currencyCodeIndex(String code) { 1048 int size = specialCasesList.size(); 1049 for (int index = 0; index < size; index++) { 1050 SpecialCaseEntry scEntry = specialCasesList.get(index); 1051 if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE 1052 || System.currentTimeMillis() < scEntry.cutOverTime)) { 1053 //consider only when there is no new currency or cutover time is not passed 1054 return index; 1055 } else if (scEntry.newCurrency.equals(code) 1056 && System.currentTimeMillis() >= scEntry.cutOverTime) { 1057 //consider only if the cutover time is passed 1058 return index; 1059 } 1060 } 1061 return -1; 1062 } 1063 1064 1065 // convert the special case entry to sc arrays index 1066 private static int toIndex(int tableEntry) { 1067 return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA; 1068 } 1069 1070 } 1071 1072 /* Used to represent Other currencies 1073 * - currencyCode: currency codes that are not the main currency 1074 * of a simple country 1075 * - otherCurrenciesDFD: decimal format digits for other currencies 1076 * - otherCurrenciesNumericCode: numeric code for other currencies 1077 * 1078 private static class OtherCurrencyEntry { 1079 1080 private final String currencyCode; 1081 private final int fraction; 1082 private final int numericCode; 1083 1084 private OtherCurrencyEntry(String currencyCode, int fraction, 1085 int numericCode) { 1086 this.currencyCode = currencyCode; 1087 this.fraction = fraction; 1088 this.numericCode = numericCode; 1089 } 1090 1091 //get the instance of the other currency code 1092 private static OtherCurrencyEntry findEntry(String code) { 1093 int size = otherCurrenciesList.size(); 1094 for (int index = 0; index < size; index++) { 1095 OtherCurrencyEntry ocEntry = otherCurrenciesList.get(index); 1096 if (ocEntry.currencyCode.equalsIgnoreCase(code)) { 1097 return ocEntry; 1098 } 1099 } 1100 return null; 1101 } 1102 1103 } 1104 1105 1106 /* 1107 * Used to represent an entry of the properties file that 1108 * java.util.currency.data designates 1109 * 1110 * - country: country representing the currency entry 1111 * - currencyCode: currency code 1112 * - fraction: default fraction digit 1113 * - numericCode: numeric code 1114 * - date: cutover date 1115 * 1116 private static class CurrencyProperty { 1117 private final String country; 1118 private final String currencyCode; 1119 private final int fraction; 1120 private final int numericCode; 1121 private final String date; 1122 1123 private CurrencyProperty(String country, String currencyCode, 1124 int fraction, int numericCode, String date) { 1125 this.country = country; 1126 this.currencyCode = currencyCode; 1127 this.fraction = fraction; 1128 this.numericCode = numericCode; 1129 this.date = date; 1130 } 1131 1132 /** 1133 * Check the valid currency data and create/return an Optional instance 1134 * of CurrencyProperty 1135 * 1136 * @param ctry country representing the currency data 1137 * @param curData currency data of the given {@code ctry} 1138 * @param pattern regex pattern for the properties entry 1139 * @return Optional containing CurrencyProperty instance, If valid; 1140 * empty otherwise 1141 * 1142 private static Optional<CurrencyProperty> getValidEntry(String ctry, 1143 String curData, 1144 Pattern pattern) { 1145 1146 CurrencyProperty prop = null; 1147 1148 if (ctry.length() != 2) { 1149 // Invalid country code. Ignore the entry. 1150 } else { 1151 1152 prop = parseProperty(ctry, curData, pattern); 1153 // if the property entry failed any of the below checked 1154 // criteria it is ignored 1155 if (prop == null 1156 || (prop.date == null && curData.chars() 1157 .map(c -> c == ',' ? 1 : 0).sum() >= 3)) { 1158 // format is not recognized. ignore the data if date 1159 // string is null and we've 4 values, bad date value 1160 prop = null; 1161 } else if (prop.fraction 1162 > SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) { 1163 prop = null; 1164 } else { 1165 try { 1166 if (prop.date != null 1167 && !isPastCutoverDate(prop.date)) { 1168 prop = null; 1169 } 1170 } catch (ParseException ex) { 1171 prop = null; 1172 } 1173 } 1174 } 1175 1176 if (prop == null) { 1177 info("The property entry for " + ctry + " is invalid." 1178 + " Ignored.", null); 1179 } 1180 1181 return Optional.ofNullable(prop); 1182 } 1183 1184 /* 1185 * Parse properties entry and return CurrencyProperty instance 1186 * 1187 private static CurrencyProperty parseProperty(String ctry, 1188 String curData, Pattern pattern) { 1189 Matcher m = pattern.matcher(curData); 1190 if (!m.find()) { 1191 return null; 1192 } else { 1193 return new CurrencyProperty(ctry, m.group(1), 1194 Integer.parseInt(m.group(3)), 1195 Integer.parseInt(m.group(2)), m.group(4)); 1196 } 1197 } 1198 1199 /** 1200 * Checks if the given list contains multiple inconsistent currency instances 1201 * 1202 private static boolean containsInconsistentInstances( 1203 List<CurrencyProperty> list) { 1204 int numCode = list.get(0).numericCode; 1205 int fractionDigit = list.get(0).fraction; 1206 return list.stream().anyMatch(prop -> prop.numericCode != numCode 1207 || prop.fraction != fractionDigit); 1208 } 1209 1210 private static boolean isPastCutoverDate(String s) 1211 throws ParseException { 1212 SimpleDateFormat format = new SimpleDateFormat( 1213 "yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT); 1214 format.setTimeZone(TimeZone.getTimeZone("UTC")); 1215 format.setLenient(false); 1216 long time = format.parse(s.trim()).getTime(); 1217 return System.currentTimeMillis() > time; 1218 1219 } 1220 1221 private static void info(String message, Throwable t) { 1222 PlatformLogger logger = PlatformLogger 1223 .getLogger("java.util.Currency"); 1224 if (logger.isLoggable(PlatformLogger.Level.INFO)) { 1225 if (t != null) { 1226 logger.info(message, t); 1227 } else { 1228 logger.info(message); 1229 } 1230 } 1231 } 1232 1233 } 1234 */ 1235 } 1236