Java Money и API за валута

1. Общ преглед

JSR 354 - „Валута и пари“ разглежда стандартизацията на валутите и паричните суми в Java.

Целта му е да добави гъвкав и разширяем API към екосистемата на Java и да направи работата с парични суми по-проста и по-безопасна.

JSR не проникна в JDK 9, но е кандидат за бъдещи версии на JDK.

2. Настройка

Първо, нека дефинираме зависимостта в нашия файл pom.xml :

 org.javamoney moneta 1.1  

Най-новата версия на зависимостта може да бъде проверена тук.

3. Характеристики на JSR-354

Целите на API „Валута и пари“:

  • Да предостави API за обработка и изчисляване на парични суми
  • Да се ​​дефинират класове, представляващи валути и парични суми, както и парично закръгляване
  • За справяне с валутните курсове
  • Да се ​​занимава с форматиране и синтактичен анализ на валути и парични суми

4. Модел

Основните класове на спецификацията JSR-354 са изобразени на следната диаграма:

Моделът съдържа два основни интерфейса CurrencyUnit и MonetaryAmount, обяснени в следващите раздели.

5. CurrencyUnit

CurrencyUnit моделира минималните свойства на дадена валута. Неговите екземпляри могат да бъдат получени чрез метода Monetary.getCurrency :

@Test public void givenCurrencyCode_whenString_thanExist() { CurrencyUnit usd = Monetary.getCurrency("USD"); assertNotNull(usd); assertEquals(usd.getCurrencyCode(), "USD"); assertEquals(usd.getNumericCode(), 840); assertEquals(usd.getDefaultFractionDigits(), 2); }

Създаваме CurrencyUnit, използвайки String представяне на валутата, това може да доведе до ситуация, в която се опитваме да създадем валута с несъществуващ код. Създаването на валути с несъществуващи кодове води до изключение UnknownCurrency :

@Test(expected = UnknownCurrencyException.class) public void givenCurrencyCode_whenNoExist_thanThrowsError() { Monetary.getCurrency("AAA"); } 

6. MonetaryAmount

MonetaryAmount е числово представяне на парична сума. Винаги се свързва с CurrencyUnit и определя парично представяне на валута.

Сумата може да бъде приложена по различни начини, като се фокусира върху поведението на изискванията за парично представителство, определени от всеки конкретен случай на употреба. Например. Пари и FastMoney са изпълнения на интерфейса MonetaryAmount .

FastMoney реализира MonetaryAmount, използвайки дълго като числово представяне и е по-бърз от BigDecimal с цената на прецизността; може да се използва, когато се нуждаем от производителност и точността не е проблем.

Общ екземпляр може да бъде създаден с помощта на фабрика по подразбиране. Нека покажем различния начин за получаване на екземпляри на MonetaryAmount :

@Test public void givenAmounts_whenStringified_thanEquals() { CurrencyUnit usd = Monetary.getCurrency("USD"); MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory() .setCurrency(usd).setNumber(200).create(); Money moneyof = Money.of(12, usd); FastMoney fastmoneyof = FastMoney.of(2, usd); assertEquals("USD", usd.toString()); assertEquals("USD 200", fstAmtUSD.toString()); assertEquals("USD 12", moneyof.toString()); assertEquals("USD 2.00000", fastmoneyof.toString()); }

7 . Парична аритметика

Можем да извършваме парична аритметика между Money и FastMoney, но трябва да бъдем внимателни, когато комбинираме екземпляри от тези два класа.

Например, когато сравним един евро екземпляр на FastMoney с един евро екземпляр на пари, резултатът е, че те не са еднакви:

@Test public void givenCurrencies_whenCompared_thanNotequal() { MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); Money oneEuro = Money.of(1, "EUR"); assertFalse(oneEuro.equals(FastMoney.of(1, "EUR"))); assertTrue(oneDolar.equals(Money.of(1, "USD"))); }

Можем да извършваме събиране, изваждане, умножаване, разделяне и други парични аритметични операции, като използваме методите, предоставени от класа MonetaryAmount .

Arithmetic operations should throw an ArithmeticException, if the arithmetic operations between amounts outperform the capabilities of the numeric representation type used, for example, if we try to divide one by three, we get an ArithmeticException because the result is an infinite number:

@Test(expected = ArithmeticException.class) public void givenAmount_whenDivided_thanThrowsException() { MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); oneDolar.divide(3); }

When adding or subtracting amounts, it's better to use parameters which are instances of MonetaryAmount, as we need to ensure that both amounts have the same currency to perform operations between amounts.

7.1. Calculating Amounts

A total of amounts can be calculated in multiple ways, one way is simply to chain the amounts with:

@Test public void givenAmounts_whenSummed_thanCorrect() { MonetaryAmount[] monetaryAmounts = new MonetaryAmount[] { Money.of(100, "CHF"), Money.of(10.20, "CHF"), Money.of(1.15, "CHF")}; Money sumAmtCHF = Money.of(0, "CHF"); for (MonetaryAmount monetaryAmount : monetaryAmounts) { sumAmtCHF = sumAmtCHF.add(monetaryAmount); } assertEquals("CHF 111.35", sumAmtCHF.toString()); }

Chaining can also be applied to subtracting:

Money calcAmtUSD = Money.of(1, "USD").subtract(fstAmtUSD); 

Multiplying:

MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);

Or dividing:

MonetaryAmount divideAmount = oneDolar.divide(0.25);

Let's compare our arithmetic results using Strings, given that with Strings because the result also contains the currency:

@Test public void givenArithmetic_whenStringified_thanEqualsAmount() { CurrencyUnit usd = Monetary.getCurrency("USD"); Money moneyof = Money.of(12, usd); MonetaryAmount fstAmtUSD = Monetary.getDefaultAmountFactory() .setCurrency(usd).setNumber(200.50).create(); MonetaryAmount oneDolar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); Money subtractedAmount = Money.of(1, "USD").subtract(fstAmtUSD); MonetaryAmount multiplyAmount = oneDolar.multiply(0.25); MonetaryAmount divideAmount = oneDolar.divide(0.25); assertEquals("USD", usd.toString()); assertEquals("USD 1", oneDolar.toString()); assertEquals("USD 200.5", fstAmtUSD.toString()); assertEquals("USD 12", moneyof.toString()); assertEquals("USD -199.5", subtractedAmount.toString()); assertEquals("USD 0.25", multiplyAmount.toString()); assertEquals("USD 4", divideAmount.toString()); }

8. Monetary Rounding

Monetary rounding is nothing else than a conversion from an amount with an undetermined precision to a rounded amount.

We'll use the getDefaultRounding API provided by the Monetary class to make the conversion. The default rounding values are provided by the currency:

@Test public void givenAmount_whenRounded_thanEquals() { MonetaryAmount fstAmtEUR = Monetary.getDefaultAmountFactory() .setCurrency("EUR").setNumber(1.30473908).create(); MonetaryAmount roundEUR = fstAmtEUR.with(Monetary.getDefaultRounding()); assertEquals("EUR 1.30473908", fstAmtEUR.toString()); assertEquals("EUR 1.3", roundEUR.toString()); }

9. Currency Conversion

Currency conversion is an important aspect of dealing with money. Unfortunately, these conversions have a great variety of different implementations and use cases.

The API focuses on the common aspects of currency conversion based on the source, target currency, and exchange rate.

Currency conversion or the access of exchange rates can be parametrized:

@Test public void givenAmount_whenConversion_thenNotNull() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory().setCurrency("USD") .setNumber(1).create(); CurrencyConversion conversionEUR = MonetaryConversions.getConversion("EUR"); MonetaryAmount convertedAmountUSDtoEUR = oneDollar.with(conversionEUR); assertEquals("USD 1", oneDollar.toString()); assertNotNull(convertedAmountUSDtoEUR); }

A conversion is always bound to currency. MonetaryAmount can simply be converted by passing a CurrencyConversion to the amount’s with method.

10. Currency Formatting

The formatting allows the access of formats based on java.util.Locale. Contrary to the JDK, the formatters defined by this API are thread-safe:

@Test public void givenLocale_whenFormatted_thanEquals() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); MonetaryAmountFormat formatUSD = MonetaryFormats.getAmountFormat(Locale.US); String usFormatted = formatUSD.format(oneDollar); assertEquals("USD 1", oneDollar.toString()); assertNotNull(formatUSD); assertEquals("USD1.00", usFormatted); }

Here we're using the predefined format and creating a custom format for our currencies. The use of the standard format is straightforward using the method format of the MonetaryFormats class. We defined our custom format setting the pattern property of the format query builder.

As before because the currency is included in the result we test our results using Strings:

@Test public void givenAmount_whenCustomFormat_thanEquals() { MonetaryAmount oneDollar = Monetary.getDefaultAmountFactory() .setCurrency("USD").setNumber(1).create(); MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(AmountFormatQueryBuilder. of(Locale.US).set(CurrencyStyle.NAME).set("pattern", "00000.00 ¤").build()); String customFormatted = customFormat.format(oneDollar); assertNotNull(customFormat); assertEquals("USD 1", oneDollar.toString()); assertEquals("00001.00 US Dollar", customFormatted); }

11. Обобщение

В тази бърза статия разгледахме основите на Java Money & Currency JSR.

Паричните стойности се използват навсякъде, а Java предоставя започва да поддържа и обработва парични стойности, аритметика или конвертиране на валута.

Както винаги, можете да намерите кода от статията в Github.