001 /* 002 * Copyright 2009-2013 Stephen Colebourne 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.joda.money; 017 018 import java.io.InvalidObjectException; 019 import java.io.ObjectInputStream; 020 import java.io.Serializable; 021 import java.util.ArrayList; 022 import java.util.Collections; 023 import java.util.Currency; 024 import java.util.List; 025 import java.util.Locale; 026 import java.util.concurrent.ConcurrentHashMap; 027 import java.util.concurrent.ConcurrentMap; 028 029 import org.joda.convert.FromString; 030 import org.joda.convert.ToString; 031 032 /** 033 * A unit of currency. 034 * <p> 035 * This class represents a unit of currency such as the British Pound, Euro 036 * or US Dollar. 037 * <p> 038 * The set of loaded currencies is provided by an instance of {@link CurrencyUnitDataProvider}. 039 * The provider used is determined by the system property {@code org.joda.money.CurrencyUnitDataProvider} 040 * which should be the fully qualified class name of the provider. The default provider loads the first 041 * resource named {@code /org/joda/money/MoneyData.csv} on the classpath. 042 * <p> 043 * This class is immutable and thread-safe. 044 */ 045 public final class CurrencyUnit implements Comparable<CurrencyUnit>, Serializable { 046 047 /** 048 * The serialisation version. 049 */ 050 private static final long serialVersionUID = 327835287287L; 051 /** 052 * Map of registered currencies by text code. 053 */ 054 private static final ConcurrentMap<String, CurrencyUnit> currenciesByCode = new ConcurrentHashMap<String, CurrencyUnit>(); 055 /** 056 * Map of registered currencies by numeric code. 057 */ 058 private static final ConcurrentMap<Integer, CurrencyUnit> currenciesByNumericCode = new ConcurrentHashMap<Integer, CurrencyUnit>(); 059 /** 060 * Map of registered currencies by country. 061 */ 062 private static final ConcurrentMap<String, CurrencyUnit> currenciesByCountry = new ConcurrentHashMap<String, CurrencyUnit>(); 063 static { 064 // load one data provider by system property 065 try { 066 try { 067 String clsName = System.getProperty( 068 "org.joda.money.CurrencyUnitDataProvider", "org.joda.money.DefaultCurrencyUnitDataProvider"); 069 Class<? extends CurrencyUnitDataProvider> cls = 070 CurrencyUnit.class.getClassLoader().loadClass(clsName).asSubclass(CurrencyUnitDataProvider.class); 071 cls.newInstance().registerCurrencies(); 072 } catch (SecurityException ex) { 073 new DefaultCurrencyUnitDataProvider().registerCurrencies(); 074 } 075 } catch (RuntimeException ex) { 076 throw ex; 077 } catch (Exception ex) { 078 throw new RuntimeException(ex.toString(), ex); 079 } 080 } 081 082 // a selection of commonly traded, stable currencies 083 /** 084 * The currency 'USD' - United States Dollar. 085 */ 086 public static final CurrencyUnit USD = of("USD"); 087 /** 088 * The currency 'EUR' - Euro. 089 */ 090 public static final CurrencyUnit EUR = of("EUR"); 091 /** 092 * The currency 'JPY' - Japanese Yen. 093 */ 094 public static final CurrencyUnit JPY = of("JPY"); 095 /** 096 * The currency 'GBP' - British pound. 097 */ 098 public static final CurrencyUnit GBP = of("GBP"); 099 /** 100 * The currency 'CHF' - Swiss Franc. 101 */ 102 public static final CurrencyUnit CHF = of("CHF"); 103 /** 104 * The currency 'AUD' - Australian Dollar. 105 */ 106 public static final CurrencyUnit AUD = of("AUD"); 107 /** 108 * The currency 'CAD' - Canadian Dollar. 109 */ 110 public static final CurrencyUnit CAD = of("CAD"); 111 112 /** 113 * The currency code, not null. 114 */ 115 private final String code; 116 /** 117 * The numeric currency code. 118 */ 119 private final short numericCode; 120 /** 121 * The number of decimal places. 122 */ 123 private final short decimalPlaces; 124 125 //----------------------------------------------------------------------- 126 /** 127 * Registers a currency allowing it to be used. 128 * <p> 129 * This class only permits known currencies to be returned. 130 * To achieve this, all currencies have to be registered in advance, at 131 * application startup. 132 * 133 * @param currencyCode the currency code, not null 134 * @param numericCurrencyCode the numeric currency code, -1 if none 135 * @param decimalPlaces the number of decimal places that the currency 136 * normally has, from 0 to 9 (normally 0, 2 or 3), or -1 for a pseudo-currency 137 * @param countryCodes the country codes to register the currency under, not null 138 * @return the new instance, never null 139 */ 140 static synchronized CurrencyUnit registerCurrency( 141 String currencyCode, int numericCurrencyCode, int decimalPlaces, List<String> countryCodes) { 142 MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null"); 143 if (numericCurrencyCode < -1 || numericCurrencyCode > 999) { 144 throw new IllegalArgumentException("Invalid numeric code"); 145 } 146 if (decimalPlaces < -1 || decimalPlaces > 9) { 147 throw new IllegalArgumentException("Invalid number of decimal places"); 148 } 149 MoneyUtils.checkNotNull(countryCodes, "Country codes must not be null"); 150 151 CurrencyUnit currency = new CurrencyUnit(currencyCode, (short) numericCurrencyCode, (short) decimalPlaces); 152 if (currenciesByCode.containsKey(currencyCode) || currenciesByNumericCode.containsKey(numericCurrencyCode)) { 153 throw new IllegalArgumentException("Currency already registered: " + currencyCode); 154 } 155 for (String countryCode : countryCodes) { 156 if (currenciesByCountry.containsKey(countryCode)) { 157 throw new IllegalArgumentException("Currency already registered for country: " + countryCode); 158 } 159 } 160 currenciesByCode.putIfAbsent(currencyCode, currency); 161 if (numericCurrencyCode >= 0) { 162 currenciesByNumericCode.putIfAbsent(numericCurrencyCode, currency); 163 } 164 for (String countryCode : countryCodes) { 165 currenciesByCountry.put(countryCode, currency); 166 } 167 return currenciesByCode.get(currencyCode); 168 } 169 170 /** 171 * Gets the list of all registered currencies. 172 * <p> 173 * This class only permits known currencies to be returned, thus this list is 174 * the complete list of valid singleton currencies. The list may change after 175 * application startup, however this isn't recommended. 176 * 177 * @return the sorted, independent, list of all registered currencies, never null 178 */ 179 public static List<CurrencyUnit> registeredCurrencies() { 180 ArrayList<CurrencyUnit> list = new ArrayList<CurrencyUnit>(currenciesByCode.values()); 181 Collections.sort(list); 182 return list; 183 } 184 185 //----------------------------------------------------------------------- 186 /** 187 * Obtains an instance of {@code CurrencyUnit} matching the specified JDK currency. 188 * <p> 189 * This converts the JDK currency instance to a currency unit using the code. 190 * 191 * @param currency the currency, not null 192 * @return the singleton instance, never null 193 */ 194 public static CurrencyUnit of(Currency currency) { 195 MoneyUtils.checkNotNull(currency, "Currency must not be null"); 196 return of(currency.getCurrencyCode()); 197 } 198 199 /** 200 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 three letter currency code. 201 * <p> 202 * A currency is uniquely identified by ISO-4217 three letter code. 203 * 204 * @param currencyCode the currency code, not null 205 * @return the singleton instance, never null 206 * @throws IllegalCurrencyException if the currency is unknown 207 */ 208 @FromString 209 public static CurrencyUnit of(String currencyCode) { 210 MoneyUtils.checkNotNull(currencyCode, "Currency code must not be null"); 211 CurrencyUnit currency = currenciesByCode.get(currencyCode); 212 if (currency == null) { 213 throw new IllegalCurrencyException("Unknown currency '" + currencyCode + '\''); 214 } 215 return currency; 216 } 217 218 /** 219 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code. 220 * <p> 221 * The numeric code is an alternative to the three letter code. 222 * This method is lenient and does not require the string to be left padded with zeroes. 223 * 224 * @param numericCurrencyCode the currency code, not null 225 * @return the singleton instance, never null 226 * @throws IllegalCurrencyException if the currency is unknown 227 */ 228 public static CurrencyUnit ofNumericCode(String numericCurrencyCode) { 229 MoneyUtils.checkNotNull(numericCurrencyCode, "Currency code must not be null"); 230 switch (numericCurrencyCode.length()) { 231 case 1: 232 return ofNumericCode(numericCurrencyCode.charAt(0) - '0'); 233 case 2: 234 return ofNumericCode((numericCurrencyCode.charAt(0) - '0') * 10 + 235 numericCurrencyCode.charAt(1) - '0'); 236 case 3: 237 return ofNumericCode((numericCurrencyCode.charAt(0) - '0') * 100 + 238 (numericCurrencyCode.charAt(1) - '0') * 10 + 239 numericCurrencyCode.charAt(2) - '0'); 240 default: 241 throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\''); 242 } 243 } 244 245 /** 246 * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217 numeric currency code. 247 * <p> 248 * The numeric code is an alternative to the three letter code. 249 * 250 * @param numericCurrencyCode the numeric currency code, not null 251 * @return the singleton instance, never null 252 * @throws IllegalCurrencyException if the currency is unknown 253 */ 254 public static CurrencyUnit ofNumericCode(int numericCurrencyCode) { 255 CurrencyUnit currency = currenciesByNumericCode.get(numericCurrencyCode); 256 if (currency == null) { 257 throw new IllegalCurrencyException("Unknown currency '" + numericCurrencyCode + '\''); 258 } 259 return currency; 260 } 261 262 /** 263 * Obtains an instance of {@code CurrencyUnit} for the specified locale. 264 * <p> 265 * Only the country is used from the locale. 266 * 267 * @param locale the locale, not null 268 * @return the singleton instance, never null 269 * @throws IllegalCurrencyException if the currency is unknown 270 */ 271 public static CurrencyUnit of(Locale locale) { 272 MoneyUtils.checkNotNull(locale, "Locale must not be null"); 273 CurrencyUnit currency = currenciesByCountry.get(locale.getCountry()); 274 if (currency == null) { 275 throw new IllegalCurrencyException("Unknown currency for locale '" + locale + '\''); 276 } 277 return currency; 278 } 279 280 /** 281 * Obtains an instance of {@code CurrencyUnit} for the specified country code. 282 * <p> 283 * Country codes should generally be in upper case. 284 * This method is case sensitive. 285 * 286 * @param countryCode the country code, not null 287 * @return the singleton instance, never null 288 * @throws IllegalCurrencyException if the currency is unknown 289 */ 290 public static CurrencyUnit ofCountry(String countryCode) { 291 MoneyUtils.checkNotNull(countryCode, "Country code must not be null"); 292 CurrencyUnit currency = currenciesByCountry.get(countryCode); 293 if (currency == null) { 294 throw new IllegalCurrencyException("Unknown currency for country '" + countryCode + '\''); 295 } 296 return currency; 297 } 298 299 //----------------------------------------------------------------------- 300 /** 301 * Obtains an instance of {@code CurrencyUnit} for the specified currency code. 302 * <p> 303 * This method exists to match the API of {@link Currency}. 304 * 305 * @param currencyCode the currency code, not null 306 * @return the singleton instance, never null 307 * @throws IllegalCurrencyException if the currency is unknown 308 */ 309 public static CurrencyUnit getInstance(String currencyCode) { 310 return CurrencyUnit.of(currencyCode); 311 } 312 313 /** 314 * Obtains an instance of {@code CurrencyUnit} for the specified locale. 315 * <p> 316 * This method exists to match the API of {@link Currency}. 317 * 318 * @param locale the locale, not null 319 * @return the singleton instance, never null 320 * @throws IllegalCurrencyException if the currency is unknown 321 */ 322 public static CurrencyUnit getInstance(Locale locale) { 323 return CurrencyUnit.of(locale); 324 } 325 326 //----------------------------------------------------------------------- 327 /** 328 * Constructor, creating a new currency instance. 329 * 330 * @param code the currency code, not null 331 * @param numericCurrencyCode the numeric currency code, -1 if none 332 * @param decimalPlaces the decimal places, not null 333 */ 334 CurrencyUnit(String code, short numericCurrencyCode, short decimalPlaces) { 335 assert code != null : "Joda-Money bug: Currency code must not be null"; 336 this.code = code; 337 this.numericCode = numericCurrencyCode; 338 this.decimalPlaces = decimalPlaces; 339 } 340 341 /** 342 * Block malicious data streams. 343 * 344 * @param ois the input stream, not null 345 * @throws InvalidObjectException 346 */ 347 private void readObject(ObjectInputStream ois) throws InvalidObjectException { 348 throw new InvalidObjectException("Serialization delegate required"); 349 } 350 351 /** 352 * Uses a serialization delegate. 353 * 354 * @return the replacing object, never null 355 */ 356 private Object writeReplace() { 357 return new Ser(Ser.CURRENCY_UNIT, this); 358 } 359 360 //----------------------------------------------------------------------- 361 /** 362 * Gets the ISO-4217 three letter currency code. 363 * <p> 364 * Each currency is uniquely identified by a three-letter code. 365 * 366 * @return the three letter ISO-4217 currency code, never null 367 */ 368 public String getCode() { 369 return code; 370 } 371 372 /** 373 * Gets the ISO-4217 numeric currency code. 374 * <p> 375 * The numeric code is an alternative to the standard three letter code. 376 * 377 * @return the numeric currency code 378 */ 379 public int getNumericCode() { 380 return numericCode; 381 } 382 383 /** 384 * Gets the ISO-4217 numeric currency code as a three digit string. 385 * <p> 386 * This formats the numeric code as a three digit string prefixed by zeroes if necessary. 387 * If there is no valid code, then an empty string is returned. 388 * 389 * @return the three digit numeric currency code, empty is no code, never null 390 */ 391 public String getNumeric3Code() { 392 if (numericCode < 0) { 393 return ""; 394 } 395 String str = Integer.toString(numericCode); 396 if (str.length() == 1) { 397 return "00" + str; 398 } 399 if (str.length() == 2) { 400 return "0" + str; 401 } 402 return str; 403 } 404 405 /** 406 * Gets the number of decimal places typically used by this currency. 407 * <p> 408 * Different currencies have different numbers of decimal places by default. 409 * For example, 'GBP' has 2 decimal places, but 'JPY' has zero. 410 * Pseudo-currencies will return zero. 411 * <p> 412 * See also {@link #getDefaultFractionDigits()}. 413 * 414 * @return the decimal places, from 0 to 9 (normally 0, 2 or 3) 415 */ 416 public int getDecimalPlaces() { 417 return decimalPlaces < 0 ? 0 : decimalPlaces; 418 } 419 420 /** 421 * Checks if this is a pseudo-currency. 422 * 423 * @return true if this is a pseudo-currency 424 */ 425 public boolean isPseudoCurrency() { 426 return decimalPlaces < 0; 427 } 428 429 //----------------------------------------------------------------------- 430 /** 431 * Gets the ISO-4217 three-letter currency code. 432 * <p> 433 * This method matches the API of {@link Currency}. 434 * 435 * @return the currency code, never null 436 */ 437 public String getCurrencyCode() { 438 return code; 439 } 440 441 /** 442 * Gets the number of fractional digits typically used by this currency. 443 * <p> 444 * Different currencies have different numbers of fractional digits by default. 445 * For example, 'GBP' has 2 fractional digits, but 'JPY' has zero. 446 * Pseudo-currencies are indicated by -1. 447 * <p> 448 * This method matches the API of {@link Currency}. 449 * The alternative method {@link #getDecimalPlaces()} may be more useful. 450 * 451 * @return the fractional digits, from 0 to 9 (normally 0, 2 or 3), or -1 for pseudo-currencies 452 */ 453 public int getDefaultFractionDigits() { 454 return decimalPlaces; 455 } 456 457 //----------------------------------------------------------------------- 458 /** 459 * Gets the symbol for this locale from the JDK. 460 * <p> 461 * If this currency doesn't have a JDK equivalent, then the currency code 462 * is returned. 463 * <p> 464 * This method matches the API of {@link Currency}. 465 * 466 * @return the JDK currency instance, never null 467 */ 468 public String getSymbol() { 469 try { 470 return Currency.getInstance(code).getSymbol(); 471 } catch (IllegalArgumentException ex) { 472 return code; 473 } 474 } 475 476 /** 477 * Gets the symbol for this locale from the JDK. 478 * <p> 479 * If this currency doesn't have a JDK equivalent, then the currency code 480 * is returned. 481 * <p> 482 * This method matches the API of {@link Currency}. 483 * 484 * @param locale the locale to get the symbol for, not null 485 * @return the JDK currency instance, never null 486 */ 487 public String getSymbol(Locale locale) { 488 MoneyUtils.checkNotNull(locale, "Locale must not be null"); 489 try { 490 return Currency.getInstance(code).getSymbol(locale); 491 } catch (IllegalArgumentException ex) { 492 return code; 493 } 494 } 495 496 //----------------------------------------------------------------------- 497 /** 498 * Gets the JDK currency instance equivalent to this currency. 499 * <p> 500 * This attempts to convert a {@code CurrencyUnit} to a JDK {@code Currency}. 501 * 502 * @return the JDK currency instance, never null 503 * @throws IllegalArgumentException if no matching currency exists in the JDK 504 */ 505 public Currency toCurrency() { 506 return Currency.getInstance(code); 507 } 508 509 //----------------------------------------------------------------------- 510 /** 511 * Compares this currency to another by alphabetical comparison of the code. 512 * 513 * @param other the other currency, not null 514 * @return negative if earlier alphabetically, 0 if equal, positive if greater alphabetically 515 */ 516 public int compareTo(CurrencyUnit other) { 517 return code.compareTo(other.code); 518 } 519 520 /** 521 * Checks if this currency equals another currency. 522 * <p> 523 * The comparison checks the 3 letter currency code. 524 * 525 * @param obj the other currency, null returns false 526 * @return true if equal 527 */ 528 @Override 529 public boolean equals(Object obj) { 530 if (obj == this) { 531 return true; 532 } 533 if (obj instanceof CurrencyUnit) { 534 return code.equals(((CurrencyUnit) obj).code); 535 } 536 return false; 537 } 538 539 /** 540 * Returns a suitable hash code for the currency. 541 * 542 * @return the hash code 543 */ 544 @Override 545 public int hashCode() { 546 return code.hashCode(); 547 } 548 549 //----------------------------------------------------------------------- 550 /** 551 * Gets the currency code as a string. 552 * 553 * @return the currency code, never null 554 */ 555 @Override 556 @ToString 557 public String toString() { 558 return code; 559 } 560 561 }