Cálculo do Prazo em Dias Úteis no Sql Server
Em aplicações financeiras, em especial no Brasil (e outros países que passaram por períodos de inflação prolongados) é usual que os juros devidos sejam cálculados por dia útil, especialmente no mercado interbancário.
No Brasil é usual expressar as taxas na convenção “Over 252”, o exemplo clássico é a meta Selic ou a taxa CDI. CDBs pré-fixados são muitas vezes negociados com essa convenção. Nas mesas de operação e nos back-offices a excelente "Redoma" é presença constante, facilitando o cálculo dos prazos em dias úteis.
A convenção de contagem é que seja incluído o primeiro dia (se este for útil), e excluído o último (se este for útil). No Excel é comum usar a função NETWORKDAYS (DIATRABALHOTOTAL nas versões em português) com o mesmo propósito, lembrando que NETWORKDAYS inclui todos os dias úteis do período.
Em aplicações é comum o uso de classes e métodos para fazerem esses cálculos (Veja uma discussão interessante sobre isso no Fórum C# Brasil, incluindo questões de performance), mas, algumas vezes, pode ser útil computar o prazo em dias úteis no Sql Server. Um modo é usando módulos CLR, outro é usando uma tabela de Feriados e uma função escalar, como mostrado a seguir.
O Script
A ideia é criar uma função escalar, GetDeltaWorkDays, que retorne o prazo em dias úteis e saiba lidar, opcionalmente, com feriados definidos em uma tabela. O script seguinte cria a estrutura necessária.
use [pubs];
go
-- Esquema para armazenar esses dados
create schema Calendar authorization dbo;
go
-- Cria a tabela que conterá os feriados
create table Calendar.Holiday
(
[BusinessCenterCode] nvarchar(15) not null,
[Date] datetime not null,
CONSTRAINT [PK_Holiday] PRIMARY KEY CLUSTERED
(
[BusinessCenterCode] ASC,
[Date] ASC
)
);
go
-- Função para calcular o número de dias uteis entre duas datas
-- Segue o padrão financeiro de incluir o primeiro dia (se útil) e excluir o último (se útil)
create function [Calendar].[GetDeltaWorkDays]
(
@startDate datetime, -- data inicial
@endDate datetime, -- data final
@BusinessCenterCode nvarchar(15) = null -- Nome, opcional, do local de feriados
)
returns int with schemabinding
as begin
if @startDate = @endDate return 0;
declare @isInverse bit;
set @isInverse = 0;
if @startDate > @endDate
begin
declare @tmp datetime;
set @tmp = @endDate;
set @endDate = @startDate;
set @startDate = @tmp;
set @isInverse = 1;
end;
-- deve contar o primeiro dia e excluir o ultimo
declare @workDays int;
set @workDays =
(SELECT
(DATEDIFF(dd, @StartDate, @EndDate))
-(DATEDIFF(wk, @StartDate, @EndDate) * 2)
-(CASE WHEN datepart(dw, @StartDate) = 1 THEN 1 ELSE 0 END)
+(CASE WHEN datepart(dw, @EndDate) = 1 THEN 1 ELSE 0 END)
);
if @BusinessCenterCode is not null
begin
declare @nonWorkDayCount int;
select @nonWorkDayCount = count(*)
from Calendar.Holiday
where (BusinessCenterCode = @BusinessCenterCode)
and datepart(dw, [Date]) not in (1, 7)
and ([Date] >= @StartDate) and ([Date] < @EndDate);
set @workDays = @workDays - @nonWorkDayCount;
end;
if @isInverse = 1 set @workDays = -@workDays;
return @workDays;
end;
go
O script acima assume que você pode criar objeto no BD "pubs" (e que ele existe), cria um esquema "Calendar" (para manter a organização), seta uma tabela de Feriados e, finalmente, cria a função Calendar.GetDeltaWorDays.
A função é relativamente simples: entre as linhas 49 e 52 ela computa o número total de dias (úteis ou não) entre as datas, subtrai os finais de semana (linha 50) e faz pequenos ajustes para o caso de data inicial ou final serem finais de semana. Caso a função seja invocada passando-se o nome de um calendário a tabela Calendar.Holiday é consultada (linhas 58 a 62) para subtrair do prazo os feriados que ocorrem no meio da semana.
Antes de experimentarmos com nossa função vamos popular alguns feriados. Para não poluir o artigo com um script de 1.500 linhas (os 1.490 feriados brasileiros de 1950 até 2078) baixe e execute o script a partir de nosso site.
Para usar a função você pode fazer algo como:
-- Dias úteis até agora
select Calendar.GetDeltaWorkDays('1974-07-22', GETDATE(), 'br-BC');
-- Só contando os finais de semana
select Calendar.GetDeltaWorkDays('1974-07-22', GETDATE(), null);
-- Quanto dias úteis no ano a seguir?
select Calendar.GetDeltaWorkDays(GETDATE(), DATEADD(year, 1, GETDATE()), 'br-BC');
A função só funciona com a configuração padrão de que a semana começe no domingo (set datefirst 7). Não parece ser uma restrição séria, dado que nunca encontramos máquina ou necessidade de configurar isso de modo diferente do padrão.
A Performance
E a performance? O script abaixo exercita a função executando mil cálculos aleatórios entre as datas definidas na tabela de feriados:
-- para testar a performance do calculo de dias uteis
declare @minDate datetime;
set @minDate = (select min(Date) from Calendar.Holiday);
declare @maxDate datetime;
set @maxDate = (select max(Date) from Calendar.Holiday);
declare @maxActualDays int;
set @maxActualDays = DATEDIFF(day, @minDate, @maxDate);
declare @count int;
set @count = 0;
declare @startTime datetime;
set @startTime = GETUTCDATE();
declare @ini datetime;
declare @end datetime;
declare @add int;
declare @delta int;
while @count < 1000
begin
-- sorteia a data inicial
set @add = cast((RAND() * @maxActualDays) as int);
set @ini = DATEADD(DAY, @add, @minDate);
set @add = cast((RAND() * @maxActualDays) as int);
set @end = DATEADD(DAY, @add, @minDate);
set @delta = Calendar.GetDeltaWorkDays(@ini, @end, 'br-BC');
--print 'Calendar.GetDeltaWorkDays(''' + convert(char(10), @ini, 120) + ''', ''' + convert(char(10), @end, 120) + ''', ''br-BC'') = ' + cast(@delta as varchar(10));
set @count = @count + 1;
end;
declare @endTime datetime;
set @endTime = GETUTCDATE();
print 'Executados ' + cast(@count as varchar(10)) + ' cálculos em ' + cast(datediff(millisecond, @startTime, @endTime) as varchar(10)) + 'ms';
Num servidor nada especial com Sql Server 2012 são cerca de 4.000 cálculos/s. Nada espetacular considerando que uma boa implementação em C# pode fazer poucos milhões por segundo; ainda assim suficiente para boa parte das aplicações.
Notas
- Todo o código deste artigo usa a licença MIT, ou seja: o uso é livre em qualquer circunstância, mas não damos qualquer garantia.
- Este artigo aparecia originalmente no blog pessoal do autor, eu mesmo, que o atualizou, melhorou e transferiu para cá.