segunda-feira, 19 de maio de 2014

O problema das Cifras... E a dor de cabeça que levou uma noite a resolver!

Recentemente num projecto em que estive a trabalhar, tive de cifrar uma quantidade considerável de dados. A primeira coisa que me surgiu na mente foi usar hash, mas a necessidade de reverter a string cifrada para plain-text invalidou essa ideia.

Foi por essa altura que pensei "bem isto era porreiro encriptar em AES!", mas aí surgiu-me uma outra questão: Como o fazer ? Como gerar key-rings para cada registo ? Depois de indagar umas horas, acabei adoptando uma forma de encriptar registos e desencripar os mesmos de forma simples e quase instantânea, usando key-rings diferentes para cada registo.  

Problemas: 
  • Não repetir key-rings
  • Evitar paterns
  • Encriptar quantidades massivas de dados 
  • Fazê-lo de forma minimamente robusta 
A solução:

Umas pesquisas na web, não resultaram em grande coisa. Ou encontrava implementações que não funcionávam, ou tinham memory leaks, e nenhuma delas fazia tudo o que eu precisava.

Chegado a esta conclusão "volta-se ao quadro e desenha-se"! Depois de muitos rabiscos, lá começou a tomar forma aquilo que eu pretendia.

Uma classe que gera-se strings aleatórias de tamanho definido no construtor, e uma classe que suporta-se a encriptação, AES usando algoritmo Rijndael. Nesta faze decidi colocar a private key hardcoded na app, uma vez que o meu objectivo não se concentrava em proteger a app, mas proteger os dados, e o elo mais fraco seria o servidor de base de dados.


Neste caso a classe que encriptaria os dados é a seguinte:

Class: EncryptStringDino 
 


 using System;  
 using System.Text;  
 using System.Security.Cryptography;  
 using System.IO;  
   
 namespace EncryptStringDino  
 {  
   public static class StringCipher  
   {  
     private const string initVector = "EfK7yhF3HKywfvXp";  
   
     private const int keysize = 256;  
   
     //sumary  
     //metodo Encrypt  
     //exemplo: string cifrado = Encrypt(texto_a_cifrar, key)  
     //key é uma contra-senha que tanto pode ser uma constante como um valor de uma veriável  
     //devolve uma string correspondente ao texto a cifrar, cifrado recorrendo ao standard AES metodologia Rijndael  
     public static string Encrypt(string plainText, string passPhrase)  
     {  
       byte[] initVectorBytes = Encoding.UTF8.GetBytes(initVector);  
       byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText);  
       PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);  
       byte[] keyBytes = password.GetBytes(keysize / 8);  
       RijndaelManaged symmetricKey = new RijndaelManaged();  
       symmetricKey.Mode = CipherMode.CBC;  
       ICryptoTransform encryptor = symmetricKey.CreateEncryptor(keyBytes, initVectorBytes);  
       MemoryStream memoryStream = new MemoryStream();  
       CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write);  
       cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);  
       cryptoStream.FlushFinalBlock();  
       byte[] cipherTextBytes = memoryStream.ToArray();  
       memoryStream.Close();  
       cryptoStream.Close();  
       return Convert.ToBase64String(cipherTextBytes);  
     }  
   
     //sumary  
     //metodo Dncrypt  
     //exemplo: string cifrado = Encrypt(texto_cifrado, key)  
     //key é uma contra-senha que tanto pode ser uma constante como um valor de uma veriável  
     //devolve uma string correspondente ao texto a cifrar, cifrado recorrendo ao standard AES metodologia Rijndael  
     public static string Decrypt(string cipherText, string passPhrase)  
     {  
       byte[] initVectorBytes = Encoding.ASCII.GetBytes(initVector);  
       byte[] cipherTextBytes = Convert.FromBase64String(cipherText);  
       PasswordDeriveBytes password = new PasswordDeriveBytes(passPhrase, null);  
       byte[] keyBytes = password.GetBytes(keysize / 8);  
       RijndaelManaged symmetricKey = new RijndaelManaged();  
       symmetricKey.Mode = CipherMode.CBC;  
       ICryptoTransform decryptor = symmetricKey.CreateDecryptor(keyBytes, initVectorBytes);  
       MemoryStream memoryStream = new MemoryStream(cipherTextBytes);  
       CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read);  
       byte[] plainTextBytes = new byte[cipherTextBytes.Length];  
       int decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);  
       memoryStream.Close();  
       cryptoStream.Close();  
       return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);  
     }  
   }  
 }  


 Class: RamdomStringGeneratorDino , que vai gerar as strings aleatórias que vão fazer parte do processo de encriptação.

Este código foi alterado diversas vezes porque causava constantemente erros e problemas com a CLR, ao ponto de ter de ser alterado de novo, quando começou a gerar exceptions quando pedido que gera-se um volume de cerca de 2000 strings. 

Class: RamdomStringGeneratorDino
  
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Security.Cryptography;  
   
 namespace RamdomStringGeneratorDino  
 {  
   /// <summary>  
   /// Classe geradora de strings aleatórias de acordo com as opções abaixo listadas  
   /// 1) 4 caracteres (maiusculo, minusculo, numerico e caracteres especiais)  
   /// 2) numero variável de caracteres em uso  
   /// 3) numero minimo de caracteres de cada tipo a serem usados na string  
   /// 4) Geração orientada a patterns  
   /// 5) geração de strings unicas  
   /// 6) usar cada caracter apenas uma vez  
   /// feito para gerar "keyt" para senhas Rjindael  
   /// </summary>  
   public class RandomStringGenerator  
   {  
     public RandomStringGenerator(bool UseUpperCaseCharacters = true,  
                    bool UseLowerCaseCharacters = true,  
                    bool UseNumericCharacters = true,  
                    bool UseSpecialCharacters = true)  
     {  
       m_UseUpperCaseCharacters = UseUpperCaseCharacters;  
       m_UseLowerCaseCharacters = UseLowerCaseCharacters;  
       m_UseNumericCharacters = UseNumericCharacters;  
       m_UseSpecialCharacters = UseSpecialCharacters;  
       CurrentGeneralCharacters = new char[0]; // evita excepções de null  
       UpperCaseCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();  
       LowerCaseCharacters = "abcdefghijklmnopqrstuvwxyz".ToCharArray();  
       NumericCharacters = "0123456789".ToCharArray();  
       SpecialCharacters = ",.;:?!/@#$%^&()=+*-_{}[]<>|~".ToCharArray();  
       MinUpperCaseCharacters = MinLowerCaseCharacters = MinNumericCharacters = MinSpecialCharacters = 0;  
       RepeatCharacters = true;  
       PatternDriven = false;  
       Pattern = "";  
       Random = new RNGCryptoServiceProvider();  
       ExistingStrings = new List<string>();  
     }  
   
     #region character sets managers  
     /// <summary>  
     /// True se precisar-mos de um numero de caracteres fixo  
     /// </summary>  
     public bool UseUpperCaseCharacters  
     {  
       get  
       {  
         return m_UseUpperCaseCharacters;  
       }  
       set  
       {  
         if (CurrentUpperCaseCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentUpperCaseCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentUpperCaseCharacters).ToArray();  
         m_UseUpperCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obter a definição de caracteres maiusculos.  
     /// </summary>  
     public char[] UpperCaseCharacters  
     {  
       get  
       {  
         return CurrentUpperCaseCharacters;  
       }  
       set  
       {  
         if (UseUpperCaseCharacters)  
         {  
           if (CurrentUpperCaseCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentUpperCaseCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentUpperCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é obritatório o uso de minusculas  
     /// </summary>  
     public bool UseLowerCaseCharacters  
     {  
       get  
       {  
         return m_UseLowerCaseCharacters;  
       }  
       set  
       {  
         if (CurrentLowerCaseCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentLowerCaseCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentLowerCaseCharacters).ToArray();  
         m_UseLowerCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres minusculos  
     /// </summary>  
     public char[] LowerCaseCharacters  
     {  
       get  
       {  
         return CurrentLowerCaseCharacters;  
       }  
       set  
       {  
         if (UseLowerCaseCharacters)  
         {  
           if (CurrentLowerCaseCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentLowerCaseCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentLowerCaseCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é necessário o uso de caracteres numéricos  
     /// </summary>  
     public bool UseNumericCharacters  
     {  
       get  
       {  
         return m_UseNumericCharacters;  
       }  
       set  
       {  
         if (CurrentNumericCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentNumericCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentNumericCharacters).ToArray();  
         m_UseNumericCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres numéricos  
     /// </summary>  
     public char[] NumericCharacters  
     {  
       get  
       {  
         return CurrentNumericCharacters;  
       }  
       set  
       {  
         if (UseNumericCharacters)  
         {  
           if (CurrentNumericCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentNumericCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentNumericCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// True se é para usar caracteres especiais  
     /// </summary>  
     public bool UseSpecialCharacters  
     {  
       get  
       {  
         return m_UseSpecialCharacters;  
       }  
       set  
       {  
         if (CurrentSpecialCharacters != null)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentSpecialCharacters).ToArray();  
         if (value)  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(CurrentSpecialCharacters).ToArray();  
         m_UseSpecialCharacters = value;  
       }  
     }  
   
     /// <summary>  
     /// define ou obtem a definição de uso de caracteres especiais  
     /// </summary>  
     public char[] SpecialCharacters  
     {  
       get  
       {  
         return CurrentSpecialCharacters;  
       }  
       set  
       {  
         if (UseSpecialCharacters)  
         {  
           if (CurrentSpecialCharacters != null)  
             CurrentGeneralCharacters = CurrentGeneralCharacters.Except(CurrentSpecialCharacters).ToArray();  
           CurrentGeneralCharacters = CurrentGeneralCharacters.Concat(value).ToArray();  
         }  
         CurrentSpecialCharacters = value;  
       }  
     }  
     #endregion  
   
     #region character limits  
     /// <summary>  
     /// Define ou obtem o numero minumo de caracteres maiusculos a serem usados.  
     /// </summary>  
     public int MinUpperCaseCharacters  
     {  
       get { return m_MinUpperCaseCharacters; }  
       set { m_MinUpperCaseCharacters = value; }  
     }  
   
     /// <summary>  
     /// Define ou obtem o numero minimo de caracteres minusculos a serem usados.  
     /// </summary>  
     public int MinLowerCaseCharacters  
     {  
       get { return m_MinLowerCaseCharacters; }  
       set { m_MinLowerCaseCharacters = value; }  
     }  
   
     /// <summary>  
     /// define ou obtem o numero minimo de caracteres numéricos a sere utilizados.  
     /// </summary>  
     public int MinNumericCharacters  
     {  
       get { return m_MinNumericCharacters; }  
       set { m_MinNumericCharacters = value; }  
     }  
   
     /// <summary>  
     /// define ou obtem o numero minimo de caracteres especiais a serem utilizados.  
     /// </summary>  
     public int MinSpecialCharacters  
     {  
       get { return m_MinSpecialCharacters; }  
       set { m_MinSpecialCharacters = value; }  
     }  
     #endregion  
   
     #region pattern  
     private string m_pattern;  
   
     /// <summary>  
     /// Define o padrão a ser seguido para gerar uma string.   
     /// Este valor é ignorado se for igual string vazia.   
     /// Os padrões são:   
     /// L - para letra maiúscula   
     /// L - para letra minúscula   
     /// N - de número   
     /// S - para caractere especial   
     /// * - Para qualquer caractere  
     /// </summary>  
     private string Pattern  
     {  
       get  
       {  
         return m_pattern;  
       }  
       set  
       {  
         if (!value.Equals(String.Empty))  
           PatternDriven = true;  
         else  
           PatternDriven = false;  
         m_pattern = value;  
       }  
     }  
     #endregion  
   
     #region generators  
     /// <summary>  
     /// Gerar uma string que segue o padrão.   
     /// Caracteres possíveis são:   
     /// L - para letra maiúscula   
     /// L - para letra minúscula   
     /// N - de número   
     /// S - para caractere especial   
     /// * - Para qualquer caracter  
     /// </summary>  
     /// <param name="Pattern">o pattern na ser seguido enquanto gera as strings</param>  
     /// <returns>um padrão aleatório que é retornado após a geração</returns>  
     public string Generate(string Pattern)  
     {  
       this.Pattern = Pattern;  
       string res = GenerateString(Pattern.Length);  
       this.Pattern = "";  
       return res;  
     }  
   
     /// <summary>  
     /// gera uma string de comprimento variável compreendido entre MinLength e MaxLength. Os caracteres   
     /// devem ser definidos antes de ser chamada esta função  
     /// </summary>  
     /// <param name="MinLength">cumprimento minimo da string string</param>  
     /// <param name="MaxLength">Cumprimento maximo da string</param>  
     /// <returns>uma string aleatória de um tamanho compreendido entre o minimo e o maximo</returns>  
     public string Generate(int MinLength, int MaxLength)  
     {  
       if (MaxLength < MinLength)  
         throw new ArgumentException("Maximal length should be grater than minumal");  
       int length = MinLength + (GetRandomInt() % (MaxLength - MinLength));  
       return GenerateString(length);  
     }  
   
     /// <summary>  
     /// Gera uma string de comprimento fixo  
     /// os conjuntos de caracteres utilizaveis devem ser definidos antes de chamar esta função  
     /// </summary>  
     /// <param name="FixedLength">cumprimento da string</param>  
     /// <returns>uma string aleatória do comprimento desejado</returns>  
     public string Generate(int FixedLength)  
     {  
       return GenerateString(FixedLength);  
     }  
   
     /// <summary>  
     /// Metodo de geração principal que escolhe o metodo de geração adequado.  
     /// procura situações excepcionais também.  
     /// </summary>  
     private string GenerateString(int length)  
     {  
       if (length == 0)  
         throw new ArgumentException("Não se pode gerar uma string com zero caracteres");  
       if (!UseUpperCaseCharacters && !UseLowerCaseCharacters && !UseNumericCharacters && !UseSpecialCharacters)  
         throw new ArgumentException("Tem de se usar pelo menos um conjunto de caracteres! É que é burro alvin! :D");  
       if (!RepeatCharacters && (CurrentGeneralCharacters.Length < length))  
         throw new ArgumentException("não existem caracteres suficientes para gerar a string sem repetir caracteres");  
       string result = ""; // Esta string contem o resultado  
       if (PatternDriven)  
       {  
         // usando a pattern para gerar algo  
         result = PatternDrivenAlgo(Pattern);  
       }  
       else if (MinUpperCaseCharacters == 0 && MinLowerCaseCharacters == 0 &&  
            MinNumericCharacters == 0 && MinSpecialCharacters == 0)  
       {  
         // usando o algoritmo mais simples, neste caso  
         result = SimpleGenerateAlgo(length);  
       }  
       else  
       {  
         // atenção ao limite  
         result = GenerateAlgoWithLimits(length);  
       }  
       // suporte para strings unicas  
       // recursão, a possibilidade de stack overflow é grande para strings maiores que 3 chars.  
       try  
       {  
         if (UniqueStrings && ExistingStrings.Contains(result))  
           return GenerateString(length);  
         AddExistingString(result); // guarda histórico  
       }  
       catch { throw; }// intercepta o overflow e manda-o de volta (pro raio que o parta)  
       return result;  
     }  
   
     /// <summary>  
     /// gera uma string aleatória baseada na pattern  
     /// </summary>  
     private string PatternDrivenAlgo(string Pattern)  
     {  
       string result = "";  
       List<char> Characters = new List<char>();  
       foreach (char character in Pattern.ToCharArray())  
       {  
         char newChar = ' ';  
         switch (character)  
         {  
           case 'L':  
             {  
               newChar = GetRandomCharFromArray(CurrentUpperCaseCharacters, Characters);  
               break;  
             }  
           case 'l':  
             {  
               newChar = GetRandomCharFromArray(CurrentLowerCaseCharacters, Characters);  
               break;  
             }  
           case 'n':  
             {  
               newChar = GetRandomCharFromArray(CurrentNumericCharacters, Characters);  
               break;  
             }  
           case 's':  
             {  
               newChar = GetRandomCharFromArray(CurrentSpecialCharacters, Characters);  
               break;  
             }  
           case '*':  
             {  
               newChar = GetRandomCharFromArray(CurrentGeneralCharacters, Characters);  
               break;  
             }  
           default:  
             {  
               throw new Exception("O caracter '" + character + "' não é suportado");  
             }  
         }  
         Characters.Add(newChar);  
         result += newChar;  
       }  
       return result;  
     }  
   
     /// <summary>  
     ///   
     ///   
     /// </summary>  
     private string SimpleGenerateAlgo(int length)  
     {  
       string result = "";  
   
       for (int i = 0; i < length; i++)  
       {  
         char newChar = CurrentGeneralCharacters[GetRandomInt() % CurrentGeneralCharacters.Length];  
         if (!RepeatCharacters && result.Contains(newChar))  
         {  
           do  
           {  
             newChar = CurrentGeneralCharacters[GetRandomInt() % CurrentGeneralCharacters.Length];  
           } while (result.Contains(newChar));  
         }  
         result += newChar;  
       }  
       return result;  
     }  
   
     /// <summary>  
     ///   
     /// </summary>  
     private string GenerateAlgoWithLimits(int length)  
     {  
   
       if (MinUpperCaseCharacters + MinLowerCaseCharacters +  
         MinNumericCharacters + MinSpecialCharacters > length)  
       {  
         throw new ArgumentException("Sum of MinUpperCaseCharacters, MinLowerCaseCharacters," +  
           " MinNumericCharacters and MinSpecialCharacters is greater than length");  
       }  
       if (!RepeatCharacters && (MinUpperCaseCharacters > CurrentUpperCaseCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinUpperCaseCharacters");  
       if (!RepeatCharacters && (MinLowerCaseCharacters > CurrentLowerCaseCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinLowerCaseCharacters");  
       if (!RepeatCharacters && (MinNumericCharacters > CurrentNumericCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinNumericCharacters");  
       if (!RepeatCharacters && (MinSpecialCharacters > CurrentSpecialCharacters.Length))  
         throw new ArgumentException("Can't generate a string with this number of MinSpecialCharacters");  
       int AllowedNumberOfGeneralChatacters = length - MinUpperCaseCharacters - MinLowerCaseCharacters  
         - MinNumericCharacters - MinSpecialCharacters;  
   
       string result = "";  
   
       List<char> Characters = new List<char>();  
   
   
       for (int i = 0; i < MinUpperCaseCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(UpperCaseCharacters, Characters));  
       for (int i = 0; i < MinLowerCaseCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(LowerCaseCharacters, Characters));  
       for (int i = 0; i < MinNumericCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(NumericCharacters, Characters));  
       for (int i = 0; i < MinSpecialCharacters; i++)  
         Characters.Add(GetRandomCharFromArray(SpecialCharacters, Characters));  
       for (int i = 0; i < AllowedNumberOfGeneralChatacters; i++)  
         Characters.Add(GetRandomCharFromArray(CurrentGeneralCharacters, Characters));  
   
   
       for (int i = 0; i < length; i++)  
       {  
         int position = GetRandomInt() % Characters.Count;  
         char CurrentChar = Characters[position];  
         Characters.RemoveAt(position);  
         result += CurrentChar;  
       }  
       return result;  
     }  
   
     #endregion  
   
   
     public bool RepeatCharacters;  
   
   
     public bool UniqueStrings;  
   
   
     public void AddExistingString(string s)  
     {  
       ExistingStrings.Add(s);  
     }  
   
     #region misc tools  
   
     private int GetRandomInt()  
     {  
       byte[] buffer = new byte[2]; // 16 bit = 2^16 = 65576   
       Random.GetNonZeroBytes(buffer);  
       int index = BitConverter.ToInt16(buffer, 0);  
       if (index < 0)  
         index = -index; //handle de numeros negativos  
       return index;  
     }  
   
     private char GetRandomCharFromArray(char[] array, List<char> existentItems)  
     {  
       char Character = ' ';  
       do  
       {  
         Character = array[GetRandomInt() % array.Length];  
       } while (!RepeatCharacters && existentItems.Contains(Character));  
       return Character;  
     }  
     #endregion  
   
     #region internal state  
     private bool m_UseUpperCaseCharacters, m_UseLowerCaseCharacters, m_UseNumericCharacters, m_UseSpecialCharacters;  
     private int m_MinUpperCaseCharacters, m_MinLowerCaseCharacters, m_MinNumericCharacters, m_MinSpecialCharacters;  
     private bool PatternDriven;  
     private char[] CurrentUpperCaseCharacters;  
     private char[] CurrentLowerCaseCharacters;  
     private char[] CurrentNumericCharacters;  
     private char[] CurrentSpecialCharacters;  
     private char[] CurrentGeneralCharacters;  
     private RNGCryptoServiceProvider Random;  
     private List<string> ExistingStrings;  
     #endregion  
   }  
 }  
   

E por fim o código que utilizei para cifrar os dados, depois de os ter numa datagridview em windows forms:

   
  foreach (DataGridViewCell cell in row.Cells)  
           {  
             deviceid = row.Cells[0].Value.ToString();  
             plainText = row.Cells[3].Value.ToString(); //lê o código da celula 4 da grid  passPhrase = RSG.Generate(7); //gera a contrasenha  
             passcritped = EncryptStringDino.StringCipher.Encrypt(plainText, passPhrase); //encripta e armazena o valor na variável na passcripted  
             SqlCommand commands2 = new SqlCommand("UPDATE tabela SET codigo = '" + passcritped + "' , key = '" + passPhrase + "' WHERE campo1 = '" + arg1 + "' ;", conex); //conex é a conection string à base de dados 
             commands2.ExecuteNonQuery();  
               
             richTextBox1.AppendText(Environment.NewLine + arg1 + ";");  
           }  
 
E pronto, foi esta a solução que dei à questão.

Provávelmente existem soluções melhores que esta. De futuro pensarei noutras. Também estou a ponderar portar ambas as classes para .net 4.5 usando async e await para evitar alguma lentidão quando o volume de registos é grande.

E pronto, fica aqui um pedaço de código que pode dar jeito a alguém e foi implementado numa noite de insónias.

"Enquanto houver paixão pela programação e café... Haverá código!"

segunda-feira, 31 de março de 2014

Dias uteis (WorkDays) do Excell mas mais completo

Dias e horas úteis

Mais um desafio que recentemente tive de superar, num projecto. Engraçado foi o quão simples é a solução e o quão rebuscada a mente consegue ser para a encontrar.

Neste caso uma simples classe em C# permite achar as horas uteis decorridas entre duas datas e horas, sendo estas passadas como objectos Datetime.

Outras particularidades da classe é asseitar como parametro os dias feriados (excludeDays), que com pouco código até podem vir de uma tabela de uma BD.

Achei engraçada a facilidade com que se consegue instanciar, fazer os calculos e obter o tempo decorrido com precisão ao segundo.

using System;
using System.Linq;
namespace duteis
{
    public class uteis
    {
        private TimeSpan startingTime;
        private TimeSpan endingTime;
        private DayOfWeek[] excludeDays;
        public uteis(TimeSpan? startingTime, TimeSpan? endingTime, DayOfWeek[] excludeDays)
        {
            this.startingTime = startingTime ?? new TimeSpan(8, 30, 0);
            this.endingTime = endingTime ?? new TimeSpan(17, 30, 0);
            this.excludeDays = excludeDays ?? new DayOfWeek[]   
   {   
    DayOfWeek.Saturday ,   
    DayOfWeek.Sunday   
   };
        }
        public uteis()
            : this(null, null, null)
        {
        }
        public double Calculate(DateTime startDate, DateTime endDate)
        {
            var counter = startDate;
            double hours = 0;
            while (counter <= endDate)
            {
                var dayStart = counter.Date.Add(startingTime);
                var dayEnd = counter.Date.Add(endingTime);
                var nextDayStart = startDate.Date.Add(startingTime).AddDays(1);
                if (counter < dayStart)
                    counter = dayStart;
                if (excludeDays == null ||
                  excludeDays.Contains(counter.DayOfWeek) == false)
                {
                    if (endDate < nextDayStart)
                    {
                        var ticks = Math.Min(endDate.Ticks, dayEnd.Ticks) - counter.Ticks;
                        hours = TimeSpan.FromTicks(ticks).TotalHours;
                        break;
                    }
                    else if (counter.Date == startDate.Date)
                    {
                        if (counter >= dayStart && counter <= dayEnd)
                        {
                            hours += (dayEnd - counter).TotalHours;
                        }
                    }
                    else if (counter.Date == endDate.Date &&
                         startDate.Date != endDate.Date)
                    {
                        if (counter >= dayStart && counter <= dayEnd)
                        {
                            hours += (counter - dayStart).TotalHours;
                        }
                        else if (counter > dayEnd)
                        {
                            hours += (endingTime - startingTime).TotalHours;
                        }
                    }
                    else
                    {
                        hours += (endingTime - startingTime).TotalHours;
                    }
                }
                counter = counter.AddDays(1);
                if (counter.Date == endDate.Date)
                    counter = endDate;
            }
            return hours;
        }
    }
}


Atenção a estas linhas onde se define a duração do dia ùtil, e são tratados os "excludeDays" que podem ser os feriados como escrevi anteriormente.

this.startingTime = startingTime ?? new TimeSpan(8, 30, 0);  
this.endingTime = endingTime ?? new TimeSpan(17, 30, 0);  
this.excludeDays = excludeDays ?? new DayOfWeek[]  

Eliminar duplicados numa tabela SQL

Nos ultimos tempos tenho-me deparado com desafios cada vez mais interessantes e incomuns.

Desta feita, precisei de localizar e eliminar de forma eficiente registos duplicados numa tabela, onde não existia um campo identificador unico nem um autonumber, ou outro que me permitisse isolar duplicados e eliminar apenas os duplicados, deixando um registo e não as várias duplicações do mesmo.

Aqui fica o script feito para resolver esta questão, na esperança que seja útil para mais alguém! Porque cooperar é mais produtivo que competir.


 USE BD  
 GO  
 -- ADICIONA UM CAMPO ID AUTONUMER  
 ALTER TABLE dbo.tabela  
   ADD ID INT IDENTITY  
 GO  
 -- SELECT  
 SELECT *  
 FROM tabela  
 GO  
 -- DETECTA DUPLICADOS  
 SELECT campo, COUNT(*) TotalCount  
 FROM tabela  
 GROUP BY campo  
 HAVING COUNT(*) > 1  
 ORDER BY COUNT(*) DESC  
 GO  
 -- ELEMINA DUPS  
 DELETE  
 FROM tabela  
 WHERE ID NOT IN  
 (  
 SELECT MAX(ID)  
 FROM tabela  
 GROUP BY campo)  
 GO  
 -- SELECT DE CONFIRMAÇÃO  
 SELECT *  
 FROM tabela  
 GO  
 -- REMOVE INDEX CRIADO PARA REMOVER DUPLICADOS  
 ALTER TABLE tabela  
 DROP COLUMN ID  
 GO