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. 

12 comentarios:

  1. Aunque mi blog es de programación para Sage SalesLogix Web, si alguien desea que comente y puedo hacer adaptaciones a la función, o convertirla a VB.NET o C# puro (sin SalesLogix). Esta función se ha utilizado en al menos más de 25 clientes distintos por varios años y nunca ha devuelto mal las fechas.

    ResponderEliminar
  2. Hola.. la verdad es que este ejemplo es exactamente lo que estaba buscando..
    el problema es que yo trabajo con c#
    y bueno, el problema secundario es que no se que datos y que estructura tiene la tabla feriados..
    podrías explicarme mas por favor??
    Saludos cordiales
    Camilo

    ResponderEliminar
  3. Uppss... se me ha pasado revisar las entradas... bueno, la tabla feriados contiene un campo con un ID, un campo de descripción y un campo tipo datetime para poner el día feriado. Es el que en el código ves como "fecha_feriado". Este último es el campo importante, los otros que mencioné son sólo para que al darle mantenimiento la persona pueda saber por qué ese día era feriado.

    ResponderEliminar
  4. Hola puedes comentarlo en vb.net quisiera saber como sumar días sin contar sábados ni domingos. Saludos, me ayudarías mucho

    ResponderEliminar
  5. Yo lo pase a PB y vaya que si funciona como debe de ser :D, muestro el código y puse los mismos comentarios que está en el original. No pongo la función, ya que es algo obvio el select que estas poniendo.

    /***************************************************/
    Argumentos:
    Integer ai_add
    Date ad_fecha_inicial
    /***************************************************/

    Integer li_weeks
    Long ll_libres
    Date ld_fecha_fin

    /* 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. */
    IF DayName(ad_fecha_inicial) = "Saturday" THEN ad_fecha_inicial = RelativeDate(ad_fecha_inicial, 2)
    IF DayName(ad_fecha_inicial) = "Sunday" THEN ad_fecha_inicial = RelativeDate(ad_fecha_inicial, 1)

    /* Los días se dividen entre 5 y se multiplica el resultado por 2 para sacar las semanas. */
    li_weeks = ai_add / 5
    ai_add += li_weeks * 2

    /* 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 1 en PowerBuilder), y si se estima que el día final es
    sábado, se suman nuevamente 2 días. */
    IF DayNumber(ad_fecha_inicial) > DayNumber(RelativeDate(ad_fecha_inicial, ai_add)) THEN ai_add += 2
    IF DayName(RelativeDate(ad_fecha_inicial, ai_add)) = "Saturday" THEN ai_add +=2

    /***********************************************************************************/
    /* Al final se está consultando a la tabla dia_no_habil 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. */
    ld_fecha_fin = RelativeDate(ad_fecha_inicial, ai_add)
    DECLARE pb_sp_num_dias_festivos PROCEDURE FOR sp_num_dias_festivos
    @fecha_ini = :ad_fecha_inicial,
    @fecha_fin = :ld_fecha_fin;
    EXECUTE pb_sp_num_dias_festivos;
    FETCH pb_sp_num_dias_festivos INTO :ll_libres;
    CLOSE pb_sp_num_dias_festivos;
    /***********************************************************************************/

    IF ll_libres > 0 THEN
    RETURN suma_fecha_dias_habiles(ll_libres, ld_fecha_fin)
    ELSE
    RETURN ld_fecha_fin
    END IF

    Saludos!!

    ResponderEliminar
  6. Me sirvio para la fecha de vencimiento en las facturas de una empresa, Muchas gracias. Saludos

    ResponderEliminar
  7. Les voy a compartir la otra función para incluso considerar horarios laborales en una próxima entrada. Saludos y éxito!

    ResponderEliminar
  8. static void Main(string[] args)
    {
    DateTime FechaIni = new DateTime(2014, 5, 2);
    DateTime FechaFin = new DateTime(2014, 5, 27);
    int xvDiferenciaFechas = (FechaFin - FechaIni).Days;
    int xvDiasHabiles = 0;

    for (int i = 0; i < xvDiferenciaFechas; i++)
    {

    Console.WriteLine(string.Format("{0}{1}", "Dia a Validar: ",FechaIni.AddDays(i).DayOfWeek.ToString()));
    if (FechaIni.AddDays(i).DayOfWeek != DayOfWeek.Saturday && FechaIni.AddDays(i).DayOfWeek != DayOfWeek.Sunday)
    {
    xvDiasHabiles = xvDiasHabiles + 1;
    Console.WriteLine(string.Format("{0}{1}", "Es Valido :", xvDiasHabiles.ToString()));
    }
    //Console.ReadLine();
    }

    Console.ReadLine();
    }


    creo que esta respuesta es mas rapida y sencilla

    ResponderEliminar
    Respuestas
    1. Puede parecer sencilla, pero como código es el equivalente a contar con los dedos. Lo que yo necesitaba era un código que hiciera el cálculo en unas pocas iteraciones. Imagina que tus fechas son del 1 de enero de 1900 al 1 de enero de 2000. Tu código va a realizar más de 36500 iteraciones, mientras mi código, sólo 2! La salida fácil no siempre es la más eficiente, y mucho menos, la más elegante.

      Eliminar
  9. Saludos,Muy bueno tu codigo pero tengo una pregunta, te pongo este ejemplo: tengo una actividad y la voy a suspender, tiene fecha de inicio de suspension y fecha final de la suspension. necesito hacer que en el rango de esas dos fechas me omita los fines de semana, los dias no laborales y los festivos,no se como hacer la modificacion a tu codigo.si eres tan amable, gracias. c#

    ResponderEliminar
  10. Hola, perdona que no pude ver tu comentario a tiempo. Te comento, el código que di es para calcular la fecha final, y lo que tu solicitas suena a que quieres calcular cuántos días hay entre las dos fechas dadas. Para ese caso si hay muchos ejemplos en internet que puedes utilizar.

    ResponderEliminar
  11. Les paso una implementación en C# que incluye el calculo de los días hábiles hacia atrás (negativos)

    // A partir de una Fecha y una cantidad de días hábiles devuelve la fecha que resulta de aplicarle a la fecha inicial los días hábiles (Acepta NEGATIVOS).
    // Fecha a partir de la cual se quiere trabajar.
    // Cantidad de días hábiles a sumar.
    // Fecha inicial + los días hábiles.
    public static DateTime AddDiasHabiles(DateTime fechaInicial, int díasHábiles) {
    // verificamos 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.
    if (fechaInicial.DayOfWeek == DayOfWeek.Saturday) {
    fechaInicial = díasHábiles > 0 ? fechaInicial.AddDays(2) : fechaInicial.AddDays(-2);
    }
    if (fechaInicial.DayOfWeek == DayOfWeek.Sunday) {
    fechaInicial = díasHábiles > 0 ? fechaInicial.AddDays(1) : fechaInicial.AddDays(-1);
    }
    // los días se dividen entre 5 y se multiplica el resultado por 2 para sacar los días libres por cada semana.
    int semanas = díasHábiles / 5;
    díasHábiles += semanas * 2;

    // 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.
    if (díasHábiles > 0) {
    if (fechaInicial.DayOfWeek > fechaInicial.AddDays(díasHábiles).DayOfWeek) {
    díasHábiles += 2;
    }
    if (fechaInicial.AddDays(díasHábiles).DayOfWeek == DayOfWeek.Saturday) {
    díasHábiles += 2;
    }
    }
    else {
    // idem y opuesto para Negativos
    if (fechaInicial.DayOfWeek < fechaInicial.AddDays(díasHábiles).DayOfWeek) {
    díasHábiles -= 2;
    }
    if (fechaInicial.AddDays(díasHábiles).DayOfWeek == DayOfWeek.Sunday) {
    díasHábiles += -2;
    }
    }

    // se llama a la función "Feriados" 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.
    int feriados = Feriados(fechaInicial, fechaInicial.AddDays(díasHábiles));

    if (feriados > 0) {
    return AddDiasHabiles(fechaInicial.AddDays(feriados + díasHábiles), 0);
    }
    else {
    return fechaInicial.AddDays(díasHábiles);
    }
    }

    // A partir de una Fecha y una cantidad de días hábiles devuelve la cantidad de días CORRIDOS que resulta de aplicarle a la fecha inicial los días hábiles. (Acepta NEGATIVOS).
    // Fecha a partir de la cual se quiere trabajar.
    // Cantidad de días hábiles a sumar.
    // Fecha inicial + los días hábiles.
    public static int GetDiasCorridos(DateTime fechaInicial, int díasHábiles) {
    return (AddDiasHabiles(fechaInicial, díasHábiles) - fechaInicial).Days;
    }

    ResponderEliminar