Search code examples
javajsfjsf-2bigdecimal

JSF incorrect BigDecimal conversions


Problem: Need to format a BigDecimal through JSF, but JSF is destroying the precision of the BigDecimal.

JSF:

<h:outputText value="#{webUtilMB.roundUp(indexPrice.percentage, 2)}"/>

Java:

public class IndexPrice {
  public BigDecimal getPercentage(){ return new BigDecimal("1.325"); }
}

@ManagedBean("webUtilMb")
public class WebUtilManagedBean{
  public BigDecimal roundUp(BigDecimal dbvalue, int scale){
    return dbvalue.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP);
  }
}

Having a break point in the WebUtilManagedBean.roundUp method showed me 'dbvalue' is '1.3249999999999999555910790149937383830547332763671875' and not '1.325'.

I then overloaded the roundUp method in WebUtilManagedBean with:

public Double roundUp(Double dvalue, int scale){
  System.out.println(dvalue);
}

What surprised me when having a break point in this overloaded method was: - 'dvalue' is '1.325', which is correct. - the method actually got called instead of the roundUp(BigDecimal, int) method.

I later experimented with the BigDecimal constructor, with the following result:

BigDecimal db1 = new BigDecimal("1.325"); -> 1.325
BigDecimal db2 = new BigDecimal(1.325d); -> 1.3249999999999999555910790149937383830547332763671875

Theory: From the above, it seems that JSF was taking my BigDecimal value converting it to String, than to Double, than calling a 'new BigDecimal(double)' on the value to get the BigDecimal - which returns the wrong value.


Fix: One way to resolve this is to use the following code:

@ManagedBean("webUtilMb")
public class WebUtilManagedBean{
  public Double roundUp(Double dvalue, int scale){
    return this.roundUp(**new BigDecimal(dvalue.toString())**, BigDecimal.ROUND_HALF_UP);
  }
  public BigDecimal roundUp(BigDecimal dbvalue, int scale){
    return dbvalue.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP);
  }
}

But that just seems like a hack to me.

Any ideas on fixing this and the reasons behind it. Thanks.


Solution

  • Ideally this would be done via f:convertNumber if it had a 'rounding' attribute, but it doesn't.

    The clean way to do this would be to write your own roundup f:converter class, and use an f:converter tag where appropriate in the XHTML instead of using the EL method call. Your converter will get the value as an Object, i.e. a BigDecimal, and is responsible for converting it to a String itself. See the documentation for f:converter. Surprised that JSF doesn't do this correctly but I guess once you get into EL method calls everything is a String. Something like this (warning: untested):

    import java.math.BigDecimal;
    import javax.faces.component.UIComponent;
    import javax.faces.context.FacesContext;
    import javax.faces.convert.Converter;
    import javax.faces.convert.FacesConverter;
    
    /**
     * Use via e.g.:
     * <pre>
     * &lt;h:outputText value="#{EL}"&gt;
     *  &lt;f:converter id="com.edsi.jsf.RoundHalfUp"/&gt;
     *  &lt;f:attribute name="decimalPlaces" value="2"/&gt;
     * &lt;/h:outputText&gt;
     * <pre>
     * @author Esmond Pitt
     */
    @FacesConverter(value="com.edsi.jsf.RoundHalfUp")
    public class RoundHalfUpConverter implements Converter
    {
    
        @Override
        public Object getAsObject(FacesContext context, UIComponent component, String value)
        {
            throw new UnsupportedOperationException("Not supported yet.");
        }
    
        @Override
        public String getAsString(FacesContext context, UIComponent component, Object value)
        {
            BigDecimal  bd = (BigDecimal)value;
            int decimalPlaces;
            try
            {
                decimalPlaces = Integer.parseInt((String)component.getAttributes().get("decimalPlaces"));
            }
            catch (Exception exc)
            {
                decimalPlaces = 2;  // or whatever
            }
            return bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP).toString();
        }
    
    }