Um requisito em um projeto recente era relatar quando um recurso seria totalmente consumido. Além da data do calendário de exaustão, fui solicitado a mostrar o tempo restante em formato semelhante ao inglês, algo como "1 ano, 3 meses para o fim".
A DATEDIFF
função incorporada
Retorna a contagem ... dos limites especificados do datepart cruzados entre a data inicial e a data final especificadas.
Se usado como está, isso pode produzir resultados enganosos ou confusos. Por exemplo, usar um intervalo de ANO mostraria 1999-12-31 (AAAA-MM-DD) e 2000-01-01 com um ano de diferença, enquanto o senso comum diria que essas datas estão separadas por apenas 1 dia. Por outro lado, usando um intervalo de DIA 1999-12-31 e 2010-12-31 são separados por 4.018 dias, enquanto a maioria das pessoas veria "11 anos" como uma descrição melhor.
Partir do número de dias e calcular meses e anos a partir daí seria propenso a erros de ano bissexto e tamanho do mês.
Eu comecei a me perguntar como isso poderia ser implementado nos vários dialetos SQL? A saída de exemplo inclui:
create table TestData(
FromDate date not null,
ToDate date not null,
ExpectedResult varchar(100) not null); -- exact formatting is unimportant
insert TestData (FromDate, ToDate, ExpectedResult)
values ('1999-12-31', '1999-12-31', '0 days'),
('1999-12-31', '2000-01-01', '1 day'),
('2000-01-01', '2000-02-01', '1 month'),
('2000-02-01', '2000-03-01', '1 month'), -- month length not important
('2000-01-28', '2000-02-29', '1 month, 1 day'), -- leap years to be accounted for
('2000-01-01', '2000-12-31', '11 months, 30 days'),
('2000-02-28', '2000-03-01', '2 days'),
('2001-02-28', '2001-03-01', '1 day'), -- not a leap year
('2000-01-01', '2001-01-01', '1 year'),
('2000-01-01', '2011-01-01', '11 years'),
('9999-12-30', '9999-12-31', '1 day'), -- catch overflow in date calculations
('1900-01-01', '9999-12-31', '8099 years 11 months 30 days'); -- min(date) to max(date)
Acontece que estou usando o SQL Server 2008R2, mas estou interessado em saber como outros dialetos lidariam com isso.
Esta resposta mostra uma implementação usando uma função CLR do SQL Server (2005+).
Montagem e função
Uso
Resultado
Fonte
Eu não sou um programador C #!
A solução a seguir é para SQL Server. A abordagem é semelhante à de Serg, pois a consulta usa apenas as funções DATEADD e DATEDIFF. No entanto, não considera intervalos negativos ( FromDate > ToDate ) e deriva anos e meses da diferença total do mês:
Resultado:
Minha versão, implementada no SQL Server 2008R2 SP2.
Com os dados de teste fornecidos, os resultados são
Explicação
Minha abordagem geral é avançar a partir da data anterior, primeiro em anos, depois em meses e depois em dias. Em cada nível de granularidade, o objetivo é chegar o mais próximo possível da data final sem ultrapassá-la e continuar no próximo nível inferior.
Eu uso uma tabela de números para facilitar o cálculo próximo, mas não superior. A partir desta tabela e
DATEADD
posso encontrar o maior número de anos/meses/dias que antecedemToDate
- comentário (B) no código.Como eu estava procurando o número MAX e minha tabela Numbers está agrupada nele, o otimizador estava executando uma varredura descendente, alimentando valores para DATEADD. Isso estava causando erros de estouro de data, pois o Numbers contém mais de 100.000 linhas.
DATEADD(YEAR, 100000, @FromDate)
é maior que 9999-12-31 e um erro é levantado. O predicado (A) fornece um limite superior no valor do número a partir do qual a varredura reversa começa, evitando o estouro de data. Consequentemente, o plano de consulta percorre muito poucas linhas, mesmo para intervalos de datas muito grandes.Essa abordagem é usada para localizar anos e meses, exceto que o ponto de partida para os meses é antecipado por quantos anos encontrei no primeiro CTE. DAYS é meu nível mais baixo de granularidade, então um simples DATEDIFF é suficiente.
Isso pode ser estendido para granularidade mais fina, retornando o intervalo em horas, minutos e segundos, se necessário.
O PostgreSQL suporta a
age
função pronta para uso:Isso fornece o resultado desejado, mais ou menos alguns valores de tempo adicionais.
Versão sem necessidade de
number
mesa ou talão. Dá o mesmo resultado nos dados de teste de Michael Green. Eles diferem em dados onde@FromDate > @ToDate
.ReadableInterval2
retorna valores negativos contrários a nulos.