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.

Calendário Maia

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

  1. 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.
  2. Este artigo aparecia originalmente no blog pessoal do autor, eu mesmo, que o atualizou, melhorou e transferiu para cá.
Share