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