CShared #1 – [C#] Contagem de dias úteis entre duas datas.

Dando inicio a série de postagens com dicas úteis, códigos simples e funcionais que durante o dia-a-dia do desenvolvedor podem ser necessárias.
Hoje vou deixar uma função que pode ajudar bastante, ela conta quantos dias úteis existem entre duas datas, melhor dizendo em um período e a principio sem considerar feriados. Claro que apesar de não ter feriados é possível que você estenda esta ideia e incluir na lógica para não contar feriados também, por exemplo, ter um cadastro com os feriados e dias não trabalhados e ir consultando por uma lista com LINQ (find).
Segue o método:


        public int GetDiffDays(DateTime initialDate, DateTime finalDate)
        {
            int days = 0;
            int daysCount = 0;
            days = initialDate.Subtract(finalDate).Days;

            //Módulo 
            if (days < 0)
                days = days * -1;

            for (int i = 1; i <= days; i++)
            {
                initialDate = initialDate.AddDays(1);
                //Conta apenas dias da semana.
                if (initialDate.DayOfWeek != DayOfWeek.Sunday && 
                    initialDate.DayOfWeek != DayOfWeek.Saturday)
                    daysCount++;
            }
            return daysCount;
        }

Por hoje é isso, simples assim. “Quem complica se estrumbica”.
Aproveitando, caso você queria algum tópico escreva e deixe a sua sugestão.
Siga-me no twitter: @thiagosatoshi

“A mente que se abre a uma nova idéia jamais voltará a seu tamanho original.” – Albert Einstein

17 comentários sobre “CShared #1 – [C#] Contagem de dias úteis entre duas datas.

  1. Caro Thiago,

    parabens pela iniciativa de postar código. É mais do que a maioria de nós faz, e por isso é raro achar coisa boa em português.

    Quanto ao seu código, embora para um cálculo ou outro seja rápido, a performance dele será sofrivel, devido ao loop, especialmente para intervalos grandes.

    Uma alternativa, embora custosa em inicialização, é implementar uma classe com um array com os dias uteis contatos a partir de uma data inicial. O cálculo de prazos torna-se então mero lookup no array e subtração.

    Nossa biblioteca de calendários, usada em softwares financeiros, que implementa essa idéia, faz cerca de 1 milhão de operações dessas por segundo. Um algoritmo como o seu fará cerca de 10 mil/s.

    Abração! Keep posting!

    1. JP Negri,

      E de que maneira é possível aumentar tão absurdamente a performance?
      Não tenho como mensurar seu código otimizado, porque na verdade, não tenho o código.
      É muito bom ter essa sua visão, mas como posso saber se a performance aumenta sem comparar os dois códigos?
      Se puder deixe um exemplo, quem sabe com ele muitos mais poderão ter um norte neste tipo de implantação.
      Abraço

  2. Ola JP Negri,
    Legal ter comentado a postagem, afinal temos que debater e encontrar a melhor solução.
    De acordo com meus testes uma contagem com esta função de uma data do ano de 1900 até outra do ano de 2010, é um calculo que o retorno é apresentado em questão de mili-segundos (em média 15 pra ser mais extato). Visto ainda que, acredito ser meio dificil você fazer uma conta destas por um periodo tão longo. Por exemplo pro meu caso a função é feita para um periodo de 1 ou talvez 2 meses (que no meu caso da 1 mili-segundo).
    Sinceramente não entendi muito bem a solução que você esta propondo, mas fique a vontade em postar pra que todos possamos aprender.
    Outro ponto fundamental, é que não sei o custo de sua implementação. Aparentemente é meio alto (pelo que pude notar em seu comentário) e não sei se irá otimizar uma performance da questão de milisegundos e se vale o tempo para implementa-la, afinal isso impacta em custo para você e no final você repassa este custo pra seu cliente, ou seja, não sei se esta “melhor implentação” existe de fato para ter um ganho tão absurdo assim.

    Abraço

  3. Olá JP Negri,

    Nos do C# Brasil agradecemos o seu comentário.
    Complementando os demais comentarios, você disse que sua “biblioteca de calendários, usada em softwares financeiros, que implementa essa idéia, faz cerca de 1 milhão de operações dessas por segundo”.
    Mais ai te pergunto, qual a vantagem de utilizar sua biblioteca para realizar a mesma operação. A diferença que sua solução está mascarando o calculo. Não sei o que ele faz no final das contas. Alias acredito que ele faz o mesmo calculo.
    Eu mesmo fiz o teste com o algoritmo do Thiago Suzuki e o resultado é satisfatório.
    Outro pequeno detalhe, a solução postada é algo simples e não é viavel utilizar soluções de terceiros para esse fim.

    Esperamos que você posso detalhar mais sua solução para nós. Alias se nos disponibilizar poderemos testar e criar um artigo sobre ele.

    Obrigado pelo seu comentário.
    Abraço

  4. Caros,

    Obrigado por comentarem!

    Quero sim compartilhar o código que usamos. Não é uma biblioteca de terceiros, foi feita por nós. Como faço para postar um pequeno projeto?

    De fato, o algoritmo de Thiago funciona e é simples (Simples == Bom), mas se for aplicado dentro de um loop apertado, por exemplo no cálculo de 10 milhões de operações financeiras com juros calculados em dias úteis pode causar um impacto significativo. Fica ainda mais sério se for necessário calcular esse set de operações 100 mil vezes para fazer uma simulação Monte-Carlo.

    Obviamente não existe mágica. É sempre um Trade de Simplicidade do Código x Performance e Velocidade x Memória. O algoritmo (que quero muito mostrar) é inspirado na caminhada de Eratosthenes e a inicialização ocorre em tempo linear, e consome alguns vetores.

    Aguardo instruções de como postar um projeto.

    Abraços,

  5. Apenas algumas observações sobre o código postado.
    Seria interessante trocar as linhas

    int days = 0;
    int daysCount = 0;
    days = initialDate.Subtract(finalDate).Days;

    //Módulo
    if (days < 0)
    days = days * -1;

    (…)

    Por

    if(initialDate > finalDate)
    {
    return GetDiffDays(finalDate, initialDate);
    }

    int days = finalDate.Subtract(initialDate).Days;

    int daysCount = 0;
    (…)

    Explico:

    1 – A váriavel days é iniciada com seu valor, não precisando fazer o módulo. Aliás, para fazer o módulo poderia utilizar a biblioteca Math.
    2 – Fica mais claro que daysCount será utilizada no loop.
    3 – No código original, caso a data final for menor que a data inicial, a conta é feita errada e não é retornado nenhum erro. Da nova forma ele faz o cálculo não importando qual data é maior, portanto talvez devesse trocar os nomes para date1 e date2, ou algo similar.
    – Se considerar que a initialDate é sempre menor ou igual a finalDate então era melhor colocar um Debug.Assert ou um teste similar ao da nova forma. E, mesmo assim trocando a inicialização de days.

  6. Caros,

    Assumindo um cálculo de dias úteis seguindo a regra bancária, que inclui o primeiro dia (se útil) e exclui o último dia (se útil) fiz duas implementações da pequena interface definida abaixo.

    public interface ISimpleCalendar
    {
    int GetDeltaWorkDays(DateTime initialDate, DateTime finalDate);
    bool IsWorkDay(DateTime date);
    int GetDeltaActualDays(DateTime initialDate, DateTime finalDate);
    }

    O algoritmo inicial de Thiago, correto sem dúvida, apenas ajustado à regra bancária pode ser implementado como

    public class SimpleNonCachedCalendar : ISimpleCalendar
    {

    public int GetDeltaWorkDays(DateTime initialDate, DateTime finalDate)
    {
    initialDate = initialDate.Date;
    finalDate = finalDate.Date;

    if (initialDate == finalDate) return 0;

    if (initialDate > finalDate)
    return -GetDeltaWorkDays(finalDate, initialDate);

    Debug.Assert(initialDate 0);

    for (int i = 0; (i < days) && (initialDate < finalDate); i++)
    {
    //Conta apenas dias da semana.
    if (IsWorkDay(initialDate))
    {
    daysCount++;
    }

    initialDate = initialDate.AddDays(1);
    }
    return daysCount;
    }

    public bool IsWorkDay(DateTime date)
    {
    return (date.DayOfWeek != DayOfWeek.Sunday) && (date.DayOfWeek != DayOfWeek.Saturday);
    }

    public int GetDeltaActualDays(DateTime initialDate, DateTime finalDate)
    {
    return initialDate == finalDate ? 0 : (finalDate.Date – initialDate.Date).Days;
    }
    }

    Esta versão, numa maquina Core i7, 2.4GHz, 8 CPUs fez quase 8 mil operações de dias úteis por segundo, com um prazo médio entre as datas de 5 anos.

    Uma implementação suportando feriados, e mais rápida, está abaixo.

    public class CachedCalendar : ISimpleCalendar
    {
    // Os feriados
    private readonly HashSet _holidays = new HashSet();

    // os periodos, em dias uteis, acumulados
    private readonly DateTime _maxDate;
    private readonly DateTime _minDate;
    private readonly int[] _period;

    public CachedCalendar(IEnumerable holidays)
    {
    _minDate = DateTime.MaxValue;
    _maxDate = DateTime.MinValue;
    foreach (DateTime date in holidays)
    {
    DateTime dateDay = date.Date;
    if (dateDay _maxDate) _maxDate = dateDay;
    _holidays.Add(dateDay);
    }

    if (_holidays.Count == 0)
    {
    // Se não houverem feriados, inventa um, num domingo
    DateTime today = DateTime.Today;
    DateTime sunday = today.AddDays((-1)*(int) today.DayOfWeek);
    _holidays.Add(sunday.Date);
    _minDate = _maxDate = sunday.Date;
    }

    // As datas maximas e minimas serão os feriados extremos mais 20 anos no passado ou no futuro
    _minDate = _minDate.AddMonths(-12*20);
    _maxDate = _maxDate.AddMonths(+12*20);

    // dimensiona os vetores de calculo rapido
    TimeSpan deltaMax = _maxDate – _minDate;
    int size = deltaMax.Days + 1;

    var days = new int[size];
    _period = new int[size];

    // preenche o vetor de trabalho
    DateTime curDate = _minDate;
    for (int i = 0; i < size; ++i)
    {
    days[i] = IsWorkDay(curDate) ? 1 : 0;
    curDate = curDate.AddDays(1);
    }

    // Preenche o vetor de dias uteis acumulados entre a data inicial e data corrente.
    _period[0] = days[0];
    for (int i = 1; i finalDate)
    return -GetDeltaWorkDays(finalDate, initialDate);

    Debug.Assert(initialDate < finalDate);

    if ((initialDate _maxDate))
    throw new ArgumentOutOfRangeException(“initialDate”, initialDate,
    string.Format(
    “A data inicial deve estar em [{0:yyyy-MM-dd}, {1:yyyy-MM-dd}].”,
    _minDate, _maxDate));

    if ((finalDate _maxDate))
    throw new ArgumentOutOfRangeException(“finalDate”, finalDate,
    string.Format(
    “A data final deve estar em [{0:yyyy-MM-dd}, {1:yyyy-MM-dd}].”,
    _minDate, _maxDate));

    // cálculo dos Índices
    TimeSpan timeSpan = initialDate – _minDate;
    int initialIndex = timeSpan.Days;

    timeSpan = finalDate – _minDate;
    int finalIndex = timeSpan.Days;

    int delta = _period[finalIndex] – _period[initialIndex];

    bool iniWorkday = IsWorkDay(initialDate);
    bool endWorkday = IsWorkDay(finalDate);

    // Ajustes para obedecer a regra de contar o primeiro (se util), e excluir o ultimo (se util)
    if ((delta == 0) && iniWorkday && !endWorkday)
    {
    ++delta;
    }
    else if (!iniWorkday && endWorkday)
    {
    –delta;
    }
    else if (iniWorkday && !endWorkday)
    {
    ++delta;
    }

    return delta;
    }

    public bool IsWorkDay(DateTime date)
    {
    return (date.DayOfWeek != DayOfWeek.Sunday) && (date.DayOfWeek != DayOfWeek.Saturday) &&
    !_holidays.Contains(date.Date);
    }

    public int GetDeltaActualDays(DateTime initialDate, DateTime finalDate)
    {
    return initialDate == finalDate ? 0 : (finalDate.Date – initialDate.Date).Days;
    }
    }

    A versão cached pré-calcula, em tempo linear, na inicialização os prazos em dias úteis em relação a uma data base.

    Essa versão dá os mesmos resultados, mas na mesma maquina, faz pouco mais de 2.7 milhões de operações por segundo.

    Novamente, a versão original de Thiago está correta e pode ser usada em aplicações simples em que esse cálculo não seja solicitado em condições extremas.

    Em condições extremas, melhor usar um algoritmo mais eficiente, ainda que pagando o custo de memória e inicialização.

    Uma solução completa, do Visual Studio 2008, incluindo testes unitários e aplicação de comparação pode ser baixado aqui. O uso do código é livre, de acordo com a WTFPL 2, sem garantias.

    Saudações,

    JP Negri

  7. Acho que o comentário anterior meu ficou muito longo… houve corte do código da implementação CachedCalendar… melhor baixar o projeto.

    []´s JP Negri

  8. Galera,

    Eu achei muito bacana a idéia de vocês, mas o algorítimo continua sendo pelo menos O(n) por causa do loop, então achei legal compartilhar o me algorítimo com vocês, que é O(1), atualmente ele leva 0,055ms pra executar (sem contar o tempo que gasto pra obter a lista de feriados do meu cache corporativo)

    —->
    A idéia é muito simples:
    * Pego a quantidade de dias corridos
    * Pra cada sete dias completos tiro 2 do total
    * Pro resto, somo com o DayOfWeek da datainicial e se for 6 significa q peguei só o sábado da última semana, então subtraio 1 do total, se for 7 ou mais significa que peguei sábado e domingo da última samana, então subtraio 2 do total.
    * Depois subtraio o número de feriados que não deram em final de semana. (busco a lista do meu cache corporativo)

    int value = 0;
    // Cache corporativo
    var feriados = GetFeriadosNoWeekend();

    var dt = dtInicio;

    // Na minha regra de negócio não pode começar a contagem em dia não útil, então pulo a data início pro primeiro dia útil a partir da data início informada. Esse while pode ser comentado, caso não seja o caso.
    while (dt.DayOfWeek == DayOfWeek.Saturday || dt.DayOfWeek == DayOfWeek.Sunday || feriados.Contains(dt)) {
    dt = dt.AddDays(1);
    }

    var qtdFeriados = (from d in feriados
    where d > dt && d 0 ? diasCorridos : 0;

    value = diasCorridos;
    value -= (diasCorridos / 7) * 2;
    value -= (diasCorridos % 7) + (int)dt.DayOfWeek >= 7 ? 1 : 0;
    value -= (diasCorridos % 7) + (int)dt.DayOfWeek >= 6 ? 1 : 0;
    value -= qtdFeriados;

  9. Tentando denovo…

    var qtdFeriados = (from d in feriados
    where d > dt && d <= dtFim
    select d).Count();

    var diasCorridos = (dtFim – dt).Days;
    diasCorridos = diasCorridos > 0 ? diasCorridos : 0;

  10. Altere a linha:

    days = initialDate.Subtract(finalDate).Days;

    para:

    days = initialDate.Subtract(finalDate).TotalDays;

    para realizar o cálculo corretamente, o algoritmo atual somente dá certo se a diferença entre as datas for menor que um mês

  11. Como faço para contar quantas segundas e terças tem entre uma data inicial e outra

    exemplo (30/09/2013 ao 10/10/2013): 2 segundas, 2 terças.

    mais ou meno isso.

    valeu!

    1. Eu faria dessa forma:


      DateTime data1 = new DateTime(2013, 09, 30);
      DateTime data2 = new DateTime(2013, 10, 10);
      DateTime dataaux = data1;

      List< DateTime > lst = new List< DateTime >();
      while (data2 != dataaux)
      {
      lst.Add(dataaux);
      dataaux = dataaux.AddDays(1);
      }

      int domingo = lst.Where(x => x.DayOfWeek == DayOfWeek.Sunday).Count();
      int segunda = lst.Where(x => x.DayOfWeek == DayOfWeek.Monday).Count();
      int terca = lst.Where(x => x.DayOfWeek == DayOfWeek.Tuesday).Count();
      int quarta = lst.Where(x => x.DayOfWeek == DayOfWeek.Wednesday).Count();
      int quinta = lst.Where(x => x.DayOfWeek == DayOfWeek.Thursday).Count();
      int sexta = lst.Where(x => x.DayOfWeek == DayOfWeek.Friday).Count();
      int sabado = lst.Where(x => x.DayOfWeek == DayOfWeek.Saturday).Count();

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *