Как я могу работать с неспособностью 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
и т.д. с перехватывает код. Когда вы приступите к обновлению, вы можете просто удалить крючки.