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