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