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.

Nenhum comentário:

Postar um comentário

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)