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. 

martes, 7 de septiembre de 2010

Transacciones en Oracle desde ASP con C#, distribución de datos

Reciéntemente me encontré con un requerimiento en un cliente donde se necesitaba que al presionar un botón, el sistema le trajera al usuario el siguiente registro que debía ser procesado.

Para esta funcionalidad hay que considerar varios aspectos, como el hecho de que pueden haber 1 o N usuarios presionando el botón o que los registros entregados nunca deben coincidir.

Como estamos en IIS hay varias posibles soluciones, adicional, tenemos una consideración mucho más importante que hacer, y es que el cliente utiliza Oracle en su BD.  Este tipo de funcionalidad puede traer bloqueos (deadlocks), o dependiendo de la cantidad de usuarios, pueden degradar el performance de la aplicación Web a un punto frustrante.

Para no alargar la historia, vamos directo a la solución. 

Primero, yo he escogido marcar como bandera (flag) un campo de una tabla para descartar los que ya han sido actualizados, esta tabla la carga un supervisor y se la asigna a un grupo de usuarios, y en ese momento el campo viene en NULL.  Para sortear los problemas de bloqueo utilizaremos un SELECT FOR UPDATE OF NOWAIT SKIP LOCKED.

Ok, nos creamos una función en el Oracle:

create or replace FUNCTION BLOQUEADA(rid in rowid) 
return number
is
ret_id number := 0;
pragma autonomous_transaction;
begin
select 1 into ret_id from TABLA where rowid = rid FOR UPDATE OF CAMPOACTUALIZAR NOWAIT SKIP LOCKED;
rollback;
return 1;
exception
when others then
if sqlcode = -54 then
rollback;
return 0;
else
raise;
end if;
end;

Obviamente el FOR UPDATE nos hará un bloqueo (lock) de la tabla y al definir OF le estamos diciendo al query que sólo queremos que bloquee el campo específico.  El NOWAIT evita que el query se quede "pegado" esperando a que otra actualización termine y el SKIP LOCKED hace que se salte los registros que están bloqueados.  El resto de la función es mandatorio para que todo funcione bien.  Importante recordar que el bloqueo sólo dura hasta el primer COMMIT.

Luego, vamos al formulario donde queremos la funcionalidad, insertamos un botón (llamémosle Button1) y le asociamos el siguiente código al evento click (aquí van a encontrar también, cómo crear una transacción, ejecutar múltiples sentencias para hacer commit al final, y muy importante, cómo conectarse a Oracle sin usar el famoso OracleClient Net Namespace o como se llame):

string LeeSiguiente = "select ID_TABLA FROM TABLA where (nvl(CAMPOACTUALIZAR,'-1') = '-1' OR (CAMPOACTUALIZAR='1' AND (BLOQUEADA(rowid) = 1 and rownum = 1))) for update of CAMPOACTUALIZAR skip locked";

string siguiente = "SIGS";
string LockSiguiente = "UPDATE TABLA SET CAMPOACTUALIZAR='1' WHERE ID_TABLA ='$iguiente'";
// así nos conectamos a Oracle sin usar el OracleClient para NETSystem.Data.Common.DbProviderFactory dbf = System.Data.Common.DbProviderFactories.GetFactory("System.Data.OracleClient");
IDbConnection connection = dbf.CreateConnection();
connection.ConnectionString = "Data Source=SERVICIOORACLE;Persist Security Info=True;User ID=USUARIO;Password=CLAVE;Unicode=True";
// aquí se crea el comando y la transacción
IDbCommand command = connection.CreateCommand();
command.CommandType = CommandType.Text;
IDbTransaction transaction = null;
connection.Open();
// aquí comienza la transacción y asignamos el comando a la tranacción
transaction = connection.BeginTransaction();
command.Transaction = transaction;
// ejecutamos el primer comando y verificamos si trae valores
command.CommandText = LeeSiguiente;
siguiente=Convert.ToString(command.ExecuteScalar());
if (siguiente + "$" != "$" )
{
// ejecutamos el segundo comando
command.CommandText = LockSiguiente.Replace("$iguiente",siguiente);
command.ExecuteNonQuery();

} else {
siguiente = "NO_HAY_MAS";
}
// hacemos COMMIT lo cual termina la transacción
transaction.Commit();
connection.Close();

Ok, con ese código, bloqueamos el siguiente registro disponible en la tabla, pero sólo 1!, traerá los registros instantaneamente en el campo siguiente, y si no hay más disponibles, traerá el valor "NO_HAY_MAS" para que hagamos nuestras excepciones.

Espero este código les sirva de guía, porque cuando se requiere procesamiento de datos en paralelo, ya sea por usuarios, threads, etc, y en Oracle, es un verdadero problema encontrar una solución razonable.

Recuerden hacer clic en los anuncios de Google!

IIS, Sin privilegios a la Metabase

Si en alguna ocasión tienen que desarrollar algo montado sobre IIS y ASP, y tienen la mala suerte de encontrarse con este error, pueden perder horas y horas de research, cuando en realidad la solución es muy simple:
  1. Abren una ventana de comando.
  2. Se cambian al directorio C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ (ojo si su Windows está en otro disco duro, por ejemplo D...).
  3. Ejecutan el comando aspnet_regiis -ga miusuario (recuerden cambiar a miusuario por el usuario que está configurado en la página que están programando, o por el usuario que está "impersonando" el IIS).
  4. Ejecutan el comando iisreset y esperan a que termine para intentar nuevamente con su página.
Sin mucho que analizar, está más que claro que el parámetro -ga significa "grant access".

Para que no se queden con las dudas, este error ocurre por algo muy sencillo:  instalaste tu IIS luego de haber instalado el NET Framework.  Anótenlo en algún lugar, primero instalar el IIS y luego el NET Framework.

Con esto deben solucionar el problema y seguir con su desarrollo.

lunes, 6 de septiembre de 2010

SalesLogix, dulce, complejo, indispensable...

Esta es la herramienta que ADR Technologies promociona más en el área de centroamérica.

Es un CRM que incorpora muchas ideas novedosas en este tipo de sistemas.

Totalmente configurable y personalizable.

Esta herramienta cuenta con sus propios IDE's, y es en su plataforma Web que quemo mis pestañas constantemente...

Por el momento creo que será el tema principal de mis publicaciones.