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