ColdFusion Muse

Number Formatting in Coldfusion - a look "Under the Hood"

Mark Kruger February 18, 2006 3:24 PM Coldfusion MX 7, Coldfusion Tips and Techniques Comments (5)

Formatting numbers is a pain. It would be great if our little human pea brains could read a number without commas in it to group the hundreds (or periods for my European readers). Sadly, we find ourselves unable to cope without the commas, so a good deal of display code regarding numbers is written to simply output them in the correct format. Most CF programmers use numberFormat( ) or decimalFormat( ) to control the output. Decimal format seems handy because it doesn't require a mask and produces the typical format you would expect. The 2 functions are quite different however and it may cause some perplexity when you are working with large numbers. Let me explain.

Note: Examples provided by Russ "Snake" Michaels from the CFGURU list :)

The decimalFormat() Function

The decimalFormat() seems to convert the argument into an actual Java data type like double or float, then it applies a specific mask to it and returns the result. That works fine, except that there is a maximum size for decimal notation of 18 with a scale of 2 for this function. That means if you get beyond 18 characters the number object (for the function) will not contain the expected data. Instead, it will contain scientific notation. You can use the javaCast() function to demonstrate what is happening behind the scenes. Check out this example:

<cfset num = 123456789123456789.12>

<cfset javaNum = Javacast("float",num)>
<cfoutput>
Java casted Number: #javanum#
</cfoutput>
Result is: 1.23456791E17

<cfset num = 123456789123456789.12>

<cfset javaNum = Javacast("float",num)>
<cfoutput>
Java casted Number: #javanum#
</cfoutput>
Result is: 1.23456789123E+017
The decimalFormat() function isn't programmed to convert this number back into the necessary character array for the mask. So the results are wrong with any number that has more than 16 digits. Why 16 and not 18? Because decimalFormat() tacks on the 2 decimal places if they are not passed in - so a 16 digit number with no decimal places looks like an 18 digit number to the function. The example above has 20 digits. When the mask is applied the result is never exactly what is excepted.
<cfset num = 123456789123456789.12>
<cfoutput>
decimal format: #decimalformat(num)#
</cfoutput>
Result is: 92,233,720,368,547,760.00
Obviously the result of trying to mask the scientific notation results in some sort of calculation being run or perhaps a reference problem. In any case, the result is wildly inaccurate. The lesson, don't use decimalFormat() when dealing with very large numbers.

The numberFormat() Function

The numberFormat() function isn't exempt from this problem either. Internally it is doing something very similar, but it has a different approach. numberFormat() simply takes the first 18 digits starting at the left and formats them with the mask. So this code:

<cfset num = 123456789123456789.12>
<cfoutput>
numberformat: #numberformat(num,'999,999,999,999,999,999,999.99')#
</cfoutput>
Returns a nearly correct result of 123,456,789,123,456,784.00. What's wrong with this result? It has 2 zeros (.00) trailing it. It should have .12 at the end? So the function returns the first 18 digits from the left and the mask inserts the zeros. This can lead to odd (meaning strange) numbers being returned. for example, let's add a few digits to the front of our number:
Adding a 9 to the left side:
<cfset num = 9123456789123456789.123>
<cfoutput>
numberformat: #numberformat(num,'999,999,999,999,999,999,999.99')#
</cfoutput>
Result is: 9,123,456,789,123,457,000.00

Adding two 9s to the left side:
<cfset num = 99123456789123456789.123>
<cfoutput>
numberformat: #numberformat(num,'999,999,999,999,999,999,999.99')#
</cfoutput>
Result is: 99,123,456,789,123,450,000.00
The function maintains the correct number of decimal places but only populates the first 16 places. That's strange because it seemed to handle 18 places until the 19th digit was added. In either case (decimalFormat() or numberFormat()) you are going to get innacurate results when dealing with very large numbers.

Obviously if you are working with such large numbers it's time to consider a different approach anyway - scientific notation is a good place to start. Still, it's worth remembering that outputting numbers using these 2 functions does have it's limitations. I will leave you with a neat trick I just learned from Jacob Munson on CF Talk.

numberFormat() Mask Tip

If the reason you choose decimalFormat( ) over numberFormat() is because you don't like typing out long masks, this tip is worth it's weight in Gold. Instead of typing out a long mask, try a comma, a period and two 9s - as in ",.99". This code:

<cfset num = 99123456789123456789.123>
<cfoutput>
number format ',.99': #numberformat(num,',.99')#
</cfoutput>
Produces the exact same result as the long mask of 9's above. Thanks Jacob for a great time saving tip.

  • Share:

5 Comments

  • PaulH's Gravatar
    Posted By
    PaulH | 2/18/06 9:43 PM
    fortunately icu4j's number formatter understands big decimals, etc. where core java's formatter doesn't (though it has BigDecimal, etc. as part of its math bits, go figure):
    <cfscript>
    theNumber="123456789123456789.12";
    // using default server locale
    nF=createObject("java","com.ibm.icu.text.NumberFormat").getInstance();
    bigDecimal=createObject("java","java.math.BigDecimal").init(theNumber);
    formattedNumber=nf.format(bigDecimal);
    writeoutput("original number:=#theNumber#<br>
       big decimal representation:=#bigDecimal#<br>
       number Formated:=#formattedNumber#<br>");
    </cfscript>

    which yields:
    original number:=123456789123456789.12
    big decimal representation:=123456789123456789.12
    number Formated:=123,456,789,123,456,789.12

    i really wish cf used icu4j. would make i18n work much simpler & also help solve problems like these.....
  • Sean Corfield's Gravatar
    Posted By
    Sean Corfield | 2/18/06 9:53 PM
    The problem here is that the number is being converted to a Java float under the covers and float simply doesn't have sufficient precision to hold that many significant digits. It's nothing to do with the formatting functions. This is a basic issue that people encounter whenever you are dealing with floating point numbers. It's why currency values should never be represented as floating point but as integers (as a number of pennies) and care taken not to convert the value via floating point data types.

    The example Paul shows relies on a specific Java data type (BigDecimal) that supports much larger precisions (hence the name).

    It's a trade off between the efficiency of using a native floating point type for calculations (float is much faster than BigDecimal) and the precision necessary for large numbers of significant digits.
  • PaulH's Gravatar
    Posted By
    PaulH | 2/18/06 10:09 PM
    sean, for me the funny thing is that core java's formatting classes don't understand any of it's own "Big" math classes. icu4j was all over that several versions ago.

    and since this is basically a display function, i don't understand why cf concerned is w/calculation speed? when does the conversion to float take place? immediately that the var is assigned a value or when it's pumped into the format function? or are you just doing a NumberFormat under the covers anyway?
  • mkruger's Gravatar
    Posted By
    mkruger | 2/19/06 1:59 PM
    Paul, Thanks for the icu4j example. As I was preparing this post I considered whether colfusion might be using that math.BigDecimal object to handle this function. It seemed like an appropriate choice and it appeared to handle very large numbers with precision. I came to the conclusion that CF was converting to a primitive type - either double or float.

    Sean, thanks for your comments. I take your currency comment to mean that 10 dollars and 12 cents should be represented as 1012 (1012 pennies). Sure makes display code more tedious to have to divide all those numbers by 100 - but I have worked with financial systems that store all financial values exactly like that - for the same reason I suppoe.

    Anyway, thanks as always for the input.
  • Mike Henke's Gravatar
    Posted By
    Mike Henke | 8/21/09 1:42 PM
    Thanks Mark. I had to round a number and add commas so your entry helped to make it cleaner then what I may have done.

    #numberformat(round(variables.box5),',')#