martes, 12 de octubre de 2010

Sumar sólo días laborales en C#

Una funcionalidad que he buscado durante mucho tiempo en Internet es cómo hacer un AddDays que agregue sólo los días laborales.

Adicional a esto, la funcionalidad debe ser para C# porque la quiero utilizar en un proyecto WEB en IIS de SalesLogix.  Si alguien conoce una funcionalidad de SalesLogix que lo haga le agradezco me notifique porque no hay documentación de esto.

Al final, si existe o no esta funcionalidad, yo hice mi propia función y aquí les detallo las consideraciones y el código fuente de la función para que puedan adaptarla a sus necesidades.  La función que yo diseñé tenía que cumplir con lo siguiente:

  1. Debe permitir el AddDays sólo de días laborales de lunes a viernes (aquí en Panamá no se trabaja sábados ni domingos en algunas empresas).
  2. Debe contemplarse una tabla de días feriados la cual tendrá una vista de mantenimiento manual.
  3. Sólo se van a agregar días completos, no fracciones de días, y no importa la hora del día en que se haga el AddDays.
Luego de mucho investigar no encontré ninguna función que cumpliera al 100% estos requerimientos, o que funcionara completamente sin errores ni cálculos errados.

Al analizar un poco los códigos encontrados, todos terminaban en 1 bucle para contar los días 1 por 1 como si estuviéramos frente a un calendario.

Las otras funciones que no utilizan el conteo, se basan en cálculos para contar los días, pero todos caen en un mismo error, que consiste en dividir la cantidad de días que queremos sumar entre 7 para saber el número de fines de semana que hay, y agregar este número al número original.

Esto es un completo error, porque están dividiendo días que aún no han transcurrido, por ejemplo, si quiero una actividad que va a durar 6 días, no me está contemplando el fin de semana en el que ya estaríamos cayendo.  Lo correcto, es dividirlo entre 5, que son los días realmente laborales, y al final sumarle los 2, ven?  No es lo mismo sumarle 2 por cada 7, que sumarle 2 por cada 5.  Pero mucho cuidado, esto es considerando que es una suma, si por el contrario quisieran sacar las semanas o el número de días laborales entre 2 fechas (diferencia de días, subtract o datediff), no es la misma situación y entonces si es correcto dividir entre 7, porque las 2 fechas si tienen los 2 días del fin de semana de por medio.

Lo otro es lo de la tabla de feriados.  La mayoría de los códigos verifican esta tabla por cada uno de los días a sumar, esto también me pareció muy ineficiente.

Mi solución es la siguiente:

protected DateTime DateAgregarLaborales(Int32 add, DateTime FechaInicial)
{
    if (FechaInicial.DayOfWeek == DayOfWeek.Saturday) { FechaInicial=FechaInicial.AddDays(2); }
    if (FechaInicial.DayOfWeek == DayOfWeek.Sunday) { FechaInicial=FechaInicial.AddDays(1); }
    Int32 weeks = add / 5;
    add += weeks * 2;
    if (FechaInicial.DayOfWeek > FechaInicial.AddDays(add).DayOfWeek) { add += 2; }
    if (FechaInicial.AddDays(add).DayOfWeek == DayOfWeek.Saturday) { add +=2; }
    Int32 libres =  LibresEntre(FechaInicial,FechaInicial.AddDays(add));

    if (libres>0) { return DateAgregarLaborales(0,FechaInicial.AddDays(libres+
add)); }
    else { return FechaInicial.AddDays(add); }
}

Las primeras 2 líneas verifican si el día en que se está haciendo la suma es sábado o domingo, esto para no contemplarlo porque queremos saltarnos esos días.

Los días se dividen entre 5 y se multiplica el resultado por 2 para sacar los días libres por cada semana.

Se verifica si el día final es menor al día actual lo cual valida sumas de intervalos menores a 5 o que la fecha quede en domingo (es 0 en C#), y si se estima que el día final es sábado, se suman nuevamente 2 días.

Al final se está consultando una función que se llama LibresEntre para consultar a la tabla de feriados y ver si hay días libres entre la fecha inicial y la resultante, y si resulta haber uno o más días libres, se llama recursivamente a la misma función para evitar caer nuevamente en días no laborales.  Esto traerá como resultado que a lo sumo, sólo habrá 1 iteración entre la función inicial y su re-llamada.

Esta es la función para los días libres, que está pensada para MS SQL:

protected Int32 LibresEntre(DateTime Fi, DateTime Ff) 
{
    Sage.Platform.Data.IDataService service = Sage.Platform.Application.ApplicationContext.Current.Services.Get<Sage.Platform.Data.IDataService>();
    System.Data.IDbConnection cnn = service.GetConnection();
    cnn.Open();
    System.Data.IDbCommand cmd = cnn.CreateCommand();
    string sql = "select isnull(sum(case when datepart(dw,fecha_feriado)=1 or datepart(dw,fecha_feriado)=7 then 0 else 1 end),0)";
    sql += " from dias_feriados where fecha_feriado>='" + Fi.ToString("MM/dd/yyyy") + " 00:00:00' and fecha_feriado<='" + Ff.ToString("MM/dd/yyyy") + " 24:00:00'";
    cmd.CommandText = sql;
    System.Data.IDataReader MyDataReader = cmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
    Int32 dias  = 0;
    while (MyDataReader.Read())
    {
        dias = MyDataReader.GetInt32(0);
    }
    MyDataReader.Close();
    cnn.Close();
    cmd.Dispose();

    return dias; 
  }

Esta función devuelve los días feriados no sábado y no domingo entre el rango de fechas.

Lo último que deben tomar en cuenta es que cuando hagan su programa, recuerden hacer que FechaInicial =  DateAgregarLaborales(0, DateTime.Now()) para que la fecha inicial también sea considerada.

Por favor, si esta función les ayuda en su trabajo, hagan clic en los Ads que vean.