1230 lines
51 KiB
Java
1230 lines
51 KiB
Java
/*
|
|
* Copyright (C) 2014 The Android Open Source Project
|
|
* Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package java.util;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.DataInputStream;
|
|
import java.io.File;
|
|
import java.io.FileReader;
|
|
import java.io.InputStream;
|
|
import java.io.IOException;
|
|
import java.io.Serializable;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.ConcurrentMap;
|
|
import java.util.regex.Pattern;
|
|
import java.util.regex.Matcher;
|
|
import java.util.stream.Collectors;
|
|
|
|
import sun.util.logging.PlatformLogger;
|
|
|
|
import libcore.icu.ICU;
|
|
|
|
// BEGIN Android-changed: Removed docs about superseding runtime currency data.
|
|
// Doing so via a properties file is not supported on Android.
|
|
/**
|
|
* Represents a currency. Currencies are identified by their ISO 4217 currency
|
|
* codes. Visit the <a href="http://www.iso.org/iso/home/standards/currency_codes.htm">
|
|
* ISO web site</a> for more information.
|
|
* <p>
|
|
* The class is designed so that there's never more than one
|
|
* {@code Currency} instance for any given currency. Therefore, there's
|
|
* no public constructor. You obtain a {@code Currency} instance using
|
|
* the {@code getInstance} methods.
|
|
*
|
|
* <p>
|
|
* It is recommended to use {@link java.math.BigDecimal} class while dealing
|
|
* with {@code Currency} or monetary values as it provides better handling of floating
|
|
* point numbers and their operations.
|
|
*
|
|
* @see java.math.BigDecimal
|
|
* @since 1.4
|
|
*/
|
|
// END Android-changed: Removed docs about superseding runtime currency data.
|
|
@SuppressWarnings("removal")
|
|
public final class Currency implements Serializable {
|
|
|
|
@java.io.Serial
|
|
private static final long serialVersionUID = -158308464356906721L;
|
|
|
|
/**
|
|
* ISO 4217 currency code for this currency.
|
|
*
|
|
* @serial
|
|
*/
|
|
private final String currencyCode;
|
|
|
|
// BEGIN Android-changed: Use ICU.
|
|
// We do not keep track of defaultFractionDigits and numericCode separately.
|
|
/*
|
|
/**
|
|
* Default fraction digits for this currency.
|
|
* Set from currency data tables.
|
|
*
|
|
private final transient int defaultFractionDigits;
|
|
*/
|
|
|
|
/*
|
|
* ISO 4217 numeric code for this currency.
|
|
* Set from currency data tables.
|
|
*
|
|
private final transient int numericCode;
|
|
*/
|
|
private transient final android.icu.util.Currency icuCurrency;
|
|
// END Android-changed: Use ICU.
|
|
|
|
|
|
// class data: instance map
|
|
|
|
private static ConcurrentMap<String, Currency> instances = new ConcurrentHashMap<>(7);
|
|
private static HashSet<Currency> available;
|
|
|
|
// BEGIN Android-removed: Use ICU.
|
|
// We don't need any of these static fields nor the static initializer.
|
|
/*
|
|
// Class data: currency data obtained from currency.data file.
|
|
// Purpose:
|
|
// - determine valid country codes
|
|
// - determine valid currency codes
|
|
// - map country codes to currency codes
|
|
// - obtain default fraction digits for currency codes
|
|
//
|
|
// sc = special case; dfd = default fraction digits
|
|
// Simple countries are those where the country code is a prefix of the
|
|
// currency code, and there are no known plans to change the currency.
|
|
//
|
|
// table formats:
|
|
// - mainTable:
|
|
// - maps country code to 32-bit int
|
|
// - 26*26 entries, corresponding to [A-Z]*[A-Z]
|
|
// - \u007F -> not valid country
|
|
// - bits 20-31: unused
|
|
// - bits 10-19: numeric code (0 to 1023)
|
|
// - bit 9: 1 - special case, bits 0-4 indicate which one
|
|
// 0 - simple country, bits 0-4 indicate final char of currency code
|
|
// - bits 5-8: fraction digits for simple countries, 0 for special cases
|
|
// - bits 0-4: final char for currency code for simple country, or ID of special case
|
|
// - special case IDs:
|
|
// - 0: country has no currency
|
|
// - other: index into specialCasesList
|
|
|
|
static int formatVersion;
|
|
static int dataVersion;
|
|
static int[] mainTable;
|
|
static List<SpecialCaseEntry> specialCasesList;
|
|
static List<OtherCurrencyEntry> otherCurrenciesList;
|
|
|
|
// handy constants - must match definitions in GenerateCurrencyData
|
|
// magic number
|
|
private static final int MAGIC_NUMBER = 0x43757244;
|
|
// number of characters from A to Z
|
|
private static final int A_TO_Z = ('Z' - 'A') + 1;
|
|
// entry for invalid country codes
|
|
private static final int INVALID_COUNTRY_ENTRY = 0x0000007F;
|
|
// entry for countries without currency
|
|
private static final int COUNTRY_WITHOUT_CURRENCY_ENTRY = 0x00000200;
|
|
// mask for simple case country entries
|
|
private static final int SIMPLE_CASE_COUNTRY_MASK = 0x00000000;
|
|
// mask for simple case country entry final character
|
|
private static final int SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK = 0x0000001F;
|
|
// mask for simple case country entry default currency digits
|
|
private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK = 0x000001E0;
|
|
// shift count for simple case country entry default currency digits
|
|
private static final int SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT = 5;
|
|
// maximum number for simple case country entry default currency digits
|
|
private static final int SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS = 9;
|
|
// mask for special case country entries
|
|
private static final int SPECIAL_CASE_COUNTRY_MASK = 0x00000200;
|
|
// mask for special case country index
|
|
private static final int SPECIAL_CASE_COUNTRY_INDEX_MASK = 0x0000001F;
|
|
// delta from entry index component in main table to index into special case tables
|
|
private static final int SPECIAL_CASE_COUNTRY_INDEX_DELTA = 1;
|
|
// mask for distinguishing simple and special case countries
|
|
private static final int COUNTRY_TYPE_MASK = SIMPLE_CASE_COUNTRY_MASK | SPECIAL_CASE_COUNTRY_MASK;
|
|
// mask for the numeric code of the currency
|
|
private static final int NUMERIC_CODE_MASK = 0x000FFC00;
|
|
// shift count for the numeric code of the currency
|
|
private static final int NUMERIC_CODE_SHIFT = 10;
|
|
|
|
// Currency data format version
|
|
private static final int VALID_FORMAT_VERSION = 3;
|
|
|
|
static {
|
|
AccessController.doPrivileged(new PrivilegedAction<>() {
|
|
@Override
|
|
public Void run() {
|
|
try {
|
|
try (InputStream in = getClass().getResourceAsStream("/java/util/currency.data")) {
|
|
if (in == null) {
|
|
throw new InternalError("Currency data not found");
|
|
}
|
|
DataInputStream dis = new DataInputStream(new BufferedInputStream(in));
|
|
if (dis.readInt() != MAGIC_NUMBER) {
|
|
throw new InternalError("Currency data is possibly corrupted");
|
|
}
|
|
formatVersion = dis.readInt();
|
|
if (formatVersion != VALID_FORMAT_VERSION) {
|
|
throw new InternalError("Currency data format is incorrect");
|
|
}
|
|
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);
|
|
}
|
|
|
|
// look for the properties file for overrides
|
|
String propsFile = System.getProperty("java.util.currency.data");
|
|
if (propsFile == null) {
|
|
propsFile = StaticProperty.javaHome() + File.separator + "lib" +
|
|
File.separator + "currency.properties";
|
|
}
|
|
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("([A-Z]{3})\\s*,\\s*(\\d{3})\\s*,\\s*" +
|
|
"(\\d+)\\s*,?\\s*(\\d{4}-\\d{2}-\\d{2}T\\d{2}:" +
|
|
"\\d{2}:\\d{2})?");
|
|
List<CurrencyProperty> currencyEntries
|
|
= getValidCurrencyData(props, propertiesPattern);
|
|
currencyEntries.forEach(Currency::replaceCurrencyData);
|
|
}
|
|
} catch (IOException e) {
|
|
CurrencyProperty.info("currency.properties is ignored"
|
|
+ " because of an IOException", e);
|
|
}
|
|
return null;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Constants for retrieving localized names from the name providers.
|
|
*
|
|
private static final int SYMBOL = 0;
|
|
private static final int DISPLAYNAME = 1;
|
|
*/
|
|
// END Android-removed: Use ICU.
|
|
|
|
/**
|
|
* Constructs a {@code Currency} instance. The constructor is private
|
|
* so that we can insure that there's never more than one instance for a
|
|
* given currency.
|
|
*/
|
|
// BEGIN Android-changed: Use ICU.
|
|
// We do not keep track of defaultFractionDigits and numericCode separately.
|
|
/*
|
|
private Currency(String currencyCode, int defaultFractionDigits, int numericCode) {
|
|
this.currencyCode = currencyCode;
|
|
this.defaultFractionDigits = defaultFractionDigits;
|
|
this.numericCode = numericCode;
|
|
}
|
|
*/
|
|
private Currency(android.icu.util.Currency icuCurrency) {
|
|
this.icuCurrency = icuCurrency;
|
|
this.currencyCode = icuCurrency.getCurrencyCode();
|
|
}
|
|
// END Android-changed: Use ICU.
|
|
|
|
/**
|
|
* Returns the {@code Currency} instance for the given currency code.
|
|
*
|
|
* @param currencyCode the ISO 4217 code of the currency
|
|
* @return the {@code Currency} instance for the given currency code
|
|
* @throws NullPointerException if {@code currencyCode} is null
|
|
* @throws IllegalArgumentException if {@code currencyCode} is not
|
|
* a supported ISO 4217 code.
|
|
*/
|
|
public static Currency getInstance(String currencyCode) {
|
|
// BEGIN Android-changed: Use ICU.
|
|
// Upstream uses a private static helper method, implemented differently.
|
|
Currency instance = instances.get(currencyCode);
|
|
if (instance != null) {
|
|
return instance;
|
|
}
|
|
android.icu.util.Currency icuInstance =
|
|
android.icu.util.Currency.getInstance(currencyCode);
|
|
if (icuInstance == null) {
|
|
return null;
|
|
}
|
|
/*
|
|
if (defaultFractionDigits == Integer.MIN_VALUE) {
|
|
// Currency code not internally generated, need to verify first
|
|
// A currency code must have 3 characters and exist in the main table
|
|
// or in the list of other currencies.
|
|
boolean found = false;
|
|
if (currencyCode.length() != 3) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
char char1 = currencyCode.charAt(0);
|
|
char char2 = currencyCode.charAt(1);
|
|
int tableEntry = getMainTableEntry(char1, char2);
|
|
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
|
|
&& tableEntry != INVALID_COUNTRY_ENTRY
|
|
&& currencyCode.charAt(2) - 'A' == (tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
|
|
defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
|
|
numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
|
|
found = true;
|
|
} else { //special case
|
|
int[] fractionAndNumericCode = SpecialCaseEntry.findEntry(currencyCode);
|
|
if (fractionAndNumericCode != null) {
|
|
defaultFractionDigits = fractionAndNumericCode[0];
|
|
numericCode = fractionAndNumericCode[1];
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
OtherCurrencyEntry ocEntry = OtherCurrencyEntry.findEntry(currencyCode);
|
|
if (ocEntry == null) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
defaultFractionDigits = ocEntry.fraction;
|
|
numericCode = ocEntry.numericCode;
|
|
}
|
|
}
|
|
*/
|
|
|
|
Currency currencyVal = new Currency(icuInstance);
|
|
// END Android-changed: Use ICU.
|
|
instance = instances.putIfAbsent(currencyCode, currencyVal);
|
|
return (instance != null ? instance : currencyVal);
|
|
}
|
|
|
|
// Android-changed: Remove "rg" support in the javadoc. See http://b/228322300.
|
|
/**
|
|
* Returns the {@code Currency} instance for the country of the
|
|
* given locale. The language and variant components of the locale
|
|
* are ignored. The result may vary over time, as countries change their
|
|
* currencies. For example, for the original member countries of the
|
|
* European Monetary Union, the method returns the old national currencies
|
|
* until December 31, 2001, and the Euro from January 1, 2002, local time
|
|
* of the respective countries.
|
|
* <p>
|
|
* If the specified {@code locale} contains "cu"
|
|
* <a href="./Locale.html#def_locale_extension">Unicode extensions</a>,
|
|
* the instance returned from this method reflects
|
|
* the values specified with those extensions.
|
|
* <p>
|
|
* The method returns {@code null} for territories that don't
|
|
* have a currency, such as Antarctica.
|
|
*
|
|
* @param locale the locale for whose country a {@code Currency}
|
|
* instance is needed
|
|
* @return the {@code Currency} instance for the country of the given
|
|
* locale, or {@code null}
|
|
* @throws NullPointerException if {@code locale}
|
|
* is {@code null}
|
|
* @throws IllegalArgumentException if the country of the given {@code locale}
|
|
* is not a supported ISO 3166 country code.
|
|
*/
|
|
public static Currency getInstance(Locale locale) {
|
|
// check for locale overrides
|
|
String override = locale.getUnicodeLocaleType("cu");
|
|
if (override != null) {
|
|
try {
|
|
return getInstance(override.toUpperCase(Locale.ROOT));
|
|
} catch (IllegalArgumentException iae) {
|
|
// override currency is invalid. Fall through.
|
|
}
|
|
}
|
|
|
|
// BEGIN Android-changed: Use ICU.
|
|
/*
|
|
String country = CalendarDataUtility.findRegionOverride(locale).getCountry();
|
|
|
|
if (country == null || !country.matches("^[a-zA-Z]{2}$")) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
|
|
char char1 = country.charAt(0);
|
|
char char2 = country.charAt(1);
|
|
int tableEntry = getMainTableEntry(char1, char2);
|
|
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
|
|
&& tableEntry != INVALID_COUNTRY_ENTRY) {
|
|
char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
|
|
int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
|
|
int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
|
|
StringBuilder sb = new StringBuilder(country);
|
|
sb.append(finalChar);
|
|
return getInstance(sb.toString(), defaultFractionDigits, numericCode);
|
|
} else {
|
|
// special cases
|
|
if (tableEntry == INVALID_COUNTRY_ENTRY) {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
if (tableEntry == COUNTRY_WITHOUT_CURRENCY_ENTRY) {
|
|
return null;
|
|
} else {
|
|
int index = SpecialCaseEntry.toIndex(tableEntry);
|
|
SpecialCaseEntry scEntry = specialCasesList.get(index);
|
|
if (scEntry.cutOverTime == Long.MAX_VALUE
|
|
|| System.currentTimeMillis() < scEntry.cutOverTime) {
|
|
return getInstance(scEntry.oldCurrency,
|
|
scEntry.oldCurrencyFraction,
|
|
scEntry.oldCurrencyNumericCode);
|
|
} else {
|
|
return getInstance(scEntry.newCurrency,
|
|
scEntry.newCurrencyFraction,
|
|
scEntry.newCurrencyNumericCode);
|
|
}
|
|
}
|
|
}
|
|
*/
|
|
String country = locale.getCountry();
|
|
android.icu.util.Currency icuInstance =
|
|
android.icu.util.Currency.getInstance(locale);
|
|
// Unknown historical reason to append variant to country code. The API documentation
|
|
// does not mention the effect of locale variant. The actual effect here is throwing
|
|
// IllegalArgumentException because the code like FR_EURO is not a valid country code.
|
|
String variant = locale.getVariant();
|
|
if (!variant.isEmpty() && (variant.equals("EURO") || variant.equals("HK") ||
|
|
variant.equals("PREEURO"))) {
|
|
country = country + "_" + variant;
|
|
}
|
|
if (!ICU.isIsoCountry(country)) {
|
|
// Throws IllegalArgumentException as required by the API documentation.
|
|
throw new IllegalArgumentException("Unsupported ISO 3166 country: " + locale);
|
|
}
|
|
String currencyCode = ICU.getCurrencyCode(country);
|
|
if (currencyCode == null || icuInstance == null ||
|
|
icuInstance.getCurrencyCode().equals("XXX")) { // XXX is not a real currency.
|
|
return null;
|
|
}
|
|
return getInstance(currencyCode);
|
|
// END Android-changed: Use ICU.
|
|
}
|
|
|
|
/**
|
|
* Gets the set of available currencies. The returned set of currencies
|
|
* contains all of the available currencies, which may include currencies
|
|
* that represent obsolete ISO 4217 codes. The set can be modified
|
|
* without affecting the available currencies in the runtime.
|
|
*
|
|
* @return the set of available currencies. If there is no currency
|
|
* available in the runtime, the returned set is empty.
|
|
* @since 1.7
|
|
*/
|
|
public static Set<Currency> getAvailableCurrencies() {
|
|
synchronized(Currency.class) {
|
|
if (available == null) {
|
|
// BEGIN Android-changed: Use ICU.
|
|
/*
|
|
available = new HashSet<>(256);
|
|
|
|
// Add simple currencies first
|
|
for (char c1 = 'A'; c1 <= 'Z'; c1 ++) {
|
|
for (char c2 = 'A'; c2 <= 'Z'; c2 ++) {
|
|
int tableEntry = getMainTableEntry(c1, c2);
|
|
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
|
|
&& tableEntry != INVALID_COUNTRY_ENTRY) {
|
|
char finalChar = (char) ((tableEntry & SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK) + 'A');
|
|
int defaultFractionDigits = (tableEntry & SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK) >> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
|
|
int numericCode = (tableEntry & NUMERIC_CODE_MASK) >> NUMERIC_CODE_SHIFT;
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append(c1);
|
|
sb.append(c2);
|
|
sb.append(finalChar);
|
|
available.add(getInstance(sb.toString(), defaultFractionDigits, numericCode));
|
|
} else if ((tableEntry & COUNTRY_TYPE_MASK) == SPECIAL_CASE_COUNTRY_MASK
|
|
&& tableEntry != INVALID_COUNTRY_ENTRY
|
|
&& tableEntry != COUNTRY_WITHOUT_CURRENCY_ENTRY) {
|
|
int index = SpecialCaseEntry.toIndex(tableEntry);
|
|
SpecialCaseEntry scEntry = specialCasesList.get(index);
|
|
|
|
if (scEntry.cutOverTime == Long.MAX_VALUE
|
|
|| System.currentTimeMillis() < scEntry.cutOverTime) {
|
|
available.add(getInstance(scEntry.oldCurrency,
|
|
scEntry.oldCurrencyFraction,
|
|
scEntry.oldCurrencyNumericCode));
|
|
} else {
|
|
available.add(getInstance(scEntry.newCurrency,
|
|
scEntry.newCurrencyFraction,
|
|
scEntry.newCurrencyNumericCode));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now add other currencies
|
|
for (OtherCurrencyEntry entry : otherCurrenciesList) {
|
|
available.add(getInstance(entry.currencyCode));
|
|
}
|
|
*/
|
|
available = new HashSet<>();
|
|
Set<android.icu.util.Currency> icuAvailableCurrencies
|
|
= android.icu.util.Currency.getAvailableCurrencies();
|
|
for (android.icu.util.Currency icuCurrency : icuAvailableCurrencies) {
|
|
Currency currency = getInstance(icuCurrency.getCurrencyCode());
|
|
if (currency == null) {
|
|
currency = new Currency(icuCurrency);
|
|
instances.put(currency.currencyCode, currency);
|
|
}
|
|
available.add(currency);
|
|
}
|
|
// END Android-changed: Use ICU.
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
Set<Currency> result = (Set<Currency>) available.clone();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Gets the ISO 4217 currency code of this currency.
|
|
*
|
|
* @return the ISO 4217 currency code of this currency.
|
|
*/
|
|
public String getCurrencyCode() {
|
|
return currencyCode;
|
|
}
|
|
|
|
// Android-changed: Remove "rg" support in the javadoc. See http://b/228322300.
|
|
/**
|
|
* Gets the symbol of this currency for the default
|
|
* {@link Locale.Category#DISPLAY DISPLAY} locale.
|
|
* For example, for the US Dollar, the symbol is "$" if the default
|
|
* locale is the US, while for other locales it may be "US$". If no
|
|
* symbol can be determined, the ISO 4217 currency code is returned.
|
|
* <p>
|
|
* This is equivalent to calling
|
|
* {@link #getSymbol(Locale)
|
|
* getSymbol(Locale.getDefault(Locale.Category.DISPLAY))}.
|
|
*
|
|
* @return the symbol of this currency for the default
|
|
* {@link Locale.Category#DISPLAY DISPLAY} locale
|
|
*/
|
|
public String getSymbol() {
|
|
return getSymbol(Locale.getDefault(Locale.Category.DISPLAY));
|
|
}
|
|
|
|
// Android-changed: Remove "rg" support in the javadoc. See http://b/228322300.
|
|
/**
|
|
* Gets the symbol of this currency for the specified locale.
|
|
* For example, for the US Dollar, the symbol is "$" if the specified
|
|
* locale is the US, while for other locales it may be "US$". If no
|
|
* symbol can be determined, the ISO 4217 currency code is returned.
|
|
*
|
|
* @param locale the locale for which a display name for this currency is
|
|
* needed
|
|
* @return the symbol of this currency for the specified locale
|
|
* @throws NullPointerException if {@code locale} is null
|
|
*/
|
|
public String getSymbol(Locale locale) {
|
|
// BEGIN Android-changed: Use ICU.
|
|
/*
|
|
LocaleServiceProviderPool pool =
|
|
LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
|
|
locale = CalendarDataUtility.findRegionOverride(locale);
|
|
String symbol = pool.getLocalizedObject(
|
|
CurrencyNameGetter.INSTANCE,
|
|
locale, currencyCode, SYMBOL);
|
|
if (symbol != null) {
|
|
return symbol;
|
|
}
|
|
|
|
// use currency code as symbol of last resort
|
|
return currencyCode;
|
|
*/
|
|
if (locale == null) {
|
|
throw new NullPointerException("locale == null");
|
|
}
|
|
return icuCurrency.getSymbol(locale);
|
|
// END Android-changed: Use ICU.
|
|
}
|
|
|
|
/**
|
|
* Gets the default number of fraction digits used with this currency.
|
|
* Note that the number of fraction digits is the same as ISO 4217's
|
|
* minor unit for the currency.
|
|
* For example, the default number of fraction digits for the Euro is 2,
|
|
* while for the Japanese Yen it's 0.
|
|
* In the case of pseudo-currencies, such as IMF Special Drawing Rights,
|
|
* -1 is returned.
|
|
*
|
|
* @return the default number of fraction digits used with this currency
|
|
*/
|
|
public int getDefaultFractionDigits() {
|
|
// BEGIN Android-changed: Use ICU.
|
|
// return defaultFractionDigits;
|
|
if (icuCurrency.getCurrencyCode().equals("XXX")) {
|
|
return -1;
|
|
}
|
|
return icuCurrency.getDefaultFractionDigits();
|
|
// END Android-changed: Use ICU.
|
|
}
|
|
|
|
/**
|
|
* Returns the ISO 4217 numeric code of this currency.
|
|
*
|
|
* @return the ISO 4217 numeric code of this currency
|
|
* @since 1.7
|
|
*/
|
|
public int getNumericCode() {
|
|
// Android-changed: Use ICU.
|
|
// return numericCode;
|
|
return icuCurrency.getNumericCode();
|
|
}
|
|
|
|
/**
|
|
* Returns the 3 digit ISO 4217 numeric code of this currency as a {@code String}.
|
|
* Unlike {@link #getNumericCode()}, which returns the numeric code as {@code int},
|
|
* this method always returns the numeric code as a 3 digit string.
|
|
* e.g. a numeric value of 32 would be returned as "032",
|
|
* and a numeric value of 6 would be returned as "006".
|
|
*
|
|
* @return the 3 digit ISO 4217 numeric code of this currency as a {@code String}
|
|
* @since 9
|
|
*/
|
|
public String getNumericCodeAsString() {
|
|
// Android-added: We don't store the code as a field. Call getNumericCode().
|
|
int numericCode = getNumericCode();
|
|
/* numeric code could be returned as a 3 digit string simply by using
|
|
String.format("%03d",numericCode); which uses regex to parse the format,
|
|
"%03d" in this case. Parsing a regex gives an extra performance overhead,
|
|
so String.format() approach is avoided in this scenario.
|
|
*/
|
|
if (numericCode < 100) {
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append('0');
|
|
if (numericCode < 10) {
|
|
sb.append('0');
|
|
}
|
|
return sb.append(numericCode).toString();
|
|
}
|
|
return String.valueOf(numericCode);
|
|
}
|
|
|
|
/**
|
|
* Gets the name that is suitable for displaying this currency for
|
|
* the default {@link Locale.Category#DISPLAY DISPLAY} locale.
|
|
* If there is no suitable display name found
|
|
* for the default locale, the ISO 4217 currency code is returned.
|
|
* <p>
|
|
* This is equivalent to calling
|
|
* {@link #getDisplayName(Locale)
|
|
* getDisplayName(Locale.getDefault(Locale.Category.DISPLAY))}.
|
|
*
|
|
* @return the display name of this currency for the default
|
|
* {@link Locale.Category#DISPLAY DISPLAY} locale
|
|
* @since 1.7
|
|
*/
|
|
public String getDisplayName() {
|
|
return getDisplayName(Locale.getDefault(Locale.Category.DISPLAY));
|
|
}
|
|
|
|
/**
|
|
* Gets the name that is suitable for displaying this currency for
|
|
* the specified locale. If there is no suitable display name found
|
|
* for the specified locale, the ISO 4217 currency code is returned.
|
|
*
|
|
* @param locale the locale for which a display name for this currency is
|
|
* needed
|
|
* @return the display name of this currency for the specified locale
|
|
* @throws NullPointerException if {@code locale} is null
|
|
* @since 1.7
|
|
*/
|
|
public String getDisplayName(Locale locale) {
|
|
// Android-changed: Use ICU.
|
|
/*
|
|
LocaleServiceProviderPool pool =
|
|
LocaleServiceProviderPool.getPool(CurrencyNameProvider.class);
|
|
String result = pool.getLocalizedObject(
|
|
CurrencyNameGetter.INSTANCE,
|
|
locale, currencyCode, DISPLAYNAME);
|
|
if (result != null) {
|
|
return result;
|
|
}
|
|
|
|
// use currency code as symbol of last resort
|
|
return currencyCode;
|
|
*/
|
|
return icuCurrency.getDisplayName(Objects.requireNonNull(locale));
|
|
}
|
|
|
|
/**
|
|
* Returns the ISO 4217 currency code of this currency.
|
|
*
|
|
* @return the ISO 4217 currency code of this currency
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
// Android-changed: Use ICU.
|
|
// return currencyCode;
|
|
return icuCurrency.toString();
|
|
}
|
|
|
|
/**
|
|
* Resolves instances being deserialized to a single instance per currency.
|
|
*/
|
|
@java.io.Serial
|
|
private Object readResolve() {
|
|
return getInstance(currencyCode);
|
|
}
|
|
|
|
// Android-removed: Use ICU.
|
|
// Removed a bunch of private helper methods that are unused on Android.
|
|
/**
|
|
* Gets the main table entry for the country whose country code consists
|
|
* of char1 and char2.
|
|
*
|
|
private static int getMainTableEntry(char char1, char char2) {
|
|
if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
return mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')];
|
|
}
|
|
|
|
/**
|
|
* Sets the main table entry for the country whose country code consists
|
|
* of char1 and char2.
|
|
*
|
|
private static void setMainTableEntry(char char1, char char2, int entry) {
|
|
if (char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z') {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
mainTable[(char1 - 'A') * A_TO_Z + (char2 - 'A')] = entry;
|
|
}
|
|
|
|
/**
|
|
* Obtains a localized currency names from a CurrencyNameProvider
|
|
* implementation.
|
|
*
|
|
private static class CurrencyNameGetter
|
|
implements LocaleServiceProviderPool.LocalizedObjectGetter<CurrencyNameProvider,
|
|
String> {
|
|
private static final CurrencyNameGetter INSTANCE = new CurrencyNameGetter();
|
|
|
|
@Override
|
|
public String getObject(CurrencyNameProvider currencyNameProvider,
|
|
Locale locale,
|
|
String key,
|
|
Object... params) {
|
|
assert params.length == 1;
|
|
int type = (Integer)params[0];
|
|
|
|
switch(type) {
|
|
case SYMBOL:
|
|
return currencyNameProvider.getSymbol(key, locale);
|
|
case DISPLAYNAME:
|
|
return currencyNameProvider.getDisplayName(key, locale);
|
|
default:
|
|
assert false; // shouldn't happen
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static int[] readIntArray(DataInputStream dis, int count) throws IOException {
|
|
int[] ret = new int[count];
|
|
for (int i = 0; i < count; i++) {
|
|
ret[i] = dis.readInt();
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
private static List<SpecialCaseEntry> readSpecialCases(DataInputStream dis,
|
|
int count)
|
|
throws IOException {
|
|
|
|
List<SpecialCaseEntry> list = new ArrayList<>(count);
|
|
long cutOverTime;
|
|
String oldCurrency;
|
|
String newCurrency;
|
|
int oldCurrencyFraction;
|
|
int newCurrencyFraction;
|
|
int oldCurrencyNumericCode;
|
|
int newCurrencyNumericCode;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
cutOverTime = dis.readLong();
|
|
oldCurrency = dis.readUTF();
|
|
newCurrency = dis.readUTF();
|
|
oldCurrencyFraction = dis.readInt();
|
|
newCurrencyFraction = dis.readInt();
|
|
oldCurrencyNumericCode = dis.readInt();
|
|
newCurrencyNumericCode = dis.readInt();
|
|
SpecialCaseEntry sc = new SpecialCaseEntry(cutOverTime,
|
|
oldCurrency, newCurrency,
|
|
oldCurrencyFraction, newCurrencyFraction,
|
|
oldCurrencyNumericCode, newCurrencyNumericCode);
|
|
list.add(sc);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private static List<OtherCurrencyEntry> readOtherCurrencies(DataInputStream dis,
|
|
int count)
|
|
throws IOException {
|
|
|
|
List<OtherCurrencyEntry> list = new ArrayList<>(count);
|
|
String currencyCode;
|
|
int fraction;
|
|
int numericCode;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
currencyCode = dis.readUTF();
|
|
fraction = dis.readInt();
|
|
numericCode = dis.readInt();
|
|
OtherCurrencyEntry oc = new OtherCurrencyEntry(currencyCode,
|
|
fraction,
|
|
numericCode);
|
|
list.add(oc);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/**
|
|
* Parse currency data found in the properties file (that
|
|
* java.util.currency.data designates) to a List of CurrencyProperty
|
|
* instances. Also, remove invalid entries and the multiple currency
|
|
* code inconsistencies.
|
|
*
|
|
* @param props properties containing currency data
|
|
* @param pattern regex pattern for the properties entry
|
|
* @return list of parsed property entries
|
|
*
|
|
private static List<CurrencyProperty> getValidCurrencyData(Properties props,
|
|
Pattern pattern) {
|
|
|
|
Set<String> keys = props.stringPropertyNames();
|
|
List<CurrencyProperty> propertyEntries = new ArrayList<>();
|
|
|
|
// remove all invalid entries and parse all valid currency properties
|
|
// entries to a group of CurrencyProperty, classified by currency code
|
|
Map<String, List<CurrencyProperty>> currencyCodeGroup = keys.stream()
|
|
.map(k -> CurrencyProperty
|
|
.getValidEntry(k.toUpperCase(Locale.ROOT),
|
|
props.getProperty(k).toUpperCase(Locale.ROOT),
|
|
pattern)).flatMap(o -> o.stream())
|
|
.collect(Collectors.groupingBy(entry -> entry.currencyCode));
|
|
|
|
// check each group for inconsistencies
|
|
currencyCodeGroup.forEach((curCode, list) -> {
|
|
boolean inconsistent = CurrencyProperty
|
|
.containsInconsistentInstances(list);
|
|
if (inconsistent) {
|
|
list.forEach(prop -> CurrencyProperty.info("The property"
|
|
+ " entry for " + prop.country + " is inconsistent."
|
|
+ " Ignored.", null));
|
|
} else {
|
|
propertyEntries.addAll(list);
|
|
}
|
|
});
|
|
|
|
return propertyEntries;
|
|
}
|
|
|
|
/**
|
|
* Replaces currency data found in the properties file that
|
|
* java.util.currency.data designates. This method is invoked for
|
|
* each valid currency entry.
|
|
*
|
|
* @param prop CurrencyProperty instance of the valid property entry
|
|
*
|
|
private static void replaceCurrencyData(CurrencyProperty prop) {
|
|
|
|
|
|
String ctry = prop.country;
|
|
String code = prop.currencyCode;
|
|
int numeric = prop.numericCode;
|
|
int fraction = prop.fraction;
|
|
int entry = numeric << NUMERIC_CODE_SHIFT;
|
|
|
|
int index = SpecialCaseEntry.indexOf(code, fraction, numeric);
|
|
|
|
|
|
// If a new entry changes the numeric code/dfd of an existing
|
|
// currency code, update it in the sc list at the respective
|
|
// index and also change it in the other currencies list and
|
|
// main table (if that currency code is also used as a
|
|
// simple case).
|
|
|
|
// If all three components do not match with the new entry,
|
|
// but the currency code exists in the special case list
|
|
// update the sc entry with the new entry
|
|
int scCurrencyCodeIndex = -1;
|
|
if (index == -1) {
|
|
scCurrencyCodeIndex = SpecialCaseEntry.currencyCodeIndex(code);
|
|
if (scCurrencyCodeIndex != -1) {
|
|
//currency code exists in sc list, then update the old entry
|
|
specialCasesList.set(scCurrencyCodeIndex,
|
|
new SpecialCaseEntry(code, fraction, numeric));
|
|
|
|
// also update the entry in other currencies list
|
|
OtherCurrencyEntry oe = OtherCurrencyEntry.findEntry(code);
|
|
if (oe != null) {
|
|
int oIndex = otherCurrenciesList.indexOf(oe);
|
|
otherCurrenciesList.set(oIndex, new OtherCurrencyEntry(
|
|
code, fraction, numeric));
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If a country switches from simple case to special case or
|
|
* one special case to other special case which is not present
|
|
* in the sc arrays then insert the new entry in special case arrays.
|
|
* If an entry with given currency code exists, update with the new
|
|
* entry.
|
|
*
|
|
if (index == -1 && (ctry.charAt(0) != code.charAt(0)
|
|
|| ctry.charAt(1) != code.charAt(1))) {
|
|
|
|
if(scCurrencyCodeIndex == -1) {
|
|
specialCasesList.add(new SpecialCaseEntry(code, fraction,
|
|
numeric));
|
|
index = specialCasesList.size() - 1;
|
|
} else {
|
|
index = scCurrencyCodeIndex;
|
|
}
|
|
|
|
// update the entry in main table if it exists as a simple case
|
|
updateMainTableEntry(code, fraction, numeric);
|
|
}
|
|
|
|
if (index == -1) {
|
|
// simple case
|
|
entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
|
|
| (code.charAt(2) - 'A');
|
|
} else {
|
|
// special case
|
|
entry = SPECIAL_CASE_COUNTRY_MASK
|
|
| (index + SPECIAL_CASE_COUNTRY_INDEX_DELTA);
|
|
}
|
|
setMainTableEntry(ctry.charAt(0), ctry.charAt(1), entry);
|
|
}
|
|
|
|
// update the entry in maintable for any simple case found, if a new
|
|
// entry as a special case updates the entry in sc list with
|
|
// existing currency code
|
|
private static void updateMainTableEntry(String code, int fraction,
|
|
int numeric) {
|
|
// checking the existence of currency code in mainTable
|
|
int tableEntry = getMainTableEntry(code.charAt(0), code.charAt(1));
|
|
int entry = numeric << NUMERIC_CODE_SHIFT;
|
|
if ((tableEntry & COUNTRY_TYPE_MASK) == SIMPLE_CASE_COUNTRY_MASK
|
|
&& tableEntry != INVALID_COUNTRY_ENTRY
|
|
&& code.charAt(2) - 'A' == (tableEntry
|
|
& SIMPLE_CASE_COUNTRY_FINAL_CHAR_MASK)) {
|
|
|
|
int numericCode = (tableEntry & NUMERIC_CODE_MASK)
|
|
>> NUMERIC_CODE_SHIFT;
|
|
int defaultFractionDigits = (tableEntry
|
|
& SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_MASK)
|
|
>> SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT;
|
|
if (numeric != numericCode || fraction != defaultFractionDigits) {
|
|
// update the entry in main table
|
|
entry |= (fraction << SIMPLE_CASE_COUNTRY_DEFAULT_DIGITS_SHIFT)
|
|
| (code.charAt(2) - 'A');
|
|
setMainTableEntry(code.charAt(0), code.charAt(1), entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Used to represent a special case currency entry
|
|
* - cutOverTime: cut-over time in millis as returned by
|
|
* System.currentTimeMillis for special case countries that are changing
|
|
* currencies; Long.MAX_VALUE for countries that are not changing currencies
|
|
* - oldCurrency: old currencies for special case countries
|
|
* - newCurrency: new currencies for special case countries that are
|
|
* changing currencies; null for others
|
|
* - oldCurrencyFraction: default fraction digits for old currencies
|
|
* - newCurrencyFraction: default fraction digits for new currencies, 0 for
|
|
* countries that are not changing currencies
|
|
* - oldCurrencyNumericCode: numeric code for old currencies
|
|
* - newCurrencyNumericCode: numeric code for new currencies, 0 for countries
|
|
* that are not changing currencies
|
|
*
|
|
private static class SpecialCaseEntry {
|
|
|
|
private final long cutOverTime;
|
|
private final String oldCurrency;
|
|
private final String newCurrency;
|
|
private final int oldCurrencyFraction;
|
|
private final int newCurrencyFraction;
|
|
private final int oldCurrencyNumericCode;
|
|
private final int newCurrencyNumericCode;
|
|
|
|
private SpecialCaseEntry(long cutOverTime, String oldCurrency, String newCurrency,
|
|
int oldCurrencyFraction, int newCurrencyFraction,
|
|
int oldCurrencyNumericCode, int newCurrencyNumericCode) {
|
|
this.cutOverTime = cutOverTime;
|
|
this.oldCurrency = oldCurrency;
|
|
this.newCurrency = newCurrency;
|
|
this.oldCurrencyFraction = oldCurrencyFraction;
|
|
this.newCurrencyFraction = newCurrencyFraction;
|
|
this.oldCurrencyNumericCode = oldCurrencyNumericCode;
|
|
this.newCurrencyNumericCode = newCurrencyNumericCode;
|
|
}
|
|
|
|
private SpecialCaseEntry(String currencyCode, int fraction,
|
|
int numericCode) {
|
|
this(Long.MAX_VALUE, currencyCode, "", fraction, 0, numericCode, 0);
|
|
}
|
|
|
|
//get the index of the special case entry
|
|
private static int indexOf(String code, int fraction, int numeric) {
|
|
int size = specialCasesList.size();
|
|
for (int index = 0; index < size; index++) {
|
|
SpecialCaseEntry scEntry = specialCasesList.get(index);
|
|
if (scEntry.oldCurrency.equals(code)
|
|
&& scEntry.oldCurrencyFraction == fraction
|
|
&& scEntry.oldCurrencyNumericCode == numeric
|
|
&& scEntry.cutOverTime == Long.MAX_VALUE) {
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// get the fraction and numericCode of the sc currencycode
|
|
private static int[] findEntry(String code) {
|
|
int[] fractionAndNumericCode = null;
|
|
int size = specialCasesList.size();
|
|
for (int index = 0; index < size; index++) {
|
|
SpecialCaseEntry scEntry = specialCasesList.get(index);
|
|
if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE
|
|
|| System.currentTimeMillis() < scEntry.cutOverTime)) {
|
|
//consider only when there is no new currency or cutover time is not passed
|
|
fractionAndNumericCode = new int[2];
|
|
fractionAndNumericCode[0] = scEntry.oldCurrencyFraction;
|
|
fractionAndNumericCode[1] = scEntry.oldCurrencyNumericCode;
|
|
break;
|
|
} else if (scEntry.newCurrency.equals(code)
|
|
&& System.currentTimeMillis() >= scEntry.cutOverTime) {
|
|
//consider only if the cutover time is passed
|
|
fractionAndNumericCode = new int[2];
|
|
fractionAndNumericCode[0] = scEntry.newCurrencyFraction;
|
|
fractionAndNumericCode[1] = scEntry.newCurrencyNumericCode;
|
|
break;
|
|
}
|
|
}
|
|
return fractionAndNumericCode;
|
|
}
|
|
|
|
// get the index based on currency code
|
|
private static int currencyCodeIndex(String code) {
|
|
int size = specialCasesList.size();
|
|
for (int index = 0; index < size; index++) {
|
|
SpecialCaseEntry scEntry = specialCasesList.get(index);
|
|
if (scEntry.oldCurrency.equals(code) && (scEntry.cutOverTime == Long.MAX_VALUE
|
|
|| System.currentTimeMillis() < scEntry.cutOverTime)) {
|
|
//consider only when there is no new currency or cutover time is not passed
|
|
return index;
|
|
} else if (scEntry.newCurrency.equals(code)
|
|
&& System.currentTimeMillis() >= scEntry.cutOverTime) {
|
|
//consider only if the cutover time is passed
|
|
return index;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
// convert the special case entry to sc arrays index
|
|
private static int toIndex(int tableEntry) {
|
|
return (tableEntry & SPECIAL_CASE_COUNTRY_INDEX_MASK) - SPECIAL_CASE_COUNTRY_INDEX_DELTA;
|
|
}
|
|
|
|
}
|
|
|
|
/* Used to represent Other currencies
|
|
* - currencyCode: currency codes that are not the main currency
|
|
* of a simple country
|
|
* - otherCurrenciesDFD: decimal format digits for other currencies
|
|
* - otherCurrenciesNumericCode: numeric code for other currencies
|
|
*
|
|
private static class OtherCurrencyEntry {
|
|
|
|
private final String currencyCode;
|
|
private final int fraction;
|
|
private final int numericCode;
|
|
|
|
private OtherCurrencyEntry(String currencyCode, int fraction,
|
|
int numericCode) {
|
|
this.currencyCode = currencyCode;
|
|
this.fraction = fraction;
|
|
this.numericCode = numericCode;
|
|
}
|
|
|
|
//get the instance of the other currency code
|
|
private static OtherCurrencyEntry findEntry(String code) {
|
|
int size = otherCurrenciesList.size();
|
|
for (int index = 0; index < size; index++) {
|
|
OtherCurrencyEntry ocEntry = otherCurrenciesList.get(index);
|
|
if (ocEntry.currencyCode.equalsIgnoreCase(code)) {
|
|
return ocEntry;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* Used to represent an entry of the properties file that
|
|
* java.util.currency.data designates
|
|
*
|
|
* - country: country representing the currency entry
|
|
* - currencyCode: currency code
|
|
* - fraction: default fraction digit
|
|
* - numericCode: numeric code
|
|
* - date: cutover date
|
|
*
|
|
private static class CurrencyProperty {
|
|
private final String country;
|
|
private final String currencyCode;
|
|
private final int fraction;
|
|
private final int numericCode;
|
|
private final String date;
|
|
|
|
private CurrencyProperty(String country, String currencyCode,
|
|
int fraction, int numericCode, String date) {
|
|
this.country = country;
|
|
this.currencyCode = currencyCode;
|
|
this.fraction = fraction;
|
|
this.numericCode = numericCode;
|
|
this.date = date;
|
|
}
|
|
|
|
/**
|
|
* Check the valid currency data and create/return an Optional instance
|
|
* of CurrencyProperty
|
|
*
|
|
* @param ctry country representing the currency data
|
|
* @param curData currency data of the given {@code ctry}
|
|
* @param pattern regex pattern for the properties entry
|
|
* @return Optional containing CurrencyProperty instance, If valid;
|
|
* empty otherwise
|
|
*
|
|
private static Optional<CurrencyProperty> getValidEntry(String ctry,
|
|
String curData,
|
|
Pattern pattern) {
|
|
|
|
CurrencyProperty prop = null;
|
|
|
|
if (ctry.length() != 2) {
|
|
// Invalid country code. Ignore the entry.
|
|
} else {
|
|
|
|
prop = parseProperty(ctry, curData, pattern);
|
|
// if the property entry failed any of the below checked
|
|
// criteria it is ignored
|
|
if (prop == null
|
|
|| (prop.date == null && curData.chars()
|
|
.map(c -> c == ',' ? 1 : 0).sum() >= 3)) {
|
|
// format is not recognized. ignore the data if date
|
|
// string is null and we've 4 values, bad date value
|
|
prop = null;
|
|
} else if (prop.fraction
|
|
> SIMPLE_CASE_COUNTRY_MAX_DEFAULT_DIGITS) {
|
|
prop = null;
|
|
} else {
|
|
try {
|
|
if (prop.date != null
|
|
&& !isPastCutoverDate(prop.date)) {
|
|
prop = null;
|
|
}
|
|
} catch (ParseException ex) {
|
|
prop = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (prop == null) {
|
|
info("The property entry for " + ctry + " is invalid."
|
|
+ " Ignored.", null);
|
|
}
|
|
|
|
return Optional.ofNullable(prop);
|
|
}
|
|
|
|
/*
|
|
* Parse properties entry and return CurrencyProperty instance
|
|
*
|
|
private static CurrencyProperty parseProperty(String ctry,
|
|
String curData, Pattern pattern) {
|
|
Matcher m = pattern.matcher(curData);
|
|
if (!m.find()) {
|
|
return null;
|
|
} else {
|
|
return new CurrencyProperty(ctry, m.group(1),
|
|
Integer.parseInt(m.group(3)),
|
|
Integer.parseInt(m.group(2)), m.group(4));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the given list contains multiple inconsistent currency instances
|
|
*
|
|
private static boolean containsInconsistentInstances(
|
|
List<CurrencyProperty> list) {
|
|
int numCode = list.get(0).numericCode;
|
|
int fractionDigit = list.get(0).fraction;
|
|
return list.stream().anyMatch(prop -> prop.numericCode != numCode
|
|
|| prop.fraction != fractionDigit);
|
|
}
|
|
|
|
private static boolean isPastCutoverDate(String s)
|
|
throws ParseException {
|
|
SimpleDateFormat format = new SimpleDateFormat(
|
|
"yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT);
|
|
format.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
format.setLenient(false);
|
|
long time = format.parse(s.trim()).getTime();
|
|
return System.currentTimeMillis() > time;
|
|
|
|
}
|
|
|
|
private static void info(String message, Throwable t) {
|
|
PlatformLogger logger = PlatformLogger
|
|
.getLogger("java.util.Currency");
|
|
if (logger.isLoggable(PlatformLogger.Level.INFO)) {
|
|
if (t != null) {
|
|
logger.info(message, t);
|
|
} else {
|
|
logger.info(message);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
*/
|
|
}
|