All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.joda.money.CurrencyUnit Maven / Gradle / Ivy

There is a newer version: 2.0.0
Show newest version
/*
 *  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 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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy