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

13

我是 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'))));

这不是我使用的确切代码,所有内容都在变量中并在另一个上下文中使用,但我认为您可以看到问题所在。为什么计算结果是错误的?我该如何解决这个问题?


Delphi的TDateTime存在一个大问题,它们是表示天数的双精度浮点数,而不是固定点数。这意味着它们无法准确地表示分钟。(不确定是否正是这个原因导致了问题,但我并不感到惊讶) - CodesInChaos
1
@CodesInChaos:我被告知了。这对我来说不是一个合适的借口。他们本可以让它正常工作,而不是使用一个泄漏的抽象 - marco-fiset
在较新版本的Delphi(2006或更高版本)中,您可以创建自己的记录来表示日期/时间并重载运算符。然后,您可以放弃本质上有缺陷的双重日期时间。 - CodesInChaos
@Andreas,从XE看来,它似乎可以正常工作。 - jachguate
2
有关 Delphi 的 TDateTime 比较例程的大多数问题,请参阅 John Herbsters 的 QC 报告 56957,他提出了一些改进建议。这个建议也被 Zarko Gajic 描述在:accurate-difference-between-two-delphi-tdatetime-values。正如评论中所指出的,这些缺陷在后来的 Delphi 版本中得到了纠正。 - LU RD
显示剩余2条评论
2个回答

21

给定

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

ShowMessage(FloatToStr(a));
ShowMessage(FloatToStr(b));

您的代码使用 MinutesBetween 实现了以下效果:
ShowMessage(IntToStr(trunc(MinuteSpan(a, b)))); // Gives 629

然而,四舍五入可能更好:

ShowMessage(IntToStr(round(MinuteSpan(a, b)))); // Gives 630

实际上是什么浮点数值?

ShowMessage(FloatToStr(MinuteSpan(a, b))); // Gives 630

所以你显然正在遭受传统浮点问题的困扰。

更新:

Round 的主要优点是,如果分钟跨度非常接近整数,则四舍五入后的值保证是该整数,而截断后的值很可能是前面的整数。

Trunc 的主要优点在于你可能确实需要这种逻辑:实际上,如果你再过五天就满18岁了,法律上仍然不允许你申请瑞典驾照。

因此,如果你想使用 Round 而不是 Trunc,只需添加

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

将此函数添加到您的单元中。然后,标识符MinutesBetween将引用同一单元中的此函数,而不是DateUtils中的函数。通常规则是编译器将使用最新找到的函数。因此,例如,如果您将此函数放在自己的单元DateUtilsFix中,则

implementation

uses DateUtils, DateUtilsFix

将使用新的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;

这将返回round(spn),如果在模糊范围内为整数,则为spn,否则返回trunc(spn)
例如,使用此方法:
07:00:00 and 07:00:58

将会得到0分钟,就像使用原始的基于trunc的版本一样,也就是瑞典Trafikverket所希望的。但它不会遇到引发问题的OP的问题。

1
这种方法是防弹的。 - David Heffernan
如果间隔为3.9分钟,它将向下舍入为3,就像trunc一样。但是如果它是3.999999999999999999999或其他数值,它将使用round并返回4。因此,基本上它是DateUtils中原始函数的修复版本。特别是,在你的例子中,它将产生630。[为了进行比较:如果你简单地使用round而不是trunc3.9将被舍入为4]。 - Andreas Rejbrand
1
更新2没有帮助。坚持使用将Trunc替换为Round的原始方法。 - David Heffernan
@David:嗯,是的,它确实可以...(至少在理论上它是可行的,而且我也在我的系统上测试过了。) - Andreas Rejbrand
@David,MinutesBetween函数应该也适用于具有秒数的时间。我仍然认为我的“更新2”是可行的。当然,所需的行为取决于应用程序! “更新2”的重点是它的行为类似于原始的MinutesBetween函数,对于“几乎所有”的输入都是如此,而仅使用round的方法通常会有所不同。[但是,我的两个解决方案都修复了629的原始问题。] - Andreas Rejbrand
显示剩余7条评论

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.

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;

因此,MinutesBetween 实质上是两个日期/时间值的浮点数相减。由于浮点算术的固有不精确性,该减法可能产生略高或略低于真实值的值。当它低于真实值时,使用 Trunc 将使您完全降至前一分钟。只需用 Round 替换 Trunc 即可解决问题。
恰好最新版本的 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;

请注意使用了Round。使用Round而不是Trunc是最新Delphi代码处理MinutesBetween的健壮方式的原因。
假设您现在无法升级,我建议您按如下方式解决问题:
  1. 保持您的代码不变。继续调用MinutesBetween等函数。
  2. 当您升级时,调用MinutesBetween等函数的代码将会正常工作。
  3. 同时,使用代码钩子修复MinutesBetween等函数。当您准备升级时,可以简单地删除钩子。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接