Delphi Roundto and FormatFloat Inconsistency
Asked Answered
H

2

11

I'm getting a rounding oddity in Delphi 2010, where some numbers are rounding down in roundto, but up in formatfloat.

I'm entirely aware of binary representation of decimal numbers sometimes giving misleading results, but in that case I would expect formatfloat and roundto to give the same result.

I've also seen advice that this is the sort of thing "Currency" should be used for, but as you can see below, Currency and Double give the same results.

program testrounding;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,Math;

var d:Double;
    c:Currency;
begin
  d:=534.50;
  c:=534.50;
  writeln('Format: '  +formatfloat('0',d));
  writeln('Roundto: '+formatfloat('0',roundto(d,0)));
  writeln('C Format: '  +formatfloat('0',c));
  writeln('C Roundto: '+formatfloat('0',roundto(c,0)));
  readln;
end.

The results are as follows:

Format: 535
Roundto: 534
C Format: 535
C Roundto: 534

I've looked at Why is the result of RoundTo(87.285, -2) => 87.28 and the suggested remedies do not seem to apply.

Hamhung answered 25/7, 2019 at 2:23 Comment(4)
There's a huge difference between this question and the one you link to. The number here is exactly representable. As for why there is a difference we'll need to dig into the rtl source. The documentation offers nothing.Sprang
Both FormatFloat and RoundTo work with Extended, so the original float type should not matter. BTW, I wonder how System.Sysutils is found by the Delphi 2010 compiler. Unit scope names were introduced in XE2.Radiometeorograph
There are different rounding strategies for 0.5 One strategy is to round up (or round down). Another is to round to nearest even integer. Probably one function uses one strategy and one uses another. Statisticians and accountants prefer the last (roundto) option but only because statistically errors tend to cancel rather than reinforce, but that does not make other strategies wrong. If you think about it, 535 is just as wrong (far from the true value) as 354.Dentation
In 32-bit mode the FormatFloat number is evaluated in assembly code. A bit difficult to debug. Since the last Delphi version gives the same result in 64-bit mode, I tracked it down to function SysUtils.ExtToDecimal. It boils down to that if the last digit is 5 or more, its rounded up.Rincon
S
11

First of all, we can remove Currency from the question, because the two functions that you use don't have Currency overloads. The value is converted to an IEEE754 floating point value and then follows the same path as your Double code.

Let's look at RoundTo first of all. It is quick to check, using the debugger, or an additional Writeln that RoundTo(d,0) = 534. Why is that?

Well, the documentation for RoundTo says:

Rounds a floating-point value to a specified digit or power of ten using "Banker's rounding".

Indeed in the implementation of RoundTo we see that the rounding mode is temporarily switched to TRoundingMode.rmNearest before being restored to its original value. The rounding mode only applies when the value is exactly half way between two integers. Which is precisely the case we have here.

So Banker's rounding applies. Which means that when the value is exactly half way between two integers, the rounding algorithm chooses the adjacent even integer.

So it makes sense that RoundTo(534.5,0) = 534, and equally you can check that RoundTo(535.5,0) = 536.

Understanding FormatFloat is quite a different matter. Quite frankly its behaviour is somewhat opaque. It performs an ad hoc rounding in code that differs for different platforms. For instance it is assembler on 32 bit Windows, but Pascal on 64 bit Windows. The overall approach appears to be to take the mantissa of the floating point value, convert it to an integer, convert that to text digits, and then perform the rounding based on those text digits. No respect is paid to the current rounding mode when the rounding is performed, and the algorithm appears to implement the round half away from zero policy. However, even that is not implemented robustly for all possible floating point values. It works correctly for your value, but for values with more digits in the mantissa the algorithm breaks down.

In fact it is fairly well known that the Delphi RTL routines for converting between floating point values and text are fundamentally broken by design. There are no routines in the Delphi RTL that can correctly convert from text to float, or from float to text. In fact, I have recently implemented my own conversion routines, that do this correctly, based on existing open source code used by other language runtimes. One of these days I will get around to publishing this code for use by others.

I'm not sure what your exact needs are, but if you are wishing to exert some control over rounding, then you can do so if you take charge of the rounding. Whilst RoundTo always uses Banker's rounding, you can instead use Round which uses the current rounding mode. This will allow you to perform the round using the rounding algorithm of your choice (by calling SetRoundMode), and then you can convert the rounded value to text. That's the key. Keep the value in an arithmetic type, perform the rounding, and only convert to text at the very last moment, after the correct rounding has been applied.

Sprang answered 25/7, 2019 at 8:11 Comment(1)
That does explain a lot. This came up while investigating a discrepancy between rounding on SQL Server and Delphi on the same data, and I was thrown by the fact that there was only one number in several hundred dollar figures that was out.. but it was probably the only one ending in 0.50.Hamhung
R
4

In this case, the value 534.5 is exactly representable in Double precision.

Looking into source code, reveals that the FormatFloat function rounds upwards if the last pending digit is 5 or more.

RoundTo uses the Banker's rounding, and rounds to nearest even number (534) in this case.

Rincon answered 25/7, 2019 at 8:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.