org.joda.money.CurrencyUnit Maven / Gradle / Ivy
Show all versions of joda-money Show documentation
/*
* Copyright 2009-present, Stephen Colebourne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joda.money;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Currency;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.regex.Pattern;
import org.joda.convert.FromString;
import org.joda.convert.ToString;
/**
* A unit of currency.
*
* This class represents a unit of currency such as the British Pound, Euro
* or US Dollar.
*
* The set of loaded currencies is provided by an instance of {@link CurrencyUnitDataProvider}.
* The provider used is determined by the system property {@code org.joda.money.CurrencyUnitDataProvider}
* which should be the fully qualified class name of the provider. The default provider loads the first
* resource named {@code /org/joda/money/MoneyData.csv} on the classpath.
*
* This class is immutable and thread-safe.
*/
public final class CurrencyUnit implements Comparable, Serializable {
/**
* The serialisation version.
*/
private static final long serialVersionUID = 327835287287L;
/**
* The currency code pattern.
*/
private static final Pattern CODE = Pattern.compile("[A-Z][A-Z][A-Z]");
/**
* Map of registered currencies by text code.
*/
private static final ConcurrentMap currenciesByCode = new ConcurrentSkipListMap();
/**
* Map of registered currencies by numeric code.
*/
private static final ConcurrentMap currenciesByNumericCode = new ConcurrentHashMap();
/**
* Map of registered currencies by country.
*/
private static final ConcurrentMap currenciesByCountry = new ConcurrentSkipListMap();
static {
// load one data provider by system property
try {
try {
String clsName = System.getProperty(
"org.joda.money.CurrencyUnitDataProvider",
"org.joda.money.DefaultCurrencyUnitDataProvider");
Class extends CurrencyUnitDataProvider> cls =
CurrencyUnit.class.getClassLoader().loadClass(clsName).asSubclass(CurrencyUnitDataProvider.class);
cls.getDeclaredConstructor().newInstance().registerCurrencies();
} catch (SecurityException ex) {
new DefaultCurrencyUnitDataProvider().registerCurrencies();
}
} catch (RuntimeException ex) {
System.err.println("ERROR: " + ex.getMessage());
ex.printStackTrace();
throw ex;
} catch (Exception ex) {
System.err.println("ERROR: " + ex.getMessage());
ex.printStackTrace();
throw new RuntimeException(ex.toString(), ex);
}
}
// a selection of commonly traded, stable currencies
/**
* The currency 'USD' - United States Dollar.
*/
public static final CurrencyUnit USD = of("USD");
/**
* The currency 'EUR' - Euro.
*/
public static final CurrencyUnit EUR = of("EUR");
/**
* The currency 'JPY' - Japanese Yen.
*/
public static final CurrencyUnit JPY = of("JPY");
/**
* The currency 'GBP' - British pound.
*/
public static final CurrencyUnit GBP = of("GBP");
/**
* The currency 'CHF' - Swiss Franc.
*/
public static final CurrencyUnit CHF = of("CHF");
/**
* The currency 'AUD' - Australian Dollar.
*/
public static final CurrencyUnit AUD = of("AUD");
/**
* The currency 'CAD' - Canadian Dollar.
*/
public static final CurrencyUnit CAD = of("CAD");
/**
* The currency code, not null.
*/
private final String code;
/**
* The numeric currency code.
*/
private final short numericCode;
/**
* The number of decimal places.
*/
private final short decimalPlaces;
//-----------------------------------------------------------------------
/**
* Registers a currency and associated countries allowing it to be used.
*
* This class only permits known currencies to be returned.
* To achieve this, all currencies have to be registered in advance.
*
* Since this method is public, it is possible to add currencies in
* application code. It is recommended to do this only at startup, however
* it is safe to do so later as the internal implementation is thread-safe.
*
* The currency code must be three upper-case ASCII letters, based on ISO-4217.
* The numeric code must be from 0 to 999, or -1 if not applicable.
*
* @param currencyCode the three-letter upper-case currency code, not null
* @param numericCurrencyCode the numeric currency code, from 0 to 999, -1 if none
* @param decimalPlaces the number of decimal places that the currency
* normally has, from 0 to 30 (normally 0, 2 or 3), or -1 for a pseudo-currency
* @param countryCodes the country codes to register the currency under, not null
* @return the new instance, never null
* @throws IllegalArgumentException if the code is already registered, or the
* specified data is invalid
*/
public static synchronized CurrencyUnit registerCurrency(
String currencyCode,
int numericCurrencyCode,
int decimalPlaces,
List countryCodes) {
return registerCurrency(currencyCode, numericCurrencyCode, decimalPlaces, countryCodes, false);
}
/**
* Registers a currency and associated countries allowing it to be used, allowing replacement.
*
* This class only permits known currencies to be returned.
* To achieve this, all currencies have to be registered in advance.
*
* Since this method is public, it is possible to add currencies in
* application code. It is recommended to do this only at startup, however
* it is safe to do so later as the internal implementation is thread-safe.
*
* This method uses a flag to determine whether the registered currency
* must be new, or can replace an existing currency.
*
* The currency code must be three upper-case ASCII letters, based on ISO-4217.
* The numeric code must be from 0 to 999, or -1 if not applicable.
*
* @param currencyCode the three-letter upper-case currency code, not null
* @param numericCurrencyCode the numeric currency code, from 0 to 999, -1 if none
* @param decimalPlaces the number of decimal places that the currency
* normally has, from 0 to 30 (normally 0, 2 or 3), or -1 for a pseudo-currency
* @param countryCodes the country codes to register the currency under,
* use of ISO-3166 is recommended, not null
* @param force true to register forcefully, replacing any existing matching currency,
* false to validate that there is no existing matching currency
* @return the new instance, never null
* @throws IllegalArgumentException if the code is already registered and {@code force} is false;
* or if the specified data is invalid
*/
public static synchronized CurrencyUnit registerCurrency(
String currencyCode,
int numericCurrencyCode,
int decimalPlaces,
List countryCodes,
boolean force) {
MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null");
if (currencyCode.length() != 3) {
throw new IllegalArgumentException("Invalid string code, must be length 3");
}
if (CODE.matcher(currencyCode).matches() == false) {
throw new IllegalArgumentException("Invalid string code, must be ASCII upper-case letters");
}
if (numericCurrencyCode < -1 || numericCurrencyCode > 999) {
throw new IllegalArgumentException("Invalid numeric code");
}
if (decimalPlaces < -1 || decimalPlaces > 30) {
throw new IllegalArgumentException("Invalid number of decimal places");
}
MoneyUtils.checkNotNull(countryCodes, "Country codes must not be null");
CurrencyUnit currency = new CurrencyUnit(currencyCode, (short) numericCurrencyCode, (short) decimalPlaces);
if (force) {
currenciesByCode.remove(currencyCode);
currenciesByNumericCode.remove(numericCurrencyCode);
for (String countryCode : countryCodes) {
currenciesByCountry.remove(countryCode);
}
} else {
if (currenciesByCode.containsKey(currencyCode) || currenciesByNumericCode.containsKey(numericCurrencyCode)) {
throw new IllegalArgumentException("Currency already registered: " + currencyCode);
}
for (String countryCode : countryCodes) {
if (currenciesByCountry.containsKey(countryCode)) {
throw new IllegalArgumentException("Currency already registered for country: " + countryCode);
}
}
}
currenciesByCode.putIfAbsent(currencyCode, currency);
if (numericCurrencyCode >= 0) {
currenciesByNumericCode.putIfAbsent(numericCurrencyCode, currency);
}
for (String countryCode : countryCodes) {
registerCountry(countryCode, currency);
}
return currenciesByCode.get(currencyCode);
}
/**
* Registers a currency allowing it to be used, allowing replacement.
*
* This class only permits known currencies to be returned.
* To achieve this, all currencies have to be registered in advance.
*
* Since this method is public, it is possible to add currencies in
* application code. It is recommended to do this only at startup, however
* it is safe to do so later as the internal implementation is thread-safe.
*
* This method uses a flag to determine whether the registered currency
* must be new, or can replace an existing currency.
*
* The currency code must be three upper-case ASCII letters, based on ISO-4217.
* The numeric code must be from 0 to 999, or -1 if not applicable.
*
* @param currencyCode the three-letter upper-case currency code, not null
* @param numericCurrencyCode the numeric currency code, from 0 to 999, -1 if none
* @param decimalPlaces the number of decimal places that the currency
* normally has, from 0 to 30 (normally 0, 2 or 3), or -1 for a pseudo-currency
* use of ISO-3166 is recommended, not null
* @param force true to register forcefully, replacing any existing matching currency,
* false to validate that there is no existing matching currency
* @return the new instance, never null
* @throws IllegalArgumentException if the code is already registered and {@code force} is false;
* or if the specified data is invalid
*/
public static synchronized CurrencyUnit registerCurrency(
String currencyCode,
int numericCurrencyCode,
int decimalPlaces,
boolean force) {
List countryCodes = Collections.emptyList();
return registerCurrency(currencyCode, numericCurrencyCode, decimalPlaces, countryCodes, force);
}
/**
* Registers a country code, typically ISO 3166-1-alpha-2.
*
* This registers a country code and the associated currency.
*
* The country code is typically from ISO 3166-1-alpha-2, and is therefore two upper-case ASCII letters.
*
* If the country code already exists, the data is replaced.
*
* @param countryCode the country code, two upper case letters if following ISO 3166-1-alpha-2, not null
* @param currency the associated currency
* @throws IllegalArgumentException if the code is already registered and {@code force} is false;
* or if the specified data is invalid
*/
public static synchronized void registerCountry(String countryCode, CurrencyUnit currency) {
currenciesByCountry.put(countryCode, currency);
}
//-----------------------------------------------------------------------
/**
* Gets the list of all registered currencies.
*
* This class only permits known currencies to be returned, thus this list is
* the complete list of valid singleton currencies. The list may change after
* application startup, however this isn't recommended.
*
* @return the sorted, independent, list of all registered currencies, never null
*/
public static List registeredCurrencies() {
return new ArrayList<>(currenciesByCode.values());
}
/**
* Gets the list of all registered countries.
*
* This returns the list of known countries.
* The list may change after application startup, however this isn't recommended.
*
* @return the sorted, independent, list of all registered countries, never null
*/
public static List registeredCountries() {
return new ArrayList<>(currenciesByCountry.keySet());
}
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code CurrencyUnit} matching the specified JDK currency.
*
* This converts the JDK currency instance to a currency unit using the code.
*
* @param currency the currency, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
public static CurrencyUnit of(Currency currency) {
MoneyUtils.checkNotNull(currency, "Currency must not be null");
return of(currency.getCurrencyCode());
}
/**
* Obtains an instance of {@code CurrencyUnit} for the specified three letter currency code.
*
* A currency is uniquely identified by a three letter code, based on ISO-4217.
* Valid currency codes are three upper-case ASCII letters.
*
* @param currencyCode the three-letter currency code, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
@FromString
public static CurrencyUnit of(String currencyCode) {
MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null");
CurrencyUnit currency = currenciesByCode.get(currencyCode);
if (currency == null) {
throw new IllegalCurrencyException("Unknown currency '" + currencyCode + '\'');
}
return currency;
}
/**
* Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code.
*
* The numeric code is an alternative to the three letter code.
* This method is lenient and does not require the string to be left padded with zeroes.
*
* @param numericCurrencyCode the currency code, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
public static CurrencyUnit ofNumericCode(String numericCurrencyCode) {
MoneyUtils.checkNotNull(numericCurrencyCode, "Currency code must not be null");
switch (numericCurrencyCode.length()) {
case 1:
return ofNumericCode(numericCurrencyCode.charAt(0) - '0');
case 2:
return ofNumericCode(
(numericCurrencyCode.charAt(0) - '0') * 10 +
numericCurrencyCode.charAt(1) - '0');
case 3:
return ofNumericCode(
(numericCurrencyCode.charAt(0) - '0') * 100 +
(numericCurrencyCode.charAt(1) - '0') * 10 +
numericCurrencyCode.charAt(2) - '0');
default:
throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\'');
}
}
/**
* Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code.
*
* The numeric code is an alternative to the three letter code.
*
* @param numericCurrencyCode the numeric currency code, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
public static CurrencyUnit ofNumericCode(int numericCurrencyCode) {
CurrencyUnit currency = currenciesByNumericCode.get(numericCurrencyCode);
if (currency == null) {
throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\'');
}
return currency;
}
/**
* Obtains an instance of {@code CurrencyUnit} for the specified locale.
*
* Only the country is used from the locale.
*
* @param locale the locale, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
public static CurrencyUnit of(Locale locale) {
MoneyUtils.checkNotNull(locale, "Locale must not be null");
CurrencyUnit currency = currenciesByCountry.get(locale.getCountry());
if (currency == null) {
throw new IllegalCurrencyException("No currency found for locale '" + locale + '\'');
}
return currency;
}
/**
* Obtains an instance of {@code CurrencyUnit} for the specified ISO-3166 country code.
*
* Country codes should generally be in upper case.
* This method is case sensitive.
*
* @param countryCode the country code, typically ISO-3166, not null
* @return the singleton instance, never null
* @throws IllegalCurrencyException if the currency is unknown
*/
public static CurrencyUnit ofCountry(String countryCode) {
MoneyUtils.checkNotNull(countryCode, "Country code must not be null");
CurrencyUnit currency = currenciesByCountry.get(countryCode);
if (currency == null) {
throw new IllegalCurrencyException("No currency found for country '" + countryCode + '\'');
}
return currency;
}
//-----------------------------------------------------------------------
/**
* Constructor, creating a new currency instance.
*
* @param code the three-letter currency code, not null
* @param numericCode the numeric currency code, from 0 to 999, -1 if none
* @param decimalPlaces the decimal places, not null
*/
CurrencyUnit(String code, short numericCode, short decimalPlaces) {
assert code != null : "Joda-Money bug: Currency code must not be null";
this.code = code;
this.numericCode = numericCode;
this.decimalPlaces = decimalPlaces;
}
/**
* Block malicious data streams.
*
* @param ois the input stream, not null
* @throws InvalidObjectException if an error occurs
*/
private void readObject(ObjectInputStream ois) throws InvalidObjectException {
throw new InvalidObjectException("Serialization delegate required");
}
/**
* Uses a serialization delegate.
*
* @return the replacing object, never null
*/
private Object writeReplace() {
return new Ser(Ser.CURRENCY_UNIT, this);
}
//-----------------------------------------------------------------------
/**
* Gets the ISO-4217 three-letter currency code.
*
* Each currency is uniquely identified by a three-letter upper-case code, based on ISO-4217.
*
* @return the three-letter upper-case currency code, never null
*/
public String getCode() {
return code;
}
/**
* Gets the ISO-4217 numeric currency code.
*
* The numeric code is an alternative to the standard string-based code.
*
* @return the numeric currency code, -1 if no numeric code
*/
public int getNumericCode() {
return numericCode;
}
/**
* Gets the ISO-4217 numeric currency code as a three digit string.
*
* This formats the numeric code as a three digit string prefixed by zeroes if necessary.
* If there is no valid code, then an empty string is returned.
*
* @return the three digit numeric currency code, empty is no code, never null
*/
public String getNumeric3Code() {
if (numericCode < 0) {
return "";
}
String str = Integer.toString(numericCode);
if (str.length() == 1) {
return "00" + str;
}
if (str.length() == 2) {
return "0" + str;
}
return str;
}
/**
* Gets the country codes applicable to this currency.
*
* A currency is typically valid in one or more countries.
* The codes are typically defined by ISO-3166.
* An empty set indicates that no the currency is not associated with a country code.
*
* @return the country codes, may be empty, not null
*/
public Set getCountryCodes() {
Set countryCodes = new HashSet();
for (Entry entry : currenciesByCountry.entrySet()) {
if (this.equals(entry.getValue())) {
countryCodes.add(entry.getKey());
}
}
return countryCodes;
}
//-----------------------------------------------------------------------
/**
* Gets the number of decimal places typically used by this currency.
*
* Different currencies have different numbers of decimal places by default.
* For example, 'GBP' has 2 decimal places, but 'JPY' has zero.
* Pseudo-currencies will return zero.
*
* @return the decimal places, from 0 to 9 (normally 0, 2 or 3)
*/
public int getDecimalPlaces() {
return decimalPlaces < 0 ? 0 : decimalPlaces;
}
/**
* Checks if this is a pseudo-currency.
*
* @return true if this is a pseudo-currency
*/
public boolean isPseudoCurrency() {
return decimalPlaces < 0;
}
//-----------------------------------------------------------------------
/**
* Gets the symbol for this locale from the JDK.
*
* If this currency doesn't have a JDK equivalent, then the currency code
* is returned.
*
* This method matches the API of {@link Currency}.
*
* @return the JDK currency instance, never null
*/
public String getSymbol() {
// Java 21 currency data uses a symbol, we want to retain this as XXX
if ("XXX".equals(code)) {
return code;
}
try {
return Currency.getInstance(code).getSymbol();
} catch (IllegalArgumentException ex) {
return code;
}
}
/**
* Gets the symbol for this locale from the JDK.
*
* If this currency doesn't have a JDK equivalent, then the currency code
* is returned.
*
* This method matches the API of {@link Currency}.
*
* @param locale the locale to get the symbol for, not null
* @return the JDK currency instance, never null
*/
public String getSymbol(Locale locale) {
MoneyUtils.checkNotNull(locale, "Locale must not be null");
// Java 21 currency data uses a symbol, we want to retain this as XXX
if ("XXX".equals(code)) {
return code;
}
try {
return Currency.getInstance(code).getSymbol(locale);
} catch (IllegalArgumentException ex) {
return code;
}
}
//-----------------------------------------------------------------------
/**
* Gets the JDK currency instance equivalent to this currency.
*
* This attempts to convert a {@code CurrencyUnit} to a JDK {@code Currency}.
*
* @return the JDK currency instance, never null
* @throws IllegalArgumentException if no matching currency exists in the JDK
*/
public Currency toCurrency() {
return Currency.getInstance(code);
}
//-----------------------------------------------------------------------
/**
* Compares this currency to another by alphabetical comparison of the code.
*
* @param other the other currency, not null
* @return negative if earlier alphabetically, 0 if equal, positive if greater alphabetically
*/
@Override
public int compareTo(CurrencyUnit other) {
return code.compareTo(other.code);
}
/**
* Checks if this currency equals another currency.
*
* The comparison checks the 3 letter currency code.
*
* @param obj the other currency, null returns false
* @return true if equal
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof CurrencyUnit) {
return code.equals(((CurrencyUnit) obj).code);
}
return false;
}
/**
* Returns a suitable hash code for the currency.
*
* @return the hash code
*/
@Override
public int hashCode() {
return code.hashCode();
}
//-----------------------------------------------------------------------
/**
* Gets the currency code as a string.
*
* @return the currency code, never null
*/
@Override
@ToString
public String toString() {
return code;
}
}