如何避免 TDateTime 数据舍入

How to avoid TDateTime Data Rounding

我正在为 FMX TGrid 编写列和单元格 类,每个单元格中将包含 TCalendarEditTTimeEdit 实例。除了正确处理这些子控件中所做的更改外,一切正常。

type
  TFMTValue<T> = record
    FieldValue: T;
    Modified: boolean;
    Appended: boolean;
    Deleted: boolean;
  end;

  TDateTimeCell = class(TStyledControl)
    private
      FDate_Time: TFMTValue<TDateTime>;
      procedure SetDateTime(const Value: TFMTValue<TDateTime>);
      function GetDateTime: TFMTValue<TDateTime>;
    protected
      procedure SetData(const Value: TValue); override;
    public
      property Date_Time: TFMTValue<TDateTime> read GetDateTime write SetDateTime;
    ...   
   end;
...     
  function TDateTimeCell.GetDateTime: TFMTValue<TDateTime>;
    begin
      FDate_Time.Modified := (FDate_Time.Modified) or
        (FDate_Time.FieldValue <> FCalendarEdit.Date +
         + FTimeEdit.Time);
      FDate_Time.FieldValue := FCalendarEdit.Date + FTimeEdit.Time;
      Result := FDate_Time;
    end;

    procedure TDateTimeCell.SetData(const Value: TValue);
    begin
      Date_Time := Value.AsType<TFMTValue<TDateTime>>;
      inherited SetData(TValue.From<TDateTime>(FDate_Time.FieldValue));
      ApplyStyling;
    end;

    procedure TDateTimeCell.SetDateTime(const Value: TFMTValue<TDateTime>);
    begin
      FDate_Time := Value;
      FCalendarEdit.Date := DateOf(FDate_Time.FieldValue);
      FTimeEdit.Time := TimeOF(FDate_Time.FieldValue);
      FDate_Time.FieldValue:=FCalendarEdit.Date + FTimeEdit.Time; //this line helps but not in all cases
    end;

想法是通过 TGrid OnGetValue 事件处理程序分配数据。显示日期和时间。用户 activity 被捕获并设置了 Modified 标志。问题是即使没有任何用户活动,这个标志有时也会设置为 true。我怀疑这是由于 TDateTime 的时间部分四舍五入所致。代码没有其他方式将值分配给 FCalendarEdit.DateFTimeEdit.Time

如何正确比较 FCalendarEdit.DateFTimeEdit.Time 中存储的数据与 FDate_Time.FieldValue 中存储的数据?

追加

以这种方式设置标志并不能解决问题。

  FDate_Time.Modified := (FDate_Time.Modified) or
    (DateOf(FDate_Time.FieldValue) <> FCalendarEdit.Date) or
    (TimeOf(FDate_Time.FieldValue)<> FTimeEdit.Time);

附加 2。 根据 @Ken-White 的宝贵建议。 如果我们用

替换比较线
FDate_Time.Modified := (FDate_Time.Modified) or
(not SameDateTime(FDate_Time.FieldValue,
 FCalendarEdit.Date + FTimeEdit.Time));

它工作正常。所以TDataTime的比较只能通过这个函数来完成。

TDateTimetype Double,这意味着它是一个浮点值,因此在不指定可接受的增量(差异)的情况下进行相等比较时,会遇到二进制表示的常见问题..

特别是对于 TDateTime 值,您可以使用 DateUtils.SameDateTime 将相等性比较到小于一毫秒:

FDate_Time.Modified := (FDate_Time.Modified) or
           (not SameDateTime(FDate_Time.FieldValue, 
            FCalendarEdit.Date + FTimeEdit.Time));

TCalendarEdit 中存在一个错误(实际上有几个),这是您问题的根本原因,但您只需对代码进行少量更改即可修复它。

问题

TCalendarEdit 在应用新的 Date 值时会出现一些严重错误。

A TDate 类型实际上只是一个普通的 TDateTime 类型,您应该忽略其中的时间部分。同样,TTimeTDateTime,您应该忽略日期部分。

但是您必须在代码中正确使用这些类型 - 没有什么可以神奇地使 TTime 忽略日期或 TDate 忽略时间

例如,如果您检查 TCalendarEdit 的构造函数,您会发现它使用 Now 将内部 date/time 初始化为当前系统日期和时间,但 t运行 将其归类为去掉时间元素:

Date := Trunc(Now);

到目前为止一切顺利。

但是当您通过 Date 属性 应用新值时,它会执行以下操作(简化):

if Date <> Value then
  FDateTime := Value + Time;

这两行代码都包含严重错误:

  1. 它将 Date(属性 返回控件的 Date 值)与 Value 被分配 - 包括 date/time 中的任何时间值。它应该只比较 Value.

  2. date 部分
  3. 将新值分配给内部 date/time 时,它会将 Time 添加到 Value 你指定。

第一个错误导致对内部 属性 进行不必要的更改,但在其他方面相对无害。然而,第二个错误要严重得多,这是导致您出现问题的原因。

我推测控件作者的意图是保留内部 date/time 值的时间部分不变。但是,Value 未被 t运行 处理,因此它保留了分配给 属性 时指定的时间值。更糟糕的是,此控件上没有 Time 属性,因此这实际上将当前系统时间添加到 Value 中指定的任何时间.

这如何影响您的代码和测试用例

由于您的测试用例涉及中午的时间 - 12 小时 - 结果是当您在下午 运行 此代码时,您的 日期 TCalendarEdit 实际上设置为 25-Sep-2015 + 12 小时 + 控件初始化的时间.

如果你 运行 早上的代码,它似乎可以工作,因为添加的时间导致的值仍然是 9 月 25 日。

但是当你在下午 运行 代码时,12 小时会添加到当前时间,因此日期会滚动 到第二天

使用更有帮助的诊断错误消息,或者如果您通过调试器检查了代码中的属性,您就会看到这种情况发生。

DT := EncodeDate(2015, 9, 25) + EncodeTime(12, 0, 0, 0); 
CalendarEdit1.Date := DT;

ShowMessage(DateTimeToString(CalendarEdit1.Date));

// When executed at e.g. 9am, displays:  25 Sep 2015
// When executed at e.g. 1pm, displays:  26 Sep 2015

所以你比较失败的原因是日期实际上完全不同!

如果您曾尝试简单地使用 SameDateTime() 进行比较,如果您在早上测试它,它可能看起来有效] 但是你的问题会在下午回来!!

解决方案

您可以解决 TCalendarEdit 中的这些错误,方法是确保您自己尊重 属性 值的预期用途,仅分配 [=81] 的那些部分=]DT date/time 每种情况下的适当值:

TimeEdit1.Time     := TimeOf(DT);
CalendarEdit1.Date := DateOf(DT);

虽然在 TTimeEdit 的情况下不是绝对必要的,但这将防止 TCalendarEdit 中的这些错误导致这些问题并使它在您的代码中清楚地表明您知道需要什么(如果您愿意,可以考虑自记录代码)。 :)

如果您的 Delphi 版本中没有 TimeOf()DateOf() 函数,则以下是等效的:

TimeEdit1.Time     := DT - Trunc(DT);
CalendarEdit1.Date := Trunc(DT);

您当然可以在此基础上编写自己的 TimeOf()DateOf() 版本,以使意图更清楚.

注意

由于 Delphi 中 date/time 值的浮点性质, 存在 精度复杂化,这可能会导致与某些特定值的直接比较出现问题日期和时间,因此强烈建议您使用 SameDateTime() 函数来执行此类比较。

但这绝对不是导致您在这种情况下出现问题的原因,SameDateTime() 不是解决你的问题。

SameDateTime() 消除了因 date/time 值的差异小于 1 毫秒而引起的问题。本例中的差异是 24 小时!

值得注意的是 TCalendarEdit 控件在 XE7 中已被弃用,并已从 XE8 中完全删除。