All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.masterdata.currency.CurrencyHelper Maven / Gradle / Ivy

/**
 * Copyright (C) 2014-2018 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.helger.masterdata.currency;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Comparator;
import java.util.Currency;
import java.util.Locale;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.Immutable;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.impl.CommonsEnumMap;
import com.helger.commons.collection.impl.CommonsHashMap;
import com.helger.commons.collection.impl.CommonsTreeSet;
import com.helger.commons.collection.impl.ICommonsMap;
import com.helger.commons.collection.impl.ICommonsSortedSet;
import com.helger.commons.concurrent.SimpleReadWriteLock;
import com.helger.commons.locale.LocaleCache;
import com.helger.commons.locale.LocaleParser;
import com.helger.commons.string.StringHelper;

/**
 * Currency helper methods. Heavily extended in 6.1.0.
 *
 * @author Philip Helger
 */
@Immutable
public final class CurrencyHelper
{
  /**
   * The default rounding mode to be used for currency values. It may be
   * overridden for each currency individually.
   */
  public static final RoundingMode DEFAULT_ROUNDING_MODE = RoundingMode.HALF_EVEN;

  /** The default currency */
  public static final ECurrency DEFAULT_CURRENCY = ECurrency.EUR;

  /** The default scale to be used if no JDK Currency is available */
  public static final int DEFAULT_SCALE = 2;

  // Sorted set of all available currencies
  private static ICommonsSortedSet  s_aAllCurrencies = new CommonsTreeSet <> (Comparator.comparing (Currency::getCurrencyCode));
  private static ICommonsMap  s_aLocaleToCurrency = new CommonsHashMap <> ();
  private static final SimpleReadWriteLock s_aRWLock = new SimpleReadWriteLock ();
  @GuardedBy ("s_aRWLock")
  private static final ICommonsMap  s_aSettingsMap = new CommonsEnumMap <> (ECurrency.class);

  static
  {
    // For all locales
    for (final Locale aLocale : LocaleCache.getInstance ().getAllLocales ())
    {
      try
      {
        final Currency aCurrency = Currency.getInstance (aLocale);
        if (aCurrency != null)
        {
          s_aAllCurrencies.add (aCurrency);
          s_aLocaleToCurrency.put (aLocale, aCurrency);
        }
      }
      catch (final IllegalArgumentException ex)
      {
        // No currency present for locale
      }
    }

    reinitializeCurrencySettings ();
  }

  /**
   * Reinitialize all the {@link PerCurrencySettings} to the original state.
   */
  public static void reinitializeCurrencySettings ()
  {
    s_aRWLock.writeLocked ( () -> {
      s_aSettingsMap.clear ();
      for (final ECurrency e : ECurrency.values ())
      {
        s_aSettingsMap.put (e, new PerCurrencySettings (e));

        for (final Locale aLocale : e.matchingLocales ())
          if (!localeSupportsCurrencyRetrieval (aLocale))
            throw new IllegalArgumentException ("Passed locale " + aLocale + " does not support currency retrieval!");
      }
    });
  }

  private CurrencyHelper ()
  {}

  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsSortedSet  getAllSupportedCurrencies ()
  {
    return s_aAllCurrencies.getClone ();
  }

  public static boolean isSupportedCurrency (@Nullable final Currency aCurrency)
  {
    return aCurrency != null && s_aAllCurrencies.contains (aCurrency);
  }

  public static boolean isSupportedCurrency (@Nullable final ECurrency eCurrency)
  {
    return eCurrency != null && isSupportedCurrency (eCurrency.getAsCurrency ());
  }

  public static boolean isSupportedCurrencyCode (@Nonnull final String sCurrencyCode)
  {
    try
    {
      return isSupportedCurrency (Currency.getInstance (sCurrencyCode));
    }
    catch (final IllegalArgumentException ex)
    {
      // No such currency code
      return false;
    }
  }

  /**
   * Check if a currency could be available for the given locale.
   *
   * @param aLocale
   *        The locale to check.
   * @return true if a currency is available for the given locale,
   *         false otherwise.
   */
  public static boolean localeSupportsCurrencyRetrieval (@Nullable final Locale aLocale)
  {
    return aLocale != null && aLocale.getCountry () != null && aLocale.getCountry ().length () == 2;
  }

  @Nullable
  public static Currency getCurrencyOfLocale (@Nonnull final Locale aContentLocale)
  {
    if (!localeSupportsCurrencyRetrieval (aContentLocale))
      throw new IllegalArgumentException ("Cannot get currency of locale " + aContentLocale);

    return s_aLocaleToCurrency.get (aContentLocale);
  }

  /**
   * @return A map from {@link Locale} to {@link Currency} as offered by the
   *         JDK.
   */
  @Nonnull
  @ReturnsMutableCopy
  public static ICommonsMap  getLocaleToCurrencyMap ()
  {
    return s_aLocaleToCurrency.getClone ();
  }

  /**
   * Parse a currency value from string using the currency default rounding
   * mode.
* Source: * http://wheelworkshop.blogspot.com/2006/02/parsing-currency-into-bigdecimal.html * * @param sStr * The string to be parsed. * @param aFormat * The formatting object to be used. May not be null. * @param aDefault * The default value to be returned, if parsing failed. * @return Either default value or the {@link BigDecimal} value with the * correct scaling. */ @Nullable public static BigDecimal parseCurrency (@Nullable final String sStr, @Nonnull final DecimalFormat aFormat, @Nullable final BigDecimal aDefault) { return parseCurrency (sStr, aFormat, aDefault, DEFAULT_ROUNDING_MODE); } /** * Parse a currency value from string using a custom rounding mode. * * @param sStr * The string to be parsed. * @param aFormat * The formatting object to be used. May not be null. * @param aDefault * The default value to be returned, if parsing failed. * @param eRoundingMode * The rounding mode to be used. May not be null. * @return Either default value or the {@link BigDecimal} value with the * correct scaling. */ @Nullable public static BigDecimal parseCurrency (@Nullable final String sStr, @Nonnull final DecimalFormat aFormat, @Nullable final BigDecimal aDefault, @Nonnull final RoundingMode eRoundingMode) { // Shortcut if (StringHelper.hasNoText (sStr)) return aDefault; // So that the call to "parse" returns a BigDecimal aFormat.setParseBigDecimal (true); aFormat.setRoundingMode (eRoundingMode); // Parse as double final BigDecimal aNum = LocaleParser.parseBigDecimal (sStr, aFormat); if (aNum == null) return aDefault; // And finally do the correct scaling, depending of the decimal format // fraction return aNum.setScale (aFormat.getMaximumFractionDigits (), eRoundingMode); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return Never null. */ @Nonnull public static PerCurrencySettings getSettings (@Nullable final ECurrency eCurrency) { s_aRWLock.readLock ().lock (); try { return s_aSettingsMap.get (eCurrency != null ? eCurrency : DEFAULT_CURRENCY); } finally { s_aRWLock.readLock ().unlock (); } } @Nonnull public static String getCurrencySymbol (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getCurrencySymbol (); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The pattern to be used in {@link DecimalFormat} to format this * currency. This pattern includes the currency string. */ @Nonnull @Nonempty public static String getCurrencyPattern (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getCurrencyPattern (); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The pattern to be used in {@link DecimalFormat} to format this * currency. This pattern does NOT includes the currency string. */ @Nonnull @Nonempty public static String getValuePattern (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getValuePattern (); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The {@link DecimalFormat} used to format this currency. Always * returns a copy of the contained formatter for thread-safety and * modification. */ @Nonnull public static DecimalFormat getCurrencyFormat (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getCurrencyFormat (); } @Nonnull public static String getCurrencyFormatted (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue) { return getSettings (eCurrency).getCurrencyFormatted (aValue); } @Nonnull public static String getCurrencyFormatted (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue, @Nonnegative final int nFractionDigits) { return getSettings (eCurrency).getCurrencyFormatted (aValue, nFractionDigits); } /** * @return The {@link DecimalFormat} object that formats an object like the * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * {@link #getCurrencyFormat(ECurrency)} but without the currency sign. * Always returns a copy of the contained formatter for thread-safety * and modification. */ @Nonnull public static DecimalFormat getValueFormat (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getValueFormat (); } @Nonnull public static String getValueFormatted (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue) { return getSettings (eCurrency).getValueFormatted (aValue); } @Nonnull public static String getValueFormatted (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue, @Nonnegative final int nFractionDigits) { return getSettings (eCurrency).getValueFormatted (aValue, nFractionDigits); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The minimum fraction digits to be used for formatting. */ @Nonnegative public static int getMinimumFractionDigits (@Nullable final ECurrency eCurrency) { return getCurrencyFormat (eCurrency).getMinimumFractionDigits (); } /** * Set the minimum fraction digits to be used for formatting. Applies to the * currency-formatting and the value-formatting. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param nDecimals * The new minimum fraction digits. May not be negative. */ public static void setMinimumFractionDigits (@Nullable final ECurrency eCurrency, @Nonnegative final int nDecimals) { getSettings (eCurrency).setMinimumFractionDigits (nDecimals); } @Nullable public static EDecimalSeparator getDecimalSeparator (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getDecimalSeparator (); } @Nullable public static EGroupingSeparator getGroupingSeparator (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getGroupingSeparator (); } /** * Adopt the passed text value according to the requested decimal separator. * * @param sTextValue * The text to be manipulated. May be null. * @param eDecimalSep * The decimal separator that is required. May not be null * . * @param eGroupingSep * The grouping separator that is required. May not be * null . * @return The manipulated text so that it matches the required decimal * separator or the original text */ @Nullable private static String _getTextValueForDecimalSeparator (@Nullable final String sTextValue, @Nonnull final EDecimalSeparator eDecimalSep, @Nonnull final EGroupingSeparator eGroupingSep) { ValueEnforcer.notNull (eDecimalSep, "DecimalSeparator"); ValueEnforcer.notNull (eGroupingSep, "GroupingSeparator"); final String ret = StringHelper.trim (sTextValue); // Replace only, if the desired decimal separator is not present if (ret != null && ret.indexOf (eDecimalSep.getChar ()) < 0) switch (eDecimalSep) { case COMMA: { // Decimal separator is a "," if (ret.indexOf ('.') > -1 && eGroupingSep.getChar () != '.') { // Currency expects "," but user passed "." return StringHelper.replaceAll (ret, '.', eDecimalSep.getChar ()); } break; } case POINT: { // Decimal separator is a "." if (ret.indexOf (',') > -1 && eGroupingSep.getChar () != ',') { // Pattern contains no "," but value contains "," return StringHelper.replaceAll (ret, ',', eDecimalSep.getChar ()); } break; } default: throw new IllegalStateException ("Unexpected decimal separator [" + eDecimalSep + "]"); } return ret; } /** * Try to parse a string value formatted by the {@link NumberFormat} object * returned from {@link #getCurrencyFormat(ECurrency)}. E.g. * € 5,00 * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param sTextValue * The string value. It will be trimmed, and the decimal separator will * be adopted. * @param aDefault * The default value to be used in case parsing fails. May be * null. * @return The {@link BigDecimal} value matching the string value or the * passed default value. */ @Nullable public static BigDecimal parseCurrencyFormat (@Nullable final ECurrency eCurrency, @Nullable final String sTextValue, @Nullable final BigDecimal aDefault) { final PerCurrencySettings aPCS = getSettings (eCurrency); final DecimalFormat aCurrencyFormat = aPCS.getCurrencyFormat (); // Adopt the decimal separator final String sRealTextValue = _getTextValueForDecimalSeparator (sTextValue, aPCS.getDecimalSeparator (), aPCS.getGroupingSeparator ()); return parseCurrency (sRealTextValue, aCurrencyFormat, aDefault, aPCS.getRoundingMode ()); } /** * Try to parse a string value formatted by the {@link NumberFormat} object * returned from {@link #getCurrencyFormat(ECurrency)}. E.g. * € 5,00 * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param sTextValue * The string value. It will be parsed unmodified! * @param aDefault * The default value to be used in case parsing fails. May be * null. * @return The {@link BigDecimal} value matching the string value or the * passed default value. */ @Nullable public static BigDecimal parseCurrencyFormatUnchanged (@Nullable final ECurrency eCurrency, @Nullable final String sTextValue, @Nullable final BigDecimal aDefault) { final PerCurrencySettings aPCS = getSettings (eCurrency); final DecimalFormat aCurrencyFormat = aPCS.getCurrencyFormat (); return parseCurrency (sTextValue, aCurrencyFormat, aDefault, aPCS.getRoundingMode ()); } /** * Try to parse a string value formatted by the {@link DecimalFormat} object * returned from {@link #getValueFormat(ECurrency)} * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param sTextValue * The string value. It will be trimmed, and the decimal separator will * be adopted. * @param aDefault * The default value to be used in case parsing fails. May be * null. * @return The {@link BigDecimal} value matching the string value or the * passed default value. */ @Nullable public static BigDecimal parseValueFormat (@Nullable final ECurrency eCurrency, @Nullable final String sTextValue, @Nullable final BigDecimal aDefault) { final PerCurrencySettings aPCS = getSettings (eCurrency); final DecimalFormat aValueFormat = aPCS.getValueFormat (); // Adopt the decimal separator final String sRealTextValue = _getTextValueForDecimalSeparator (sTextValue, aPCS.getDecimalSeparator (), aPCS.getGroupingSeparator ()); return parseCurrency (sRealTextValue, aValueFormat, aDefault, aPCS.getRoundingMode ()); } /** * Try to parse a string value formatted by the {@link DecimalFormat} object * returned from {@link #getValueFormat(ECurrency)} * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param sTextValue * The string value. It will be parsed unmodified! * @param aDefault * The default value to be used in case parsing fails. May be * null. * @return The {@link BigDecimal} value matching the string value or the * passed default value. */ @Nullable public static BigDecimal parseValueFormatUnchanged (@Nullable final ECurrency eCurrency, @Nullable final String sTextValue, @Nullable final BigDecimal aDefault) { final PerCurrencySettings aPCS = getSettings (eCurrency); final DecimalFormat aValueFormat = aPCS.getValueFormat (); return parseCurrency (sTextValue, aValueFormat, aDefault, aPCS.getRoundingMode ()); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The scaling to be used for BigDecimal operations. Always ≥ 0. If * no underlying JDK currency is present, {@value #DEFAULT_SCALE} is * returned. */ @Nonnegative public static int getScale (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getScale (); } /** * Special currency division method. This method solves the problem of * dividing "1/3" as it would result in a never ending series of * "0.33333333..." which results in an {@link ArithmeticException} thrown by * the divide method!
* The default scaling of this currency is used. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param aDividend * Dividend * @param aDivisor * Divisor * @return The divided value with the correct scaling */ @Nonnull @CheckReturnValue public static BigDecimal getDivided (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aDividend, @Nonnull final BigDecimal aDivisor) { ValueEnforcer.notNull (aDividend, "Dividend"); ValueEnforcer.notNull (aDivisor, "Divisor"); final PerCurrencySettings aPCS = getSettings (eCurrency); return aDividend.divide (aDivisor, aPCS.getScale (), aPCS.getRoundingMode ()); } /** * Special currency division method. This method solves the problem of * dividing "1/3" as it would result in a never ending series of * "0.33333333..." which results in an {@link ArithmeticException} thrown by * the divide method!
* This method takes a custom scaling. If the default scaling of this currency * should be used, than {@link #getDivided(ECurrency,BigDecimal, BigDecimal)} * should be used instead. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param aDividend * Dividend * @param aDivisor * Divisor * @param nFractionDigits * A custom scaling to be used. * @return The divided value with the provided scaling */ @Nonnull @CheckReturnValue public static BigDecimal getDivided (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aDividend, @Nonnull final BigDecimal aDivisor, @Nonnegative final int nFractionDigits) { ValueEnforcer.notNull (aDividend, "Dividend"); ValueEnforcer.notNull (aDivisor, "Divisor"); final PerCurrencySettings aPCS = getSettings (eCurrency); return aDividend.divide (aDivisor, nFractionDigits, aPCS.getRoundingMode ()); } /** * Get the passed value rounded to the appropriate number of fraction digits, * based on this currencies default fraction digits.
* The default scaling of this currency is used. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param aValue * The value to be rounded. May not be null. * @return The rounded value. Never null. */ @Nonnull public static BigDecimal getRounded (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue) { ValueEnforcer.notNull (aValue, "Value"); final PerCurrencySettings aPCS = getSettings (eCurrency); return aValue.setScale (aPCS.getScale (), aPCS.getRoundingMode ()); } /** * Get the passed value rounded to the appropriate number of fraction digits, * based on this currencies default fraction digits.
* This method takes a custom scaling. If the default scaling of this currency * should be used, than {@link #getRounded(ECurrency,BigDecimal)} should be * used instead. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param aValue * The value to be rounded. May not be null. * @param nFractionDigits * A custom scaling to be used. * @return The rounded value. Never null. */ @Nonnull public static BigDecimal getRounded (@Nullable final ECurrency eCurrency, @Nonnull final BigDecimal aValue, @Nonnegative final int nFractionDigits) { ValueEnforcer.notNull (aValue, "Value"); final PerCurrencySettings aPCS = getSettings (eCurrency); return aValue.setScale (nFractionDigits, aPCS.getRoundingMode ()); } /** * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @return The rounding mode of this currency. If non is specified, * {@link #DEFAULT_ROUNDING_MODE} is returned instead. May not be * null. */ @Nonnull public static RoundingMode getRoundingMode (@Nullable final ECurrency eCurrency) { return getSettings (eCurrency).getRoundingMode (); } /** * Change the rounding mode of this currency. * * @param eCurrency * The currency it is about. If null is provided * {@link #DEFAULT_CURRENCY} is used instead. * @param eRoundingMode * The rounding mode to be used. May be null. */ public static void setRoundingMode (@Nullable final ECurrency eCurrency, @Nullable final RoundingMode eRoundingMode) { getSettings (eCurrency).setRoundingMode (eRoundingMode); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy