1 minute Error in time difference calculation

425 views Asked by At

My Programming Environment is Borland C++ Builder 6.

I have encountered a problem with a bad result in the following code:

TDateTime dtEnter, dtExit;

dtEnter = EncodeDateTime(2016, 11, 29, 0, 49, 0, 0);
dtExit = EncodeDateTime(2016, 11, 29, 0, 50, 0, 0);

ShowMessage(dtEnter);
ShowMessage(dtExit);
ShowMessage(IntToStr(MinutesBetween(dtEnter, dtExit)));

The result is 0 instead of 1!

Why is this?

1

There are 1 answers

0
Remy Lebeau On

This is a known issue in older versions of the DateUtils unit, which was first introduced in Delphi/C++Builder 6. The issue lasted for several years until it was finally fixed in Delphi/C++Builder XE.

TDateTime is essentially just a double, where the date is stored in the integral portion and the time is stored in the fractional portion. As such, it is subject to approximate representations and rounding.

In your example, dtEnter is 42703.0340277778 and dtExit is 42703.0347222222.

The span between two TDateTime values is calculated using simple floating-point math:

function SpanOfNowAndThen(const ANow, AThen: TDateTime): TDateTime;
begin
  if ANow < AThen then
    Result := AThen - ANow
  else
    Result := ANow - AThen;
end;

In your example, SpanOfNowAndThen(dtEnter, dtExit) is 0.000694444439432118.

In the case of the MinutesBetween() function, prior to XE it would call MinuteSpan(), which returns a Double that is the result of SpanOfNowAndThen() multiplied by the MinsPerDay constant, and then it would truncate off the fractional portion to produce the final integer:

function MinuteSpan(const ANow, AThen: TDateTime): Double;
begin
  Result := MinsPerDay * SpanOfNowAndThen(ANow, AThen);
end;

function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
  Result := Trunc(MinuteSpan(ANow, AThen));
end;

In your example, MinuteSpan() produces a decimal value that is slightly less than 1.0 (0.99999999278225, to be exact), which becomes 0 when the decimal is truncated off.

In XE, many of the DateUtils functions were re-written to use more reliable calculations that are not based on floating-point math. Although MinuteSpan() is still the same, MinutesBetween() no longer uses MinuteSpan(). Instead, it now converts the two TDateTime values to milliseconds (which is lossless since TDateTime has millisecond precision), subtracts the values, and then divides the absolute value of the difference by a constant number of milliseconds per minute:

function DateTimeToMilliseconds(const ADateTime: TDateTime): Int64;
var
  LTimeStamp: TTimeStamp;
begin
  LTimeStamp := DateTimeToTimeStamp(ADateTime);
  Result := LTimeStamp.Date;
  Result := (Result * MSecsPerDay) + LTimeStamp.Time;
end;

function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
  Result := Abs(DateTimeToMilliseconds(ANow) - DateTimeToMilliseconds(AThen))
    div (MSecsPerSec * SecsPerMin);
end;

In your example, DateTimeToMilliseconds(dtEnter) is 63616063740000 and DateTimeToMilliseconds(dtExit) is 63616063800000, so the difference is 60000 ms, which is exactly 1 minute.

For versions prior to XE, you will have to implement a similar fix manually in your own code. This is discussed in various online blogs, such as:

How do I work around Delphi's inability to accurately handle datetime manipulations?

Accurate Difference Between Two TDateTime Values