domingo, 13 de abril de 2014

Como ordenar o conteúdo de um Memo com regras personalizadas

Recentemente um colega delpheiro me perguntou como ordenar um memo não pelo seu conteúdo, mas por alguma regra especial.

No caso dele, ele tinha no memo várias strings separadas por ":" com números do outro lado. Algo assim:

A:3
B:5
C:1

Devendo ficar:

C:1
A:3
B:5

Para isso podemos usar a função CustomSort do memo, passando como argumento uma função. Essa é uma função especial que aceita uma lista como parâmetro, dois índices da lista e retorna um integer. Assim ela pode comparar dois itens da lista e retornar 1 se o primeiro for maior, 0 se os itens forem iguais ou -1 se o segundo for maior.
Essa função é que fará todo o trabalho de comparação, portanto você decide como os itens serão comparados. A função CustomSort saberá que deve chamar a sua função para decidir, na varredura, qual item é maior porque você passará o nome da sua função, como se fosse um delegate ou ponteiro para função, como argumento para  a função customsort.

Os memos podem ser usados como dictionaries, ou como arquivos ini, eles podem ter pares de namve/value, e para isso você define a propriedade NameValueSeparator. Definindo esta propriedade como ":" os valores são automaticamente separados em duas partes.

O código ficou como abaixo:

//crio uma function inline que compara só dois valores e diz qual é o maior
//ela é usada automaticamente para comparar os valores 2 a 2 dentro da função
//customsort do stringlist e ordenar a parada toda
function compara(List: TStringList; Index1, Index2: Integer): Integer;
var val1: integer;
val2: integer;
begin

  //defino que o separador e chave / valor NameValueSeparator é ':', a string separadora.
  List.NameValueSeparator := ':';
  val1 :=  StrToInt( List.ValueFromIndex[Index1]);
  val2 := StrToInt( List.ValueFromIndex[Index2]);

  //quando você coloca o delimitador de um tstringlist  você pode separar ele em key e value
  //essa função pode ser anonima
  //se o valor do indice 1 for maior que o 2 retorna 1
  if(val1 > val2) then
    Result := 1
  //se for menor retorna -1
  else if(val2 > val1) then
    Result := -1
  else
    //se for igual retorna zero
    Result := 0;

  //esses resultados -1, 0 e  1 são o padrão de todos os comparers
end;



procedure TForm1.Button1Click(Sender: TObject);
  //crio um stringlist temporário para guardar o conteudo ordenado
  var str: TStringList;

begin
  str := TStringList.Create;

  //mando  o conteudo do memo para o stringlist
  str.AddStrings(Memo1.Lines);

  //mando ordenar passando como parâmetro a função ordenadora (existem
  //funções que aceitam outras funções como parâmetro, não o retorno dela, mas seu escopo, ela será executada internamente)
  str.CustomSort( compara );

  //limpa  o  memo1
  Memo1.Clear;

  //adiciona o valor ja ordenado
  Memo1.Lines.AddStrings(str);

  //destroi o stringlist
  str.Free;
end;


Have fun :)

quarta-feira, 2 de abril de 2014

Validação e conversão de datas

Dentre os problemas mais recorrentes em sistemas podemos citar os relacionados à cultura: charset, datas e formatação de números.

Datas são especialmente problemáticas porque podem ser ambíguas dependendo da cultura, por exemplo 02/01/1990 pode ser dois de janeiro no Brasil ou primeiro de fevereiro nos EUA.

Ainda mais agravante é o fato de nem sempre podermos validar as datas nos imputs, pois as mesmas podem vir de arquivos de integração, outros sistemas, CSV, TXT, Excel ou webservices.

Pensando nisso eu fiz uma pequea demonstração do uso dos comandos cast e convert para converter varchar para datas e datas para varchar, e mostrando onde estão os erros mais comuns.



/*******************************************************************************************************
******************************** DEMONSTRAÇÃO DE CONVERSÃO DE DATAS ************************************
********************************************************************************************************/

--imagine que tipo de coisa pode vir em um campo de "data" que não é datetime, ou um arquivo xml, txt, csv mal convertido
--imagine também o problema de gravar a data em uma linguagem, formatar para exibição em outra, e os problemas que sempre acontecem ao se converter string de um campo de texto para datetime

/*************************************** teste do convert *********************************/

--qual é a data  zero
select convert (datetime, 0, 103)
--R: é a data 1900-01-01 00:00:00.000

--e se a data for null?
select convert (datetime, null, 103)
--R: null

--E se a data for uma string vazia?
select convert (datetime, '', 103)
--R: é a mesma coisa que a data zero 1900-01-01 00:00:00.000 (aqui eu acho que deveria dar um erro)

--mesma coisa só que usando o padrão americano
select convert (datetime, 0, 101) 
--1900-01-01 00:00:00.000
select convert (datetime, null, 101)
--null
select convert (datetime, '', 101)
--1900-01-01 00:00:00.000


/*************************************** teste do cast *********************************/
select cast (null as datetime)
--como esperado, null
select cast (0 as datetime)
--como esperado, a data zero 1900-01-01 00:00:00.000
select cast ('' as datetime)
--1900-01-01 00:00:00.000


--agora, se em um arquivo de texto ou xml você tem uma data 0 (zero) ou '' (string vazia), isso é essencialmente um erro. Não quer dizer que a data real seja em primeiro de janeiro de 1900, mas sim que o campo não foi preenchido, logo, certos literais deveriam ser convertidos para null
select CAST(nullif(/*campo_que_pode_ser_string_vazia*/'','') as datetime)
select CAST(nullif(/*campo_que_pode_ser_zero*/0,0) as datetime)

--para todos os casos
select CAST(
  coalesce(
   nullif(/*campo_que_pode_ser_string_vazia*/'',''), --se for null tenta o proximo ...
   nullif(/*campo_que_pode_ser_zero*/0,0), --- que se for null de novo vai null mesmo
   null) as datetime)
   
   
   
/***********************************************************************************************************************
******************************************** exemplos de problemas de conversão de datas *******************************
***********************************************************************************************************************/

--fique atento quando as datas podem vir de outros bancos de dados, de sistemas que formatam de uma forma não canônica e quando você está usando o cast em vez do convert com formato específico
--troque 2014 pelo ano corrente e não faça esse teste em 1º de janeiro

--o que é maior, a data de hoje ou 2014-12-01 ?
select ( cast ('2014-12-1' as datetime)), getdate(), case when cast ('2014-12-1' as datetime) > getdate() then 'Maior' else 'Menor' end
--como estamos em março, primeiro de dezembro é maior

--o que é maior, a data de hoje ou 2014.12.1 ?
select (cast ('2014.12.1' as datetime)), getdate(), case when cast ('2014.12.1' as datetime) > getdate() then 'Maior' else 'Menor' end


--padrão Brasileiro  
--o que é maior, a data de hoje ou 2014-12-01 ?
select ( convert (datetime,'2014-12-1', 103)), getdate(), case when convert (datetime,'2014-12-1', 103) > getdate() then 'Maior' else 'Menor' end
--como estamos em março, primeiro de dezembro é maior

--o que é maior, a data de hoje ou 2014.12.1 ?
select (convert (datetime,'2014.12.1', 103)), getdate(), case when convert (datetime,'2014.12.1', 103) > getdate() then 'Maior' else 'Menor' end

   
   
--padrão americano   
--o que é maior, a data de hoje ou 2014-12-01 ?
select ( convert (datetime,'2014-12-1', 101)), getdate(), case when convert (datetime,'2014-12-1', 101) > getdate() then 'Maior' else 'Menor' end
--como estamos em março, primeiro de dezembro é maior

--o que é maior, a data de hoje ou 2014.12.1 ?
select (convert (datetime,'2014.12.1', 101)), getdate(), case when convert (datetime,'2014.12.1', 101) > getdate() then 'Maior' else 'Menor' end

     
--isso mostra que o formato AAAA-MM-DD ou AAAA.MM.DD, nessa ordem, sempre é interpretado corretamente pelo SQL, independente da linguagem. Agora coisas estranhas podem ocorrer se você obtiver uma data da qual não sabe o formato e tentar converter para datetime forçando uma formatação brasileira ou americana     
--lembre-se também que o CAST depende da data do sistema. 
--Faça de tudo para, tanto para datas como para formatos numéricos, nunca depender das informações de localização e linguagem do servidor de SQL mas depender somente do servidor WEB e do .net framework, ou, na melhor das hipóteses, o que o usuário informar em suas preferências. 

--padrão Brasileiro  
select ( convert (datetime,'01/12/2014', 103)), getdate(), case when convert (datetime,'01/12/2014', 103) > getdate() then 'Maior' else 'Menor' end
--beleza
select (convert (datetime,'01.12.2014', 103)), getdate(), case when convert (datetime,'01.12.2014', 103) > getdate() then 'Maior' else 'Menor' end
--beleza
   
   
--padrão americano   
select ( convert (datetime,'01/12/2014', 101)), getdate(), case when convert (datetime,'01/12/2014', 101) > getdate() then 'Maior' else 'Menor' end
--epa!
select (convert (datetime,'01.12.2014', 101)), getdate(), case when convert (datetime,'01.12.2014', 101) > getdate() then 'Maior' else 'Menor' end
--epa!

Porém não é apenas nas rotinas de banco de dados que devemos nos preocupar com as datas. Em sistemas data centric ou orientados a dados, ou com muitas stored procedures, as dicas acima são uma mão na roda, mas em sistemas orientados a objetos onde o problema das datas é resolvido no domínio da aplicação, via código, é bom conhecer as ferramentas disponibilizadas pelo seu ambiente.

No caso do C# temos Culture, e as ferramentas para identificar a culture corrente, sugerir uma padrão ou modificar a Culture.

Um outro problema relacionado são as datas no nosso domínio que podem ser nulas, mas que devemos em algum momento converter para string dependendo da aplicação para sua exibição. Nesses casos provavelmente você tem no seu banco de dados um campo null, e na aplicação um DateTime? nulável. Soma-se a isso o fato de a data mínima do banco de dados ser muito maior do que a data mínima na aplicação. Isso gera muito código repetitivo onde se verifica se uma data é nula, depois verifica-se se ela é a data mínima para só então fazer a conversão.
Isso pode ser feito via extension method de DateTime ou um método de uma classe estática, mais ou menos como abaixo, e poupar muito código repetitivo:

        public static string DateToString(DateTime? data)
        {
            //se for nula retorna string vazia
            if (data == null)
            {
                return "";
            }

            //não sei se é possível não ser nula e não ter um valor, acho que essas três linhas são inúteis, mas são uma alternativa às três linhas de cima
            if (!data.HasValue)
            {
                return "";
            }

            //se a data for menor ou igual a data mínima do banco de dados é porque era para ser originalmente nula e foi erroneamente gravada ou exportada de forma incorreta (para um arquivo texto com a data mínima em vez de branco, por exemplo).
            if (data.Value <= new DateTime(1900, 1, 1))
            {
                return "";
            }
            
            //...
            //código para obter a culture corrente ou escolhida pelo usuário
            //...
            
            //devolvendo a data como string
            //em um formato padrão
            //return data.Value.ToString("dd/MM/yyyy");
            //ou dependente da culture
            return data.Value.ToString(CultureEncontrada);
        }

Uma dica simples, em breve mais dicas sobre cultura, encoding e afins.

Postagens populares

Marcadores

delphi (60) C# (31) poo (21) Lazarus (19) Site aos Pedaços (15) sql (13) Reflexões (10) .Net (9) Humor (9) javascript (9) ASp.Net (8) api (8) Básico (6) Programação (6) ms sql server (5) Web (4) banco de dados (4) HTML (3) PHP (3) Python (3) design patterns (3) jQuery (3) livros (3) metaprogramação (3) Ajax (2) Debug (2) Dicas Básicas Windows (2) Pascal (2) games (2) linguagem (2) música (2) singleton (2) tecnologia (2) Anime (1) Api do Windows (1) Assembly (1) Eventos (1) Experts (1) GNU (1) Inglês (1) JSON (1) SO (1) datas (1) developers (1) dicas (1) easter egg (1) firebird (1) interfaces (1) introspecção (1) memo (1) oracle (1) reflexão (1)