Moneta JSR 354 Reference Implementation

repository·master·Indexed 18 days ago

https://github.com/javamoney/jsr354-ri

The official reference implementation for the JSR 354 Money and Currency API. Moneta provides robust handling of monetary amounts via implementations like Money, FastMoney, and RoundedMoney, as well as tools for currency conversion, custom currency registration (including Bitcoin), and monetary amount formatting.

Tokens
6.3K
Snippets
19
Records
21
Agent score
63%

What's inside Moneta

  1. Overview of Moneta capabilities

    master

    Moneta is the reference implementation (RI) of the JSR 354 Money & Currency API. It provides the following core functionalities:

    • Monetary amounts: Support for fixed-sized FastMoney and Money for large amounts.
    • Currency conversion: Tools for currency conversion and rate providers.
    • Custom currencies: Support for non-standard currencies, such as Bitcoin.
  2. Register custom formats via MonetaryAmountFormatProviderSpi

    master

    To add new formatting capabilities, implement the MonetaryAmountFormatProviderSpi interface. These providers are discovered using the Java ServiceLoader mechanism.

    An implementation must define:

    • getProviderName(): A unique identifier for the provider.
    • getAmountFormats(AmountFormatQuery): Logic to return MonetaryAmountFormat instances based on the query context.
    • getAvailableLocales(): The set of Locale objects supported by this provider.
    • getAvailableFormatNames(): The set of format names (styles) supported by this provider.
    public final class GeeCoinFormatProviderSpi implements MonetaryAmountFormatProviderSpi {
        private static final String PROVIDER_NAME = "GeeCoin";
        private static final String STYLE_NAME = "GeeCoin";
    
        private Set<Locale> supportedSets = new HashSet<>();
        private Set<String> formatNames = new HashSet<>();
    
        public GeeCoinFormatProviderSpi() {
            supportedSets.add(Locale.CHINA);
            supportedSets = Collections.unmodifiableSet(supportedSets);
            formatNames.add("GeeCoin");
            formatNames = Collections.unmodifiableSet(formatNames);
        }
    
        @Override
        public String getProviderName() {
            return PROVIDER_NAME;
        }
    
        @Override
        public Collection<MonetaryAmountFormat> getAmountFormats(AmountFormatQuery amountFormatQuery) {
            // Implementation logic to return formats...
        }
    
        @Override
        public Set<Locale> getAvailableLocales() {
            return supportedSets;
        }
    
        @Override
        public Set<String> getAvailableFormatNames() {
            return formatNames;
        }
    }
  3. Choose and Create Monetary Amount Implementations

    master

    Moneta provides several implementations of MonetaryAmount. Choosing the right one depends on your performance and precision requirements:

    • Money: The standard implementation based on java.math.BigDecimal. It supports arbitrary precision and is generally the safest choice.
    • FastMoney: An optimized implementation using a long with a fixed scale of 100,000 ($10^5$). Use this when speed is critical and the scale is sufficient.
    • RoundedMoney: An implementation that performs implicit rounding after every operation. Use with caution to avoid unwanted side effects.

    You can create instances using the Monetary.getAmountFactory(Class<T>) method or via static factory methods provided by the implementation classes.

    // Using MonetaryAmountFactory
    FastMoney m = Monetary.getAmountFactory(FastMoney.class).setCurrency("USD").setNumber(200.20).create();
    
    // Using static factory methods (preferred)
    FastMoney m2 = FastMoney.of(200.20, "USD");
    Money m3 = Money.of(200.20, "USD");
  4. Register custom MonetaryAmountFormatProviderSpi implementations

    master

    To add custom formatting capabilities, implement the MonetaryAmountFormatProviderSpi interface. These providers are discovered using the Java ServiceLoader mechanism.

    In your implementation, you must define:

    • getProviderName(): A unique identifier for your provider.
    • getAvailableLocales(): The set of Locale objects supported by this provider.
    • getAvailableFormatNames(): The names of the formats provided.
    • getAmountFormats(AmountFormatQuery amountFormatQuery): Logic to return the appropriate MonetaryAmountFormat based on the query context.
    public final class GeeCoinFormatProviderSpi implements MonetaryAmountFormatProviderSpi {
    
    private static final String PROVIDER_NAME = "GeeCoin";
    private static final String STYLE_NAME = "GeeCoin";
    
        private Set<Locale> supportedSets = new HashSet<>();
        private Set<String> formatNames = new HashSet<>();
    
    public GeeCoinFormatProviderSpi() {
            supportedSets.add(Locale.CHINA);
            supportedSets = Collections.unmodifiableSet(supportedSets);
            formatNames.add("GeeCoin");
            formatNames = Collections.unmodifiableSet(formatNames);
    }
    
        @Override
        public String getProviderName() {
            return PROVIDER_NAME;
        }
    
        @Override
        public Collection<MonetaryAmountFormat> getAmountFormats(AmountFormatQuery amountFormatQuery) {
            Objects.requireNonNull(amountFormatQuery, "AmountFormatContext required");
            if (!amountFormatQuery.getProviderNames().isEmpty()
                && !amountFormatQuery.getProviderNames().contains(getProviderName())) {
                return Collections.emptySet();
            }
            if (!(amountFormatQuery.getFormatName() == null
                || STYLE_NAME.equals(amountFormatQuery.getFormatName()))) {
                return Collections.emptySet();
            }
            AmountFormatContextBuilder builder = AmountFormatContextBuilder.of(PROVIDER_NAME);
            if (amountFormatQuery.getLocale() != null) {
                builder.setLocale(amountFormatQuery.getLocale());
            }
            builder.importContext(amountFormatQuery, false);
            builder.setMonetaryAmountFactory(amountFormatQuery.getMonetaryAmountFactory());
            return Arrays.asList(new MonetaryAmountFormat[]{new GeeCoinAmountFormat(builder.build())});
        }
    
    @Override
    public Set<Locale> getAvailableLocales() {
        return supportedSets;
    }
    
    @Override
    public Set<String> getAvailableFormatNames() {
        return formatNames;
    }
    
    }
  5. Install Moneta via Maven, Gradle, or SBT

    master

    To use the Moneta reference implementation in your project, add the following dependency to your build configuration. Note that the dependency type is pom.

    Maven

    Add the dependency to your pom.xml:

    Gradle

    Add the dependency to your build.gradle:

    SBT

    Add the dependency to your build.sbt:

    <!-- Maven -->
    <dependency>
      <groupId>org.javamoney</groupId>
      <artifactId>moneta</artifactId>
      <version>1.4.5</version>
      <type>pom</type>
    </dependency>
    
    <!-- Gradle -->
    compile group: 'org.javamoney', name: 'moneta', version: '1.4.5', ext: 'pom'
    
    <!-- SBT -->
    libraryDependencies += "org.javamoney" % "moneta" % "1.4.5" pomOnly()
  6. Access Currency Units

    master

    You can access CurrencyUnit instances through the MonetaryCurrencies singleton (accessed via Monetary.getCurrencies()).

    Common ways to retrieve currencies include:

    • By Currency Code: Use the ISO 4217 code (e.g., "USD", "EUR"). All codes available in java.util.Currency are mapped by default.
    • By Locale: Use a java.util.Locale representing a country to retrieve its corresponding currency.
    • All Currencies: Retrieve a collection of all currently known currencies.
    // By code
    CurrencyUnit currencyCHF = Monetary.getCurrency("CHF");
    
    // By Locale
    CurrencyUnit currencyEUR = Monetary.getCurrency(new Locale("", "GER")); // Germany
    
    // All currencies
    Collection<CurrencyUnit> allCurrencies = Monetary.getCurrencies();
  7. Implement a Custom Currency Provider

    master

    To provide new currencies via the SPI mechanism (e.g., for a cryptocurrency like Bitcoin), implement the CurrencyProviderSpi interface.

    Once implemented, you must register the provider so it can be loaded by java.util.ServiceLoader. This is typically done by creating a file named META-INF/services/javax.money.spi.CurrencyProviderSpi containing the fully qualified name of your implementation class. Alternatively, if using CDI, you can register it as a @Singleton bean.

    public final class BitCoinProvider implements CurrencyProviderSpi {
        private Set<CurrencyUnit> bitcoinSet = new HashSet<>();
    
        public BitCoinProvider() {
           bitcoinSet.add(CurrencyUnitBuilder.of("BTC", "MyCurrencyBuilder").build());
           bitcoinSet = Collections.unmodifiableSet(bitcoinSet);
        }
    
        @Override
        public Set<CurrencyUnit> getCurrencies(CurrencyQuery query) {
           if (query.isEmpty() || query.getCurrencyCodes().contains("BTC") || query.getCurrencyCodes().isEmpty()) {
               return bitcoinSet;
           }
           return Collections.emptySet();
        }
    }
  8. Mix Monetary Amount Implementations

    master

    The JSR 354 API allows mixing different implementation types (e.g., performing operations between Money and FastMoney). However, this can impact performance and may trigger silent scale reduction/rounding.

    Best Practice: Explicitly convert to the target type before performing operations using the from() method to ensure predictable behavior.

    MyMoney myMoney = ...;
    Money money = Money.from(myMoney);
    FastMoney fastMoney = FastMoney.from(myMoney);
    
    // Converting back
    money = Money.from(fastMoney);
    fastMoney = FastMoney.from(money);
  9. Register Additional Currency Units

    master

    You can programmatically add new CurrencyUnit instances to the MonetaryCurrencies singleton using Monetary.registerCurrency() or by using the CurrencyUnitBuilder with the register flag set to true.

    To provide currencies via the SPI (Service Provider Interface), implement CurrencyProviderSpi and register it using the Java ServiceLoader mechanism by adding a file to META-INF/services/javax.money.spi.CurrencyProviderSpi containing the fully qualified name of your provider class.

    // Using CurrencyUnitBuilder with auto-registration
    CurrencyUnit unit = CurrencyUnitBuilder.of("FLS22", "MyCurrencyProvider")
        .setDefaultFractionDigits(3)
        .build(true /* register */);
    
    // Manual registration
    Monetary.registerCurrency(unit);
  10. Mix and Convert Monetary Amount Implementations

    master

    The JSR 354 API allows mixing different implementation types (e.g., performing operations between Money and FastMoney). However, this can impact performance and may cause silent scale reduction due to internal rounding.

    To avoid issues, it is best practice to explicitly convert to the target type before performing operations using the from() method provided by Moneta implementations.

    MyMoney myMoney = ...;
    Money money = Money.from(myMoney);
    FastMoney fastMoney = FastMoney.from(myMoney);
    
    // Converting back
    money = Money.from(fastMoney);
    fastMoney = FastMoney.from(money);
  11. Perform Currency Conversion

    master

    Currency conversion is handled by ExchangeRateProvider instances, which can be accessed via the MonetaryConversions singleton.

    • Accessing Providers: You can request a specific provider by name (e.g., "IMF", "ECB") or define a provider chain (e.g., "ECB", "IMF") to fallback to secondary providers if the first doesn't have the rate.
    • Using CurrencyConversion: An ExchangeRateProvider can provide a CurrencyConversion object for a specific target currency. This object is a MonetaryOperator that can be applied directly to any MonetaryAmount using .with(conversion).

    Default Provider Chain: IDENT,ECB,IMF,ECB-HIST,ECB-HIST90.

    // Get a provider and a specific rate
    ExchangeRateProvider rateProvider = MonetaryConversions.getExchangeRateProvider("IMF");
    ExchangeRate chfToUsdRate = rateProvider.getExchangeRate("CHF", "USD");
    
    // Use a CurrencyConversion operator
    ExchangeRateProvider provider = MonetaryConversions.getExchangeRateProvider();
    CurrencyConversion conversion = provider.getCurrencyConversion("CHF");
    MonetaryAmount amountInCHF = amountInUSD.with(conversion);
  12. Format Monetary Amounts

    master

    Formatting is handled by MonetaryAmountFormat instances, which are thread-safe and immutable. They can be accessed via the MonetaryFormats singleton.

    Formats can be retrieved by:

    • Locale: e.g., MonetaryFormats.getAmountFormat(Locale.GERMANY).
    • Name: e.g., MonetaryFormats.getAmountFormat("MyCustomFormat").
    • Query: Using AmountFormatQueryBuilder to specify complex parameters like strict, omitNegative, or custom negative sign strings.

    Once you have a format, use .format(amount) to get a string, or use the format to parse a string back into a MonetaryAmount.

    // Accessing formats
    MonetaryAmountFormat formatCountry = MonetaryFormats.getAmountFormat(Locale.GERMANY);
    
    MonetaryAmountFormat formatQueried = MonetaryFormats.getAmountFormat(
      AmountFormatQueryBuilder.of("MyCustomFormat2")
        .set("strict", true)
        .set("omitNegative", true)
        .set("omitNegativeSign", "N/A")
        .build()
    );
    
    // Formatting
    String formattedString = format.format(amount);