How to avoid TDateTime Data Rounding

前端 未结 2 1743
北荒
北荒 2021-01-21 10:11

I am writing column and cell classes for FMX TGrid that will contain TCalendarEdit and TTimeEdit instances in every cell. Everything works

相关标签:
2条回答
  • 2021-01-21 11:04

    There is a bug in TCalendarEdit (a few actually) which is the underlying cause of your problem, but you can fix it with only a small change to your code.

    The Problem

    The TCalendarEdit makes a number of crucial errors when it applies a new Date value.

    A TDate type is actually just an ordinary TDateTime in which you are supposed to ignore the time portion. Similarly a TTime is a TDateTime where you are supposed to ignore the date portion.

    But you have to use these types correctly in your code - there is nothing that magically makes a TTime ignore the date or a TDate ignore the time.

    For example, if you examine the constructor of the TCalendarEdit, you will see that it initialises the internal date/time to the current system date and time using Now, but truncates this to eliminate the time element:

    Date := Trunc(Now);
    

    So far so good.

    But when you apply a new value via the Date property, it performs the following (simplified):

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

    Both of these lines of code contain serious bugs:

    1. It compares the Date (property returning the Date value of the control) with the Value being assigned - including any time value in that date/time. It should instead compare only the date part of the Value.

    2. When assigning the new value to the internal date/time it adds Time to the Value you specified.

    The first bug results in unnecessary changes to the internal property but is otherwise relatively innocuous. The second bug however is far more serious and is what is causing your problem.

    I presume that the intention of the author of the control was to leave the time portion of the internal date/time value unchanged. However, the Value is not truncated, so it retains the time value specified in the assignment to the property. To make matters even worse, there is no Time property on this control, so this in fact adds the current system time to whatever time is specified in Value.

    How This Affects Your Code and Test Case

    Since your test case involved a time of midday - 12 hours - the result is that when you run this code in the afternoon, the Date of your TCalendarEdit is actually set to 25-Sep-2015 + 12 hours + the time when the control was initialised.

    If you run the code in the morning, it seems to work because the time added results in a value that is still on the 25th Sep.

    But when you run the code in the afternoon, the 12 hours are added to the current time and so the date rolls over to the next day!

    With a more helpful diagnostic error message, or if you had inspected the properties in your code via the debugger, you would have seen this occurring.

    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
    

    So the reason your comparison then fails is because the date is actually completely different!

    If you had tried simply using SameDateTime() for the comparison it may have appeared to have worked if you tested it in the morning but your problem would have returned in the afternoon !!

    The Solution

    You can work around these bugs in TCalendarEdit by ensuring that you respect the intended use of the property values yourself, assigning only those parts of the DT date/time value as appropriate in each case:

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

    Although not strictly necessary in the case of the TTimeEdit, this will prevent these bugs in TCalendarEdit from causing these problems and makes it clear in your code that you are aware of what is required (consider it self documenting code if you like). :)

    If you do not have TimeOf() and DateOf() functions in your version of Delphi, then the following is equivalent:

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

    You could of course write your own versions of TimeOf() and DateOf() based on this, to make the intention clearer.

    NOTE

    There are precision complications arising from the floating point nature of date/time values in Delphi that could cause problems with direct comparisons with some specific values of date and time and for that reason it is highly recommended that you use the SameDateTime() function for performing such comparisons.

    But this was absolutely not the cause of your problem in this case and SameDateTime() does not solve your problem.

    SameDateTime() eliminates problems arising from differences in date/time values of less than 1 millisecond. The difference in this case was 24 hours!

    Worth noting is that the TCalendarEdit control was deprecated in XE7 and has been removed entirely from XE8.

    0 讨论(0)
  • 2021-01-21 11:13

    TDateTime is of type Double, which means it's a floating point value, and therefore is subject to the usual issues of binary representation when doing comparisons for equality without specifying an acceptable delta (difference)..

    Specifically for TDateTime values, you can use DateUtils.SameDateTime to compare equality down to less than one millisecond:

    FDate_Time.Modified := (FDate_Time.Modified) or
               (not SameDateTime(FDate_Time.FieldValue, 
                FCalendarEdit.Date + FTimeEdit.Time));
    
    0 讨论(0)
提交回复
热议问题