【问题标题】:How to avoid TDateTime Data Rounding如何避免 TDateTime 数据舍入
【发布时间】:2015-10-06 17:23:41
【问题描述】:

我正在为 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;

这个想法是通过TGridOnGetValue事件处理程序分配数据。显示日期和时间。捕获用户活动并设置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的比较只能通过这个函数来完成。

【问题讨论】:

  • 好吧,如果FDate_Time.FieldValue &lt;&gt; FCalendarEdit.Date + FTimeEdit.Time 那么值真的发生了变化。除此之外,我无法弄清楚您的代码要做什么。 minimal reproducible example 会很好。也许您还需要进行更多调试,以便了解发生了什么。
  • TDateTime 是一个浮点(双精度)值。您可以使用DateUtils.SameDateTime 在一毫秒内查看它们是否相同,但如果它们是 MS 或更多,则它们不是相同的日期/时间值。您认为您需要为它们使用的准确度与数据库值相等吗?
  • @David,谢谢,我已将问题本地化并附加了帖子。拜托,请看一看。
  • @Ken-White,谢谢,我已将代码更改为SameDateTime 并附加了帖子。请,请看一看。我只需要 1 分钟的精度。
  • @David,请随意对问题做任何您想做的事情。您以顶级 IT 专业人士的角度来评判人们,我因此而尊重您。但是这个论坛的大多数访问者都不是专业人士(至少不是在 IT 领域)。我们根本不明白您希望在这里提出什么样的问题。另一方面,您不明白其他人的想法可能与您不同。

标签: delphi datetime firemonkey


【解决方案1】:

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

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

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

【讨论】:

  • 十进制精度 有点误导,因为数据类型是 binary 而不是 *decimal。这些问题与定义非常明确的比较关系不大,而是问题在于,在真实数学中相同的算术表达式在有限浮点算术中可能不同。
  • @David:好的。随意添加指向您在该主题上发布的先前广泛答案的链接。这比我在回答这个问题时所能提供的要好得多。
  • 在这种情况下,浮点精度不是问题。相反,这是一个基本的失败,即没有编译器强制执行离散时间和日期值,只有日期/时间值具有预期用途的变化,并且当这些变化被忽略时导致实现错误。您还可以争辩说,所涉及的控制没有帮助问题(至少,在最坏的情况下实际上是错误的),因为未能适当地“修剪”内部日期/时间值以适应它们在每种情况下的使用。
  • 尽管最初发布的问题存在问题,但它并没有说明一个非常有用的观点,尽管它不是人们认为的那个。即以编译器未反映或强制执行的目的和意图命名的类型的危险。出于这个原因,我支持目前的问题,并建议它仍然开放,尽管早期存在问题。
  • @Deltics:感谢您的反馈(以及您的投票)。在这种情况下,在没有指定可接受的增量的情况下测试浮点相等实际上是问题所在,而指定增量(SameDateTime 使用的 浮点精度 的问题;在比较通过不同方式获得的两个浮点值时,期望精确的浮点相等是一个问题。
【解决方案2】:

TCalendarEdit 中存在一个错误(实际上是一些错误),这是导致问题的根本原因,但您只需对代码稍作更改即可修复它。

问题

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

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

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

例如,如果您检查 TCalendarEdit 的构造函数,您会看到它使用 Now 将内部日期/时间初始化为当前系统日期和时间,但将其截断以消除时间元素:

Date := Trunc(Now);

到目前为止一切顺利。

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

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

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

  1. 它将 Date(返回控件的 Date 值的属性)与分配的 Value 进行比较 - 包括该日期的任何时间值/时间。它应该只比较 Valuedate 部分。

  2. 将新值分配给内部日期/时间时,它会将 Time 添加到您指定的 Value

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

我推测控件作者的意图是保持内部日期/时间值的时间部分不变。但是,Value 不会被截断,因此它会保留在分配给属性时指定的时间值。更糟糕的是,该控件没有 Time 属性,因此这实际上将当前系统时间添加到 Value 中指定的任何时间。

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

由于您的测试用例涉及中午的时间 - 12 小时 - 结果是当您在下午运行此代码时,您的 TCalendarEditDate 实际上是设置为 2015 年 9 月 25 日 + 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 中解决这些错误,方法是确保您自己尊重属性值的预期用途,只分配 DT 日期/时间值的那些部分视情况而定:

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 中日期/时间值的浮点性质会导致精度复杂化,这可能会导致直接比较某些特定日期和时间值时出现问题,因此强烈推荐您使用 SameDateTime() 函数进行此类比较。

但这绝对不是在这种情况下您的问题的原因,SameDateTime() 确实解决您的问题。

SameDateTime() 消除了小于 1 毫秒的日期/时间值差异引起的问题。在这种情况下,差异是 24 小时!

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

【讨论】:

  • 但在这种情况下,这不是您的问题的原因。几乎可以肯定不是。因为提问者改变了问题,并且以目前的形式,它并不能反映所面临的实际问题。
  • 感谢您的回答和建议! (+1)我已经附加了这个问题。希望它会很有趣。
  • @asd - 我刚刚更新了答案 - 问题确实涉及使用 TDate 和 TTime 但实际上是 TCalendarEdit 控件中的一个错误,它使您的代码容易受到问题的影响。不过,您可以解决它:查看更新的答案。
  • @David - 虽然最初的问题有问题,但我怀疑问题的根本原因实际上是相同的,因为 TCalendarEdit 控件在此用例中绑定失败在一天中的特定时间!。最初发布的问题只是让我们更难看到真正的问题。问题的当前形式更准确地反映了问题,尤其是在结合我的答案中对问题的检查时(恕我直言)。
  • 谢谢你,Deltics。我尝试了 DateOf 和 TimeOf。实际上,使用它们来分配值并没有帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-22
  • 1970-01-01
  • 1970-01-01
  • 2022-06-11
  • 2015-07-16
  • 2020-12-27
  • 2019-12-15
相关资源
最近更新 更多