Как я могу работать с неспособностью Delphi точно обрабатывать манипуляции с датами?

Я новичок в Delphi (программировал в нем около 6 месяцев). До сих пор это был очень неприятный опыт, большинство из которых исходило из того, насколько плохим Дельфи занимается обработка дат и времени. Может быть, я думаю, что это плохо, потому что я не знаю, как правильно использовать 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'))));

Это не точный код, который я использую, все в переменных и используется в другом контексте, но я думаю, что вы можете увидеть проблему. Почему это неправильное вычисление? Как я могу решить эту проблему?

Ответ 1

Учитывая

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

поэтому вы явно страдаете от традиционных проблем с плавающей запятой.

Update:

Основное преимущество Round заключается в том, что если минутный интервал очень близок к целому числу, то округленное значение будет гарантией того, что целое число, в то время как усеченное значение может быть очень хорошим предшествым целым числом.

Основное преимущество Trunc заключается в том, что вы действительно можете хотеть такую ​​логику: действительно, если вам исполнится 18 лет за пять дней, вам по-прежнему не разрешается подавать заявку на получение водительских прав Швеции.

Итак, если вы хотите использовать Round вместо Trunc, вы можете просто добавить

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

к вашему устройству. Тогда идентификатор MinutesBetween будет ссылаться на этот, в том же блоке, а не на номер в DateUtils. Общее правило заключается в том, что компилятор будет использовать функцию, которая была найдена последней. Так, например, если бы вы поставили эту функцию выше в своем собственном блоке DateUtilsFix, тогда

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), если диапазон находится внутри диапазона fuzz целого числа, а trunc(spn) в противном случае.

Например, используя этот подход

07:00:00 and 07:00:58

даст 0 минут, как и исходная версия на основе Trunc, и точно так же, как хотел бы шведский Trafikverket. Но он не будет страдать от проблемы, вызвавшей вопрос ОП.

Ответ 2

Это проблема, которая разрешена в последних версиях 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, а не Trunc является причиной того, что последний код Delphi обрабатывает MinutesBetween надежным способом.


Предполагая, что вы не можете обновить прямо сейчас, я бы рассмотрел проблему следующим образом:

  • Оставьте свой код без изменений. Продолжайте звонить MinutesBetween и т.д.
  • При обновлении ваш код, который вызывает MinutesBetween и т.д., теперь будет работать.
  • Тем временем fix MinutesBetween и т.д. с перехватывает код. Когда вы приступите к обновлению, вы можете просто удалить крючки.