Skip to content

Using BigDecimals in JSF

23 July 2010

One funny thing I had to struggle with is using BigDecimal with Seam and JSF.
As you probably know if you are reading this, you shouldn’t really use double if you care about operations on decimal numbers.
Even the simplest operation like

  • 2.35 – 1.75 = 0.6000000000000001

Can give you this kind of results, which sometimes (and that was my case) it’s not the right thing.
Plus, you probably want to define the scale of the numbers you are dealing with. In my case, all the operations were to be made using at last three decimals, and all the rounding was to be made by flooring:

  • 4.0001 should become 4

I want the BigDecimal numbers to be shown always with three decimals after the dot, even if there wasn’t any of it, and since I live in an european country, I have to use the comma (,) instead of (.) as a separator.

  • 1 => 1,000
  • 1.4 => 1,500

So many things… unfortunately JSF doesn’t give you any help on it:

  1. The f:convertNumber tag doesn’t work on BigDecimal
  2. The toString() method of the bigDecimal ignores the locale.

After struggling with built-in method, I decided to go all by myself.

First of all, we have to understand that we have two different problems: a data type problem and a formatting problem.

The former means that we have to be sure that each time the database gives us the number we have the right scale set.
There are different ways to set the scale on the BigDecimal, one of that is to use the constructor

new BigDecimal(double val, MathContext ctx) 

and

new MathContext(int setPrecision, RoundingMode setRoundingMode)

But since I’m using JPA, there is now way I can tell it to use a special constructor. I relied on a simple hack

	@Column
	public void getQuantity() {
        return this.quantity;
    }

    public void setQuantity(BigDecimal q) {
        this.quantity = q.setScale(NumberUtils.RE_SCALE);
    }

This assures that JPA will use the getter and setter to populate the bean, and when the setter will be called, the quantity will have the right scale.

As for the operation on BigDecimals, this paragraph of the JDK documentation is interesting:

Preferred Scales for Results of Arithmetic Operations
Operation Preferred Scale of Result
Add max(addend.scale(), augend.scale())
Subtract max(minuend.scale(), subtrahend.scale())
Multiply multiplier.scale() + multiplicand.scale()
Divide dividend.scale() – divisor.scale()

So a simple sum method:


    public BigDecimal getTotal() {
        BigDecimal i = BigDecimal.ZERO;
        for(BigDecimal c : getQuantities()) {
            i = i.add(c);
        }
        return i;
    }
	

Will have the right scale, since all the quantities are set with a scale of 3.

As for the latter, the formatting problem, I ended up writing a BigDecimal converter:


@Name("bigDecimalConverter")
@BypassInterceptors
@Converter(forClass = BigDecimal.class)
public class BigDecimalConverter implements javax.faces.convert.Converter {
    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String s) {
        try {
            return NumberUtils.parseBigDecimal(s);
        } catch (ParseException e) {
            final String msg = Messages.instance().get("javax.faces.converter.BigDecimalConverter.STRING");
            throw new ConverterException(new FacesMessage(msg));
        }
    }
    
    @Override
    public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object o) {
        return new DecimalFormat(NumberUtils.BIG_DECIMAL_FORMAT).format(o);
    }
}

Note that, since I’m using Seam, I can create a converter simply adding the

@BypassInterceptor
@Converter(forClass = BigDecimal.class)

annotations and the JSF converter will be called each time you use a h:outputText with a BigDecimal value.

We need a parse method too:

public class NumberUtils {
    public static final String BIG_DECIMAL_FORMAT = "0.000";
    public static final DecimalFormat format = new DecimalFormat(BIG_DECIMAL_FORMAT);
    public static final int RE_SCALE = 3;

    static {
        format.setParseBigDecimal(true);
    }

    public static BigDecimal parseBigDecimal(String s) throws ParseException {
        final int i = s.indexOf(".");
        if (i != -1) {
            throw new ParseException(s, i);
        }
        final BigDecimal bigDecimal = (BigDecimal) NumberUtils.format.parse(s);
        if (bigDecimal.scale() > RE_SCALE) {
            throw new ParseException(s, i);
        }
        return bigDecimal.setScale(RE_SCALE, RoundingMode.DOWN);
    }
}

So that I can use the string format I want, in this case 0.000 that means: always show three digits after the dot. The formatter will use the right separator according to the locale, in my case the comma (,).
Just remember to put the right locale into the faces-config.xml:

<faces-config>
    <application>
        <view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
        <locale-config>
            <default-locale>it</default-locale>
            <supported-locale>it</supported-locale>
        </locale-config>
    </application>

My parse method is a little rude. It won’t let you write any number with more than three decimal digits or using a dot.
Note that you have to wrap your ParseException into a ConverterException and write a FacesMessage if you want the error message to be shown next to the field. It will magically work!

That’s all, bye!

Advertisements

From → Programming

One Comment
  1. There is a hidden feature in Mojarra (I found out in 2.1.2): NumberConverter supports natively BigDecimals. So you can use NumberConverter instead of the described BigDecimalConverter 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: