Forum Moderators: open

Message Too Old, No Replies

Javascript Floating Point Precision Problem

Been battling this for years, someone PLEASE HELP!

         

rocknbil

6:07 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



I know the **why** of the problem below, I just don't know how to keep it under control with any reliability. Javascript apparently puts a 1 "x" decimal places to the right of the decimal point, totally hosing the simplest math problems: in the example below, the correct total 636.35 becomes 636.34999999999999! I have been fighting this for YEARS and have never found a consistent solution.

The "truncate" function below is kind of lame and indeed treats it as a string, but you will indeed see by the alerts the problem exists long before that.

I've tried parseFloat operations. Multiply numbers by 100, parseInt, and divide them by 100 at the end. Math.ceil won't work because it rounds 636.349999999 up to the next integer whole (637.) Sometimes I come to a solution with a specific set of input numbers, then on the EXACT SAME FORM with different parameters it returns, always one penny less than it's supposed to be.

Yes, I can do workarounds, like just add the missing penny, but that's stupid. There HAS to be a solution to this that doesn't involve adding an odd penny here or there.

PLEASE. Anyone, I am reduced to begging. :-(

I'm sorry for the way this message board makes the code looks a mess below, it's too bad the code and pre tags for this message board are completely worthless. It really is only three rows of items and a simple javascript function.

Copy and paste verbatim below, click the calculate button and watch the alerts. This is killing me:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Javascript Floating Point Precision Problem</title>
</head>

<body>
<h2>Javascript Floating Point Precision Problem</h2>
<form name="po_form" id="po_form" onSubmit="return false;">
<input type="hidden" name="number_of_items" id="number_of_items" value="3">
<table>
<tr><td><input type="text" name="pn_1" id="pn_1" size="15" maxlength="20" value="3H00NT" tabindex="18"></td>
<td> <input type="text" name="description_1" id="description_1" size="18" maxlength="255" value="Fuser Roller for HP4200DT" tabindex="19"></td>
<td><input type="text" onChange="calcForm(this.form);" name="quantity_1" id="quantity_1" size="2" maxlength="12" value="1" tabindex="21"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="price_1" id="price_1" size="6" maxlength="12" value="540.80" tabindex="22"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="shipping_1" id="shipping_1" size="6" maxlength="12" value="0.00" tabindex="23"></td>
<td> <strong>$</strong> <input type="text" name="subtotal_1" id="subtotal_1" onFocus="blur();" size="6" maxlength="12" value="540.80" tabindex="5018"></td>
</tr>
<tr><td><input type="text" name="pn_2" id="pn_2" size="15" maxlength="20" value="3H0006" tabindex="24"></td>
<td> <input type="text" name="description_2" id="description_2" size="18" maxlength="255" value="Feed Roller" tabindex="25"></td>
<td><input type="text" onChange="calcForm(this.form);" name="quantity_2" id="quantity_2" size="2" maxlength="12" value="1" tabindex="27"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="price_2" id="price_2" size="6" maxlength="12" value="48.75" tabindex="28"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="shipping_2" id="shipping_2" size="6" maxlength="12" value="0.00" tabindex="29"></td>
<td> <strong>$</strong> <input type="text" name="subtotal_2" id="subtotal_2" onFocus="blur();" size="6" maxlength="12" value="48.75" tabindex="5024"></td>
</tr>
<tr><td><input type="text" name="pn_3" id="pn_3" size="15" maxlength="20" value="3H007B" tabindex="30"></td>
<td> <input type="text" name="description_3" id="description_3" size="18" maxlength="255" value="Roller" tabindex="31"></td>
<td><input type="text" onChange="calcForm(this.form);" name="quantity_3" id="quantity_3" size="2" maxlength="12" value="1" tabindex="33"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="price_3" id="price_3" size="6" maxlength="12" value="46.80" tabindex="34"></td>
<td> $ <input type="text" onChange="calcForm(this.form);" name="shipping_3" id="shipping_3" size="6" maxlength="12" value="0.00" tabindex="35"></td>
<td> <strong>$</strong> <input type="text" name="subtotal_3" id="subtotal_3" onFocus="blur();" size="6" maxlength="12" value="46.80" tabindex="5030"></td>
</tr>
<tr>
<td colspan="4">&nbsp;</td>
<td> TOTAL:</td>
<td> <strong>$</strong> <input type="text" name="po_total" id="po_total" onFocus="blur();" size="6" maxlength="12" value="636.34" tabindex="5500"> </td>
</tr>
<tr><td colspan="6"> <hr width="100%" size="1"></td></tr>
<tr>
<td><input type="submit" name="addLineItem" id="addLineItem" value="Add New Item" tabindex="2001" onClick="alert('Different function, ignore.'); return false;"></td>
<td> &nbsp; </td>
<td colspan="4">
<input type="button" id="recalcButton" onClick="calcForm(this.form);" value="Recalculate Total" tabindex="2002">
<input type="submit" name="submitButton" id="submitButton" onClick="checkForm(this.form); return false;" value="Submit PO" tabindex="2003">
</td>
</tr>
</table>
</form>

<script type="text/javascript">

function calcForm (form) {

var subtotal=quan=price=ship=total=0;
var sub,cancelled;
var tot = document.getElementById('po_total');
var num_items = document.getElementById('number_of_items').value;

for (i=1;i<=num_items;i++) {
subtotal = 0;

sub = document.getElementById('subtotal_' + i);
ship = document.getElementById('shipping_' + i);
q = document.getElementById('quantity_' + i);
p = document.getElementById('price_' + i);
subtotal = (q.value * p.value) + (ship.value * 1);
total += subtotal;
alert(subtotal + ' ' + total);
sub.value = truncate(subtotal);

}
tot.value = truncate(total);
}

function checkForm (form) {
calcForm(form);
alert('various checks, then submit form.');
}

function truncate(num) {
string = "" + num;
if (string.indexOf('.') == -1)
return string + '.00';
seperation = string.length - string.indexOf('.');
if (seperation > 3)
return string.substring(0,string.length-seperation+3);
else if (seperation == 2)
return string + '0';
return string;
}
</script>

</body>
</html>

(end of code)

DrDoc, Foitman, BernardMarx, anyone, I seldom ask for help but now I am begging, this one is aging me beyond all reason. :-(

RonPK

8:48 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



What about using toFixed() [developer.mozilla.org]? It rounds any float to the number of decimals you wish.

rocknbil

9:03 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Thank you RonPK, off to give it a go . . . . do you think the floating point precision will still give 636.34? (Sure as hell going to try!) That truncate thing was an old bad habit I've carried along since JS 1.0, been meaning to ditch it, but would take care of that too . . ..

john_k

9:26 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Here are two functions I created several years ago when I ran into the same problem. There are probably easier ways to do it though.


function formatMoney(num, fractionsOk, withComma)
{
var n = '';
var sNum;
var sep = withComma? ',' : '';
var sSign=(num<0?'-':'');
if(fractionsOk)
sNum = formatNumber2(Math.abs(num), 2, -1)
else
sNum = Math.abs(num).toFixed(2);
for (var i = sNum.indexOf('.') - 3; i > 0; i -= 3)
n = sNum.substring(0, i) + sep + sNum.substring(i);
if(n!='')
return sSign+n
else
return sSign+sNum;
}
/*
formatNumber2: Formats a number with the indicated number of minimum digits following
the decimal point by adding extra 0's. Rounds off digits that exceed the
maxDigitsAfterDecimal parameter. If maxDigitsAfterDecimal==-1, then the function does
not round off any digits.
*/
function formatNumber2(expression, minDigitsAfterDecimal, maxDigitsAfterDecimal)
{
var sReturn;
var vReturn;
var vValue;
var sPadding = '';
var sSign = '';
if(expression.toString().length==0)
{
vValue = 0;
} else if(!(isNaN(expression))) {
if(expression < 0)
sSign = '-';
vValue = Math.abs(expression);
} else {
vValue = 0;
}
vReturn = vValue.toString().split('.');
if(minDigitsAfterDecimal > 0)
{
for(i=0; i<minDigitsAfterDecimal; i++)
sPadding += '0';
if(vReturn.length==1)
{
vReturn[1] = sPadding;
} else if(vReturn[1].length < minDigitsAfterDecimal) {
vReturn[1] = (vReturn[1] + sPadding).substr(0, minDigitsAfterDecimal);
} else {
if((vReturn[1].length > maxDigitsAfterDecimal) && (maxDigitsAfterDecimal!= -1))
{
vReturn[1] = (Math.round(parseFloat(vReturn[1].substr(0,maxDigitsAfterDecimal+1))/10)).toString();
//if the first digit(s) after the decimal point was a 0, it will have been lost in
//the above conversion. This while loop adds back any such 0 characters.
while(vReturn[1].length<maxDigitsAfterDecimal)
vReturn[1]='0'+vReturn[1];
if(vReturn[1].length > maxDigitsAfterDecimal)
{
vReturn[0] = (parseInt(vReturn[0]) + 1).toString();
vReturn[1] = sPadding;
}
}
}
if((vValue < 1) && (vValue!= 0) && (vReturn[0]!= '0'))
sReturn = '0' + vReturn[0] + '.' + vReturn[1];
else
sReturn = vReturn[0] + '.' + vReturn[1];
} else {
if(vReturn.length == 1)
{
if((vValue < 1) && (vValue!= 0))
{
sReturn = '0' + (vReturn[0]);
} else {
sReturn = (vReturn[0]);
}
} else {
if((vReturn[1].length > maxDigitsAfterDecimal) && (maxDigitsAfterDecimal!= -1))
vReturn[1] = (Math.round(parseFloat(vReturn[1].substr(0,maxDigitsAfterDecimal+1))/10)).toString();
if((vValue < 1) && (vValue!= 0))
sReturn = '0' + vReturn[0] + '.' + vReturn[1];
else
sReturn = vReturn[0] + '.' + vReturn[1];
}
}
return sSign + sReturn;
}

btw, the pre tags work if you 1) replace any tabs with spaces, and 2) don't have ANY blank lines.

rocknbil

9:39 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Well, RonPK, either I'm using the function wrong or it indeed has no effect, in the original code I tried several things, here was the last:

subtotal = (q.value * p.value) + (ship.value * 1);
subtotal.toFixed(2);
total += subtotal;
total.toFixed(2);
alert(subtotal + ' ' + total);

Still 646.349999999999999 :-(

jonh_k, THANK you but let me ask a few things, in looking that over doesn't it arrive at its result (mostly) by performing a complex manipulation on the data as a string? I believe that is also a large part of my problem sometimes; for q=1 and p = 34.95 and ship = 0.00,

subtotal = (q.value * p.value) + ship.value

wil often give me 34.950.00, concatenating a STRING. This is why I did it as above, to avoid that.

I cannot believe there is not a simple solution to this that will just add the numbers! :-(

john_k

9:56 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Yes, it arrives at the result by manipulating it as a string. This will not cause the problem you refer to though since the function works on one number at a time.

When doing math on the numbers, I would use a combination of parseFloat and toFixed. Then apply formatMoney to the result. Something like this:

var c=formatMoney(parseFloat(a.toFixed(2)) - parseFloat(b.toFixed(2)),false,true);

[edited by: john_k at 9:57 pm (utc) on Aug. 1, 2006]

RonPK

9:57 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



subtotal.toFixed(2);

I think you need to assign the value that toFixed() returns. It doesn't work on the variable directly.

subtotal = subtotal.toFixed(2);

Fotiman

10:09 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Unfortunately, floating point values are not precise. This is true of many languages, not just JavaScript. I don't think I've ever run into a case quite like this, where simply adding 2 somewhat simple floats yielded an incorrect value. But the true solution, in my opinion, would be to split the float value into multiple integer parts.

For example, to add 48.6 and 589.55, convert each number to 2 integers:

46.80 -> 46 & 80
589.55 -> 589 & 55

Add the integers:
46 + 589 = 635
80 + 55 = 135

Integer Divide the decimal part by 100:
135 / 100 = 1
135 % 100 = 35

635 + 1 = 636

Tie on the remainder:
636.35

This would require some conversion to string, and doing integer math and then converting back to string. But would yield more accurate results.

Hope that helps.

rocknbil

10:24 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



I think you need to assign the value that toFixed() returns. It doesn't work on the variable directly.
subtotal = subtotal.toFixed(2);

Yes, thank you, I originally tried that but kept getting "subtotal.toFixed is not a function" and moved on, obviously I was doing something else wrong.

THANKS Foitman, that also would have worked, but in the interest of the beauty of this (and other) languages, I am hunting for a method that works with them as numbers and not strings. To paraphrase Mr. BernardMarx (where's he been hiding, anyway?) Javascript is a robust lanaguage and not meant for ripping up and reassembling strings. I am trying to eliminate the workarounds from my coding (and often fail misterably. :-) )

HOWEVER - I attempted to edit the post above and obviously it was too late, for whatever reason I managed to get toFixed to do what it's supposed to, at least FOR THE TIME BEING and in this instance.

I say "In this instance" because this particular "feature" of javascript will go away completely for a few months and resurface on the very same form with different input values. Frustrates me to no end.

It also allows me to dispense with "truncate" . . . another workaround murdered. :-)

I still think there is something simpler at the root of this and hope to return to this thread for the benefit of others that may encounter this. For the time being, here is my modified function, hopefully formatted correctly (thanks again john_k:)


function calcForm (form) {
var subtotal,q,p,ship;
var total = 0;
var sub,cancelled;
var tot = document.getElementById('po_total');
var num_items = document.getElementById('number_of_items').value;
for (i=1;i<=num_items;i++) {
subtotal = 0;
sub = document.getElementById('subtotal_' + i);
ship = document.getElementById('shipping_' + i).value;
q = document.getElementById('quantity_' + i).value;
p = document.getElementById('price_' + i).value;
subtotal = (q * p) + (ship * 1); // Will still yield 123.450.00 if I don't multiply ship * 1!
total += (subtotal);
alert(subtotal + ' ' + total);
sub.value = subtotal.toFixed(2);
}
total = total.toFixed(2);
tot.value = total;
}

Note that when you run this - "total" still alerts at 636.349999999999 in the for loop but toFixed resolves it at 636.35.

john_k

10:56 pm on Aug 1, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



I realized (a little too late) that the toFixed() function is what was really "fixing" this for me also. The formatMoney and formatNumber2 functions were just for displaying the final result. So sorry about that. (like I said, this is code from several years ago).

However, after now having reviewed that code a little more, I can relay this advice: If you have to do several calculations, each building on intermittent results, you will find that you need to use the parseFloat(a.toFixed(2)) trick. If you don't, then the errors can accumulate enough to effect the 2nd and even the 1st digit after the decimal point.

rocknbil

12:23 am on Aug 2, 2006 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Oy . . .hadn't thought of that one . . . :(