How do I work around Delphi's inability to accurately handle datetime manipulations?

后端 未结 2 1161
独厮守ぢ
独厮守ぢ 2020-12-10 13:40

I am new to Delphi (been programming in it for about 6 months now). So far, it\'s been an extremely frustrating experience, most of it coming from how bad Delphi is at handl

相关标签:
2条回答
  • 2020-12-10 14:04

    Given

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

    your code, using MinutesBetween, effectively does this:

    ShowMessage(IntToStr(trunc(MinuteSpan(a, b)))); // Gives 629
    

    However, it might be better to round:

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

    What is actually the floating-point value?

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

    so you are clearly suffering from traditional floating-point problems here.

    Update:

    The major benefit of Round is that if the minute span is very close to an integer, then the rounded value will guaranteed be that integer, while the truncated value might very well be the preceding integer.

    The major benefit of Trunc is that you might actually want this kind of logic: Indeed, if you turn 18 in five days, legally you are still not allowed to apply for a Swedish driving licence.

    So you if you'd like to use Round instead of Trunc, you can just add

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

    to your unit. Then the identifier MinutesBetween will refer to this one, in the same unit, instead of the one in DateUtils. The general rule is that the compiler will use the function it found latest. So, for instance, if you'd put this function above in your own unit DateUtilsFix, then

    implementation
    
    uses DateUtils, DateUtilsFix
    

    will use the new MinutesBetween, since DateUtilsFix occurss to the right of DateUtils.

    Update 2:

    Another plausible approach might be

    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;
    

    This will return round(spn) is the span is within the fuzz range of an integer, and trunc(spn) otherwise.

    For example, using this approach

    07:00:00 and 07:00:58
    

    will yield 0 minutes, just like the original trunc-based version, and just like the Swedish Trafikverket would like. But it will not suffer from the problem that triggered the OP's question.

    0 讨论(0)
  • 2020-12-10 14:22

    This is an issue that is resolved in the latest versions of Delphi. So you could either upgrade, or simply use the new code in Delphi 2010. For example this program produces the output you expect:

    {$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.
    

    The Delphi 2010 code for MinutesBetween looks like this:

    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;
    

    So, MinutesBetween effectively boils down to a floating point subtraction of the two date/time values. Because of the inherent in-exactness of floating point arithmetic, this subtraction can yield a value that is slightly above or below the true value. When it is below the true value, the use of Trunc will take you all the way down to the previous minute. Simply replacing Trunc with Round would resolve the problem.


    As it happens the latest Delphi versions, completely overhaul the date/time calculations. There are major changes in DateUtils. It's a little harder to analyse, but the new version relies on DateTimeToTimeStamp. That converts the time portion of the value to the number of milliseconds since midnight. And it does so like this:

    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;
    

    Note the use of Round. The use of Round rather than Trunc is the reason why the latest Delphi code handles MinutesBetween in a robust fashion.


    Assuming that you cannot upgrade right now, I would deal with the problem like this:

    1. Leave your code unchanged. Continue to call MinutesBetween etc.
    2. When you do upgrade, your code that calls MinutesBetween etc. will now work.
    3. In the meantime fix MinutesBetween etc. with code hooks. When you do come to upgrade, you can simply remove the hooks.
    0 讨论(0)
提交回复
热议问题