Background
Recently my colleague add some new tests to our test project. One of them has not passed on or continuous integration system. Since we ha
The problem is most likely because something else in your code is changing the floating point rounding mode. Have a look at this program:
{$APPTYPE CONSOLE}
{$R *.res}
uses
SysUtils, Math;
const
CStrGPS = 'N5145.37936E01511.8029';
var
LLatitude, LLongitude: Integer;
LLong: Double;
LStrLong, LTmpStr: String;
LFS: TFormatSettings;
begin
FillChar(LFS, SizeOf(LFS), 0);
LFS.DecimalSeparator := '.';
LStrLong := Copy(CStrGPS, Pos('E', CStrGPS)+1, 10);
LTmpStr := Copy(LStrLong,1,3);
LLong := StrToFloatDef( LTmpStr, 0, LFS );
LTmpStr := Copy(LStrLong,4,10);
LLong := LLong + StrToFloatDef( LTmpStr, 0, LFS)*1/60;
Writeln(FloatToStr(LLong));
Writeln(FloatToStr(LLong*100000));
SetRoundMode(rmNearest);
LLongitude := Round(LLong * 100000);
Writeln(LLongitude);
SetRoundMode(rmDown);
LLongitude := Round(LLong * 100000);
Writeln(LLongitude);
SetRoundMode(rmUp);
LLongitude := Round(LLong * 100000);
Writeln(LLongitude);
SetRoundMode(rmTruncate);
LLongitude := Round(LLong * 100000);
Writeln(LLongitude);
Readln;
end.
The output is:
15.196715 1519671.5 1519671 1519671 1519672 1519671
Clearly your particular calculation depends on the floating point rounding mode as well as the actual input value and the code. Indeed the documentation does make this point:
Note: The behavior of Round can be affected by the Set8087CW procedure or System.Math.SetRoundMode function.
So you need to first of all find whatever else in your program is modifying the floating point control word. And then you must make sure that you set it back to the desired value whenever that mis-behaving code executes.
Congratulations on debugging this further. In fact it is actually the multiplication
LLong*100000
which is influenced by the precision control.
To see that this is so, look at this program:
{$APPTYPE CONSOLE}
var
d: Double;
e1, e2: Extended;
begin
d := 15.196715;
Set8087CW($1272);
e1 := d * 100000;
Set8087CW($1372);
e2 := d * 100000;
Writeln(e1=e2);
Readln;
end.
Output
FALSE
So, precision control influences the results of the multiplication, at least in the 80 bit registers of the 8087 unit.
The compiler doesn't store the result of that multiplication to a variable and it remains in the FPU, so this difference flows on to the Round
.
Project1.dpr.9: Writeln(Round(LLong*100000)); 004060E8 DD05A0AB4000 fld qword ptr [$0040aba0] 004060EE D80D84614000 fmul dword ptr [$00406184] 004060F4 E8BBCDFFFF call @ROUND 004060F9 52 push edx 004060FA 50 push eax 004060FB A1107A4000 mov eax,[$00407a10] 00406100 E827F0FFFF call @Write0Int64 00406105 E87ADEFFFF call @WriteLn 0040610A E851CCFFFF call @_IOTest
Notice how the result of the multiplication is left in ST(0)
because that's exactly where Round
expects its parameter.
In fact, if you pull the multiplication into a separate statement, and assign it to a variable, then the behaviour becomes consistent again:
tmp := LLong*100000;
LLongitude := Round(tmp);
The above code produces the same output for both $1272
and $1372
.
There basic issue remains though. You have lost control of the floating point control state. To deal with this you'll need to keep control of your FP control state. Whenever you call into a library that may modify it, store it away before calling, and then restore when the call returns. If you want to have anything like repeatable, reliable and robust floating point code, this sort of game is, unfortunately, inevitable.
Here is my code to do that:
type
TFPControlState = record
_8087CW: Word;
MXCSR: UInt32;
end;
function GetFPControlState: TFPControlState;
begin
Result._8087CW := Get8087CW;
Result.MXCSR := GetMXCSR;
end;
procedure RestoreFPControlState(const State: TFPControlState);
begin
Set8087CW(State._8087CW);
SetMXCSR(State.MXCSR);
end;
var
FPControlState: TFPControlState;
....
FPControlState := GetFPControlState;
try
// call into external library that changes FP control state
finally
RestoreFPControlState(FPControlState);
end;
Note that this code handles both floating point units and so is ready for 64-bit which uses the SSE unit rather than the 8087 unit.
For what it is worth, here is my SSCCE:
{$APPTYPE CONSOLE}
var
d: Double;
begin
d := 15.196715;
Set8087CW($1272);
Writeln(Round(d * 100000));
Set8087CW($1372);
Writeln(Round(d * 100000));
Readln;
end.
Output
1519672 1519671