Search code examples
javagwtformattingcurrencygxt

Sencha GXT Custom CurrencyFormat for input-field


I'm currently working on a GWT project where we use the Sencha GXT libs. I'm currently trying to create a CurrencyField with a custom currency format. Some examples of how I want my custom currency format:

€ 123,45
€ 98.765.432,10
€ 400,00

So, as you can see I want a prefix euro-sign; a space between the currency symbol and the decimal; dots for thousand seperators; and commas for decimals (and of course two decimals behind the comma).

In GXT however it seems it isn't possible to create your own custom format. I know with the regular java.text.NumberFormat I can do something like this:

NumberFormat format = NumberFormat.getCurrencyInstance();
DecimalFormatSymbols formatSymbols = new DecimalFormatSymbols();
formatSymbols.setCurrencySymbol("€");
formatSymbols.setGroupingSeparator('.');
formatSymbols.setMonetaryDecimalSeparator(',');
((DecimalFormat)format).setDecimalFormatSymbols(formatSymbols);

In GXT however, the com.google.gwt.i18n.client.NumberFormat has to be used for the setFormat-method of the NumberField<BigDecimal>. I know some customization can be used with the CurrencyData like this:

private CurrencyData createCurrencyData() {
  // CurrencyData docs: http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/CurrencyData.html
  return new CurrencyData() {

    @Override
    public String getCurrencySymbol() {
      return "€";
    }

    @Override
    public String getSimpleCurrencySymbol() {
      return "€";
    }

    @Override
    public String getPortableCurrencySymbol() {
      return "€";
    }

    @Override
    public String getCurrencyCode() {
      return "EUR"; // ISO4217 for this currency
    }

    @Override
    public int getDefaultFractionDigits() {
      return 2; // Amount of decimal positions
    }

    @Override
    public boolean isSymbolPrefix() {
      return true; // true to place currency symbol before the decimal
    }

    @Override
    public boolean isSymbolPositionFixed() {
      return true; // true to use the same currency symbol position regardless of locale (determined by the isSymbolPrefix-method)
    }

    @Override
    public boolean isSpacingFixed() {
      return true; // true to put a space between the currency symbol and the decimal
    }

    @Override
    public boolean isSpaceForced() {
      return true; // true to use the same spacing regardless of locale (determined by the isSpacingFixed-method)
    }

    @Override
    public boolean isDeprecated() {
      return false;
    }
  };
}

Which I now use like this:

NumberField<BigDecimal> currencyField = new NumberField<BigDecimal>(
  new NumberPropertyEditor.BigDecimalPropertyEditor());
currencyField.setFormat(NumberFormat.getCurrencyFormat(createCurrencyData()));

Two problems however:

  1. It's not fully customized how I want it. It now accepts input like this: €123,456.78; instead of € 123.456,78 (also, even though the CurrencyData#isSpacingForced should apparently be used for a space between the currency-symbol and decimal, it doesn't work on the NumberField..)
  2. When I just type a regular number like 400, it gives an error instead of auto-formatting the user-input: 400 does not have either positive or negative affixes is not a valid number.

Solution

  • Changes we made:

    We now placed the Dutch locale in our .gwt.xml file:

    <extend-property name="locale" values="nl_NL"/>
    <set-property name="locale" value="nl_NL"/>
    <set-property-fallback name="locale" value="nl_NL"/>
    

    And we use a custom format like this:

    final BigDecimalField currencyField = new BigDecimalField();
    currencyField.setFormat(CustomNumberFormat.getCurrencyFormat());
    

    With the CustomNumberFormat-class like this:

    import java.util.Map;
    
    import com.google.gwt.core.client.GWT;
    import com.google.gwt.i18n.client.CurrencyData;
    import com.google.gwt.i18n.client.CurrencyList;
    import com.google.gwt.i18n.client.NumberFormat;
    import com.google.gwt.i18n.client.constants.CurrencyCodeMapConstants;
    
    public class CustomNumberFormat extends NumberFormat {
    
      private static CurrencyCodeMapConstants currencyCodeMapConstants = GWT.create(CurrencyCodeMapConstants.class);
      private static char currencySymbol;
      private static NumberFormat cachedCurrencyFormat;
    
      /**
       * Get the default currency format
       * @return the default currency format
       */
      public static NumberFormat getCurrencyFormat() {
        if (cachedCurrencyFormat == null) {
          cachedCurrencyFormat = getCurrencyFormat(CurrencyList.get().getDefault().getCurrencyCode());
        }
        return cachedCurrencyFormat;
      }
    
      /**
       * Get the currency format
       * @param currencyCode the code to use
       * @return the {@link NumberFormat} to use
       */
      public static NumberFormat getCurrencyFormat(final String currencyCode) {
        return new CustomNumberFormat(defaultNumberConstants.currencyPattern(), lookupCurrency(currencyCode), false, true);
      }
    
      /**
       * Lookup the currency data
       * @param currencyCode the currency code e.g. EUR
       * @return the {@link CurrencyData}
       */
      private static CurrencyData lookupCurrency(final String currencyCode) {
        final CurrencyData currencyData = CurrencyList.get().lookup(currencyCode);
    
        final Map<String, String> currencyMap = currencyCodeMapConstants.currencyMap();
    
        final String code = currencyData.getCurrencyCode();
        final String symbol = currencyMap.get(currencyCode);
        final int fractionDigits = currencyData.getDefaultFractionDigits();
        final String portableSymbol = currencyData.getPortableCurrencySymbol();
        return toCurrencyData(code, symbol, fractionDigits, portableSymbol);
      }
    
      /**
       *
       * @param code the currency code e.g. EUR
       * @param symbol the currency symbol e.g. the euro sign
       * @param fractionDigits the number of fraction digits
       * @param portableSymbol the portable symbol
       * @return the {@link CurrencyData} to use
       */
      public static native CurrencyData toCurrencyData(String code, String symbol, int fractionDigits,
          String portableSymbol) /*-{
                                 //CHECKSTYLE:OFF
                                 return [ code, symbol, fractionDigits, portableSymbol ];
                                 //CHECKSTYLE:ON
                                 }-*/;
    
      private boolean currencyFormat = false;
    
      /**
       *
       * @param pattern the currency pattern
       * @param cdata the {@link CurrencyData}
       * @param userSuppliedPattern <code>true</code> if the pattern is supplied by the user
       */
      protected CustomNumberFormat(final String pattern, final CurrencyData cdata, final boolean userSuppliedPattern,
          final boolean currencyFormat) {
        super(pattern, cdata, userSuppliedPattern);
        this.currencyFormat = currencyFormat;
      }
    
      /* (non-Javadoc)
       * @see com.google.gwt.i18n.client.NumberFormat#format(boolean, java.lang.StringBuilder, int)
       */
      @Override
      protected void format(final boolean isNegative, final StringBuilder digits, final int scale) {
        super.format(isNegative, digits, scale);
        if (this.currencyFormat) {
          final char decimalSeparator = defaultNumberConstants.monetarySeparator().charAt(0);
          if (digits.toString().endsWith(decimalSeparator + "00")) {
            digits.delete(digits.length() - 3, digits.length());
          }
          if (isNegative) {
            digits.delete(digits.length() - 1, digits.length()); // Delete leading "-"
            digits.insert(0, "- "); // Insert "- " at the front
          }
        }
      }
    
      /**
       * Parse a String. The String does not start with the expected prefix so we add it first
       * @param text the text to parse
       * @param inOutPos an offset telling us
       * @return the parsed value
       * @throws NumberFormatException if the text cannot be parsed
       */
      @Override
      public double parse(final String text, final int[] inOutPos) throws NumberFormatException {
        //add the positive prefix (euro-sign plus space)
        final String temp = getPositivePrefix() + text;
        //parse the adjusted string
        final double val = super.parse(temp, inOutPos);
        //now here is the tricky bit... during parsing the inOutPos offset was updated based on the modified String
        //but a check is maded to see if the resulting offset is equal to the length of the String we have been passed
        //so we need to update inOutPos by removing the length of the positive prefix
        inOutPos[0] -= getPositivePrefix().length();
        return val;
      }
    }
    

    Now we are getting the following results:

    € 123,45                      when entering 123,45
    € 98.765.432,10               when entering 98765432,1
    - € 400,00                    when entering -400