2

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).

billc.cn
  • 6,543
  • 2
  • 33
  • 69
  • In the common format for IEEE-754 floats or doubles, you cannot enter `1.20515` "exact in decimals". – Jongware Oct 21 '15 at 09:30
  • No, it cannot be stored exactly as IEEE fp numbers. But what I meant is rather than seeing the binary representation as exact and canonical, the decimal version is. – billc.cn Oct 21 '15 at 09:37
  • What language/compiler ? [gcc (C) seems to give 1.2052](http://ideone.com/KYy3kx). – Paul R Oct 21 '15 at 09:43
  • Your question is potentially interesting, but I think you picked a bad example. `1.20515` is actually represented as `1.2051500082015991` in IEEE-754 so it's already biassed in the correct direction. You need to pick an example `x.yyyz5` where `z` is odd and the representation is `x.yyyz4999...`. `1.20715` is a possible candidate. – Paul R Oct 21 '15 at 09:50
  • @PaulR Use a double (https://ideone.com/mhSCGC). The problem is the pre-rounding bias will be different between float and double, so my example does not work in both cases, but the problem is still there. – billc.cn Oct 21 '15 at 09:51
  • OK - here's a gcc (C) example which shows the perceived problem in single precision using 1.20715: http://ideone.com/KYy3kx - I think the issue is more specifically to do with how literals constants are converted to their binary representation, but I'm not sure what the relevant standards have to say about this. – Paul R Oct 21 '15 at 09:53
  • The closest IEEE-754 64-bit binary number to 1.20515 is 1.2051499999999999435118525070720352232456207275390625. – Patricia Shanahan Oct 21 '15 at 14:18
  • "isn't this kind of rounding actually wrong?" No, it isn't. :-) It's rounding the value of the IEEE 754 double to the nearest 4-digit-after-the-point decimal, then giving you back the closest IEEE 754 double to that rounded decimal result. What else would you expect it to do? Can you propose behaviour that you'd consider more correct? – Mark Dickinson Oct 21 '15 at 18:21
  • @MarkDickinson Please refer to my revised question in "Edit:" section. (IMHO every algorithm is suitable for the exact use case it was meant for, but not necessarily every use case. In my case, the input was not meant to be an exact IEEE double for example.) – billc.cn Oct 22 '15 at 08:29
  • 1
    Okay, I see. Then the answer is: if you care about which direction halfway cases round, then yes, converting to binary before rounding is absolutely going to give you wrong results in many cases. How you fix this depends on your application. For some applications, the simple solution is not to care about exactly which way your ties round; obviously that won't fly in an accounting situation. – Mark Dickinson Oct 22 '15 at 08:52

1 Answers1

2

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)

Simon Byrne
  • 7,284
  • 1
  • 21
  • 43
  • IEEE754 specifies rounding binary values to non-integer decimals in two places: 5.4.2 specifies conversion from binary FP to decimal FP (the **convertFormat** operation) and conversion from binary FP to decimal string (the **convertToDecimalCharacter** operation). In both cases, `roundTiesToEven` is specified as the default behavior (this is a *should* not a *shall* for binaryFP -> decimalFP conversions, however). – Stephen Canon Oct 23 '15 at 16:48
  • I think you need a second rounding operation: `Math.round(Math.round(parseFloat(s)*1e6)/1e2)/1e4`. Without this, you'll get incorrectly-rounded results for some inputs (e.g., try `"0.51255"`, but there are many other examples). And "absolute value less than 10^10" should be "absolute value less than 10^9". – Mark Dickinson Oct 23 '15 at 18:13
  • @MarkDickinson: good points, thanks. FP error analysis is always more difficult than I think it's going to be. – Simon Byrne Oct 23 '15 at 18:45
  • I am a bit concerned about the use of `Math.round()`, which is defined not to round to half even (neither in the decimal nor binary space). While I agree with @MarkDickinson that rounding after converting to binary is wrong, not trying to round to half even at all is also wrong in my book. For example, could you tell me what's the differece between your solution and the `toFixed` method? – billc.cn Oct 26 '15 at 10:12
  • @billc.cn: Good point. The inner `Math.round()` doesn't care: it's just there to round a near-integer to an integer, and I think it's easy to show that it won't ever encounter a tie. But the outer `Math.round()` should really be replaced with something that does round-ties-to-even, not round-ties-to-positive-infinity (which is apparently what `Math.round` is specified to do; this shocked me a bit!). It should be quite easy to write a custom function for round-ties-to-even. – Mark Dickinson Oct 26 '15 at 11:15
  • ... and I'm surprised that there isn't already a question about manufacturing round-ties-to-even in JavaScript (or at least, not one I could find). A simple strategy is to round using `Math.round`, then if the `round` output is exactly `0.5` away from the `round` input (i.e., we've got a tie), re-round to the nearest even number (using something like `2.0 * Math.round(0.5 * x)`). – Mark Dickinson Oct 26 '15 at 12:03
  • @bill.cn: good call, I forgot about JavaScripts `Math.round` definition. – Simon Byrne Oct 26 '15 at 19:06