Desenvolvendo uma calculadora II: lógica (desktop app)

Desenvolvendo uma calculadora II: lógica (desktop app)
logo Java logo exp4j

Aqui damos continuidade ao primeiro post desta incrível série. Neste segundo, a parte Lógica, que nada mais é, que a parte funcional deste programa será construída. Bora!?

A resolução:

Iniciamos criando dois métodos em nossa classe, conforme o código abaixo. Eles tratarão as funcionalidades de limpar o visor, o conhecido, C (clear) e o de apagar (o último) caractere, o chamado backspace.

public String getClear(String exp) {
    // code
    return exp;
}
public String getBack(String exp) {        
    // code
    return exp;
}

Observem que estes dois tem uma assinatura ligeiramente diferente dos métodos criados até agora. Isto porque são métodos não-void, ou seja, métodos que retornam algum valor. Este retorno é indicado pela palavra reservada return.

Nota: O valor retornado deve ser obrigatoriamente do mesmo tipo do método.

Observem também que ambos os métodos recebem como parâmetro um atributo de nome exp, que é do tipo String, e que depois o mesmo é retornado. Este atributo, nada mais é, que a expressão que será impressa no visor, após a execução dos métodos. Compreendido isso, substituímos em nosso método getClear o comentário // code por:

exp = "";

Pronto, com isto, este método já está concluído! Afinal, limpar o visor, nada mais é, que atribuir o valor vazio à expressão.

Agora no método getBack substituímos o trecho comentado pela instrução:

exp = exp.substring(0, exp.length() - 1);

Nesta instrução é atribuído à expressão, o valor retornado do método substring da própria expressão. O método substring retorna da String o trecho delimitado pelos dois parâmetros inteiros recebidos, o primeiro, o índice inicial (incluso), e o segundo, o índice final (excluso). Para um melhor entendimento, veja o exemplo abaixo, supondo que exp valha "3+2×5":

// índice inicial: 0
// índice final: exp.length() - 1 => comprimento da String - 1 => 
// 5 - 1 => 4 => 3, já que é excluso.
// No final tens o trecho resultante que é "3+2×", 
// que é exatamente a String sem o último caractere.
exp = exp.substring(0, exp.length() - 1);
// Imprime o valor de exp
System.out.print(exp);

Nota: Em uma cadeia de caracteres (String), o índice 0, representa a posição do primeiro elemento.

Bom, agora falta vincularmos os métodos criados à seus respectivos botões. Para isso, em nosso método mouseClicked, no case btnClear, substituímos o trecho comentado pela instrução:

expression = getClear(expression);

De maneira equivalente, façamos com a instrução:

expression = getBack(expression);

Agora, salvamos, compilamos e executamos. Criemos expressões e tentemos apagá-las do visor, clicando no botão C. Testemos também o botão backspace com o visor preenchido e também vazio.

Ao testarmos o backspace com o visor vazio, uma Exception do tipo StringIndexOutOfBoundsException foi reportada, não é mesmo? Isso aconteceu porque os índices requeridos pelo método substring, não puderam ser encontrados, pois afinal, não existem. Considerando isto, devemos condicionar a execução do método substring a se e somente se, exp não for vazio. Para isso, substituímos a instrução exp = exp.substring(0, exp.length() - 1); em nosso método getBack por:

if (!exp.isEmpty()) {
    exp = exp.substring(0, exp.length() - 1);
}

Com a correção realizada, testamos novamente.

Bom, considerando a expressão "3+2×5", esperamos dela o resultado 13, mas ele não virá, pois isto é, uma String, e não uma expressão de fato. Assim, entendemos que é necessário converter "3+2×5" em 3+2×5. Essa conversão é chamada de Avaliação (Evaluation/Eval).

Eval é em linhas gerais, a verificação de caractere à caractere extraindo-os da String e separando números de operadores aritméticos, respeitando obviamente, a ordem em quem eles aparecem, e ao fim, realizando as sucessivas operações e gerando o resultado final.

Para realizar a eval, não reinventaremos a roda, isto é, utilizaremos soluções já fornecidas para este fim. Neste projeto, utilizaremos o exp4j, que é uma java library leve, robusta e principalmente fácil de usar.

Sem mais delongas, acessamos o diretório onde se encontra nosso arquivo-fonte via interpretador de comandos. Em ambiente Linux:

cd /path/to/java-file-directory

Em ambiente Windows:

cd X:\path\to\java-file-directory

Neste criamos o diretório lib.

mkdir lib

Depois disto, fizemos o download do JAR (Java ARchive) da exp4j e migramos-o para o diretório lib criado.

Ao fim, nossa estrutura de arquivos e diretórios ficou:

p3/
  Calculadora.java
  lib/
    file-name.jar

Agora criamos um novo método em nossa classe, cuja finalidade é gerar (e retornar) o resultado de uma expressão, conforme o bloco abaixo:

public String getResultExpression(String exp) {
    // Instancia a classe ExpressionBuilder, 
    // passando como parâmetro a própria expressão.
    // A partir desta classe, acione os métodos build() 
    // e evaluate() para avaliar a expressão
    // Guarde o resultado da expressão no atributo result, 
    // que é do tipo Double.
    Double result = new ExpressionBuilder(exp).build().evaluate();
    // É atribuído à exp o result, convertido novamente em String, 
    // já que o retorno à seguir, deve ser de uma String.
    exp = result.toString();
    // Retorna o exp
    return exp;
}

Para maiores detalhes da classe ExpressionBuilder, bem como de seus métodos, podemos consultar a documentação.

Agora incluímos em nossa seção de importações:

import net.objecthunter.exp4j.ExpressionBuilder;

E vinculamos o método criado, ao seu respectivo botão, isto é, o botão de igualdade. Para tal, no case btnIgual, substituímos o trecho comentado por:

expression = getResultExpression(expression);

Por fim, basta salvarmos, recompilararmos e reexecutarmos para ver o resultado das alterações feitas. Entretanto, há uma ressalva aqui: como agora temos uma dependência, que é o JAR, no caso, deves incluí-la, na escrita dos comandos de compilação e execução do nosso programa.

Assim, a partir de agora, usaremos a rotina abaixo, em nosso interpretador de comandos, sempre que formos realizar estes procedimentos:

1. Acesse o diretório onde se encontra seu arquivo-fonte, utilizando o comando cd, assim como feito nos exemplos anteriores.

2. Compile.

Em ambiente Linux:

javac -cp ".:lib/file-name.jar" Calculadora.java

Em Windows:

javac -cp ".;lib\file-name.jar" Calculadora.java

3. Execute.

Em ambiente Linux:

java -cp ".:lib/file-name.jar" Calculadora

Em Windows:

java -cp ".;lib\file-name.jar" Calculadora

Lembremos de alterar file-name pelo nome do nosso JAR.

Feito tudo isto, testamos as expressões:

  • soma: 2+2, espera-se 4
  • subtração: 2−2, espera-se 0
  • multiplicação: 2×2, espera-se 4
  • divisão: 2÷2, espera-se 1

Respostas não esperadas, não é mesmo? Para subtração, multiplicação e divisão, uma Exception do tipo IllegalArgumentException, com o message-error "Unable to parse char '−'|'×'|'÷'" é lançada. Isto acontece porque os símbolos utilizados não são os mesmos definidos pela library/linguagem, isto é, -, *, /, respectivamente. Assim sendo, faz-se necessário, substituirmos os símbolos (operadores) antes do eval, pelos equivalentes da library/linguagem. Para tal, utilizamos o método replace da classe String. Esse método retorna uma nova String, a partir de uma substituição.

Compreendido isto, substituímos a instrução Double result = new ExpressionBuilder(exp).build().evaluate(); presente em nosso método getResultExpression por:

Double result = new ExpressionBuilder(exp.replace("−", "-").replace("×", "*").replace("÷", "/")).build().evaluate();

Agora salvamos, compilamos e executamos, testando novamente as operações aritméticas.

Agora, em relação à soma, e também as outras operações, temos resultados representados por números fracionários, mesmo realizando operações que resultam em números inteiros. Isto acontece, porque o result do eval é um Double, que por sua vez, é convertido em String, antes de ser impresso. Assim, façamos um ajuste para que em caso, de resultados inteiros, este seja impresso sem casas decimais.

Note que aqui já estamos trabalhando com o valor convertido em String, assim sendo, faz-se necessário utilizarmos métodos da respectiva classe. Deste modo, em nosso método getResultExpression, logo após a instrução exp = result.toString(); incluímos o trecho:

// Armazena o índice/posição do caractere ".", 
// que é o caractere que identifica um número fracionário.
int posicaoPonto = exp.indexOf(".");
// Armazena a parte inteira da String
String parteInteira = exp.substring(0, posicaoPonto);
// Armazena a parte real/fracionária da String
String parteReal = exp.substring(posicaoPonto + 1, exp.length());
// Nota: se a parte real for por exemplo, uma sequência de 4 zeros, 
algo como 2.0000, isto é transformado em 2.0 no processo de eval.
// Verifica se a parte real é igual a "0"
if (parteReal.equals("0")) {
    // Atribui à exp à parte inteira
    exp = parteInteira;
}

Com isto feito, salvamos, recompilamos, reexecutamos e vejamos a solução aplicada, testando as expressões 2+2, esperando um resultado inteiro e 2÷3, um resultado fracionário.

Ao realizar a soma temos o resultado adequado, mas ao realizar a divisão, temos uma dízima periódica, e uma quantidade grande de casas decimais, tal que extrapolam o limite do visor, como mostra a figura abaixo.

Dízima períodica obtida da expressão 2÷3

Figura 1 - Dízima períodica obtida da expressão 2÷3
Fonte: Autor (CC BY-NC-SA 4.0)

Como estamos trabalhando com cálculos que não exigem tamanha precisão fracionária, truncaremos o resultado em até 5 casas decimais. Para tal, em nosso método getResultExpression, substituímos a instrução if (parteReal.equals("0")) { exp = parteInteira; } por:

if (parteReal.equals("0")) {
    exp = parteInteira;
} else {
    Se a parte real for maior que 5
    if (parteReal.length() > 5) {
        // Concatena a parte inteira, com o ".", que por sua vez, 
        // concatena com a parte real (5 caracteres), 
        // e, por fim, esta cadeia é atribuída à exp;
        exp = parteInteira.concat(".").concat(parteReal.substring(0, 5));
    }
}

Feito isso, salvamos, recompilamos, reexecutamos e testamos novamente a expressão 2÷3. Esperamos:

Resultado da expressão 2÷3 truncado em 5 casas decimais

Figura 2 - Resultado da expressão 2÷3 truncado em 5 casas decimais.
Fonte: Autor (CC BY-NC-SA 4.0)

Bom, outros cenários que podem nos reportar erros, são a divisão por zero, como exemplo: 2÷0, ou mesmo expressões mal formuladas, tais como 2+2+=. O primeiro produzirá uma Exception do tipo ArithmeticException com o message-error "Division by zero!". Já o segundo, uma IllegalArgumentException, com o message-error "Invalid number of operands available for '+' operator". Para facilitar, trataremos os dois casos com a mesma solução, isto é, atribuindo à exp, o valor "ERROR". Entretanto, antes disso, é necessário capturarmos a exception, para poder então tratá-la. Essa captura, impedirá a transcrição do error (stack trace) em nosso interpretador de comandos. Para isso, devemos circundar o trecho de código, que lança a(s) exception(s), utilizando um bloco try/catch. Como IllegalArgumentException e ArithmeticException, são filhas de Exception, podemos tratá-las como uma Exception diretamente. Compreendido isto, movemos todas as instruções contidas em nosso método getResultExpression, com exceção do return, para dentro de um bloco try/catch, conforme o proposto abaixo:

try {
    // inclua as instruções aqui
} catch (Exception exception) {
    exp = "ERROR";
}
return exp;

Agora, salvamos, recompilamos, reexecutamos e testamos as expressões. O resultado é:

Mensagem de erro gerada a partir do tratamento de uma Exception com try/catch

Figura 3 - Mensagem de erro gerada a partir do tratamento de uma Exception com try/catch.
Fonte: Autor (CC BY-NC-SA 4.0)

Considerando esta solução, devemos pensar também em adequar o comportamento do nosso método getBack, pois não faz sentido, remover caracter à caracter, caso a expressão impressa seja "ERROR". Para tal, substituímos a instrução if (!exp.isEmpty()) { exp = exp.substring(0, exp.length() - 1); } por:

if (!exp.isEmpty()) {
    if (!exp.equals("ERROR")) {
        exp = exp.substring(0, exp.length() - 1);
    } else {
        exp = "";
    }
}

Então, salvamos, recompilamos, reexecutamos e testamos a alteração feita.

Bom, ainda há outros desafios para este projeto que precisamos superar. São eles:

  1. Expressões não podem iniciar com os caracteres ×, ÷ e ). Exemplos: ×2+3 e )3÷1
  2. Expressões não podem ter sequências (imediatas) de operadores aritméticos. Exemplos: 2++2, 2−÷2 e 2×+÷2
  3. Expressões não podem ter sequências (imediatas e não-imediatas) de ponto-flutuante em um mesmo número. Exemplos: 2..2 + 5 e 2.05.2 + 2

Vamos solucioná-los, um à um. Pois bem, iniciamos criando em nossa classe o método abaixo:

public String getClearExpression(String exp, String car) {
    // code
    return exp;
}

Agora substituímos todas as ocorrências da instrução expression = expression.concat("caractere"); em nosso método mouseClicked pela instrução:

// Nota: substituímos o termo caractere 
// pelo símbolo equivalente ao botão acionado
expression = getClearExpression(expression, "caractere");

Já em nosso método getClearExpression, substituímos o trecho comentado por:

// Desafio 1 
// Dica: Verificar o início de uma expressão 
// é o mesmo que verificar se a expressão é vazia.
// Se exp é vazio (OU se é "ERROR")
if (exp.isEmpty() || exp.equals("ERROR")) {
    // Se o caratere NÃO for um x OU ÷ OU )
    if (!car.matches("[×|÷|)]")) {
        // em exp é atribuído o caractere
        exp = car;
    } else {
        // do contrário, é atribuído vazio
        exp = "";
    }
// Senão    
} else {
    // exp recebe a (própria) expressão concatenada ao caractere
    exp = exp.concat(car);
}

Feito isto, salvamos, recompilamos, reexecutamos, testamos e teremos o primeiro desafio superado!

Agora, no mesmo método, substituímos a instrução exp = exp.concat(car);, além do comentário que a precede, por:

// Desafio 2
// Se o caractere for um número OU ( OU )
if (car.matches("[0-9|(|)]")) {
    // o caractere é concatenado à expressão. 
    exp = exp.concat(car);
} else {
    // armazena o último caractere da expressão
    String ultCar = exp.substring(exp.length() - 1);        
    if (car.equals(".")) {
        // code
    } else {        
        // Se o último caractere já é um operador aritmético
        if (ultCar.matches("[+|−|×|÷]")) {
            // substitui o último caractere da expressão, 
            // pelo novo car
            exp = exp.substring(0, exp.length() - 1).concat(car);                    
        } else {
            // o caractere (operador) é concatenado à 
            // expressão e armazenado.
            exp = exp.concat(car);
        }
    }
}

Feito isto, salvamos, recompilamos, reexecutamos, testamos e teremos o segundo desafio superado!

Agora, ainda neste método, substituímos o comentário // code por:

// Desafio 3
// Divide a expressão em várias partes, à cada operador encontrado.
// Essas partes são armazenadas no Array de String expPart
// Exemplo: exp = "3+35×5.20", expPart[0] => "3", expPart[1] => "35" expPart[2] => 5.20
String[] expPart = exp.split("[+|−|×|÷]");
// Se o último caractere for um operador OU
// Se NÃO contêm um ponto-flutuante na última parte da expressão (isto é, no mesmo número)
if ((ultCar.matches("[+|−|×|÷]")) || (!expPart[expPart.length - 1].contains(car))) {
    // concatena o ponto-flutuante à expressão e armazena;
    exp = exp.concat(car);
}

Feito isto, salvamos, recompilamos, reexecutamos, testamos e teremos o terceiro desafio superado! Ufa!

Agora testamos seguidamente as expressões 2+2 e 3+3.

Opa! Nossa segunda expressão ficou:

Resultado da expressão 2+2 concatenado (indevidamente) com a expressão (seguinte) 3+3

Figura 4 - Resultado da expressão 2+2 concatenado à expressão (seguinte) 3+3.
Fonte: Autor (CC BY-NC-SA 4.0)

Bom, definitivamente não é este o comportamento que esperamos. Para tratá-lo, identificamos à origem do problema, que neste caso é no Resultado, ou melhor, no método acionado pelo botão de igualdade. Compreendido isto, agora podemos usar um atributo que nos sinalize quando o resultado, de fato, acontece. Para tal, façamos:

Em nossa classe, criamos o atributo global, conforme:

// Inicia o programa com o sinalizador falso, 
// isto é, resultado ainda não ocorreu
boolean resultComplete = false;

Agora no método getResultExpression incluímos após o fechamento do else (}), a instrução:

// altera o valor de resultComplete para verdadeiro,
// sinalizando que o resultado ocorreu
resultComplete = true;

Por fim, movemos todas as instruções contidas no método getClearExpression, com exceção do return para dentro da condicional if conforme:

if (resultComplete == false) {
    // inclua as instruções aqui
} else {
    // Ocorreu o resultado!  
    // Se o caractere for um número OU um ponto-flutuante OU (
    if (car.matches("[0-9|.|(]")) {
        // exp recebe o caractere 
        // (e não mais o caractere concatenado à expressão)
        exp = car;
    } else {
        // Se o caractere NÃO for um )
        if (!car.matches("[)]")) {
            // Nota: inclui a concatenacao de operadores
            // à expressão (resultante)
            exp = exp.concat(car);
        } else {
            // Atribui vazio à expressão
            exp = "";
        }
    }
    // após o tratamento, volte o sinalizador ao valor original
    resultComplete = false;
}
return exp;

Com isto feito, salvamos, recompilamos, reexecutamos e testamos as alterações, finalizando assim, a parte II desta resolução, podendo avançar para a última parte desta incrível série!! Se gostaram, por favor, deixem seus COMENTÁRIOS, nos SIGAM e principalmente COMPARTILHEM para que assim, seja possível alcançar mais pessoas, que como vocês, curtem Programação, com conteúdos PRÁTICOS e GRATUITOS!

Para ver e/ou baixar o código-fonte desta resolução, ou mesmo melhorá-lo, visite o repositório no GitHub.

Até a próxima!

Cite este material

FERNANDES, Fábio. Desenvolvendo uma calculadora II: lógica (desktop app). aprendaCodar, 07 de junho de 2022. Disponível em: <https://aprendacodar.blogspot.com/2022/06/desenvolva-uma-calculadora-desktop-app.html>. Acesso em:

Fábio Fernandes

Graduado em Ciência da Computação e Especialista em Análise de Dados com BI e Big Data. Instrutor, Desenvolvedor e Produtor de Conteúdo. Apaixonado por Tecnologia e pelo compartilhamento de conhecimentos.

Postar um comentário (0)
Postagem Anterior Próxima Postagem