我如何解决Delphi无法准确处理日期时间操作的问题?

mar*_*set 12 delphi time delphi-2010

我是Delphi的新手(现在已经编程了大约6个月).到目前为止,这是一次非常令人沮丧的经历,其中大部分来自Delphi处理日期和时间的糟糕程度.也许我认为这很糟糕,因为我不知道如何正确使用TDate和TTime,我不知道.以下是我现在正在发生的事情:

// This shows 570, as expected
ShowMessage(IntToStr(MinutesBetween(StrToTime('8:00'), StrToTime('17:30'))));

// Here I would expect 630, but instead 629 is displayed. WTF!?
ShowMessage(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30'))));
Run Code Online (Sandbox Code Playgroud)

这不是我使用的确切代码,一切都在变量中并在另一个上下文中使用,但我认为你可以看到问题.为什么这个计算错了?我怎么想解决这个问题?

And*_*and 20

特定

a := StrToTime('7:00');
b := StrToTime('17:30');

ShowMessage(FloatToStr(a));
ShowMessage(FloatToStr(b));
Run Code Online (Sandbox Code Playgroud)

你的代码使用MinutesBetween,有效地做到了这一点:

ShowMessage(IntToStr(trunc(MinuteSpan(a, b)))); // Gives 629
Run Code Online (Sandbox Code Playgroud)

但是,圆形可能更好:

ShowMessage(IntToStr(round(MinuteSpan(a, b)))); // Gives 630
Run Code Online (Sandbox Code Playgroud)

什么是浮点值?

ShowMessage(FloatToStr(MinuteSpan(a, b))); // Gives 630
Run Code Online (Sandbox Code Playgroud)

所以你明显在这里遇到传统的浮点问题.

更新:

主要的好处Round是,如果分钟跨度非常接近整数,则舍入值将保证为整数,而截断值可能很好地是前面的整数.

主要的好处Trunc是你可能真的想要这种逻辑:事实上,如果你在五天内满18岁,从法律上说你仍然不能申请瑞典驾驶执照.

所以,如果你想使用Round而不是Trunc,你可以添加

function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
  Result := Round(MinuteSpan(ANow, AThen));
end;
Run Code Online (Sandbox Code Playgroud)

到你的单位.然后标识符MinutesBetween将在同一个单元中引用此标识符,而不是中的标识符DateUtils.一般规则是编译器将使用它最新发现的函数.因此,例如,如果您将此功能放在您自己的单位中DateUtilsFix,那么

implementation

uses DateUtils, DateUtilsFix
Run Code Online (Sandbox Code Playgroud)

将使用新的MinutesBetween,因为DateUtilsFix发生在右边DateUtils.

更新2:

另一种看似合理的方法可能是

function MinutesBetween(const ANow, AThen: TDateTime): Int64;
var
  spn: double;
begin
  spn := MinuteSpan(ANow, AThen);
  if SameValue(spn, round(spn)) then
    result := round(spn)
  else
    result := trunc(spn);
end;
Run Code Online (Sandbox Code Playgroud)

这将返回round(spn)是跨度在整数的模糊范围内,trunc(spn)否则.

例如,使用这种方法

07:00:00 and 07:00:58
Run Code Online (Sandbox Code Playgroud)

将产生0分钟,就像原始trunc版本一样,就像瑞典Trafikverket所希望的那样.但它不会受到触发OP问题的问题的困扰.


Dav*_*nan 8

这是在最新版本的Delphi中解决的问题.因此,您可以升级,或者只是在Delphi 2010中使用新代码.例如,此程序生成您期望的输出:

{$APPTYPE CONSOLE}
uses
  SysUtils, DateUtils;

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;

begin
  Writeln(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30'))));
  Readln;
end.
Run Code Online (Sandbox Code Playgroud)

Delphi 2010代码MinutesBetween如下所示:

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

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;
Run Code Online (Sandbox Code Playgroud)

因此,MinutesBetween有效归结为两个日期/时间值的浮点减法.由于浮点运算固有的精确性,该减法可以产生略高于或低于真值的值.当它低于真实值时,使用Trunc将带您一直到前一分钟.简单地更换TruncRound就解决了问题.


正如最新的Delphi版本一样,彻底改变了日期/时间计算.有重大变化DateUtils.这有点难以分析,但新版本依赖于DateTimeToTimeStamp.这会将值的时间部分转换为自午夜以来的毫秒数.它是这样的:

function DateTimeToTimeStamp(DateTime: TDateTime): TTimeStamp;
var
  LTemp, LTemp2: Int64;
begin
  LTemp := Round(DateTime * FMSecsPerDay);
  LTemp2 := (LTemp div IMSecsPerDay);
  Result.Date := DateDelta + LTemp2;
  Result.Time := Abs(LTemp) mod IMSecsPerDay;
end;
Run Code Online (Sandbox Code Playgroud)

注意使用Round.使用Round而不是Trunc最新的Delphi代码MinutesBetween以强大的方式处理的原因.


假设你现在无法升级,我会像这样处理问题:

  1. 保持代码不变.继续打电话MinutesBetween
  2. 升级时,调用MinutesBetween等的代码现在可以正常工作.
  3. 在此期间MinutesBetween使用代码挂钩修复等.当你升级时,你可以简单地删除钩子.