3 Intervalos de Strings Numéricos
3.4 Definição do Tipo Number
O sistema intervalar proposto e implementado nesta dissertação utiliza a classe String de Java para representar a mantissa e expoente de um número. A classe principal da biblioteca é a classe Number que representa um “número”. Ela é descrita pelo diagrama da Figura 9. A Constante CARACTERE_ZERO representa o valor unicode do caractere zero, ou seja, 48 (código unicode 48). As constantes SINAL_NEGATIVO, SINAL_ZERO e SINAL_POSITIVO possuem os valores, -1, zero e 1, respectivamente, e representam o valor atribuído aos sinais dos expoentes e dos números.
Figura 9. Diagrama UML da Classe Number
O tipo Number possui 5 atributos:
• mantissa: É do tipo String e é o conjunto de algarismos da base do número. • sinal: Pode tomar o valor de uma das três constantes: SINAL_NEGATIVO,
SINAL_ZERO e SINAL_POSITIVO, e representam o sinal do número. Só o número zero possui o valor SINAL_ZERO.
• expoente: Determina a ordem de grandeza de um número. Quantas casas decimais depois da virgula (se o sinal do expoente for negativo) ou qual potência de 10 que multiplica a mantissa. Representa os algarismos do expoente.
• sinalExpoente: Define o sinal do expoente do número pode assumir o valor de uma das três constantes SINAL_NEGATIVO, SINAL_ZERO e SINAL_POSITIVO.
• denominador: Para podermos representar um número no formato fracionário o atributo denominador foi criado. Ele é do tipo Number podendo também ter um denominador. A seguir será visto que na construção do objeto Number, recursivamente, será eliminado denominadores de denominadores, simplificando desta forma o denominador final.
Utilizando-se este modelo atingi-se uma precisão máxima de 231 - 1 casas decimais na base 10, que é o tamanho máximo que o tipo inteiro em Java pode assumir, pois em Java laços e índices de arrays são indexados por este tipo de dado (int). Uma outra abordagem alternativa seria utilizar uma lista encadeada desta forma poderíamos aumentar a precisão para o limite da memória, porém perderíamos muitas funcionalidades que o tipo String nos fornece. O mesmo número, (231 - 1), é o limite do tamanho do expoente. Sabendo que (231 - 1) é igual a 2147483647, temos que o modulo máximo do expoente é 102147483647 – 1. Este mesmo número é o valor máximo do módulo da mantissa. Conseqüentemente o sistema numérico pode deixar uma boa margem para se efetuarem cálculos sem necessidade de arredondamentos ou truncamentos.
Conseqüentemente o sistema numérico criado pelo tipo Number possui as seguintes especificações:
N = (10, (231 - 1), -(102147483647 - 1), 102147483647 - 1).
É importante lembrar que nem todos os cálculos utilizam a precisão máxima, pois estamos utilizando strings e podemos variar a precisão da mantissa de acordo com a necessidade de casas decimais que a operação requerer, diferentemente dos sistemas de ponto-flutuantes que utilizam uma quantidade fixa de símbolos (os bits).
O tipo Number possui dois construtores públicos: o construtor padrão, não recebe qualquer parâmetro e cria o número zero e um construtor que recebe uma instância de um objeto do tipo String. Para construirmos um objeto Number a partir de uma String precisamos fazer um reconhecimento de padrão (retirar as partes relativas à mantissa, sinais, expoente e denominadores), identificando cada um de seus atributos e se o mesmo obedece a uma gramática. A gramática define a regra de formação do tipo Number e está definida no Quadro 1 a seguir.
Quadro 1: Gramática de formação do tipo Number
Esta gramática [HOPCROFT] aceita no seu conjunto de símbolos os caracteres: {‘0’, ’1’, ’2’, ‘3’ , ‘4’, ’5’, ’6’, ‘7’, ‘8’, ‘9’, ‘.’, ‘,’, ‘e’, ‘E’, ‘/’, ‘+’, ‘-’} e segue as regras de formação descritas acima.
Um autômato [HOPCROFT] foi implementado para reconhecer a gramática do Quadro 1. Quando a regra de formação da linguagem é desrespeitada a ExcecaoFormatoNumeroInvalido é lançada, indicando que a String passada como parâmetro no construtor do tipo Number [JAVA COM STRINGS] não pôde ser reconhecido pelo autômato e a instância do objeto do tipo Number não pôde ser construída. À medida que o autômato vai lendo a cadeia de caracteres de entrada, são identificados: sinal, mantissa, expoente, sinal do expoente e denominador. Segundo a gramática do quadro 1 podemos ter denominadores de denominadores e, portanto é preciso ter recursividade na função que cria o tipo Number. Os passos da função createNumber [JAVA COM STRINGS], que cria um tipo Number a partir de um tipo String (cadeia de caracteres) estão descritos abaixo. O método createNumber é responsável por implementar o autômato relativo à gramática do Quadro 1.
Passo 0: Remover os zeros à esquerda. Passo 1: Identificar o sinal:
Se a mantissa, ao removerem-se os zeros à esquerda, estiver formada apenas pelo caractere ‘0’, é atribuído o sinal relativo ao número zero.
S → + | - | D
D→ 0D | 1D | 2D | 3D | 4D | 5D | 6D | 7D | 8D | 9D | .P| ,P P → 0P | 1P | 2P | 3P | 4P | 5P | 6P | 7P | 8P | 9P | eF | EF F → + | - | N
Se o primeiro caractere for ‘.’ ou ‘,’ o sinal é positivo e o sistema saberá que deve ler caracteres depois da virgula. Assim para cada caractere lido uma unidade deverá ser subtraída do expoente.
Passo 2: Ler os caracteres da mantissa (caracteres ‘0’ a ‘9’) até não encontrarmos caracteres numéricos. Se ao ler os caracteres depois da virgula não tivermos mais caracteres numéricos para serem lidos, o sistema lança uma exceção se o próximo caractere for diferente de ‘e’, ‘E’ ou ‘/’. Caso contrário, o sistema então segue ao Passo 3 ou 4 dependendo do caractere encontrado.
Passo 3: Se foi encontrado o caractere ‘e’ ou ‘E’ o expoente deve ser lido, primeiro seu sinal e depois sua mantissa.
Passo 4: Se foi encontrado o caractere ‘/’ deve-se chamar recursivamente a função createNumber passando como parâmetro a substring que vem após o caractere ‘/’ para que seja identificado o denominador do número. Volta-se então ao Passo 0. Desta forma são válidas as seguintes construções:
1. Cria o número Zero:
Number n = new Number();
2. Cria um número sem levar em consideração o expoente:
Number n = new Number(“76345”);
3. Cria um número sem denominador:
Number n = new Number(“-76345.345E-34”);
4. Cria um número com denominador:
Number n = new Number(“76.345E-34/657.657E-98”);
5. Cria um número com um denominador que possui outro denominador
Number n = new Number(“76.45/+.7686/345E-67”);
6. Desrespeitando-se a gramática a exceção ExcecaoFormatoNumeroInvalido é lançada.
Algumas regras de validação e simplificação são usadas para auxiliar a construção do objeto Number, retirando redundâncias, diminuindo o custo do seu uso nos cálculos e diminuindo a quantidade de caracteres a serem processados futuramente:
1. Verifica-se recursivamente se existe divisão por zero na cadeia de denominadores. O algoritmo para realizar a verificação está descrito no Exemplo 8, neste exemplo EDPZ é ExcecaoDivisaoPorZero:
Exemplo 8. Algoritmo que verifica divisão por zero na cadeia de denominadores
private boolean verificaDivisaoPorZero(Number n) throws EDPZ{ boolean edpz = false;
if (n.denominador != null) {
if (n.denominador.mantissa.equals("0")){ throw new EDPZ(n.denominador.toString()); } else {
edpz = edpz || verificaDivisaoPorZero (n.denominador); }
}
if (n.mantissa.equals("0")){ throw new EDPZ(n.toString()); }
return edpz; }
2. Se a mantissa for igual a zero e não ocorreu divisão por zero na cadeia de denominadores, o atributo denominador é alterado para o valor null (Instância Nula).
3. Simplifica-se recursivamente a cadeia de denominadores, de forma que tenhamos apenas um denominador, ou seja, se um denominador possui um denominador, efetuar-se-á todas as divisões até que tenhamos apenas um denominador para o numerador e este denominador não precise ser simplificado. O algoritmo está descrito no Exemplo 9, onde multiplicacaoDesconsiderandoDenominador é a multiplicação de dois objetos do tipo Number desconsiderando-se seus denominadores [ver Exemplo 37], ou seja, só os numeradores são levados em consideração.
Exemplo 9. Resolver recursivamente os denominadores de denominadores.
private static void simplificarDenominadores (Number a) { Number c;
if (a.div != null && a.div.div != null){ simplificarDenominadores(a.div); c = multiplicacaoDesconsiderandoDenominador (a,a.div.div); a.mantissa = c.mantissa; a.sinal = c.sinal; a.sinalExpoente = c.sinalExpoente; a.expoente = c.expoente; a.div.div = null; } }
4. Resolve o sinal do número, verifica-se o sinal do numerador e do denominador e se o sinal do denominador for negativo, inverte-se sinal do número e se atribui o sinal.positivo para o denominador.
5. Efetua a divisão pelo expoente do denominador, subtraindo do expoente do numerador, o expoente do denominador.
6. Simplifica as mantissas de numeradores e denominadores se houver um m.d.c. (máximo divisor comum) entre eles diferente de 1.