MonetaryAmount Formatting in Java

Introduction

The MonetaryAmount interface from Java Money API (JSR 354) provides a standardized way to handle monetary values. Proper formatting of monetary amounts is crucial for internationalization, localization, and displaying currency values correctly across different regions and currencies.

Java Money API Setup

Dependencies

<dependencies>
<!-- Java Money API -->
<dependency>
<groupId>org.javamoney</groupId>
<artifactId>moneta</artifactId>
<version>1.4.4</version>
</dependency>
<!-- JSR 354 API -->
<dependency>
<groupId>javax.money</groupId>
<artifactId>money-api</artifactId>
<version>1.1</version>
</dependency>
</dependencies>

Basic MonetaryAmount Creation

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.CurrencyUnit;
import java.math.BigDecimal;
public class MonetaryAmountCreation {
public void createMonetaryAmounts() {
// Create currency units
CurrencyUnit usd = Monetary.getCurrency("USD");
CurrencyUnit eur = Monetary.getCurrency("EUR");
CurrencyUnit jpy = Monetary.getCurrency("JPY");
// Create monetary amounts
MonetaryAmount amount1 = Monetary.getDefaultAmountFactory()
.setCurrency(usd)
.setNumber(123.45)
.create();
MonetaryAmount amount2 = Monetary.getDefaultAmountFactory()
.setCurrency(eur)
.setNumber(new BigDecimal("89.99"))
.create();
// Using convenience methods
MonetaryAmount amount3 = Money.of(123.45, "USD");
MonetaryAmount amount4 = Money.of(1000, "JPY"); // No decimal places
MonetaryAmount amount5 = Money.of(new BigDecimal("456.78"), "EUR");
// Zero amounts
MonetaryAmount zeroUsd = Money.zero(usd);
MonetaryAmount zeroEur = Money.zero("EUR");
}
}

Basic Formatting

Using MonetaryAmountFormat

import org.javamoney.moneta.format.CurrencyStyle;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;
import java.util.Locale;
public class BasicMonetaryFormatting {
public void demonstrateBasicFormatting() {
MonetaryAmount amount = Money.of(1234.56, "USD");
MonetaryAmount jpyAmount = Money.of(1500, "JPY");
MonetaryAmount eurAmount = Money.of(89.99, "EUR");
// Default formatting
MonetaryAmountFormat defaultFormat = MonetaryFormats.getAmountFormat();
System.out.println("Default: " + defaultFormat.format(amount));
// Locale-specific formatting
MonetaryAmountFormat usFormat = MonetaryFormats.getAmountFormat(Locale.US);
MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMANY);
MonetaryAmountFormat japaneseFormat = MonetaryFormats.getAmountFormat(Locale.JAPAN);
System.out.println("US: " + usFormat.format(amount));
System.out.println("Germany: " + germanFormat.format(eurAmount));
System.out.println("Japan: " + japaneseFormat.format(jpyAmount));
// Custom pattern formatting
MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(
MonetaryFormats.createAmountFormatPattern("¤ #,##0.00", Locale.US));
System.out.println("Custom: " + customFormat.format(amount));
}
public void formatDifferentCurrencies() {
// Various currency examples
MonetaryAmount[] amounts = {
Money.of(1234.56, "USD"),
Money.of(89.99, "EUR"),
Money.of(1500, "JPY"),
Money.of(1234.56, "GBP"),
Money.of(1234.56, "CAD"),
Money.of(1234.56, "AUD")
};
Locale[] locales = {
Locale.US,
Locale.GERMANY,
Locale.JAPAN,
Locale.UK,
Locale.CANADA,
Locale.CHINA
};
for (MonetaryAmount amount : amounts) {
System.out.println("\nFormatting: " + amount);
for (Locale locale : locales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
System.out.println(locale + ": " + format.format(amount));
} catch (Exception e) {
System.out.println(locale + ": Formatting failed");
}
}
}
}
}

Advanced Formatting with MonetaryFormats

Custom Format Configuration

import org.javamoney.moneta.format.AmountFormatParams;
import javax.money.format.AmountFormatQuery;
import javax.money.format.AmountFormatQueryBuilder;
public class AdvancedMonetaryFormatting {
public void demonstrateAdvancedFormatting() {
MonetaryAmount amount = Money.of(1234567.89, "USD");
// Custom format with specific parameters
AmountFormatQuery query = AmountFormatQueryBuilder.of(Locale.US)
.set(CurrencyStyle.SYMBOL)
.set(AmountFormatParams.GROUPING_SIZES, new int[]{3, 2})
.set(AmountFormatParams.GROUPING_SEPARATORS, new char[]{',', '_'})
.build();
MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(query);
System.out.println("Custom grouped: " + customFormat.format(amount));
// Scientific notation formatting
AmountFormatQuery scientificQuery = AmountFormatQueryBuilder.of(Locale.US)
.set(CurrencyStyle.CODE)
.set("pattern", "0.###E0 ¤")
.build();
MonetaryAmountFormat scientificFormat = MonetaryFormats.getAmountFormat(scientificQuery);
System.out.println("Scientific: " + scientificFormat.format(amount));
}
public void createCustomFormats() {
MonetaryAmount amount = Money.of(1234.56, "USD");
// Pattern-based formatting
String[] patterns = {
"¤ #,##0.00",           // Standard format
"¤ #,##0.00;(¤ #,##0.00)", // With negative pattern
"#,##0.00 ¤¤",          // ISO code
"¤#,##0.00",            // No space
"0.00 ¤",               // No grouping
"'Price: '¤ #,##0.00",  // With text prefix
"¤ #,##0.00 'per item'" // With text suffix
};
for (String pattern : patterns) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(
MonetaryFormats.createAmountFormatPattern(pattern, Locale.US));
System.out.println(pattern + ": " + format.format(amount));
} catch (Exception e) {
System.out.println("Invalid pattern: " + pattern);
}
}
}
}

Locale-Specific Formatting

Internationalization Examples

import java.util.Locale;
public class LocaleSpecificFormatting {
public void demonstrateLocaleFormatting() {
MonetaryAmount amount = Money.of(1234567.89, "USD");
MonetaryAmount negativeAmount = Money.of(-1234.56, "EUR");
// Major locales
Locale[] locales = {
Locale.US,              // United States
Locale.UK,              // United Kingdom
Locale.CANADA,          // Canada
Locale.CANADA_FRENCH,   // Canada (French)
Locale.GERMANY,         // Germany
Locale.FRANCE,          // France
Locale.ITALY,           // Italy
Locale.JAPAN,           // Japan
Locale.CHINA,           // China
Locale.KOREA,           // Korea
new Locale("es", "ES"), // Spain
new Locale("pt", "BR"), // Brazil
new Locale("ru", "RU"), // Russia
new Locale("ar", "SA")  // Saudi Arabia
};
System.out.println("=== POSITIVE AMOUNTS ===");
for (Locale locale : locales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
System.out.printf("%-15s: %s%n", 
locale.getDisplayName(), format.format(amount));
} catch (Exception e) {
System.out.printf("%-15s: Formatting failed%n", locale.getDisplayName());
}
}
System.out.println("\n=== NEGATIVE AMOUNTS ===");
for (Locale locale : locales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
System.out.printf("%-15s: %s%n", 
locale.getDisplayName(), format.format(negativeAmount));
} catch (Exception e) {
System.out.printf("%-15s: Formatting failed%n", locale.getDisplayName());
}
}
}
public void currencySpecificFormatting() {
// Different currencies with same numeric value
double value = 1234.56;
String[] currencies = {"USD", "EUR", "JPY", "GBP", "CAD", "AUD", "CHF", "CNY"};
for (String currencyCode : currencies) {
MonetaryAmount amount = Money.of(value, currencyCode);
System.out.println("\nCurrency: " + currencyCode);
// Format in different locales
Locale[] locales = {Locale.US, Locale.GERMANY, Locale.JAPAN};
for (Locale locale : locales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
System.out.println(locale + ": " + format.format(amount));
} catch (Exception e) {
System.out.println(locale + ": Formatting failed");
}
}
}
}
public void demonstrateRtlLocales() {
// Right-to-left locales
Locale[] rtlLocales = {
new Locale("ar", "SA"), // Arabic - Saudi Arabia
new Locale("he", "IL"), // Hebrew - Israel
new Locale("fa", "IR")  // Persian - Iran
};
MonetaryAmount amount = Money.of(1234.56, "USD");
MonetaryAmount localCurrencyAmount = Money.of(1234.56, "SAR"); // Saudi Riyal
for (Locale locale : rtlLocales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
System.out.printf("%-20s (USD): %s%n", 
locale.getDisplayName(), format.format(amount));
System.out.printf("%-20s (Local): %s%n", 
locale.getDisplayName(), format.format(localCurrencyAmount));
} catch (Exception e) {
System.out.printf("%-20s: Formatting failed%n", locale.getDisplayName());
}
}
}
}

Custom MonetaryAmountFormat Implementation

Building Custom Formatters

import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryParseException;
import java.io.IOException;
import java.text.ParseException;
import java.util.Locale;
public class CustomMonetaryFormatters {
// Custom format for accounting style
public static class AccountingFormat implements MonetaryAmountFormat {
private final Locale locale;
public AccountingFormat(Locale locale) {
this.locale = locale;
}
@Override
public String format(MonetaryAmount amount) {
boolean isNegative = amount.isNegative();
String formatted = MonetaryFormats.getAmountFormat(locale)
.format(amount.abs());
if (isNegative) {
return "(" + formatted + ")";
} else {
return formatted;
}
}
@Override
public MonetaryAmount parse(CharSequence text) throws MonetaryParseException {
// Implementation for parsing accounting format
String cleanText = text.toString().trim();
boolean isNegative = cleanText.startsWith("(") && cleanText.endsWith(")");
if (isNegative) {
cleanText = cleanText.substring(1, cleanText.length() - 1).trim();
}
try {
MonetaryAmount amount = MonetaryFormats.getAmountFormat(locale)
.parse(cleanText);
return isNegative ? amount.negate() : amount;
} catch (MonetaryParseException e) {
throw new MonetaryParseException("Failed to parse accounting format", 
text.toString(), 0);
}
}
@Override
public Locale getLocale() {
return locale;
}
@Override
public void print(Appendable appendable, MonetaryAmount amount) throws IOException {
appendable.append(format(amount));
}
@Override
public MonetaryAmount parse(CharSequence text, java.text.ParsePosition position) {
// Implementation for parsing with position
return parse(text.subSequence(position.getIndex(), text.length()));
}
@Override
public String toString() {
return "AccountingFormat[" + locale + "]";
}
}
// Compact format for large amounts
public static class CompactFormat implements MonetaryAmountFormat {
private final Locale locale;
public CompactFormat(Locale locale) {
this.locale = locale;
}
@Override
public String format(MonetaryAmount amount) {
double value = amount.getNumber().doubleValue();
String currency = amount.getCurrency().getCurrencyCode();
if (value >= 1_000_000_000) {
return String.format(locale, "%.1fB %s", value / 1_000_000_000, currency);
} else if (value >= 1_000_000) {
return String.format(locale, "%.1fM %s", value / 1_000_000, currency);
} else if (value >= 1_000) {
return String.format(locale, "%.1fK %s", value / 1_000, currency);
} else {
return MonetaryFormats.getAmountFormat(locale).format(amount);
}
}
// Other required methods would be implemented similarly...
@Override
public MonetaryAmount parse(CharSequence text) throws MonetaryParseException {
throw new UnsupportedOperationException("Parsing not supported for compact format");
}
@Override
public Locale getLocale() {
return locale;
}
@Override
public void print(Appendable appendable, MonetaryAmount amount) throws IOException {
appendable.append(format(amount));
}
@Override
public MonetaryAmount parse(CharSequence text, java.text.ParsePosition position) {
throw new UnsupportedOperationException("Parsing not supported for compact format");
}
}
public void useCustomFormatters() {
MonetaryAmount amount = Money.of(1234567.89, "USD");
MonetaryAmount largeAmount = Money.of(1234567890.12, "USD");
// Use accounting format
AccountingFormat accountingFormat = new AccountingFormat(Locale.US);
System.out.println("Accounting: " + accountingFormat.format(amount));
System.out.println("Negative Accounting: " + 
accountingFormat.format(amount.negate()));
// Use compact format
CompactFormat compactFormat = new CompactFormat(Locale.US);
System.out.println("Compact: " + compactFormat.format(largeAmount));
System.out.println("Small Compact: " + compactFormat.format(amount));
}
}

Parsing MonetaryAmount from Strings

String to MonetaryAmount Conversion

import javax.money.format.MonetaryParseException;
public class MonetaryAmountParsing {
public void demonstrateParsing() {
// Parse from formatted strings
String[] monetaryStrings = {
"$1,234.56",
"1.234,56 €",
"¥1,500",
"GBP 1234.56",
"1 234,56 EUR",
"USD 1,234.56"
};
Locale[] locales = {
Locale.US,
Locale.GERMANY,
Locale.JAPAN,
Locale.UK,
Locale.FRANCE,
Locale.US
};
for (int i = 0; i < monetaryStrings.length; i++) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locales[i]);
MonetaryAmount amount = format.parse(monetaryStrings[i]);
System.out.printf("Parsed: %-15s -> %s%n", 
monetaryStrings[i], amount);
} catch (MonetaryParseException e) {
System.out.printf("Failed to parse: %s%n", monetaryStrings[i]);
}
}
}
public void parseWithErrorHandling() {
String[] testInputs = {
"$1,234.56",
"invalid amount",
"1234.56",  // No currency
"USD",      // No amount
"$1,234.56 USD",  // Duplicate currency
"1.234.56,78"     // Invalid number format
};
for (String input : testInputs) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.US);
MonetaryAmount amount = format.parse(input);
System.out.printf("Success: %-20s -> %s%n", input, amount);
} catch (MonetaryParseException e) {
System.out.printf("Error: %-20s -> %s%n", input, e.getMessage());
}
}
}
public void flexibleParsing() {
// Try multiple formats for parsing
String input = "1,234.56 USD";
Locale[] possibleLocales = {
Locale.US,
Locale.UK,
Locale.CANADA,
Locale.GERMANY,
Locale.JAPAN
};
for (Locale locale : possibleLocales) {
try {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(locale);
MonetaryAmount amount = format.parse(input);
System.out.printf("Parsed with %s: %s%n", locale, amount);
break; // Stop after first successful parse
} catch (MonetaryParseException e) {
// Continue to next locale
}
}
}
}

Real-World Use Cases

E-commerce Application

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class ECommerceMonetaryFormatting {
public static class ShoppingCart {
private List<MonetaryAmount> items = new ArrayList<>();
private Locale userLocale;
private String userCurrency;
public ShoppingCart(Locale userLocale, String userCurrency) {
this.userLocale = userLocale;
this.userCurrency = userCurrency;
}
public void addItem(MonetaryAmount price) {
// Convert to user's currency if different
MonetaryAmount convertedPrice = convertCurrency(price, userCurrency);
items.add(convertedPrice);
}
public MonetaryAmount getSubtotal() {
MonetaryAmount subtotal = Money.zero(userCurrency);
for (MonetaryAmount item : items) {
subtotal = subtotal.add(item);
}
return subtotal;
}
public MonetaryAmount getTotal() {
MonetaryAmount subtotal = getSubtotal();
MonetaryAmount tax = subtotal.multiply(0.08); // 8% tax
return subtotal.add(tax);
}
public void displayReceipt() {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(userLocale);
System.out.println("=== RECEIPT ===");
System.out.println("User Locale: " + userLocale);
System.out.println("Currency: " + userCurrency);
System.out.println();
for (int i = 0; i < items.size(); i++) {
System.out.printf("Item %d: %s%n", i + 1, format.format(items.get(i)));
}
System.out.println("---------------");
System.out.println("Subtotal: " + format.format(getSubtotal()));
System.out.println("Tax (8%): " + format.format(getSubtotal().multiply(0.08)));
System.out.println("Total: " + format.format(getTotal()));
}
private MonetaryAmount convertCurrency(MonetaryAmount amount, String targetCurrency) {
// In real application, this would use a currency conversion service
// For demo purposes, we'll assume 1:1 conversion for same currency
if (amount.getCurrency().getCurrencyCode().equals(targetCurrency)) {
return amount;
}
// Simple conversion (in real app, use proper exchange rates)
return Money.of(amount.getNumber().doubleValue(), targetCurrency);
}
}
public static void demoECommerce() {
// Simulate different users from different regions
Object[][] users = {
{Locale.US, "USD"},
{Locale.GERMANY, "EUR"},
{Locale.JAPAN, "JPY"},
{Locale.UK, "GBP"}
};
// Sample products with different currencies
MonetaryAmount[] products = {
Money.of(29.99, "USD"),
Money.of(19.50, "EUR"),
Money.of(1500, "JPY"),
Money.of(25.00, "GBP")
};
for (Object[] user : users) {
Locale locale = (Locale) user[0];
String currency = (String) user[1];
ShoppingCart cart = new ShoppingCart(locale, currency);
// Add some products to cart
for (MonetaryAmount product : products) {
cart.addItem(product);
}
cart.displayReceipt();
System.out.println("\n" + "=".repeat(50) + "\n");
}
}
}

Financial Reporting

import java.util.Arrays;
import java.util.List;
import java.util.Locale;
public class FinancialReporting {
public static class FinancialReport {
private List<MonetaryAmount> revenues;
private List<MonetaryAmount> expenses;
private String currency;
private Locale reportLocale;
public FinancialReport(List<MonetaryAmount> revenues, 
List<MonetaryAmount> expenses, 
String currency, 
Locale reportLocale) {
this.revenues = revenues;
this.expenses = expenses;
this.currency = currency;
this.reportLocale = reportLocale;
}
public MonetaryAmount getTotalRevenue() {
return sumAmounts(revenues);
}
public MonetaryAmount getTotalExpenses() {
return sumAmounts(expenses);
}
public MonetaryAmount getNetIncome() {
return getTotalRevenue().subtract(getTotalExpenses());
}
public void printReport() {
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(reportLocale);
MonetaryAmountFormat accountingFormat = new CustomMonetaryFormatters.AccountingFormat(reportLocale);
System.out.println("FINANCIAL REPORT");
System.out.println("Locale: " + reportLocale.getDisplayName());
System.out.println("Currency: " + currency);
System.out.println();
System.out.println("REVENUES:");
for (MonetaryAmount revenue : revenues) {
System.out.println("  " + format.format(revenue));
}
System.out.println("Total Revenue: " + accountingFormat.format(getTotalRevenue()));
System.out.println();
System.out.println("EXPENSES:");
for (MonetaryAmount expense : expenses) {
System.out.println("  " + format.format(expense));
}
System.out.println("Total Expenses: " + accountingFormat.format(getTotalExpenses()));
System.out.println();
System.out.println("NET INCOME: " + accountingFormat.format(getNetIncome()));
// Profitability indicator
if (getNetIncome().isPositive()) {
System.out.println("Status: PROFITABLE");
} else if (getNetIncome().isNegative()) {
System.out.println("Status: LOSS");
} else {
System.out.println("Status: BREAK-EVEN");
}
}
private MonetaryAmount sumAmounts(List<MonetaryAmount> amounts) {
MonetaryAmount sum = Money.zero(currency);
for (MonetaryAmount amount : amounts) {
sum = sum.add(amount);
}
return sum;
}
}
public static void demoFinancialReporting() {
// Sample financial data in different currencies
List<MonetaryAmount> usdRevenues = Arrays.asList(
Money.of(50000, "USD"),
Money.of(75000, "USD"),
Money.of(120000, "USD")
);
List<MonetaryAmount> usdExpenses = Arrays.asList(
Money.of(30000, "USD"),
Money.of(45000, "USD"),
Money.of(25000, "USD")
);
// Create reports for different locales
Locale[] reportLocales = {Locale.US, Locale.GERMANY, Locale.JAPAN, Locale.UK};
for (Locale locale : reportLocales) {
FinancialReport report = new FinancialReport(
usdRevenues, usdExpenses, "USD", locale);
report.printReport();
System.out.println("\n" + "-".repeat(50) + "\n");
}
}
}

Best Practices and Common Patterns

Utility Class for Monetary Formatting

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;
import java.util.Locale;
import java.util.HashMap;
import java.util.Map;
public final class MonetaryUtils {
private MonetaryUtils() {
// Utility class - prevent instantiation
}
// Cache for formats to avoid recreation
private static final Map<String, MonetaryAmountFormat> formatCache = new HashMap<>();
public static MonetaryAmountFormat getCachedFormat(Locale locale) {
String key = locale.toLanguageTag();
return formatCache.computeIfAbsent(key, 
k -> MonetaryFormats.getAmountFormat(locale));
}
public static String formatForDisplay(MonetaryAmount amount, Locale displayLocale) {
MonetaryAmountFormat format = getCachedFormat(displayLocale);
return format.format(amount);
}
public static String formatForStorage(MonetaryAmount amount) {
// Standardized format for database storage
return String.format("%s %.2f", 
amount.getCurrency().getCurrencyCode(),
amount.getNumber().doubleValue());
}
public static MonetaryAmount parseFromStorage(String storedValue) {
try {
String[] parts = storedValue.split(" ");
String currencyCode = parts[0];
double value = Double.parseDouble(parts[1]);
return Money.of(value, currencyCode);
} catch (Exception e) {
throw new IllegalArgumentException("Invalid stored monetary value: " + storedValue, e);
}
}
public static boolean isValidMonetaryString(String input, Locale locale) {
try {
MonetaryAmountFormat format = getCachedFormat(locale);
format.parse(input);
return true;
} catch (Exception e) {
return false;
}
}
public static MonetaryAmount safeParse(String input, Locale locale, 
MonetaryAmount defaultValue) {
try {
MonetaryAmountFormat format = getCachedFormat(locale);
return format.parse(input);
} catch (Exception e) {
return defaultValue;
}
}
// Common formatting patterns
public enum FormatStyle {
STANDARD,
ACCOUNTING,
COMPACT,
NO_DECIMALS
}
public static MonetaryAmountFormat getStyleFormat(FormatStyle style, Locale locale) {
switch (style) {
case ACCOUNTING:
return new CustomMonetaryFormatters.AccountingFormat(locale);
case COMPACT:
return new CustomMonetaryFormatters.CompactFormat(locale);
case NO_DECIMALS:
return MonetaryFormats.getAmountFormat(
MonetaryFormats.createAmountFormatPattern("#,##0 ¤", locale));
case STANDARD:
default:
return getCachedFormat(locale);
}
}
}

Conclusion

MonetaryAmount formatting in Java provides a robust, internationalization-ready solution for handling monetary values. Key takeaways include:

  • Locale-aware formatting automatically handles currency symbols, decimal separators, and grouping
  • Flexible parsing converts formatted strings back to MonetaryAmount objects
  • Custom formatters can be created for specific business requirements
  • Thread-safe operations make it suitable for multi-threaded environments
  • Comprehensive error handling for invalid inputs and parsing failures

By leveraging the Java Money API and following these patterns, developers can create financial applications that correctly handle monetary values across different currencies and locales while maintaining precision and compliance with financial formatting standards.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper