Se eu fizesse um ranking das causas de falhas de aplicações que eu tive que
socorrer em toda minha vida, o número 1 em disparado seria da famigerada
configuração regional e seus impactos nas aplicações.
Chega a ser difícil de acreditar, já que o suporte a configurações regionais do
Windows vem para facilitar a vida, e não dificultar. Mas acontece que, antes do
.Net (com enorme freqüência), e mesmo depois, percebemos uma quantidade enorme
de desenvolvedores fazendo confusão sobre como lidar com datas e números, o que
nos leva a aplicações que padecem de grande fragilidade, causando erros que
podem chegar a ser muito graves.
Já vi, por exemplo, aplicações bancárias multiplicarem o valor de um empréstimo
por 100, ou dividirem por 1000, o que deve ter deixado o cliente, o gerente e o
diretor de tecnologia com transtornos psicológicos ou problemas cardíacos. E
tudo isso poderia ter sido evitado com algumas práticas de programação não
muito complicadas.
O que eu pretendo aqui é demonstrar, tanto na plataforma ASP/VB6/COM+ quanto na
plataforma .NET, algumas boas táticas para evitarmos falhas inesperadas em
aplicações devido a variações nos formatos de datas e números.
Também sugiro ao leitor verificar como é muito mais simples lidar com esse tipo
de coisa no mundo .Net do que era no mundo VB6/ASP.
Vamos imaginar uma aplicação com a seguinte arquitetura física:

Figura 1: Arquitetura distribuída de uma aplicação WEB/ASP no modelo
Windows/DNA.
Tanta coisa pode dar errado do ponto de vista de configuração regional em uma arquitetura dessas que o primeiro impulso talvez seja colocar tudo em um servidor só e desenvolver páginas que acessam direto o banco de dados.
Não muito distante disso, podemos olhar o mundo .Net e perceber alguns riscos
parecidos:

Figura 2: Arquitetura distribuída de uma aplicação .Net.
Eu diria que a probabilidade de erros relativos à configuração regional numa aplicação .Net é 60% inferior à de uma aplicação Windows DNA, além do fato de que é muito mais fácil resolver esse tipo de problema no .Net.
Mesmo assim, já presenciei inúmeros casos onde erros são cometidos também nesta
plataforma.
Isso só nos leva a uma conclusão: Todo cuidado é pouco.
Mas para tudo nessa vida tem sempre uma solução (ou até mais de uma), então
vamos abordar item a item dessas duas arquiteturas, tentando colocar um ponto
final nessas dúvidas, a começar pelos scripts.
Como o assunto é extenso, aos poucos eu irei fazendo novos artigos para mostrar
como resolver o problema em cada camada de uma aplicação distribuída. Vou
começar neste artigo tratando dos Scripts.
Scripts
Script de cliente é sempre script. Isso não depende de ASP, VB6, .Net, CGI, JSP
ou qualquer outra coisa. Portanto, os problemas também costumam ser sempre os
mesmos. Este é um dos poucos casos onde desenvolvedores JSP, ASP, CGI, ISAPI,
.Net, PHP e outros choram juntos com as mesmas dificuldades. Talvez seja também
o ponto onde a maior parte dos problemas desse tipo ocorrem.
Para começar, VBScript e JavaScript têm comportamentos bem diferentes em
relação a configuração regional. Experimente executar o script abaixo com sua
máquina configurada para Inglês, e depois para Português:
<script language=VBScript>
MsgBox "MsgBox do VBScript:" & 1.1
window.alert "window.alert do VBScript: " & 1.1
</script>
<script language=javascript>
window.alert("window.alert do javascript:" + 1.1);
</script>
Você vai perceber que no VBScript o número é convertido de acordo com a configuração regional (“1,1” em português e “1.1” em inglês), enquanto o JavaScript permanece sem alteração, sempre trabalhando com “.” como separador decimal.
Não vou discutir aqui quem está certo e quem está errado nesse ponto. Por um
lado, os defensores do VBScript poderiam dizer que aproveitam melhor as
configurações preferidas pelo usuário, mas os fãs do JavaScript podem
argumentar que é mais fácil lidar com um formato que nunca muda, a não ser que
você diga explicitamente quando quer converter essa formatação...
Em que essas variações podem prejudicar a sua aplicação? Em absolutamente tudo.
Se você faz cálculos no cliente, via JavaScript ou VBScript, vai ter problemas
em saber se as datas ou números que o usuário digitou estão em conformidade com
a própria configuração regional dele, ou em conformidade com o JavaScript. Por
exemplo: Se o usuário digitar “100,00”, provavelmente ele quer dizer “100”, mas
se o computador dele estiver com configuração regional “Inglês/Estados Unidos”,
o VBScript vai achar que ele digitou o número “10000” (a vírgula é ignorada).
Por outro lado, se ele digitar “1.234,56”, o JavaScript vai entender “1,234” ao
fazer o parseFloat (vai entender que o ponto separa casas decimais), ou seja,
vai dividir o número por 1000 porque não sabe que você usa ponto para separar
milhares. Talvez falte um pouco de inteligência ao parseFloat para perceber que
se veio uma vírgula depois do ponto, ou o usuário está usando vírgula para
separar decimais, ou um erro de digitação foi cometido e uma exception deveria
ser disparada. A última coisa que ele deveria fazer nesse caso é justamente o
que ele faz, ignorando o que veio depois da vírgula e transformando o número em
outro completamente diferente.
Os mesmos problemas ocorrem com datas. É muito fácil uma aplicação se confundir
e trocar dias com meses pelos mesmos problemas citados anteriormente.
Cenário complicado, em?
Vamos ver algumas das possíveis soluções para esses problemas.
1-VBScript: Forçar uma configuração regional.
Essa solução depende de versão de browser e não é suportada por JavaScript. Mas
ela é tão simples e eficiente que não posso deixar de comentá-la. Vejamos o
exemplo abaixo:
<script language=VBScript>
sub AgoraEmPortugues
SetLocale "pt-Br"
end sub
sub AgoraEmIngles
SetLocale "en-Us"
end sub
AgoraEmIngles
alert cstr(1.1)
AgoraEmPortugues
alert cstr(1.1)
</script>
Com o SetLocale nós conseguimos “magicamente” forçar uma configuração regional, e transformar valores a partir disso. Infelizmente essa função não é suportada no JavaScript. Uma pena...
2-JavaScript: Converter e validar o que o usuário digita.
Já vi algumas bibliotecas bem bacanas para JavaScript que ajudam muito nesse
tipo de coisa. O próprio .Net gera scripts para os Validators que servem de
inspiração para o que eu vou mostrar aqui. Vamos supor que eu sei que meu
usuário escolheu trabalhar com configuração Português (poderia ser uma variável
de sessão informando isso, ou mesmo uma configuração de banco de dados, não
importa). Eu sei que o JavaScript vai querer usar pontos como separadores
decimais, mas o usuário vai insistir em usar vírgula para isso, e ponto para
separador de milhar. O que eu posso fazer? Bom, eu posso converter o que ele
digita, da seguinte forma:
function ValidaNumero(valor,separadorDecimal,separadorGrupo)
{
exp = new RegExp("^\\s*([-\\+])?(((((\\d+)\\" + separadorGrupo +
"))(\\d+))*)?(\\" + separadorDecimal + "(\\d+)*)?\\s*$");
m = valor.match(exp);
if (m == null) return null;
var intermed = m[2] ;
cleanInput = m[1] + intermed.replace(new RegExp("(\\" + separadorGrupo +
")", "g"), "") + ( "." + m[9]);
num = parseFloat(cleanInput);
return (isNaN(num) ? null : num);
}
</script>
Eu tomei a liberdade de roubar essa função da biblioteca de scripts gerada pelo .Net Framework para os validators. Só que fiz umas modificações: Os validators do .Net aceitam validação numérica de inteiros, doubles e currency. Inteiro não aceita casa decimal, doubles não aceitam separador de milhar e currency só aceita um número fixo de casas decimais. Então fiz umas alterações de forma a permitir separador de milhares e um número variável de casas decimais.
O que essa função faz, basicamente, é confrontar o valor digitado contra uma
expressão regular, e avaliar se foram digitados os caracteres corretos na ordem
correta. Depois, a função retira os separadores de milhar e substitui o
separador decimal digitado pelo ponto. Assim, o retorno da função é o número
convertido, ou nulo no caso do valor ser inválido.
Repare que cabe a quem chama a função dizer qual o caractere que o usuário irá
usar como separador decimal e qual será usado como separador de milhares.
Agora temos uma função que valida e converte o número, do formato digitado pelo
usuário para o formato aceito pelo JavaScript. Este número pode ser usado em
cálculos e comparações (sempre em JavaScript de cliente) sem medo de erros de
conversão.
A mesma lógica pode ser seguida com datas. Vejamos:
function DataValida(op,dateorder)
{
function GetFullYear(year) {//Transforma ano de 2 dígitos em 4
//Caso o ano seja < 20, soma 2000, senão soma 1900
return (year + 2000) - ((year < 20) ? 0 : 100);
}
var num, cleanInput, m, exp;
//Regular expression para datas
var yearFirstExp = new RegExp("^\\s*((\\d{4})|(\\d{2}))([-/]|\\. ?)(\\d{1,2})\\4(\\d{1,2})\\s*$");
m = op.match(yearFirstExp);
var day, month, year;
if (m != null && (m[2].length == 4 || dateorder == "ymd")) //Formato ano/dia/mes
{
day = m[6];
month = m[5];
year = (m[2].length == 4) ? m[2] : GetFullYear(parseInt(m[3], 10))
}
else
{
if (dateorder == "ymd")
{
return null;
}
var yearLastExp = new RegExp("^\\s*(\\d{1,2})([-/]|\\. ?)(\\d{1,2})\\2((\\d{4})|(\\d{2}))\\s*$");
m = op.match(yearLastExp);
if (m == null)
{
return null;
}
if (dateorder == "mdy") //Formato mes/dia/ano
{
day = m[3];
month = m[1];
}
else //Formato dia/mes/ano
{
day = m[1];
month = m[3];
}
year = (m[5].length == 4) ? m[5] : GetFullYear(parseInt(m[6], 10))
}
month -= 1;
var date = new Date(year, month, day);
return (typeof(date) == "object" && year == date.getFullYear()
&& month == date.getMonth() && day == date.getDate()) ? date : null;
}
O que essa função faz, basicamente, é receber um texto com uma possível data, e um segundo parâmetro informando qual a ordem dos números esperadas (y para ano, m para mês e d para dia), podendo ser “dmy”, “mdy” ou “ymd”.
A partir daí, a função testa os valores, converte para uma data válida (se for
possível) e a retorna já convertida para o formato válido no sistema
operacional. Caso a função não consiga fazer isso (data inválida, erro de
digitação ou algo assim) ela retorna um campo nulo.
Essa também é uma função JavaScript do ASP.Net (com algumas modificações para
simplificar a didática).
Com isso, temos funções que conseguem ler o que foi digitado, seja lá em que
formato for, e sejam convertidas para o formato que queremos para podermos
trabalhar.
A partir daí, podemos fazer cálculos, comparações e tudo o mais que seja
necessário.
Mas o trabalho não acabou! Muito provavelmente você vai precisar converter
novamente o resultado do cálculo para o formato que o usuário deseja utilizar.
Por exemplo, ele pode querer visualizar datas em formato português, o que vai
exigir que você, ao final dos cálculos, converta novamente a data para o
formato desejado e só então jogue o valor nos textboxes.
E mesmo que você não precise mostrar o resultado ao usuário, talvez você queira
gravar o valor numa variável hidden, para no próximo submit do formulário
efetuar uma leitura no servidor. Também precisará converter! Afinal, quando
esse valor for enviado num submit, você não vai querer ter que fazer dois
tratamentos diferentes, um para os campos visíveis e outro para os hiddens
calculados. Ou seja, parta sempre do princípio que tudo o que vem do browser
vem sempre no mesmo formato, seja algo digitado pelo usuário ou o resultado de
algum cálculo. Assim, a regra fica clara: Leu de um campo no form? Converta
através das funções acima. Fez cálculos e agora precisa escrever em um campo do
form? Converta novamente para o formato do usuário.
Evite soluções intermediárias, que podem ser práticas, mas limitadas. Por
exemplo: Alguns desenvolvedores preferem pegar dois números com duas casas
decimais cada um, retirar a vírgula (ou o ponto), fazer cálculos entre eles e
depois dividir o resultado por 100. Assim, eles não precisam saber que tipo de
pontuação está sendo usada pelo número. O método é simples, mas só se aplica a
operações elementares. Ao calcular um log, por exemplo, não podemos usar esse
tipo de recurso. Isso acaba se tornando perigoso ao longo do tempo, porque você
não pode prever quando vai cair num cálculo que não vai permitir essa solução.
E para não chegar numa situação onde de vez em quando você faz de um jeito e às
vezes faz de outro, melhor usar a solução que sempre funciona.
Além disso, é comum fazerem a mesma coisa com datas, retirando as barras e
mantendo o formato DDMMYYYY. Assim, comparam uma data com outra para saber qual
é a maior. Simples, mas também limitante, já que não é possível calcular
quantos dias existem entre as duas datas desta forma, nem fazer operações mais
elaboradas.
Existem diversas bibliotecas JavaScript disponíveis na internet que ajudam
bastante nesses tratamentos. Algumas, inclusive, possuem recursos para máscara
nos campos texto, entre outras coisas. Sugiro que você crie/copie/adapte uma e
a torne padrão em todo seu código.
Sim, é trabalhoso, mas essa é a maneira correta de imunizar uma aplicação
contra diferentes configurações regionais, permitindo, inclusive, que
diferentes usuários utilizem diferentes configurações.
Se você for criar web controls ou qualquer tipo de classe que gere JavaScript,
lembre-se dessas questões. Faça como o .Net e gere o script adaptado de acordo
com a configuração regional escolhida. Assim, você evita dores de cabeça no
futuro. Garanto que o trabalho adicional que você vai ter para fazer isso é
infinitamente menor que as dores de cabeça que você e sua equipe vão ter por
não ter feito, pode confiar.
Em futuros artigos, irei tratar de outros pontos das aplicações que podem
impactar as configurações regionais. Espero que eles ajudem a elucidar questões
relativas a esse assunto que torturam uma enorme quantidade de desenvolvedores,
em diversas tecnologias.