Como obter a real diferença entre dois valores TDateTime
Escrito por Carlos B. Feitoza Filho | |
Categoria: Artigos | |
Categoria Pai: Addicted 2 Delphi! | |
Acessos: 12746 |
Quem estiver interessado na proposta sobre mudança da VCL, é só dar uma olhada em Report #56957 - A Fix for DateUtils Date-Time Compare Functions. Eu não sei se esta página ainda está ativa, mas se não estiver, faça uma busca pelo seu título. Se você tiver sorte, achará o texto em algum outro local.
Datas e Pontos Flutuantes
Como se sabe, as datas no Delphi (TDate, TDateTime e TTime) são armazenadas como números do tipo extended, de ponto flutuante. Sabe-se também que números de ponto flutuante não tem uma precisão muito boa. Comparações e operações matemáticas com tais números causam erros de arredondamento que não são percebidos até que se precise manipular casas decimais com mais precisão, fato que acontece quando tentamos obter valores muito pequenos a partir de datas, como milissegundos. É neste ponto onde a leitura deste artigo pode ajudar você.
Primeiramente quero salientar que existem inúmeros outros algoritmos que fazem o mesmo e que até retornam a quantidade de meses, mas estejam avisados; a quantidade de meses exatos entre duas datas não pode ser conseguida de forma direta, apenas usando mais um loop, o que deixaria a função mais lenta. Deixei propositalmente a quantidade de meses de fora para que o leitor tente implementar.
Antes de começar preciso dizer que não vou simplesmente colar aqui a solução, pretendo explicar de forma didática, e estou subentendendo que o leitor já possui conhecimento básico da linguagem Delphi, bem como termos desta linguagem. Estejam avisados. O que vem a seguir é um caminho com lacunas faltando. Não espere copiar, colar e compilar sem erros. Sem mais delongas, lá vamos nós....
As mudanças propostas para a VCL
Infelizmente não lembro em qual Delphi a VCL foi corrigida, por isso não tenho como dizer com certeza se você vai ou não precisar utilizar as funções apresentadas aqui ou aquelas que VCL já dispõe. O exemplo de uso no final deste artigo demostra esta função para que ela retorne exatamente 1 milissegundo. Caso ao executar a função o valor retornado não seja 1 milissegundo, provavelmente seu Delphi não possui a VCL corrigida e assim será necessário usar as funções que estão aqui. Esta seção só será útil, portanto, caso seu Delphi ainda não tenha sido corrigido.
Precisamos criar duas funções para o cálculo preciso de milissegundos entre duas datas. Estas funções foram extraídas e modificadas a partir da proposta de alteração da unit DateUtils explicada anteriormente. O crédito por estas funções deve ser dado a John Herbster , que propôs as mudanças na unit DateUtils (Report #56957 - A Fix for DateUtils Date-Time Compare Functions). Os comentários originais, em inglês, foram preservados.
{ Converts a TDateTime variable to Int64 milliseconds from 0001-01-01.}
function DateTimeToMilliseconds(aDateTime: TDateTime): Int64;
var
TimeStamp: TTimeStamp;
begin
{ Call DateTimeToTimeStamp to convert DateTime to TimeStamp: }
TimeStamp := DateTimeToTimeStamp(aDateTime);
{ Multiply and add to complete the conversion: }
Result := Int64(TimeStamp.Date) * MSecsPerDay + TimeStamp.Time;
end;
{ Uses DateTimeToTimeStamp, TimeStampToMilliseconds, and DateTimeToMilliseconds. }
function MillisecondsBetween(const aNow, aThen: TDateTime): Int64;
begin
if aNow > aThen then
Result := DateTimeToMilliseconds(aNow) - DateTimeToMilliseconds(aThen)
else
Result := DateTimeToMilliseconds(aThen) - DateTimeToMilliseconds(aNow);
end;
A unit DateUtils também possui uma função de nome MillisecondsBetween no entanto esta função não é tão precisa como parece. A experiência mostra que ao criar dois valores TDateTime usando a função EncodeDateTime e que distam entre si apenas 1 milissegundo, o retorno da função MillisecondsBetween não retorna 1, como era de se esperar, provando que ela não é precisa.
Apresentando a função DecodeDateDiff
Primeiramente devemos declarar o tipo do resultado. Um record, com todas as variáveis retornáveis.
type
TDecodedDateDiff = record
Years: Word;
Weeks: Byte;
Days: Word;
Hours: Byte;
Minutes: Byte;
Seconds: Byte;
Milliseconds: Word;
end;
Agora vamos à função que faz todo o trabalho. Comentários internos dão as dicas do que está sendo feito. Quero salientar que esta função foi criada há cerca de 10 anos e que provavelmente hoje em dia devem haver formas mais eficientes de se obter o mesmo resultado.
function DecodeDateDiff(aStartDateTime, aFinishDateTime: TDateTime): TDecodedDateDiff;
var
MilliSeconds: Int64;
WholeStartDate, WholeEndDate: TDateTime;
Days: Cardinal;
Years: Word;
begin
{ Validando as datas, que devem ser passadas corretamente para função, isto é,
a data final tem de ser maior ou igual à data inicial. Qualquer outro caso é
inválido e lança a exceção }
if aStartDateTime >= aFinishDateTime then
raise Exception.Create('A data final é menor que a data inicial');
{ Zerando as variáveis que serão usadas no decorrer da função }
ZeroMemory(@Result,SizeOf(TDecodedDateDiff));
Years := 0;
{ Obtendo a quantidade exata de millissegundos entre as datas }
MilliSeconds := MilliSecondsBetween(aStartDateTime,aFinishDateTime);
{ A partir da quantidade exata de millissegundos, podemos obter a quantidade
exata de dias, já que sabemos quantos millissegundos hão exatamente em um dia }
Days := MilliSeconds div MSecsPerDay;
{ Abaixo estamos normalizando as datas, de forma que a data inicial começe
exatamente no início do ano subsequente a ela, e a data final termine
exatamente no final do ano anterior a ela }
WholeStartDate := IncMilliSecond(EndOfTheYear(aStartDateTime));
WholeEndDate := IncMilliSecond(StartOfTheYear(aFinishDateTime),-1);
{ O loop abaixo vai realizar duas ações: Decrementar a quantidade de dias
obtida anteriormente da quantidade de dias no ano sendo verificado no momento
e incrementar a variável Years, de forma a obter a quantidade de anos. }
while WholeStartDate < WholeEndDate do
begin
Dec(Days,DaysInYear(WholeStartDate));
Inc(Years);
WholeStartDate := IncDay(WholeStartDate,DaysInYear(WholeStartDate));
end;
{ Caso a quantidade de dias seja maior ou igual a quantidade de dias no ano
final, precisamos realizar um último ajuste para incrementar anos e
decrementar dias de acordo com a quantidade de dias no ano da data final }
if Days >= DaysInYear(aFinishDateTime) then
begin
Inc(Years);
Dec(Days,DaysInYear(aFinishDateTime));
end;
{ Neste ponto, a variável Years contém a quantidade de anos inteiros entre as
datas inicial final ... }
Result.Years := Years;
{ ... E a variável Days contém a quantidade de dias que sobraram. Obtemos
portanto a quantidade de semanas, que é um valor exato }
Result.Weeks := Days div 7;
Result.Days := Days mod 7;
{ A partir daqui a verificação é simples matemática, extraindo horas, minutos
e segundos dos millisegundos }
MilliSeconds := MilliSeconds mod MSecsPerDay;
Result.Hours := MilliSeconds div (SecsPerHour * MSecsPerSec);
MilliSeconds := MilliSeconds mod (SecsPerHour * MSecsPerSec);
Result.Minutes := MilliSeconds div (SecsPerMin * MSecsPerSec);
MilliSeconds := MilliSeconds mod (SecsPerMin * MSecsPerSec);
Result.Seconds := MilliSeconds div MSecsPerSec;
MilliSeconds := MilliSeconds mod MSecsPerSec;
{ O que sobrar no final, será apenas milissegundos! }
Result.Milliseconds := MilliSeconds;
end;
Um exemplo de uso
var
Data1, Data2: TDateTime;
Diferenca: TDecodedDateDiff;
begin
data1 := EncodeDateTime(2017,12,13,0,0,0,0);
data2 := EncodeDateTime(2017,12,13,0,0,0,1);
Diferenca := DecodeDateDiff(data1,data2);
// Todos os membros de "Diferenca" devem ser zero, exceto o membro "Milliseconds"
// que deve possuir o valor 1
end;
É isso! Agora você pode obter um intervalo entre duas datas com precisão de milissegundos! Se você quiser modificar o algoritmo, sinta-se a vontade, mas compartilhe com todos (como eu fiz).