Should IEEE (binary) rounding rules be used on exact decimal inputs?

488 Views Asked by At

Say I am rouding the number 1.20515 to 4 decimal places in IEEE-compliant languages (C, Java, etc.) using the default round-to-half-even rule, the result will be "1.2051" which is not even.

I think this is due to the fact that 1.20515 is slightly biased towards 1.2051 when stored in binary, so there isn't even a tie in binary space.

However, if the input 1.20515 is exact in decimals, isn't this kind of rounding actually wrong?

Edit:

What I really want to know is if I do not want to use exact decimal arithmetic (e.g. Java's BigDecimal), would these binary rounding rules introduce bias in the work flow: exact decimal in string (6 d.p. max) -> parse to IEEE double -> round using IEEE rules to 4 d.p.

Edit 2:

The "exact decimal" input is generated by Java using BigDecimal or String that comes directly from a database. The formatting, unfortunately, has to be done in JavaScript, which lacks a lot of support for proper rounding (and I am looking into implementing some).

1

There are 1 best solutions below

7
On

You're correct: 1.20515 isn't representable by IEEE754 binary64, so the decimal -> binary conversion will round to the nearest value which is 1.2051499999999999435118525070720352232456207275390625.

The IEEE754 standard doesn't actually have anything to say about rounding binary values to non-integer decimals (rounding to the nearest integer doesn't suffer from this problem), and so any such functionality is up to the language standard (if it chooses to define it). JavaScript toFixed clearly defines it as the exact mathematical value (i.e. 1.2051).

(UPDATE: actually, the IEEE754 standard does specify how FP -> string conversions should be performed, see Stephen Canon's comment below).

However if you want correct rounding over the whole pipeline, you can instead do

function roundeven(x) {
    return Math.sign(x)*((Math.abs(x) + 4.503599627370496e15) - 4.503599627370496e15);
}

roundeven(Math.round(parseFloat(s)*1e6)/1e2)/1e4;

which will work as long as s has fewer than 16 digits (i.e. the absolute value is less than 109).

Why is this the case?

  • Math.round(parseFloat(s)*1e6) is exact: this is because binary64 can correctly round-trip up to 15 decimal digits, and this is doing essentially the same thing by scaling to an integer value.
  • dividing 1e2 will involve some rounding (since not all values are exactly representable), but importantly, it can (i) represent values with a fractional half exactly, and (ii) won't round any other values to a fractional half (since we still have fewer than 16 decimal digits).
  • roundeven implements ties-to-even rounding to the nearest integer. This implementation is valid for any value in the above range.
  • the final division will again involve some rounding, but the values will be the closest to the correct decimal values, so converting back to a string (when required, say via _.toFixed(2)) will give the correct result.

(thanks to bill.cn and Mark Dickinson for the corrections)