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