[Patch] Rounding errors in FormatDateTime's interval mode
Summary
The function FormatDateTime()
can be operated in an interval mode in which hour numbers can be greater than 23. However, the function does not handle rounding errors in the input value correctly, and the displayed string value can be wrong by the equivalent of 1 full day. Another issue is that negative time intervals are displayed as positive.
System Information
- Operating system: Win-11
- Processor architecture: x86-64
- Compiler version: FPC/main 33dfb6cb
- Device: PC
Steps to reproduce / Example project
In forum thread https://forum.lazarus.freepascal.org/index.php/topic,62118.0.html, the poster added work times of eight hours per day over 21 days and displayed the result via FormatDateTime
in interval mode. His observation was that the displayed result, 144 hours, was off by one day from the correct result, 168 days.
See this sample project (formatdatetime_demo_project.zip )
program Project1;
uses SysUtils;
var
time_per_day: TDateTime;
sum: TDateTime;
i: Integer;
begin
time_per_day := EncodeTime(8, 0, 0, 0);
sum := 0.0;
for i := 1 to 21 do
sum := sum + time_per_day;
WriteLn('Sum: ', FormatDateTime('[h]', sum, [fdoInterval]), ' hours');
WriteLn('Expected: ', 8*21, ' hours');
WriteLn(sum);
end.
Output
Sum: 144 hours
Expected: 168 hours
6.9999999999999973E+000
Explanation
The WriteLn(sum)
in above code shows that the summed result is subject to the rounding errors typical of floating point numbers: the result is not 7 as expected, but 6.9999999999999973. The problem is magnified in FormatDateTime()
, in which the number of full days exceeding the TTime
range (<1.0) is calculated by means of the trunc()
function. After dropping the decimals the 6.9999999999999973 becomes a 6 which is off by 1 full day, 24 hours.
Fix
The current code in DateTimeToString()
(which is called by FormatDateTime()
) is as follows:
if isInterval then
StoreInt(Hour + trunc(abs(DateTime))*24, 0)
The issue can be fixed when a more elaborate function than trunc(abs(DateTime))
is used to calculate the number of full days: When the hour, minute, second and millisecond parts of the datetime value are zero the input value should correspond to a full day within reasonable accuracy. But when its fractional part is near 1 this indicates that the day count determined by trunc()
will be too small by 1 und must be incremented.
function FullDays(ADateTime: TDateTime): Integer;
begin
Result := trunc(ADateTime);
if (frac(ADateTime) > 0.9) and (Hour = 0) and (Minute = 0) and (Second = 0) and (Millisecond = 0) then
inc(Result);
end;
The attached patch fixes the issue. It also takes care of negative time intervals.
formatdatetime_roundingerror.diff
Test application
A simple test application is attached. It compares the FormatDateTime output in fdoInterval
mode for several limiting cases with the expected data.
formatdatetime_test_project.zip
Output of the test application before the patch
Format Value Result Expected
[h]:nn:ss 1.1574074051168282E-005 ---> 0:00:01 0:00:01 ---> OK
[h]:nn 5.0000000000000000E-001 ---> 12:00 12:00 ---> OK
[h]:nn 7.5000000000000000E-001 ---> 18:00 18:00 ---> OK
[h]:nn:ss 9.9999999999989997E-001 ---> 0:00:00 24:00:00 ---> ERROR
[h]:nn:ss 1.0000000000000999E+000 ---> 24:00:00 24:00:00 ---> OK
[h]:nn:ss 1.5000000000000999E+000 ---> 36:00:00 36:00:00 ---> OK
[h]:nn:ss 1.9999884259259260E+000 ---> 47:59:59 47:59:59 ---> OK
[h]:nn:ss.zzz 1.9999999884259259E+000 ---> 47:59:59.999 47:59:59.999 ---> OK
[h]:nn:ss 1.9999999999999001E+000 ---> 24:00:00 48:00:00 ---> ERROR
[h]:nn:ss 2.0000000000000999E+000 ---> 48:00:00 48:00:00 ---> OK
[h]:nn:ss.zzz 2.0000000115740741E+000 ---> 48:00:00.001 48:00:00.001 ---> OK
[h]:nn:ss 2.0000115740740743E+000 ---> 48:00:01 48:00:01 ---> OK
[h]:nn:ss -1.9999999999999001E+000 ---> 24:00:00 -48:00:00 ---> ERROR
[h]:nn:ss -2.0000000000000999E+000 ---> 48:00:00 -48:00:00 ---> ERROR
[n]:ss 4.1666666751022693E-002 ---> 60:00 60:00 ---> OK
[n]:ss 9.9999999884259261E-001 ---> 0:00 1440:00 ---> ERROR
[n]:ss 1.0000000011574075E+000 ---> 1440:00 1440:00 ---> OK
[n]:ss 1.0416666655092592E+000 ---> 1500:00 1500:00 ---> OK
[n]:ss 1.0416666678240742E+000 ---> 1500:00 1500:00 ---> OK
[n]:ss -1.0000000011574075E+000 ---> 1440:00 -1440:00 ---> ERROR
[n]:ss -9.9999999884259261E-001 ---> 0:00 -1440:00 ---> ERROR
[s] 4.1666666751022693E-002 ---> 3600 3600 ---> OK
[s] 9.9999999884259261E-001 ---> 0 86400 ---> ERROR
[s] 1.0000000011574075E+000 ---> 86400 86400 ---> OK
[s] 1.0416666655092592E+000 ---> 90000 90000 ---> OK
[s] 1.0416666678240742E+000 ---> 90000 90000 ---> OK
and after the patch: All tests are passed.
Format Value Result Expected
[h]:nn:ss 1.1574074051168282E-005 ---> 0:00:01 0:00:01 ---> OK
[h]:nn 5.0000000000000000E-001 ---> 12:00 12:00 ---> OK
[h]:nn 7.5000000000000000E-001 ---> 18:00 18:00 ---> OK
[h]:nn:ss 9.9999999999989997E-001 ---> 24:00:00 24:00:00 ---> OK
[h]:nn:ss 1.0000000000000999E+000 ---> 24:00:00 24:00:00 ---> OK
[h]:nn:ss 1.5000000000000999E+000 ---> 36:00:00 36:00:00 ---> OK
[h]:nn:ss 1.9999884259259260E+000 ---> 47:59:59 47:59:59 ---> OK
[h]:nn:ss.zzz 1.9999999884259259E+000 ---> 47:59:59.999 47:59:59.999 ---> OK
[h]:nn:ss 1.9999999999999001E+000 ---> 48:00:00 48:00:00 ---> OK
[h]:nn:ss 2.0000000000000999E+000 ---> 48:00:00 48:00:00 ---> OK
[h]:nn:ss.zzz 2.0000000115740741E+000 ---> 48:00:00.001 48:00:00.001 ---> OK
[h]:nn:ss 2.0000115740740743E+000 ---> 48:00:01 48:00:01 ---> OK
[h]:nn:ss -1.9999999999999001E+000 ---> -48:00:00 -48:00:00 ---> OK
[h]:nn:ss -2.0000000000000999E+000 ---> -48:00:00 -48:00:00 ---> OK
[n]:ss 4.1666666751022693E-002 ---> 60:00 60:00 ---> OK
[n]:ss 9.9999999884259261E-001 ---> 1440:00 1440:00 ---> OK
[n]:ss 1.0000000011574075E+000 ---> 1440:00 1440:00 ---> OK
[n]:ss 1.0416666655092592E+000 ---> 1500:00 1500:00 ---> OK
[n]:ss 1.0416666678240740E+000 ---> 1500:00 1500:00 ---> OK
[n]:ss -1.0000000011574075E+000 ---> -1440:00 -1440:00 ---> OK
[n]:ss -9.9999999884259261E-001 ---> -1440:00 -1440:00 ---> OK
[s] 4.1666666751022693E-002 ---> 3600 3600 ---> OK
[s] 9.9999999884259261E-001 ---> 86400 86400 ---> OK
[s] 1.0000000011574075E+000 ---> 86400 86400 ---> OK
[s] 1.0416666655092592E+000 ---> 90000 90000 ---> OK
[s] 1.0416666678240740E+000 ---> 90000 90000 ---> OK