Quem nunca se sentiu frustrado ao usar o método Math.random() em javascript? Esse sentimento é bastante comum, porque o modo mais tradicional de números aleatórios que usamos são os definidos em um intervalo de números inteiros, exatamente como na função rand() em PHP.
Neste tutorial vou mostrar como criar uma função em javascript que tem seu funcionamento parecido com a função rand do PHP, onde passamos dois parâmetros, um com o valor mínimo que pode ser retornado, e outra com o valor máximo. Além disso mostrar as minhas iterações na melhora do código, técnica conhecida como refactoração (refactoring em inglês).
Criando a lógica básica
O primeiro passo é criar a lógica que vai gerar nosso número aleatório. Precisamos de somente dois métodos nativos do javascript, que são:
- Math.random()
- Este método retorna valores entre zero e um (mas nunca zero e nunca um).
- Math.floor()
- Este método arredonda um número decimal para o primeiro inteiro abaixo dele.
Com esse conhecimento já é possível criar uma lógica simples pra retornar um valor randômico inteiro, conforme visto a seguir:
// Retorna um número inteiro entre 0 e 4 var n = 5 Math.floor(Math.random() * n));
Suponto que a variável n, do código acima, seja um parâmetro da nossa função, vimos que devemos somar 1 para que tenhamos um valor entre zero e o nosso máximo.
// Retorna um número inteiro entre 0 e 5 var n = 5 Math.floor(Math.random() * (n+1));
Já com o valor corrigido, podemos prosseguir com a lógica.
Passando o valor mínimo
Vamos supor que o valor nosso parâmetro de valor mínimo seja 2, e o parâmetro de valor máximo seja 5. Usando a lógica do código acima não podemos garantir que teremos um valor entre 2 e 5, porque os valores 0 e 1 também são possíveis. Uma forma de contornar esse problema é usando uma simples lógica pra verificar se o valor obtido é maior ou igual ao nosso valor mínimo, não sendo nós executamos a função novamente, até que tenhamos um valor que está entre o mínimo e o máximo. A seguir vou iniciar a criação da função em si.
function rand(min,max) {
var result = Math.floor(Math.random() * (max+1));
if(result < min){
return rand(min,max);
} else {
return result;
}
}
Faça o teste (clique em Run)
O código acima tem 2 pontos impotantes. O primeiro deles é o uso da recursividade, algo fundamental para garantir que eu consiga rodar a função o número de vezes que for necessário para que eu tenha um resultado correto. O segundo ponto é a palavra chave return imediatamente antes da função recursiva, assim podemos garantir que a função irá retornar o que a função recursiva retornar, independente de quantas vezes ela for rodar.
O que foi feito até agora já é suficiente para garantir que a função vá retornar valores inteiros aleatórios em ums intervalo, e essa foi justamente a primeira ideia que tive para a criação dessa função, mas aos poucos fui tendo outras ideias que poderiam tornar essa função bem mais poderosa. A seguir vou mostrar outras funcionalidades que podem ser implementadas nessa simples função.
Validação dos Parâmetros
A validação dos parâmetros tem uma implementação bastante simples, como visto a seguir:
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
var result = Math.floor(Math.random() * (max+1));
if(result < min){
return rand(min,max);
} else {
return result;
}
}
}
Faça o teste (clique em Run)
Se o parâmetro min ou o parâmetro max não forem números, então quero retornar false, o que faz sentido na minha opinião. NaN é considerado um número em javascript, então temos de levar isso em conta e evitar esse tipo de argumento.
Permitir a inversão dos parâmetros
Essa ideia é bem interessante. pensando no usuário final, acho que não há a necessidade dele se preocupar com a ordem dos parâmetros, pois a única responsabilidade da função é retornar um número aleatório dentro de um intervalo, e passando números diferentes como argumento já podemos interpretar isso como um intervalo válido.
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
if(max < min){
var novoMin = max,
novoMax = min;
return rand(novoMin,novoMax);
} else {
var result = Math.floor(Math.random() * (max+1));
if(result < min){
return rand(min,max);
} else {
return result;
}
}
}
}
Faça o teste (clique em Run)
A única coisa que eu fiz foi adicionar um teste, verificando se o valor máximo é menor que o mínimo, se for, crio duas novas variáveis, uma com o novo máximo e outro com um novo mínimo, daí uso recursividade novamente pra chamar a função com os novos parâmetros.
Permitir valores iguais
O que acontece se o usuário passar o valor mínimo igual ao valor máximo? Do jeito que a função está, ela itera até que obtenha como resultado o valor máximo, o que não é muito efetivo, já que isso pode acontecer centenas, milhares, ou até milhões de vezes, dependendo do valor dos argumentos.
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
if(max < min){
var novoMin = max,
novoMax = min;
return rand(novoMin,novoMax);
}else if(max == min) {
return max;
} else {
var result = Math.floor(Math.random() * (max+1));
if(result < min){
return rand(min,max);
} else {
return result;
}
}
}
}
Faça o teste (clique em Run)
Só adicionei uma cláusula if, para verificar se os argumentos são iguais. Se forem, então retorno o max (poderia ser o min)
Permitir valores negativos
Voltando à ideia de intervalo, não vi nenhum sentido em implementar uma função que não pudesse retornar resultados com números negativos, daí decidi implementar essa funcionalidade. Mas essa parte não é tão simples quanto as outras (na verdade é super simples, mas vou mostrar primeiro a maneira difícil de se fazer, que foi a primeira maneira que veio à minha cabeça).
O primeiro passo é dividir nosso intervalo em três partes. São eles:
- Ambos são positivos
- Usar a lógica já implementada.
- Ambos são negativo
- Multiplicar ambos por -1, e no fim multiplicar o resultado por -1.
- O menor é negativo
- Subtrair ao valor máximo o valor do mínimo(como o valor mínimo é negativo, a subtração se transformará em uma soma), e definir o valor mínimo como zero. No fim subtrair ambos do antigo valor mínimo.
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
if(max < min){
var novoMin = max,
novoMax = min;
return rand(novoMin,novoMax);
} else if(max == min) {
return max;
} else if(min < 0 && max < 0){
var novoMin = min*(-1),
novoMax = max*(-1);
return rand(novoMin,novoMax) * (-1) ;
} else if(min < 0 && max >= 0 ) {
var novoMin = 0,
novoMax = max - min;
return rand(novoMin,novoMax) + min;
} else {
var result = Math.floor(Math.random() * (max+1));
if(result < min){
return rand(min,max);
} else {
return result;
}
}
}
}
Faça o teste (clique em Run)
Gostaria de chamar a atenção pela ordem que os condicionais aparecem. Talvez isso não fique evidente na leitura do código, mas a ordem é de suma importância (não em todos os casos).
O primeiro condicional verifica-se se os argumentos são números, pois toda a lógica seguinte só pode ser executada se eles forem. Alguns condicionais seguintes seguem esse mesmo processo, então é fundamental prestar a atenção nesse ponto.
A função está pronta para ser usada, e faz o que promete, retornar um número aleatório que está dentro de um intervalo passado como parâmetro.
Um olhar menos atento não consegue perseber on pontos onde essa função pode ser melhorada, e é aí que a parte legal começa!
Reduzindo a duplicação de código
A primeira coisa que eu persebi nesse código foi a duplicação de lógica. Algo que poderia ser feito somente uma vez acaba sendo feito várias vezes.
Na parte que tratamos de números negativos por exemplo, qual a diferença de somente um deles ser negativo ou de ambos serem negativos? Nenhuma! é possível criar a mesma lógica para tratamento desses dois casos. Incrivelmente é possível englobar mais um caso nesse tratamento, que é quando ambos são positivos.
O meu algoritmo para resolver esses casos foi seguinte:
- Normalizar os argumentos
- Subtrair os valores máximo e mínimo do mínimo. Supondo que o mínimo seja 5 e o máximo seja 10, isso significa que o mínimo vai passar a ser 0(5-5) e o máximo vai passar a ser 5(10-5). O legal é que isso também funciona pra valores negativos. Supondo que o valor mínimo seja -7 e o valor máximo seja 4, daí o novo valor mínimo será 0(-7-(-7) == -7+7) e o novo valor máximo será 11(4-(-7) == 4+7).
- Encontrar um valor aleatório da maneira tradicional
- Aqui o código rodaria como antes, sem nenhuma modificação
- Somar o resultado final pelo valor do mínimo
- Como no começo o intervalo foi subtraído do valor mínimo, no fim o resultado deve ser acrescido do valor mínimo.
Vamos fazer um pequeno teste mental para ver se a coisa pode dar certo: Supondo que o valor mínimo tenha sido 3 e o valor máximo tenha sido 7. Daí subtraimos o valor mínimo do mínimo e do máximo, tendo como resultado um intervalo entre 0 e 4. Rodando a função randômica, teríamos um resultado que poderia variar entre 0 e 4. Supondo que o resultado tenha sido 2, daí acrescentamos o valor mínimo, que nesse caso é 3, e chegamos ao resultado final de 5, que é um número entre 3 e 7! Se quiser pode testar com outros valores, inclusive negativos, e sempre terá uma resposta correta!
Agora chegou a hora de melhorar nossa função:
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
if(max < min){
var novoMin = max,
novoMax = min;
return rand(novoMin,novoMax);
} else if(max == min) {
return max;
} else if(min !== 0){
var novoMin = 0,
novoMax = max - min;
return rand(novoMin,novoMax) + min;
} else {
var result = Math.floor(Math.random() * (max+1));
return result;
}
}
}
Faça o teste (clique em Run)
Repare que removi a lógica que tratava de valores negativos, e implementei o novo algoritmo. Dessa vez só faço um teste para ver se o valor mínimo é diferente de zero, se for eu subtraio o valor mínimo do mínimo e do máximo, e em seguida executo a função novamente somando o valor mínimo. Quando este teste lógico é feito, já podemos garantir que o valor mínimo é menos que o máximo, porque existem dois testes lógicos antes que garantem isso! (lembra de quando falei da ordem dos condicionais?)
Removi também a lógica que testava por um valor menor que o mínimo (no último else), porque podemos garantir que sempre teremos um valor entre o mínimo (zero) e nosso máximo.
Reduzindo o código
Uma coisa que me deixa com as mãos coçando é ver código sobrando quando não faz muito sentido, e nessa função existe bastante disso. O próximo passo é retirar todo o código desnecessário!
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
} else {
if(max < min){
return rand(max,min);
} else if(max == min) {
return max;
} else if(min !== 0){
return rand(0,max-min) + min;
} else {
return Math.floor(Math.random() * (max+1));
}
}
}
Faça o teste (clique em Run)
Como uma primeira iteração eu removi operações intermediárias, como criação de novas variáveis, que na verdade poderiam fazer sua operação direto no parâmetro das funções.
function rand(min,max) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
}
if(max < min){
return rand(max,min);
}
if(max == min) {
return max;
}
if(min !== 0){
return rand(0,max-min) + min;
}
return Math.floor(Math.random() * (max+1));
}
Faça o teste (clique em Run)
Agora temos uma função muito mais limpa! Dessa vez removi todos os else. O motivo pelo qual isso foi possível é que em cada teste lógico á função retorna algum valor, e quando isso acontece ela para seu fluxo naquele ponto, não executando o que tem pela frente.
Permitindo limites exclusivos
Que tal passar um intervalo e impedir que os limites possam aparecer como resultado da nossa função? Isso é bem simples pro usuário, basta subtrair uma unidade do valor máximo e somar uma unidade ao valor mínimo, mas que tal implementar isso internamente, permitindo que o usuário passe um terceiro parâmetro (opcional) dizendo se ele quer ou não um número que exclui os limites?
Para fazer isso o primeiro passo é adicionar um parâmetro à nossa função, que vou chamar de exclusivo. Depois criar os condicionais para teste e subtrair/somar o mínimo e o máximo.
function rand(min,max,exclusivo) {
if(typeof min != 'number' || typeof max != 'number' || isNaN(min) || isNaN(max)) {
return false;
}
if(max < min){
return rand(max,min,exclusivo);
}
if(exclusivo === true) {
if(max - min < 2) {
return false;
}
return rand(min+1,max-1);
}
if(max == min) {
return max;
}
if(min !== 0){
return rand(0,max-min) + min;
}
return Math.floor(Math.random() * (max+1));
}
Faça o teste (clique em Run)
Um novo teste aparece, testando o valor do parâmetro exclusivo, sendo verdadeiro ele roda o código interno, sendo falso ele segue o fluxo normal. Se for verdadeiro, ele faz um teste para ver se a diferença entre o máximo e o mínimo é menor que 2, sendo verdadeiro ele retorna falso. Pra entender essa parte, nada melhor que um exemplo: se nosso mínimo é 1 e nosso máximo é 2, qual resultado poderíamos ter de volta se não podemos ter nem 1 nem 2? daí retorno falso, caso contrário executo a função novamente, subtraindo uma unidade do máximo e somando uma unidade ao mínimo, mas dessa vez sem passar o terceiro parâmetro, já que o trabalho de exclusão dos limites já foi feito.
Na linha 7 a função recursiva recebeu um terceiro argumento, isso porque nesse ponto do código a única coisa que sabemos é que os parâmetros são números, e não fazemos ideia se eles são iguais ou tem uma diferença menor que dois. A responsabilidade desses testes é dada ao restante do código, por isso devemos passar o tereiro parâmetro novamente.
Conclusão
A implementação de uma função pode nos ensinar bastante a pensar em como resolver pequenos problemas, mas que fazem toda a diferença na escrita de um bom código. Espero que tenha tirado algum proveito desse tutorial, e se tiver alguma dúvida, crítica ou sugestão, por favor deixe nos comentários. Um abraço e até a próxima!
Tags:aleatório, algoritmo, javascript, rand
parabens, conheço o blog a um dia mais gstei muito, ja esta nos favoritos =D Muito bom os tutoriais!