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.