12

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

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

4

2 回答 2

20

给定

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. 一般规则是编译器将使用它最近找到的函数。所以,例如,如果你把这个函数放在你自己的 unitDateUtilsFix中,那么

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)跨度是否在整数的模糊范围内,trunc(spn)否则返回。

例如,使用这种方法

07:00:00 and 07:00:58

将产生 0 分钟,就像trunc基于原始的版本一样,就像瑞典的 Trafikverket 一样。但它不会受到引发 OP 问题的问题的影响。

于 2013-02-22T19:28:58.547 回答
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将带你一路下降到前一分钟。只需替换Trunc为即可Round解决问题。


碰巧最新的 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而不是最新的 Delphi 代码以健壮的方式Trunc处理的原因。MinutesBetween


假设您现在无法升级,我将处理这样的问题:

  1. 保持您的代码不变。继续打电话MinutesBetween等。
  2. 当您进行升级时,您的调用MinutesBetweenetc. 的代码现在可以工作了。
  3. 同时用代码钩子修复MinutesBetween等。当您确实要升级时,您只需卸下挂钩即可。
于 2013-02-22T19:36:11.533 回答