«1. Обзор

JSR 354 — «Валюта и деньги» касается стандартизации валют и денежных сумм в Java.

Его цель — добавить гибкий и расширяемый API в экосистему Java и сделать работу с денежными суммами проще и безопаснее.

JSR не попал в JDK 9, но является кандидатом для будущих выпусков JDK.

2. Настройка

Во-первых, давайте определим зависимость в нашем файле pom.xml:

<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.1</version>
</dependency>

Последняя версия зависимости может быть проверена здесь.

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, используя строковое представление валюты, это может привести к ситуации, когда мы попытаемся создать валюту с несуществующим кодом. Создание валют с несуществующими кодами вызывает исключение UnknownCurrency:

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

6. MonetaryAmount

MonetaryAmount — числовое представление денежной суммы. Он всегда связан с CurrencyUnit и определяет денежное представление валюты.

Сумма может быть реализована по-разному, ориентируясь на требования поведения денежного представления, определяемые каждым конкретным вариантом использования. Например. Деньги и FastMoney являются реализациями интерфейса MonetaryAmount.

FastMoney реализует MonetaryAmount, используя long в качестве числового представления, и работает быстрее, чем 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 в евро с одним экземпляром Money в евро, результат состоит в том, что они не совпадают:

@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.

Арифметические операции должны вызывать ArithmeticException, если арифметические операции между суммами превосходят возможности используемого числового типа представления, например, если мы попытаемся разделить один на три, мы получим ArithmeticException, потому что результатом является бесконечное число:

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

При добавлении или вычитании сумм лучше использовать параметры, которые являются экземплярами MonetaryAmount, так как нам нужно убедиться, что обе суммы имеют одинаковую валюту для выполнения операций между суммами.

7.1. Вычисление сумм

Сумма сумм может быть рассчитана несколькими способами, один из способов состоит в том, чтобы просто объединить суммы в цепочку с помощью:

@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());
}

Цепочка также может быть применена к вычитанию:

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

Умножение: ~~ ~

MonetaryAmount multiplyAmount = oneDolar.multiply(0.25);

Или деление:

MonetaryAmount divideAmount = oneDolar.divide(0.25);

Давайте сравним наши арифметические результаты с использованием строк, учитывая, что со строками, потому что результат также содержит валюту:

@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. Денежное округление

Денежное округление ничего не значит кроме преобразования суммы с неопределенной точностью в округленную сумму.

Мы будем использовать API getDefaultRounding, предоставляемый классом Monetary, для преобразования. Значения округления по умолчанию предоставляются валютой:

@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. Конвертация валюты

«Конвертация валюты является важным аспектом работы с деньгами. К сожалению, эти преобразования имеют большое разнообразие различных реализаций и вариантов использования.

API фокусируется на общих аспектах конвертации валюты на основе исходной, целевой валюты и обменного курса.

Конвертация валюты или доступ к курсам обмена могут быть параметризованы:

@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);
}

Конвертация всегда привязана к валюте. MonetaryAmount можно просто преобразовать, передав метод CurrencyConversion сумме with.

10. Форматирование валюты

Форматирование позволяет получить доступ к форматам, основанным на java.util.Locale. В отличие от JDK, средства форматирования, определенные этим API, являются потокобезопасными:

@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);
}

Здесь мы используем предопределенный формат и создаем собственный формат для наших валют. Использовать стандартный формат просто, используя формат метода класса MonetaryFormats. Мы определили наш пользовательский формат, установив свойство шаблона построителя запросов формата.

Как и раньше, поскольку валюта включена в результат, мы проверяем наши результаты, используя строки:

@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 \u0026 Currency JSR. .

Денежные значения используются повсеместно, и Java начинает поддерживать и обрабатывать денежные значения, арифметические операции или преобразование валюты.

Как всегда, вы можете найти код из статьи на Github.