Visualizar/Abrir

Propaganda
Pós-Graduação em Ciência da Computação
“UMA BIBLIOTECA INTERVALAR BASEADA EM
PROCESSAMENTO DE CARACTERES”
POR
Ivan Oliveira Bernardo Leite
Dissertação de Mestrado
Universidade Federal de Pernambuco
[email protected]
www.cin.ufpe.br/~posgraduacao
RECIFE, Agosto/2007
UNIVERSIDADE FEDERAL DE PERNAMBUCO
CENTRO DE INFORMÁTICA
PÓS-GRADUAÇÃO EM CIÊNCIA DA COMPUTAÇÃO
IVAN OLIVEIRA BERNARDO LEITE
“UMA BIBLIOTECA INTERVALAR BASEADA EM
PROCESSAMENTO DE STRINGS”
ESTE TRABALHO FOI APRESENTADO À PÓS-GRADUAÇÃO EM
CIÊNCIA DA COMPUTAÇÃO DO CENTRO DE INFORMÁTICA DA
UNIVERSIDADE FEDERAL DE PERNAMBUCO COMO REQUISITO
PARCIAL PARA OBTENÇÃO DO GRAU DE MESTRE EM CIÊNCIA DA
COMPUTAÇÃO.
ORIENTADOR(A): MARCÍLIA ANDRADE CAMPOS
RECIFE, AGOSTO/2007
Leite, Ivan Oliveira Bernardo
Uma
biblioteca
intervalar
baseada
em
processamento de strings / Ivan Oliveira Bernardo
Leite. – Recife: O Autor, 2007.
xi, 93 p. : il., fig., tab., quadro.
Dissertação (mestrado) – Universidade Federal
de Pernambuco. CIn. Ciência da Computação, 2007.
Inclui bibliografia.
1. Análise intervalar. 2. Computação científica.
3. Processamento de strings. 4. JAVA I. Título.
511.42
CDD (22.ed.)
MEI2008-008
DEDICATÓRIA
Dedico
esta
dissertação
em
especial a meu pai, minha mãe e meus
irmãos,
minha
Andrade
Campos
Patrícia Pires.
orientadora
Marcília
e minha analista
AGRADECIMENTOS
Agradeço a meus pais por terem, em momentos de dificuldade, priorizado a
educação e formação dos seus filhos e incentivando a todo instante a conclusão
deste projeto.
Professora Marcília Andrade Campos, sem ela este trabalho não teria sido
concluído, sempre acreditou, motivou e lutou por este trabalho. Um agradecimento
especial a Neíldes Paiva Vieira Pedrosa, minha gerente de projetos no CESAR, por
ser muito paciente, compreensiva e flexível para a realização do mestrado. Ao
Professor Antônio Carlos Monteiro do departamento de Matemática da UFPE pela
ajuda com algoritmos envolvendo números inteiros.
Aos amigos Edmo Ribeiro, Vanilson Burgos, Jorge Mascena, Taíssa Rocha,
Taciana Amorim, Eduardo Dominoni e Isabel Wanderley que foram espelhos e
pessoas que conviveram comigo durante o curso de pós-graduação, interagindo,
cooperando, dividindo angustias e incentivando.
A todos os meus amigos que me dão a alegria e o prazer da convivência. Por
entenderem minha ausência durante a conclusão deste trabalho.
RESUMO
Java é uma linguagem multiplataforma amplamente utilizada nos dias atuais.
Sistemas cliente-servidor, aplicações embarcadas e desktop são desenvolvidos a
partir da facilidade que Java oferece. A comunidade que utiliza Java cria suas
próprias bibliotecas e as disponibiliza na Web para que todos possam compartilhar de
suas facilidades. Bibliotecas para criar servidores HTTP, processar imagens, conectar
banco de dados fazem parte do núcleo da linguagem.
O objetivo deste trabalho é desenvolver uma biblioteca em Java para
representar um novo sistema numérico que utiliza a matemática intervalar e a
aritmética de exatidão máxima. As operações aritméticas são realizadas através de
processamento de Strings.
As principais conclusões deste trabalho foram: (i) a representação de números
racionais processados através de strings permite que se trabalhe com precisão e
exatidão superiores à Java-XSC e o Maple Intervalar, sendo o custo desta exatidão
refletido no tempo das operações; (ii) para qualquer uma das operações, repetidas
1000 vezes, seu tempo total de processamento é menor do que 1 segundo.
Palavras-Chaves:
Matemática
Processamento de Strings..
intervalar,
Computação
científica,
Java,
ABSTRACT
Java is a multiplatform language widely used nowadays. Client-server systems,
embedded systems and desktop applications have been developed from java
facilities. Java community creates his own libraries and publishes it on the web to
share its functionalities with other members. Libraries to create HTTP Servers,
process Images, Database connection are part of Java core.
The goal of this work is to develop a Java library to represent a new numerical
system that uses Interval mathematic and high precision arithmetic all arithmetic
operations shall be executed using string processing.
The main conclusions are: the rational number representation processed using
Strings enables exactness and precision greater than Java-XSC and Interval Maple,
being the exactness cost reflected in operations time. For any operation, 1000 times
repeated, process total time is least than one second.
Keywords: Intervals, Computational Mathematics, Java, String Processing.
LISTA DE FIGURAS
Figura 1. Seqüência de passos para processamento numérico ............................... 7
Figura 2. Distribuição dos números reais na reta ................................................... 10
Figura 3. Representação da primitiva float ............................................................. 13
Figura 4. Representação da primitiva double ......................................................... 13
Figura 5. Espaços da Computação Numérica ........................................................ 20
Figura 6. Representação gráfica do espaço dos Intervalos.................................... 24
Figura 7. Estrutura hierárquica da representação numérica................................... 25
Figura 8. Diagrama UML das Classes do Sistema. ................................................ 30
Figura 9. Diagrama UML da Classe Number.......................................................... 33
Figura 10.Somador (Modelo do full adder) ............................................................. 40
Figura 11. Resolução do carry e do caractere resultante por iteração. .................. 43
Figura 12. Elementos utilizados no algoritmo da divisão ........................................ 46
Figura 13. Definição mdc........................................................................................ 51
Figura 14. Definição mmc....................................................................................... 52
Figura 15. Dividendo da Iteração igual a Zero........................................................ 53
Figura 16 - Identificação da Periodicidade de uma Divisão .................................... 54
Figura 17. Colocando sob a mesma base de denominadores................................ 63
Figura 18. Exemplo do efeito do método que elimina o expoente e o sinal do
denominador........................................................................................................... 63
Figura 19. Ajuste de um tipo Number com denominador null para a realização de
uma adição ou subtração com outro tipo Number com denominador não-nulo...... 65
Figura 20 - Operação de normalização que iguala os expoentes de dois números 67
SUMÁRIO
1. Introdução ................................................................................................................ 1
1.2 Objetivos............................................................................................................. 4
2. Fundamentos ........................................................................................................... 6
2.1 Representação dos Reais nos Computadores ................................................... 7
2.1.1 Sistema de Ponto-flutuante .......................................................................... 8
2.2 Ponto-flutuante em Java................................................................................... 11
2.3 Aritmética intervalar .......................................................................................... 15
2.5 Aritmética de Exatidão máxima: ....................................................................... 20
2.5.1 Semimorfismo ............................................................................................ 22
2.6 Matemática Intervalar ....................................................................................... 23
2.6.1 Intervalo de Números Reais....................................................................... 23
2.6.2 Conjunto de Intervalos ............................................................................... 24
2.6.3 Operações aritméticas ............................................................................... 25
2.6.3.1 Adição Intervalar.................................................................................. 25
2.6.3.2 Pseudo-Inverso Aditivo Intervalar ........................................................ 25
2.6.3.3 Subtração Intervalar ............................................................................ 25
2.6.3.4 Multiplicação Intervalar ........................................................................ 25
2.6.3.5 Pseudo-Inverso Multiplicativo Intervalar .............................................. 25
2.6.3.6 Divisão Intervalar ................................................................................. 26
2.6.4 Operações Entre Conjuntos ....................................................................... 26
2.6.4.1 Interseção de Intervalos ...................................................................... 26
2.6.4.2 União de Intervalos.............................................................................. 26
2.6.4.3 União Convexa de Intervalos............................................................... 26
2.6.5 Outras operações....................................................................................... 26
2.6.5.1 Distância entre Intervalos .................................................................... 26
2.6.5.2 Diâmetro de um Intervalo .................................................................... 26
2.6.5.3 Ponto Médio de um Intervalo............................................................... 27
2.6.5.4 Valor Absoluto de um Intervalo............................................................ 27
2.7. Java-XSC ........................................................................................................ 28
3 Intervalos de Strings Numéricos ............................................................................ 29
3.1 Arquitetura ........................................................................................................ 30
3.2 Processamento de Strings................................................................................ 31
3.3 Processamento de Strings em Java ................................................................. 32
3.4 Definição do Tipo Number ................................................................................ 33
3.5. Operações ....................................................................................................... 38
3.5.1 Comparação entre Strings ......................................................................... 38
3.5.2 Operações Aritméticas ............................................................................... 40
3.5.2.1 Adição Natural ..................................................................................... 40
3.5.2.2 Subtração Natural................................................................................ 41
3.5.2.3 Multiplicação Natural ........................................................................... 43
3.5.2.4 Divisão Natural .................................................................................... 46
3.5.2.5 Máximo Divisor Comum (MDC) ........................................................... 51
3.5.2.6 Mínimo Multiplo Comum (MMC) .......................................................... 52
3.5.2.7 Divisão Racional Desconsiderando o Sinal ......................................... 52
3.5.3 Métodos Aritméticos Públicos do Tipo Number.......................................... 60
3.5.3.1 Adição e Subtração Racional .............................................................. 61
3.5.3.2 Multiplicação Racional ......................................................................... 67
3.5.3.3 Divisão Racional.................................................................................. 68
3.5.4 Comparação entre tipos Number ............................................................... 69
4. Resultados ............................................................................................................. 71
4.1 Operações aritméticas com Strings .................................................................. 71
4.1.1 Adição ........................................................................................................ 71
4.1.2 Subtração................................................................................................... 72
4.1.3 Multiplicação .............................................................................................. 73
4.1.4 Divisão Inteira ............................................................................................ 73
4.1.5 Resto da divisão inteira .............................................................................. 74
4.1.6 máximo divisor comum .............................................................................. 74
4.1.7 mínimo multiplo comum ............................................................................. 75
4.2 Operações aritméticas com o Tipo Number ..................................................... 75
4.2.1 Adição ........................................................................................................ 75
4.2.2 Subtração................................................................................................... 76
4.2.3 Multiplicação .............................................................................................. 76
4.2.4 Divisão ....................................................................................................... 76
4.2.5 Inverso multiplicativo.................................................................................. 77
4.3 Operações Intervalares .................................................................................... 78
4.3.1 Adição ........................................................................................................ 78
4.3.2 Subtração................................................................................................... 79
4.3.3 MultiplicaçÃo .............................................................................................. 79
4.3.4 Inverso Multiplicativo.................................................................................. 80
4.3.5 Divisão ....................................................................................................... 81
4.3.6 Intersecção................................................................................................. 82
4.3.7 União.......................................................................................................... 83
4.3.8 Distância .................................................................................................... 83
4.3.9 Diâmetro..................................................................................................... 84
4.3.10 Ponto Médio ............................................................................................. 84
4.3.11 Valor Absoluto.......................................................................................... 85
4.4 comparação entre resultados ........................................................................... 85
4.5 Comparação do Desempenho .......................................................................... 87
5 Conclusões e trabalhos futuros ............................................................................... 89
5.1 Trabalhos Futuros............................................................................................. 89
Referências ................................................................................................................ 91
1. INTRODUÇÃO
Erros numéricos à primeira vista podem errôneamente ter suas conseqüências
restringidas a domínios simplistas, sem grandes repercussões ou implicações no diaa-dia das pessoas. O mundo e as sociedades em que vivemos hoje dependem de
processos rápidos, ferrementas que auxiliem tomadas de decisão, sistemas de
informação, entre outros sistemas que utilizam computadores que realizam cálculos
através de sistemas numéricos como o de ponto-fixo ou ponto-flutuante. Estes
sistemas são limitados e apresentam problemas e conseqüências quando utilizados.
Por exemplo, em 1991 [VUIK] durante a guerra do golfo uma bateria de
mísseis patriot americanos falhou ao interceptar um missil Iraquiano scud. O Míssil
Iraquiano terminou matando 28 pessoas que estavam num acampamento militar. Um
relatorio do Incidente revelou que um problema de software levou a falha. A causa foi
um cálculo impreciso do tempo, devido a erros de aritmética computacional. O tempo
do clock do sistema era medido em decimos de segundos sendo multiplicado por 1/10
para que o sistema trabalhaste em segundos. O cálculo era efetuado utilizando-se
registradores de 24 bits de ponto-fixo. O resultado da multiplicação era truncado
depois do 24º bit. O erro de truncamento quando multiplicado por um grande número
provocava um erro significativo de aproximadamente 0.000000095. O míssil patriot
ficou armazenado cerca de 100 horas. Multiplicando-se o erro gerado pela quantidade
de decimos de segundos em 100 horas temos 0.000000095×100×60×60×10=0.34s.
Sabendo-se que um Missil scud viaja a aproximadamente 1676 metros por segundo,
Em 0.34s o míssil iraquiano percorre mais de meio Kilometro o suficiente para sair do
alcance de rastreamento do míssil patriot.
Em junho de 1996, o Ariane 5 [VUIK], foguete da companhia espacial européia
explodiu 40 segundos após o seu lançamento, por ter perdido controle e altitude. Era
sua primeira viagem e seu projeto tinha custado $7 bilhões. Investigações concluiram
que a causa da explosão aconteceu por erro no software que controlava o sistema de
referência inercial. Especificamente um número de ponto-flutuante de 64 bits relativo
a velocidade horizontal do foguete em relação a plataforma de lançamento, foi
convertido em um inteiro com sinal de 16 bits. O número era maior que 32768, o
maior inteiro armazenável em um registrador de 16 bits.
1
Erros numéricos também afetaram as eleições na Alemanha, problema só
descoberto em 1992. Existia uma cláusula que afirmava que um partido só poderia ter
cadeiras no parlamento se atingisse uma votação mínima de 5%. Se não atigiste este
valor os votos eram perdidos. Num domingo o partido verde teria atingido exatamente
5% dos votos Depois dos resultados da eleição terem sido divulgados, descobriu-se
que o partido verde tinha obtido apenas 4,97% dos votos. O programa que calculava
as percentagens utilizava apenas uma casa decimal de precisão depois da vírgula e
tinha arredondado para cima. Este sistema vinha sendo usado há muitos anos e
ninguem tinha percebido este erro. Os votos foram recontados e o partido verde
perdeu suas cadeiras no parlamento.
No Brasil, a Embratel teve problemas no seu sistema de contabilidade
implementado em Java. Uma falta de R$100.000 no faturamento mensal, gerando
prejuízos para empresa devido a erros de arredondamento.
À medida que a tecnologia de fabricação de hardware avança, a velocidade
dos processadores aumenta permitindo um poder de processamento maior. Até que
ponto pode-se abrir mão da precisão e exatidão numérica em troca de uma melhor
performance de processamento? Lembrando que o erro na matemática abstrata é
encarado como uma distância entre o valor real e sua aproximação e que num
cenário do mundo real significa desperdício ou perda, em algum momento teremos
que nos preocupar com a precisão, uma vez que a velocidade de processamento
futuramente em alguns contextos não será mais um fator determinante.
No cenário acadêmico atual de desenvolvimento de software no Brasil, a
linguagem de programação Java [Sun Microsystems] tem grande influência por ter
características como portabilidade, segurança e uma infinidade de bibliotecas para
atender às mais diversas necessidades. A deficiência que esta linguagem apresenta
no tratamento numérico é o que impulsiona a realização deste trabalho. A utilização
de Java como uma linguagem para a computação científica tem como base a
aritmética intervalar.
Java, por ser uma linguagem multiplataforma e interpretada, tem que tratar as
plataformas variadas com sistemas numéricos diversos. A solução atual para resolver
problemas de ordem numérica é utilizar o tipo “java.math.BigDecimal”, esta
abordagem, ainda assim não controla a propagação do erro e falha em algumas
operações aritméticas simples. O Exemplo 1 foi implementado utilizando-se o JDK
2
1.4.2 da Sun [Sun Microsystems]. Neste exemplo 10 iterações adicionam a razão de
0,1 à variável “d”. Todos os algoritmos estarão coloridos em vermelho e seus
resultados na saída do console estarão coloridos em azul.
Exemplo 1. Erro na adição entre tipos double
private static void main(String args[]) {
double d = 0.0;
for (int i = 0 ; i < 10; i++) {
d += 0.1;
}
System.out.println("Resultado = " + d);
}
Resultado = 0,9999999999999999.
Analisando o resultado da execução do trecho de código do Exemplo 1 acima,
escrito em linguagem Java, o resultado esperado deveria ser 1, mas o valor obtido é
0,9999999999999999.
Como é de conhecimento público, o sistema de ponto-flutuante representa
apenas um subconjunto do conjunto dos números reais. À medida que operações
aritméticas são realizadas o erro inerente a esta falha na representação dos números
reais é propagado. A aritmética intervalar surge então como a técnica para controlar o
erro máximo produzido por uma seqüência qualquer de cálculos.
Sistemas computacionais que interagem com ferramentas de medição,
amplamente utilizadas em Engenharia, Química e Física, sofrem da incerteza
intrínseca do ato de medir. Medidas sujeitas a erro podem ser substituídas por um
intervalo que contenha os limites da incerteza.
Sistemas computacionais numéricos necessitam, em algum momento, de
cálculo
de
fórmulas,
somatórios,
taxa
de
juros,
percentagens,
realizar
arredondamentos, truncamentos, etc. Em sistemas computacionais científicos esta
necessidade é crítica, como em aplicações espaciais, bancárias, sistemas de
segurança ou que tratem de grandezas numéricas de ordens elevadas. Nos
bancários, por exemplo, qualquer erro de natureza numérica pode trazer prejuízo
para os usuários e mantenedores do sistema.
3
1.2 OBJETIVOS
Sistemas computacionais, tanto antigos como os mais recentes, são incapazes
de representar todos os números reais. A representação dos números reais em
computação é realizada através dos números de ponto-flutuante [CAMPOS e
FIGUEIREDO, 2005], que provê uma representação binária capaz de armazenar uma
quantidade finita de valores.
O objetivo deste trabalho é desenvolver uma biblioteca em Java para
representar um novo sistema numérico que utiliza a matemática intervalar e a
aritmética de exatidão máxima. As operações aritméticas são realizadas através de
processamento de Strings. No modelo convencional, as operações aritméticas são
realizadas em registradores com uma quantidade finita de bits ou emuladas em
linguagem de programação (como Java) com tipos de dados com quantidades fixas
de casas decimais.
1. A nova abordagem proposta utiliza intervalos com limites superiores e
inferiores representados por um novo tipo que processa strings, assumindo a forma
de números racionais. Calcular processando Strings é mais custoso do que utilizar a
ULA, mas o quanto mais custoso é o que será avaliado. Será então medido o
desempenho deste sistema comparando com o sistema numérico padrão de Java,
Portanto o objetivo deste trabalho é alcançado através dos seguintes passos:
Desenvolver uma biblioteca que contemple a inclusão do tipo Intervalo e das
operações sobre esse tipo na linguagem Java, onde todas as operações serão
realizadas por processamento de caracteres. A biblioteca será estruturada e
implementada de forma modular da seguinte maneira:
•
Definição do tipo Number,
•
Operações com o tipo Number,
•
Definição do tipo Intervalo,
•
Operações com o tipo Intervalo.
4
2. Comparar a performance do sistema desenvolvido neste trabalho com
operações em Java e Java-XSC [JAVA-XSC]. Validação através da comparação dos
resultados com o Maple Intervalar [MAPLE, INTPAKX] será realizada.
Esta dissertação está estruturada como descrito abaixo:
O Capítulo 2 contém os fundamentos dos sistemas de ponto-flutuante,
aritmética de exatidão máxima e dos sistemas intervalares.
O Capítulo 3 define o tipo Number como um tipo de Java e mostra como as
operações são realizadas através de processamento de caracteres, suas regras de
formação e restrições. Introduz o tipo Intervalo que utiliza o tipo Number.
O Capítulo 4 apresenta o resultado das comparações das operações com os
tipos Java nativos, comparações de exatidão com os tipos Java-XSC e o Maple
Intervalar e testes comparativos de performance com o Java-XSC.
Finalmente, o Capítulo 5 mostra as conclusões obtidas com o este trabalho,
bem como explicita trabalhos futuros que podem ser realizados a partir da primeira
versão da biblioteca desenvolvida.
5
2. FUNDAMENTOS
Este capítulo apresenta os fundamentos para o desenvolvimento deste
trabalho. Portanto, serão abordados sistemas de ponto-flutuante, ponto-flutuante em
Java, matemática intervalar e aritmética de exatidão máxima.
A representação dos números reais em computadores é uma questão
importante desde os primórdios da história dos computadores. Os números reais
inicialmente eram representados no sistema numérico de ponto fixo. A passagem
para a representação em ponto-flutuante iniciou-se na década de 50 e caracterizou
uma evolução significativa na área da computação científica, principalmente pela
melhora da exatidão nos resultados de operações efetuadas em ponto-flutuante com
cada vez mais dígitos significativos na mantissa. O tamanho da mantissa era
dependente da máquina e ainda poderia ser variado, resultando os formatos
conhecidos como precisão simples, dupla precisão e precisão estendida.
A representação de um número em ponto-flutuante proporcionou muitas
vantagens, mas também introduziu algumas desvantagens com a geração do
problema do controle de erros nas computações numéricas, que muitas vezes
proporcionaram resultados totalmente errados com aparência de serem corretos, ou
seja, um procedimento correto, mas com o resultado perdendo o significado devido à
inexatidão da representação numérica e de arredondamentos aplicados nas
avaliações das operações e expressões aritméticas em ponto-flutuante.
A resolução de problemas de computador na maioria das vezes é feita através
de algoritmos. Os algoritmos numéricos são geralmente definidos e projetados no
espaço dos números reais e complexos. Os cálculos, por sua vez, são efetuados no
conjunto dos números representáveis e operáveis no computador. Este conjunto é
finito e varia de máquina para máquina.
Existem dois tipos de sistemas numéricos usados em computadores digitais, o
sistema de ponto fixo e o sistema de ponto-flutuante. Cada um deles tem seu próprio
conceito de aritmética computacional. O sistema de ponto fixo é utilizado em alguns
sistemas financeiros e comerciais. No modelo de representação de números em
ponto fixo, considera-se duas partes, uma inteira e outra fracionária. A caracterização
consiste da base numérica utilizada (b), o número de dígitos (n) e o número de dígitos
6
da parte fracionária (f). Sendo representada pela terna ordenada P(b, n , f). Na seção
seguinte veremos a caracterização dos sistemas de ponto-flutuante.
2.1 REPRESENTAÇÃO DOS REAIS NOS COMPUTADORES
Nesta seção será mostrado como os números reais são representados nos
computadores, porque têm de ser representáveis e exemplos de propriedades
algébricas dos reais que são perdidas com essa representação [CAMPOS e
FIGUEIREDO, 2005].
Por último, além dos erros citados acima, o processamento numérico nos
computadores é realizado na base 2, Figura 1, portanto, adicionalmente ainda têm-se
os erros de conversão de base.
O conjunto dos números reais é um corpo ordenado completo. O fato dos reais
constituírem um corpo possibilita que nele sejam resolvidas equações do tipo ax = b,
a ≠ 0, onde a solução única é x = a-1b. Como é completo, equações do tipo x2 = 2 têm
solução. Por razões de ordem prática, os computadores, em geral, representam um
número em ponto-flutuante com uma quantidade constante de bits.
Usuário digita
números na base 10
Base 10
Usuário recebe
resultados na base 10
Cálculos
realizados
na base 2
Base 2
Base 10
Base 2
Figura 1. Seqüência de passos para processamento numérico.
No caso dos reais é necessário substituí-los por outro conjunto que os
represente, usualmente o dos números de ponto-flutuante. O problema, porém é que
o conjunto dos números de ponto-flutuante, diferentemente dos reais, não tem
7
propriedades algébricas que garantam os resultados dos cálculos efetuados, além de
não existir uma bijeção entre os conjuntos, um conjunto finito representando um não
enumerável. Por exemplo, a soma de dois números de grande magnitude pode gerar
overflow, ou seja, nem sempre a soma de dois números de ponto-flutuante é um
número flutuante.
2.1.1 SISTEMA DE PONTO-FLUTUANTE
Um número de ponto-flutuante, x, é da forma :
x = m x be = d1.d2...dl x be,
onde, m é uma mantissa de comprimento l, b é a base, a qual é um inteiro
maior ou igual a 2, e e é o expoente, tal que emin ≤ e ≤ emax são números inteiros. Os
dígitos da mantissa são restritos a 1 ≤ d1 ≤ b -1 e 0 ≤ dk ≤ b -1, k =2,..., n. Porque d1
≠ 0, x é denominado um número de ponto-flutuante normalizado. Um sistema de
ponto-flutuante, F, é usualmente representado por:
F (b, l, emin, emax)
Sendo um número real X não nulo representado em F, na forma:
A representação do zero real, dos elementos de menor e maior valor absoluto,
xmin e xmax, e o número de elementos de F, são respectivamente:
emin
0 = + 0.000…0 x b
xmin = + 0.10…0 x b
,
emin
,
emax
xmax = + 0.(b-1)(b-1)...(b-1) x b
,
l -1
#F = 2(b-1)b (emax – emin + 1) + 1.
O termo número de ponto-flutuante deve-se ao fato de que o ponto se move no
número dependendo do expoente da base. Alguns autores usam vírgula ao invés do
ponto; neste trabalho adotou-se o ponto e que é a notação usada nas máquinas
digitais.
8
Exemplo 2. Seja o sistema de ponto-flutuante F = F(2, 3, -1, 2). Portanto F tem base
binária, mantissa de 3 dígitos, o menor expoente é emin = -1 e o maior expoente emax =
2, assim a excursão do expoente vai de -1 a 2 e todos os expoentes deste sistema
são {-1,0,1,2}, isto é, tem-se um total de 4 expoentes, que vem do cálculo emax – emin +
1. Para este sistema tem-se:
Representação do zero:
0 = + 0.000 x 2-1,
Maior elemento de F:
xmax = + 0.111 x 22,
Menor elemento de F:
-xmax = - 0.111 x 22,
Menor elemento positivo de F:
xmin = + 0.100 x 2-1,
Maior elemento negativo de F:
-xmin= - 0.100 x 2-1,
Número de elementos de F:
#F = 33.
Exemplos de sistemas de ponto-flutuante, são dados a seguir com diferentes
bases: decimal (10), binária (2), octal (8), hexadecimal (16).
a) PDP-11 : F(2, 24, -128, 127),
b) Texas SR52 : F(10, 12, -98, 100),
c) HP41C : F(10, 10, -98, 100),
d) IBM 360/370: F(16, 6, -64, 63),
e) B6700 : F(8, 13, -51, 77),
f) UNICAC 1108 : F(2, 27, -128, 127).
O subconjunto dos números reais, R, que é representável em F são os
números não igualmente espaçados localizados na região hachurada mais o zero na
Figura 2 a seguir.
9
-xmax
-xmin
0
xmin
xmax
regiões de
underflow
regiões de
overflow
Figura 2. Distribuição dos números reais na reta
Além da restrição ao número de elementos, uma vez que R é não-enumerável
e F é finito, propriedades algébricas que são válidas em R não são válidas em F. A
referência [HÖLBIG] traz vários exemplos, porém aqui será mostrado no Exemplo 3, a
falha na lei do corte aditiva que consiste na seguinte afirmação:
∀a, b, c ∈ R, a + b = a + c ⇒ b = c.
Será mostrado que em F,
∃a, b, c ∈ F, tais que a + b = a + c não implica b = c .
Exemplo 3. Sejam F = F (10, 4, -9, 9), a = 0.3245 x 102, b = 0.4587 x 10-3, c =
0.8764 x 10-4.
Colocando os números na potência do maior expoente,
a = 0.3245 x 102,
b = 0.00004587 x 102,
c = 0.000008764 x 102.
Somando,
a + b = 0.32454587 x 102,
a + c = 0.324508794 x 102.
Arredondando porque l = 4,
a + b = 0.3245 x 102,
10
a + c = 0.3245 x 102.
Portanto, tem-se que a + b = a + c, mas b ≠ c!
2.2 PONTO-FLUTUANTE EM JAVA
A linguagem Java oferece os seguintes elementos para representação de
ponto-flutuante:
•
Tipo primitivo float.
•
Tipo primitivo double.
•
Wrapped Classes – Float / Double do pacote java.lang [Sun
Microsystems, 2005].
A seguir, nos Exemplos 4, 5, 6 e 7 estão erros em operações de pontoflutuante obtidos a partir da execução de um código fonte em Java, utilizando-se o
JDK 1.4.2 da Sun:
Exemplo 4. Subtração entre tipos double.
public class exemplo4 {
private static void main(String args[]) {
double d = 3.9-3.8;
if(d==0.1) {
System.out.println("igual");
} else {
System.out.println("diferente");
}
}
}
diferente
A resposta para Exemplo 4 deveria ser a literal igual, mas executando-se o
programa Java acima, obtém-se como resposta “diferente”, devido à operação
resultar o valor 0.10000000000000009.
11
No Exemplo 5 utilizamos um laço para adicionar um tipo double com ele
mesmo quatro vezes e dividir pelo seu valor multiplicado por quatro, este processo se
repete 10 vezes:
Exemplo 5. Laço de divisões com o tipo double
public static void main(String args[]) {
double d = 0.1;
for(int i = 0; i < 10; i++) {
d = (d+d+d+d)/4*d;
}
System.out.println(“divisão==” + d);
}
divisão==0.0
O resultado desta seqüência de operações retorna na saída padrão o resultado
zero!
Costuma-se utilizar o tipo java.math.BigDecimal para contornar este tipo
de problemas com o tipo double porém o Exemplo 6 abaixo demonstra que o tipo
BigDecimal também apresenta falhas:
Exemplo 6. Adição e Subtração com o tipo java.math.BigDecimal
public static void main () {
BigDecimal f = new BigDecimal(10E29);
BigDecimal g = new BigDecimal(1);
BigDecimal h = new BigDecimal(10E28);
f = f.add(g);
f = f.subtract(h);
System.out.println("f=" + f);
}
f=900000000000000028451473981441
O
resultado
apresentado
na
saída
padrão
de
Java
para
f
é
900000000000000028451473981441!
Esses fatos ocorrem porque a implementação da representação de pontoflutuante na linguagem Java implementa de maneira parcial o padrão IEEE Standard
for Binary Floating-Point Arithmetic, ANSI/IEEE Standard 754-1985 de aritmética de
ponto-flutuante [Macaulay Institute, 2004]. Os tipos primitivos float e double da
linguagem representam a precisão simples (32 bits) e a precisão dupla (64 bits),
respectivamente, seguindo parcialmente o padrão. A seguir estão duas figuras que
ilustram a representação das primitivas da linguagem Java.
12
1 bit
8 bits
23 bits
sinal
expoente
significante
Figura 3. Representação da primitiva float
1 bit
11 bits
52 bits
sinal
expoente
significante
Figura 4. Representação da primitiva double
A representação através da primitiva float oferece de seis a nove dígitos de
precisão, enquanto que a representação double dentre quinze e dezessete dígitos
de precisão [GOSLING, 1996].
Java requer que os resultados nas operações de ponto-flutuante sejam
arredondados para o número mais exato que a máquina consiga representar. Para
resultados inexatos a resposta deve ser aproximada para o valor mais próximo
representável. Esta é mais uma definição do padrão 754 IEEE conhecida como round
to nearest [GOSLING, 1996].
Operações entre números de ponto-flutuante em Java podem produzir
exceções, que são tratadas da seguinte maneira:
•
Operações que produzem overflow como resultado, apresentam uma
representação de infinito como resposta;
•
Operações que produzem underflow como resultado, apresentam o valor
zero como resposta;
•
Operações que geram resultados matemáticos não definidos produzem
como resposta a notação NaN (Not a Number).
Uma vez que apresentamos problemas da representação de números de
ponto-flutuante na linguagem Java e detalhamos sua representação e seus
propósitos, vamos agora apresentar as discordâncias e inadequações dessa
implementação com o padrão internacionalmente reconhecido ANSI/IEEE Standard
13
754-1985. A implementação de ponto-flutuante em Java falha nos seguintes aspectos
em relação ao padrão IEEE [Macaulay Institute, 2004]:
•
Ausência de suporte às seguintes flags de exceções:
o Operação inválida,
o Overflow,
o Underflow,
o Divisão por zero,
o Resultados inexatos.
•
Não implementação dos arredondamentos direcionados;
•
Não oferecimento de suporte para os seguintes tipos de dados
próprios para aritmética intervalar de máquina:
o Tipo intervalo,
o Tipos complexos,
o Operações aritméticas intervalares,
o Matrizes intervalares,
o Operações matriciais intervalares.
Como mostrado acima, embora existam evoluções na representação numérica
para os computadores, os problemas das operações computacionais não foram
completamente resolvidos. Mesmo com o surgimento da padronização da
representação de ponto-flutuante, que podemos eleger como a primeira solução para
problema de exatidão e precisão de máquina, os erros de computação não estavam
descartados de maneira satisfatória.
Como a representação numérica de ponto-flutuante não conseguiu de maneira
satisfatória minimizar esses problemas para programas de computação científica,
surgiu a proposta de incluir a matemática intervalar como auxílio às linguagens de
computador. Dessa maneira, pelo menos é possível garantir aquela incerteza das
respostas. A definição e os propósitos da aritmética intervalar são descritos na
14
próxima seção deste documento para mostrar ao leitor como este novo tipo de
informação contribui para a garantia e exatidão de rotinas computacionais científicas.
2.3 ARITMÉTICA INTERVALAR
As pesquisas em aritmética computacional estão sendo desenvolvidas desde
os anos sessenta, com o objetivo de controlar os erros computacionais e para que os
computadores suportem uma aritmética muito mais poderosa e precisa que a
aritmética empregada na maioria das linguagens modernas de programação.
A aritmética proposta por Moore [MOORE], em 1966, possibilitou um grande
desenvolvimento destas pesquisas. Esta aritmética trata com dados na forma de
intervalos numéricos e tem como objetivo automatizar a análise de erro
computacional. Serve para controlar o erro de arredondamento e para representar
dados inexatos, aproximações e erros de truncamento de procedimentos. Atualmente,
também vem sendo empregada na elaboração de algoritmos numéricos autovalidáveis.
O uso da aritmética intervalar permite alta exatidão que é uma qualidade
necessária nos ambientes que propiciam a resolução de problemas da computação
cientifica e das engenharias. A minimização dos arredondamentos resulta em
qualidade no resultado que também é uma das características necessárias à
resolução de problemas de verificação de resultado.
A necessidade de termos linguagens de programação para estas aritméticas
computacionais com suporte à computação científica fez que surgissem, na
cooperação de institutos de pesquisas universitários e empresas (como a IBM) as
linguagens com extensões científicas, conhecidas como XSC, que é o acrônimo de
Language Extensions for Scientific Computation.
As
linguagens
XSC
provêem
características
indispensáveis
para
o
desenvolvimento de softwares numéricos modernos e aplicações científicas tais
como: controle de arredondamento, tipos de dados com exatidão após a vírgula;
bibliotecas com as principais rotinas matemáticas para a resolução de problemas,
com arrays dinâmicos, conceito de operador (operadores definidos pelo usuário),
tipos de dados não existentes nas linguagens comuns como o dado complex
(complexo), interval (intervalo), além de outras características.
15
Nos últimos anos, a visão sobre a computação de rede ganhou uma crescente
aceitação à medida que cada vez mais soluções computacionais se voltam para
internet e redes sem fio. A linguagem de programação JAVA é amplamente utilizada
para programação deste tipo de aplicações.
Java é orientada a objetos, independente de plataforma, tem API’s destinadas
a dispositivos móveis, servidores de aplicação e uma grande comunidade de usuários
que colaboram criando novas bibliotecas. Esta linguagem associada a uma extensão
científica poderá ser a primeira escolha nas aplicações computacionais e científicas
na solução de problemas físicos, químicos e das engenharias que necessitam de alta
exatidão ainda com a facilidade da programação voltada a Internet, dispositivos
móveis e tv digital.
A matemática intervalar busca resolver problemas que se concentram
basicamente em dois aspectos:
I. Na criação de um modelo computacional que reflita fidedignamente o
controle e análise dos erros que ocorrem no processo computacional e,
II. Na
escolha
de
técnicas
de
programação
adequadas
para
desenvolvimento de softwares científicos buscando minimizar os erros
nos resultados.
O usuário não pode afirmar a exatidão da resposta estimada sem o auxílio de
uma análise de erro, que é extensa, dispendiosa e nem sempre viável. Assim, a
matemática intervalar busca dar suporte a estes problemas.
Para computarmos qualquer objeto é necessário representá-lo em um
dispositivo computacional o qual é em essência finito. Infelizmente, muitos objetos
fundamentais para resolvermos problemas do dia-a-dia, não são finitamente
representáveis em máquinas. A solução de equações reais complexas é essencial
para resolvermos problemas das engenharias e ciências da natureza, e economia,
entretanto um número irracional não é finitamente representável.
A solução deste tipo de plataforma utiliza as conhecidas aproximações que
induzem a erros. O numero racional 3.14 aproxima o número irracional π =3.1415...
Esta abordagem remete ao problema das limitações dessas aproximações,
suscitando questões tais como se elas possuem as mesmas propriedades algébricas
que os objetos que aproximam.
16
No caso dos números racionais, do ponto de vista algébrico, eles são perfeitos
como aproximações de números reais, pois assim como os números reais, eles
também constituem um corpo, possibilitando a substituição de equações de
coeficientes irracionais por coeficientes racionais.
Entretanto esta abordagem nos conduz ao bem conhecido erro de
aproximação que é a distância entre o irracional e sua aproximação racional. Esse
ainda não é o principal problema desta abordagem. O problema maior reside no fato
de que esse erro de aproximação não obedece a qualquer lei durante as
computações, o que pode levar a distorções em uma computação. O controle deste
erro de aproximação durante as computações é feito por uma computação em
paralelo, requerendo esforços computacionais extras.
Existem três fontes de erros de computação numérica:
A propagação de erros nos dados e parâmetros iniciais: Ao se tentar
representar um fenômeno do mundo físico por meio de um modelo matemático
raramente se tem uma descrição correta deste fenômeno. Normalmente, são
necessárias várias simplificações do mundo físico para que se tenha um modelo
matemático com o qual se posso trabalhar, tomando-se apenas algumas grandezas
como tempo, temperatura, distancia, carga, entre outras. Estas são obtidas de
instrumentos que têm precisão limitada de modo que a incerteza destes parâmetros
iniciais levará conseqüentemente a incertezas dos resultados. A questão de como a
incerteza dos dados contribui para a incerteza da resposta pode ser ignorada ou pode
ser feita uma análise profunda com simulações ou com auxílio da experiência de
pesquisador. A análise é freqüentemente difícil ou impossível, simulações são
dispendiosas, especialmente em muitas dimensões e a experiência do pesquisador
deve ser considerada com cautela. Este tipo de erro é o mais sério porque não é
possível torná-lo arbitrariamente pequeno através da computação tradicional.
•
Se o problema é calcular a área de um círculo de raio 6, na fórmula c
= πr2, π deve ser representado por um número. A questão é que o
conjunto de números disponíveis em qualquer computador é finito,
assim como é finita a excursão de qualquer elemento deste conjunto.
Em outras palavras, no modelo da matemática, o conjunto dos
números reais, longe está de ser representável e operável por
17
qualquer computador. Portanto, expressões como “para todo x
real...”, não têm sentido em computações que exigem um
processamento
numérico
automático,
incluindo
desde
os
calculadores que realizam operações básicas, aos computadores
dos centros científicos.
Erros de Arredondamento: Para a resolução de modelos matemáticos,
muitas vezes torna-se necessária à utilização de instrumentos de cálculo como
computadores digitais. Sabemos que estes instrumentos de cálculo trabalham com
números arredondados, ou seja, representam os números em forma finita de dígitos,
de acordo com o seu sistema interno de reapresentação e que tais limitações geram
erros durante as computações numéricas.
Erros de truncamento: São erros provenientes da utilização de processos,
que deveriam ser infinitos, para a determinação de um valor e que por razões práticas
são truncados. Estes processos infinitos são muito utilizados na avaliação de funções
matemáticas, tais como a função exponencial, logarítmica, funções trigonométricas e
várias outras que uma máquina pode ter.
A limitação dos dados de entrada e a acumulação do erro de arredondamento
em qualquer seqüência finita de operações aritméticas podem ser ambas
rigorosamente controladas, simplesmente pela utilização de aritmética de máquina
ordinária. Assim espera-se que técnicas intervalares forneçam garantias e que
possam ser aplicadas quase automaticamente. Uma resposta intervalar possui a
garantia de sua incerteza.
1
 
∑
n=0  r 
∞
Como calcular
n
r ≠ 0? Não sendo possível realizar tal feito, a
ação tomada é truncar a expressão acima, ou seja, limitar sua operação para um
valor passível de cálculo.
Foi por causa destes erros que surgiram os primeiros trabalhos buscando a
sua resolução Sunaga [SUNAGA] fez o primeiro trabalho sobre intervalos munido de
18
uma aritmética. Sunaga e Moore [MOORE], aparentemente, desenvolveram a mesma
teoria, ao mesmo tempo, em lugares diferentes. Todavia sempre se refere a Moore
quando se fala em matemática intervalar. Os trabalhos de Moore e Sunaga
permitiram que uma idéia simples se transformasse numa poderosa ferramenta para
análise de erros inerentes à computação científica. Assim, os algoritmos trabalham
sobre intervalos em vez de números racionais, tendo como resultados da
computação, intervalos que contém as soluções reais desejadas, cuja amplitude dá
uma medida de sua qualidade.
As respostas aos problemas dos erros de aproximação surgiram nos anos 60
com a aritmética intervalar proposta por Moore, que introduziu operações aritméticas
de maneira a controlar este erro de aproximação, de modo que o resultado de uma
operação com intervalos é novamente um intervalo. Esta abordagem define um
intervalo fechado [a1;a2] como uma aproximação de todos os números reais
pertences a ele.
O computador pode ser definido como uma máquina digital, utilizada também
para realizar cálculos, que tem como unidade fundamental de processamento o bit.
Os bits que constituem as informações da memória do computador são utilizados
para representar letras do alfabeto, números inteiros, imagens, vídeos, sons,
aplicações comerciais, entre outros diversos tipos de informações.
Todos os elementos acima citados apresentam como característica comum e
principal o fato de poderem ser finitamente representados, ou seja, o computador
possui uma maneira exata para representá-los a partir de uma determinada
codificação. Os problemas para os computadores começam a surgir quando é
necessário operar os números reais, os quais são representados pelos números de
ponto-flutuante.
A utilização de intervalos permite diminuir e controlar a perda de exatidão
depois de repetidos cálculos numéricos com números de ponto-flutuante. Através da
aritmética intervalar [MOORE], os números reais podem ser representados na forma
de intervalos, e então passam a ser manipulados como tal, aproveitando todos os
benefícios que esta representação pode proporcionar: exatidão numérica dos cálculos
efetuados com verificação automática dos resultados.
19
2.5 ARITMÉTICA DE EXATIDÃO MÁXIMA
A aritmética de exatidão máxima garante que o resultado de operações
realizadas ou é um número de máquina ou está compreendido entre dois números de
máquinas consecutivos. A Figura 5 a seguir lista todos os espaços da computação
numérica do ponto de vista da aritmética de alta exatidão ou a aritmética
computacional avançada.[CAMPOS, 1995]
Figura 5. Espaços da Computação Numérica
•
D e S representam os conjuntos dos números de ponto-flutuante de
precisão dupla(D) e Simples (S). VD e VS são os conjuntos dos
vetores cujos elementos são números de ponto-flutuante de precisão
dupla e simples, respectivamente. MD e MS, conjunto das Matrizes
cujos elementos são números de ponto-flutuante de precisão dupla e
simples, respectivamente.
•
R, conjunto dos números reais. VR, espaço dos vetores cujas
componentes são números reais. MR, espaço das matrizes cujos
elementos são números reais.
•
C, conjunto dos complexos. VC, espaço dos vetores cujas
componentes são números complexos. MC, espaço das matrizes
cujos elementos são números complexos. VCD, MCS conjunto dos
vetores e matrizes complexas de ponto-flutuante de precisão dupla e
simples, respectivamente.
20
•
IR, espaço dos intervalos de reais. Os elementos da tabela iniciados
por I indicam espaços de intervalos. Assim IVR, por exemplo é o
espaço dos intervalos cujas componentes são vetores de números
reais; IS, é o conjunto dos intervalos cujos limites são números de
ponto-flutuante de precisão simples.
•
Os espaços iniciados por P indicam conjunto das partes. PVR, por
exemplo, é o conjunto das partes do espaço dos vetores cujas
componentes são números reais.
A aritmética de exatidão máxima [KULISCH, 1983] foi desenvolvida para
computação científica. Ela fornece um método axiomático para as operações
aritméticas realizadas em computadores que captura propriedades essenciais
associadas com arredondamentos em computações, construindo um sistema de
axiomas para problemas gerais que permite várias aplicações; seu grande mérito é
propor uma forma de operar números reais representados por números de máquina
preservando uma estrutura algébrica chamada de anelóide ou vetóide. A forma de
operar valores mantendo essa estrutura algébrica é através do semimorfismo
[KULISCH, 1983]. Semimorfismos são arredondamentos com algumas características
básicas. Contudo a definição de semimorfismo [CAMPOS, 1995] pressupõe conjuntos
onde a existência de supremos e ínfimos seja garantida.
Não somente aos anelóides, vetóides e semimorfismos, a proposta e o
desenvolvimento da aritmética computacional avançada se deve a Moore que na
década de 60 introduziu o intervalo. Erros de medições, arredondamentos e controle
do erro podem ser resolvidos através de intervalos. Uma área de pesquisa com amplo
espectro de aplicações, além do potencial de desenvolvimento teórico, têm
progredido a partir dos trabalhos de Moore.
Seja R, o conjunto dos números reais e S (b, l, emin, emax) um sistema de
ponto-flutuante, tem-se que:
Para cada x ∈ R, | x | ≤ * • d1d2...dl • bemax , di = b-1, i = 1,...,l; existem limites
inferiores para x em S e existem limites superiores para x em S;
O conjunto dos limites inferiores de x em S tem um maior elemento e o
conjunto dos limites superiores de x em S tem um menor elemento.
21
2.5.1 SEMIMORFISMO
A Questão que se coloca de modo geral, com respeito a uma computação
científica, é como operações aritméticas que são definidas numa estrutura R, *, ≤
podem ser mais bem aproximadas em uma outra estrutura S, *, ≤ com S ⊆ R.
Tomando como exemplo o conjunto dos reais R, sabe-se que R, +, • , ≤ é um
corpo [CAMPOS, 1995]. As operações aritméticas nos reais, quando realizadas em
computadores, têm que ser aproximadas em S ⊆ R, Onde S é um sistema de pontoflutuante de determinada máquina. Além disso, essa aproximação não pode ser
realizada por meio de um isomorfismo, nem por meio de um homomorfismo. A
Aritmética de alta exatidão, define uma técnica que permite aproximar as operações
aritméticas definidas nos reais, no Screen [CAMPOS, 1995]. Um subconjunto S de R
é um Screen de R se R é “visto” através de S. O sistema de ponto-flutuante é um
Screen para o conjunto dos números reais. Não é possível operar os reais em
qualquer sistema de computação existente, pois este dispõe apenas dos números de
máquina, os quais formam um conjunto finito enquanto os reais são não enumeráveis.
Assim, os reais são vistos através dos números de ponto-flutuante.
Seja
:R → S tal que
a = a, ∀a ∈ S. O mapeamento
, é denominado
arredondamento. Homomorfismos preservam operações nas transformações entre
estruturas, mas
não é um homomorfismo como pode ser visto através do exemplo a
seguir.
Seja R o conjunto dos reais e S(10,1,-1,1) um sistema de ponto-flutuante;
éo
arredondamento para o mais próximo ou, se ponto médio para o extremo superior e +
a operação de adição em S. Sejam a,b ∈ R, a = 0.34 e b = 0.54. Então:
a = 0.34 = 0.3
b = 0.54 = 0.5
(a + b) = (0.34 + 0.54) = 0.88 = 0.9
( a) + ( b) = ( 0.34) + ( 0.54) = 0.3 + 0.5 = 0.8
logo,
(a + b) ≠ (a) + (b)
22
2.6 MATEMÁTICA INTERVALAR
As primeiras pesquisas e trabalhos na área da matemática intervalar foram
desenvolvidos por Moore [MOORE, 1979] que propôs a utilização de intervalos
numéricos para operar entre si. Assim, os problemas de aproximação passaram a ser
contornados, pois a resposta das operações seria agora um intervalo que conteria o
resultado esperado, caso este não pudesse ser representado de forma exata pela
máquina. O passo seguinte foi incorporar os intervalos e sua aritmética, bem como
os princípios da aritmética de exatidão máxima [KULISCH, 1983], às linguagens de
programação.
A utilização de intervalos para expressar resultados permite controlar a perda
de exatidão depois de repetidos cálculos numéricos computacionais. Através da
aritmética intervalar, os números reais podem ser representados na forma de
intervalos, e então passam a ser manipulados como tal, aproveitando todos os
benefícios que esta representação pode proporcionar, seja na exatidão numérica dos
cálculos efetuados ou nas linguagens com verificação automática dos resultados
Esta abordagem define um intervalo como sendo uma aproximação de todos
os números reais pertencentes a ele, ou seja, se a representação de um intervalo for
[i1, i2], então todos os números reais entre i1 e i2, inclusive, farão parte deste intervalo,
abstraindo dessa maneira a representação numérica limitada da máquina.
Concluindo, a aritmética intervalar trata da representação numérica através de
intervalos e das operações neles realizadas. A seguir serão detalhadas as principais
definições, bem como as operações de maior destaque. Esta seção visa ambientar os
leitores no universo da Aritmética Intervalar, para um melhor entendimento e
compreensão do restante do documento de dissertação.
2.6.1 INTERVALO DE NÚMEROS REAIS
Um intervalo de números reais, R, é da forma
I = [x1, x2],
onde, x1 e x2 pertencem ao conjunto dos números reais, tal que x1 ≤ x2. São
exemplos de intervalos: [1,2], [-2,-1], [1.9, 4.8], [1,1].
23
2.6.2 CONJUNTO DE INTERVALOS
O conjunto de todos os intervalos de reais pode ser definido da seguinte forma:
IR = { [x1, x2] | x1, x2 ∈ R , x1 ≤ x2}.
Associando-se a cada intervalo [x1, x2] ∈ IR um ponto (x1, x2) ∈ R2, obtemos
uma representação geométrica para IR, conforme a Figura 6 a seguir:
R
[x1, x2]
x1 = x2
0
Figura 6. Representação gráfica do espaço dos Intervalos
Importante ressaltar que todo e qualquer número real x ∈ R pode ser visto
como um intervalo de IR. Basta identificar os pontos x ∈ R com os intervalos pontuais
X = [x, x] ∈ IR. Estes intervalos também são chamados de intervalos degenerados,
porém a nomenclatura de intervalo pontual é comumente utilizada. A Figura 7
representa a hierarquia numérica dos conjuntos, onde N é o conjunto dos números
naturais, Z, dos inteiros, Q, dos racionais, R, dos reais e IR dos intervalos.
24
Figura 7. Estrutura hierárquica da representação numérica
2.6.3 OPERAÇÕES ARITMÉTICAS
Nesta subseção serão exibidas as definições das operações intervalares
[MOORE, 1979, DIVÉRIO,1997]. Considerando os intervalos A = [a1, a2] e B = [b1,b2],
a definição das operações aritméticas entre intervalos pode ser generalizada através
da fórmula a seguir.
A * B = {a * b | a ∈ A, b ∈ B}, * ∈ {+, -, *, / }.
2.6.3.1 ADIÇÃO INTERVALAR
A + B = [ (a1 + b1 ) , ( a2 + b2 ) ].
2.6.3.2 PSEUDO-INVERSO ADITIVO INTERVALAR
-A = [-a2 ,- a1].
2.6.3.3 SUBTRAÇÃO INTERVALAR
A - B = A + (-B) = [ (a1 - b2 ) , ( a2 - b1 ) ].
2.6.3.4 MULTIPLICAÇÃO INTERVALAR
A x B = [ mim { a1.b1,a1.b2,a2.b1.,a2.b2} , max{ a1.b1,a1.b2,a2.b1.,a2.b2} ].
2.6.3.5 PSEUDO-INVERSO MULTIPLICATIVO INTERVALAR
A-1 = 1/A = [1 / a2 ,1 / a1], 0 ∉A.
25
2.6.3.6 DIVISÃO INTERVALAR
A / B= [ min{a1/b1,a1/b2,a2/b1,a2/b2 } , max{ a1/b1,a1/b2,a2/b1,a2/b2 ].
2.6.4 OPERAÇÕES ENTRE CONJUNTOS
Nesta subseção serão exibidas as funções envolvendo conjuntos de intervalos.
2.6.4.1 INTERSEÇÃO DE INTERVALOS
Se max {a1, b1} ≤ min {a2, b2}, então
A ∩ B = [ max {a1, b1} , min {a2, b2} ].
Caso min {a2, b2} < max {a1, b1} então
A ∩ B = Ø.
2.6.4.2 UNIÃO DE INTERVALOS
A ∪ B = [ min {a1, b1} , max {a2, b2} ], A ∩ Β ≠ Ø.
2.6.4.3 UNIÃO CONVEXA DE INTERVALOS
A ∪ B = [ min {a1, b1} , max {a2, b2} ].
Um ponto importante é que ao contrário da operação de união, em operações
de união convexa, a intersecção entre intervalos é permitida ser vazia.
2.6.5 OUTRAS OPERAÇÕES
2.6.5.1 DISTÂNCIA ENTRE INTERVALOS
d = max{|a1 − b1|, |a2 − b2|}.
2.6.5.2 DIÂMETRO DE UM INTERVALO
w = a2 – a1.
26
2.6.5.3 PONTO MÉDIO DE UM INTERVALO
Seja A = [a1, a2] um intervalo pertencente ao intervalo dos números reais. O
ponto médio deste intervalo define-se como sendo a média aritmética dos seus
valores extremos.
m = (a1 + a2) / 2.
2.6.5.4 VALOR ABSOLUTO DE UM INTERVALO
|A| = max{|a1|,| a2|}
O Exemplo 7 ilustra as definições anteriormente mostradas.
Exemplo 7. Sejam os seguintes intervalos A = [0,10], B = [-3, 6] e C = [1, 3]. Então:
A + B = [0, 10] + [-3, 6] = [-3, 16].
A - B = [0, 10] + ( - [-3, 6] ) = [0, 10] + [-6, 3] = [-6, 13].
A x B = [0, 10] X [-3, 6]
= [min {0 X -3, 0 X 6, 10 x -3, 10 X 6}, max {0 X -3, 0 X 6, 10 x -3, 10 X 6}] = [0, 60].
A / B = [0, 10] / [-3, 6]
= [ min {0 / -3, 0 / 6, 10 / -3, 10 / 6}, max {0 / -3, 0 / 6, 10 / -3, 10 / 6}] = [0, 10/6].
C-1
= [1/3, 1].
A ∩ B = [0, 10] ∩ [-3, 6] = [ max (0, -3), min (10, 6) ] = [ 0, 6].
A ∪ B = [0, 10] ∪ [-3, 6] = [ min (0, -3), max (10, 6) ] = [-3, 10].
d(A, B) = max { |0 – (-3)| , |10 - 6| } = max (3, 4) = 4
w(A)
= w (10 – 0 ) = 10
m(A)
= m ((10 + 0) / 2) = 5
|A|
= max{|0|, |10|} = 10
27
2.7. JAVA-XSC
Concebida na década de 90, a linguagem de programação Java alcançou
enorme popularidade desde o início de sua utilização. Sua rápida ascensão e grande
aceitação devem-se, principalmente, às propriedades do paradigma de orientação a
objeto, particularmente, o fato de ser portável. Ou seja, as aplicações desenvolvidas
na linguagem Java podem ser executadas em diferentes tipos de plataformas.
Uma sucinta e boa definição para a linguagem Java pode ser encontrada em
um artigo próprio de sua empresa, criadora e mantenedora, a Sun Microsystems, que
a define da seguinte maneira: Java é simples, orientada a objeto, distribuída,
interpretada, robusta, segura, neutra de arquitetura, portável, multi-thread e dinâmica
[CHOUDHARI, 2001].
Outras características que fazem de Java uma linguagem de alto nível de
abstração e de fácil manipulação pelos programadores são o suporte a herança entre
os objetos, abolição do uso de ponteiros, alocação dinâmica de memória e, por fim,
utilização do processo interno de garbage collector para desalocação de memória.
Embora apresente todas as vantagens acima citadas, Java apresenta falhas
na sua implementação que comprometem aplicações de caráter matematicamente
computacional. Conforme apresentado anteriormente, a implementação do padrão
ANSI/IEEE Standard 754-1985 referente a representação de ponto-flutuante,
inviabiliza o desenvolvimento de aplicações matemáticas que necessitem de alta
exatidão nos resultados. Um trabalho nesta linha foi desenvolvido pelo grupo de
matemática computacional da Universidade Federal de Pernambuco [JAVA-XSC] e
Universidade Federal do Rio grande do Norte [DUTRA] que implementaram uma
biblioteca intervalar utilizando os tipos primitivos de Java como limites superiores e
inferiores dos Intervalos. Porem esta abordagem está passível dos erros
apresentados nos Exemplos 4, 5, 6 e 7. Exatamente, neste ponto é que entra a
contribuição deste trabalho propondo a inclusão de uma biblioteca intervalar que
processa caracteres tendo como linguagem alvo Java .
A biblioteca desenvolvida neste trabalho conterá a definição de novos tipos de
dados, entre eles o tipo Number (Strings numéricos), e Intervalo que utilizam
Strings Numéricos. Fazendo uso dos conceitos da Matemática Intervalar [MOORE,
1979] na definição das operações entre esses novos tipos.
28
3 INTERVALOS DE STRINGS NUMÉRICOS
Os algoritmos que utilizamos para realizar cálculos sem a ajuda de
máquinas podem ser implementados numa linguagem de programação. No primário
escolar, aprendemos a processar cadeias de caracteres (Strings) em um papel,
portanto, tendo isto em mente por que não ensinar a máquina a fazer o mesmo
utilizando Strings numéricos?
Os algoritmos que realizam operações aritméticas utilizam truncamentos e
arredondamentos para contornar a sua incapacidade de representar todos os
números reais. A solução alternativa proposta para diminuir a propagação de erro ao
se trabalhar com números, outrora não representáveis, é mudar o paradigma de
representação.
O conjunto dos números racionais tem uma característica peculiar que é a
possibilidade de ser representado numericamente por uma quantidade finita de
símbolos; até uma dízima periódica pode facilmente ser representada por um
conjunto finito de símbolos na sua representação fracionária.
Uma limitação desta abordagem é representar e realizar operações sem
propagação de erro com o conjunto dos números irracionais em que é impossível
obter a representação de seus elementos por uma seqüência finita de caracteres. Por
este motivo, intervalos são utilizados para representar um número irracional. Para
calcularmos operações com o menor erro possível, um sistema intervalar foi
implementado como a representação genérica, tendo como limites superiores e
inferiores, strings numéricos.
29
3.1 ARQUITETURA
Um sistema hierárquico é o modelo percebido naturalmente quando estamos
lidando com objetos que possuem características e comportamentos comuns que não
precisam ser modificados em tempo de execução. A biblioteca numérica intervalar foi
implementada seguindo o modelo UML [BOOCH] descrito na Figura 8.
StringNumber
divisor
expoente
mantissa
sinal
sinalExpoente
Intervalo
limi teInferior : StringNumber
limi teSuperior : StringNumber
diam etro()
pontoMedio()
somar()
subtrair()
multiplicar()
dividir()
...()
Operacao
executarOperacao()
...
Interseccao
OperacaoUnaria
OperacaoBinaria
operador1 : Intervalo
operador1 : Intervalo
operador2 : Intervalo
Multiplicacao
Soma
...
InversoAditivo
inversoMultiplicativo
Divisao
Subtracao
Uniao
Figura 8. Diagrama UML das Classes do Sistema.
A classe Intervalo é a classe base do sistema. Ela possui como atributos o
limiteSuperior e o limiteInferior que são do tipo Number (detalhes sobre
esta classe serão discutidos nas seções posteriores). Operações como calcular o
diâmetro de um intervalo, ponto médio, multiplicação por um escalar, distância e valor
absoluto são definidas nesta classe.
A classe abstrata Operacao herda da classe Intervalo e impõe que as classes
filhas concretas implementem o método abstrato executarOperacao. Este método
tem por finalidade definir a operação aritmética que será executada pelas classes
30
descendentes. Nesta classe também está definido o procedimento setIntervalo
que dá valor aos limites superiores e inferiores do Intervalo resultado da operação.
Seguindo na hierarquia as classes operacaoBinaria e operacaoUnaria
são as primeiras classes descendentes de Operacao e definem construtores para
operações aritméticas binárias e unárias. No construtor dessas classes é chamado o
método executarOperacao seguido do procedimento setIntervalo para que na
construção da operação já tenhamos seu resultado computado.
As classes filhas de operacaoBinaria e operacaoUnaria só precisam
chamar em seus construtores o construtor da classe pai e implementar o método
executarOperacao,
que define como esta operação intervalar deve proceder
[JAVA COM STRINGS]. As regras específicas para a multiplicação intervalar, por
exemplo, são codificadas neste método. As operações aritméticas realizadas no
método executarOperacao destas classes utilizam a classe Number, esta classe
possui a implementação de toda a aritmética baseada em processamento de
caracteres. As sessões seguintes descrevem em detalhes como foram concebidos
esta classe, seu construtor, atributos e operações aritméticas.
3.2 PROCESSAMENTO DE STRINGS
Um símbolo é uma entidade abstrata que não é definida formalmente, assim
como o ponto não é definido na Geometria. Letras e números são exemplos de
símbolos freqüentemente usados. Uma String (ou Palavra) é uma seqüência finita
de símbolos justapostos. Se a, b, e c são símbolos abcb é uma String. O
comprimento de uma String w, denotado por |w| é definido como o número de
símbolos que compõem uma String. Por Exemplo, abcb tem comprimento 4. A
String vazia é denotado por ε, e não é composta por símbolo algum. Então |ε| = 0.
Um conceito importante é a concatenação de Strings. A concatenação de duas
Strings é a String formada pela escrita da primeira seguida pela escrita da segunda
(justaposição), sem nenhum espaço entre as duas. Por exemplo, a concatenação de
passa e tempo é passatempo. A String vazia é o a identidade do operador
concatenação, ou seja, εw = wε = w para qualquer String w [HOPCROFT, 1979].
31
3.3 PROCESSAMENTO DE STRINGS EM JAVA
A classe String em Java representa um conjunto de caracteres (símbolos).
Todos os strings literais do tipo "123" ou "abc" são instâncias desta classe. A
String vazia em Java é representada por "" (abre e fecha aspas);
String str = "abc";
é equivalente a :
char data[] = {'a', 'b', 'c'};
String str = new String(data);
A classe String
(java.lang.String) possui métodos para obter
caracteres individuais da seqüência, comparação entre instâncias de strings, extrair
subconjuntos de strings (substrings), criar cópias, alterar para maiúsculas e
minúsculas, entre outras funcionalidades. A linguagem Java provê suporte especial
para concatenação de strings através do operador (+) e para conversão de outras
instâncias de objetos em strings (método toString()). Operações matemáticas {+,
-, *, %(resto da divisão), / (divisão inteira)} com o tipo char são permitidas e leva em
consideração o valor unicode do caractere.
O Unicode [UNICODE] é o padrão de codificação de caracteres desenvolvido
pelo Unicode Consortium. Este vem sendo adotado por muitas grandes empresas no
sentido de padronizar a codificação de caracteres. Esta codificação sempre foi
problemática devido à existência de diferentes padrões (ASCII pt, en, EBCDIC, entre
outros.) e da incompatibilidade entre eles, o que fazia com que a representação de
texto entre diferentes idiomas ficasse confusa devido às diferentes interpretações, por
exemplo, dos caracteres especiais e acentuados (ç, Ç, ã, Ã, õ, Õ, ö, Ö, etc.).O
Unicode associa um número para cada caractere, independente do programa,
plataforma ou idioma, abrangendo quase todas as escritas em uso atualmente, além
das escritas históricas já extintas e os símbolos, em especial os matemáticos e os
musicais.
32
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.
33
•
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.
34
Quadro 1: Gramática de formação do tipo Number
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
N → 0N | 1N | 2N | 3N | 4N | 5N | 6N | 7N | 8N | 9N | /S
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.
35
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:
36
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;
}
}
37
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.
3.5. OPERAÇÕES
O primeiro passo para criarmos um sistema numérico baseado em
processamento de caracteres é definir suas operações mais básicas, no decorrer do
desenvolvimento do sistema naturalmente surgiu a necessidade de criar operações
de forma hierárquica, ou seja, criar as operações que efetuem cálculos considerando
apenas strings numéricos, depois levar em consideração os expoentes e os sinais,
em seguida levar em consideração os denominadores e por último realizar as
operações intervalares.
As quatro operações básicas que processam Strings são o pilar para
desenvolver as demais operações, pois elas realizam operações sob a mantissa dos
números, todas elas são private static, ou seja são métodos de classe, não é
necessário instanciar um objeto para utilizá-las e as mesmas possuem apenas
visibilidade dentro da classe.
3.5.1 COMPARAÇÃO ENTRE STRINGS
A classe String de Java possui um o método compareTo para comparar
strings lexicograficamente, a comparação é baseada no valor unicode de cada
caractere das strings. Este método recebe como parâmetro outra String a fim de
efetuar a comparação e tem como resultado um inteiro (int). Este inteiro retorna:
•
Um valor menor que zero se a instância da String que executa o método for
menor lexicograficamente que a String passada como parâmetro.
•
Zero se as strings forem idênticas.
38
•
Um valor maior que zero se a instância que executa o método de comparação
é maior lexicograficamente que a String passada como parâmetro.
Diferença lexicográfica: Se duas strings são diferentes, então elas devem ter
caracteres diferentes em algum índice para um determinado índice válido em ambas
as strings, seus comprimentos diferem ou as duas situações acontecem. Se elas
possuem caracteres diferentes em um ou mais índices, seja k o menor índice onde
esta situação acontece. Assim a String cujo caractere na posição k possui o menor
valor, determinado através do operador “<”, lexicograficamente precede a outra
String. O método compareTo retorna a diferença dos caracteres na posição k.
return this.charAt(k) - stringParametro.charAt(k);
Se não existir um índice k em que as Strings se diferenciem. Então a menor
String em comprimento (quantidade de caracteres) precede lexicograficamente o
maior String. Neste caso, compareTo retorna a diferença de comprimento dos Strings:
return this.length() - stringParametro.length();
Portanto, só pudemos usar nesta biblioteca a comparação lexicográfica
quando as mantissas fossem do mesmo tamanho. Uma vez que a String “8” seria
maior que a String “300”!
O algoritmo da comparação, usado para determinar igualdade, maior que (>) e
menor que (<) (Exemplo 10), inverte a ordem em que as avaliações são feitas:
•
Passo 1: Removem-se os possíveis zeros à esquerda.
•
Passo 2: Avalia-se o tamanho da String. Quanto maior o comprimento da
String maior seu valor numérico.
•
Passo 3: Se os tamanhos forem iguais, avalia-se lexicograficamente as
Strings.
•
Passo 4: Um inteiro é retornado com valor –1 se a String do primeiro
parâmetro for menor lexicograficamente do que a String do segundo
parâmetro, 0 (zero) se elas forem iguais e 1 se a String do primeiro
parâmetro for maior lexicograficamente do que a String do segundo
parâmetro.
39
O algoritmo da comparação está descrito no exemplo 10:
Exemplo 10. Comparação entre Strings Numéricas
private static int compareStrings(String a, String b) {
int ret = 0;
a = removeLeftZeros(a);
b = removeLeftZeros(b);
if (a.length() > b.length()) {
ret = 1;
} else if (a.length() < b.length()) {
ret= -1;
} else {
ret = a.compareTo(b);
}
return ret;
}
3.5.2 OPERAÇÕES ARITMÉTICAS
3.5.2.1 ADIÇÃO NATURAL
A adição de Strings foi implementada seguindo o modelo do Full-Adder
[GAJSKI], que é um somador binário utilizado nas unidades lógicas e aritméticas
(ULA) de um microprocessador. Só que no contexto de processamento de caracteres
a base é 10 e uma adaptação é necessária. Sejam A e B Strings que representam a
mantissa de dois números, os caracteres da mantissa da soma S é dada pela figura
10, Onde % é a operação que calcula o resto da divisão e div é a operação de
divisão natural.
C0 = 0
Ci+1 = (Ai + Bi + Ci) div 10
Si = (Ai + Bi + Ci) % 10
Figura 10.Somador (Modelo do full adder)
É importante ressaltar que o que acontece são adições de caracteres e quando
as strings possuem tamanhos diferentes, se completa com zeros à esquerda a menor
String, até que as duas Strings sejam do mesmo tamanho. O código em Java está
descrito no Exemplo 11, onde Z é a constante Number.CARACTERE_ZERO.
40
Exemplo 11. Adição Natural de Strings
private static String adicaoNaturalStrings(String a, String b){
//resultado da adicao
StringBuffer soma = new StringBuffer("");
//diferenca entre o tamanho das Strings
int difTamanho = a.length() - b.length();
if (difTamanho > 0) {
b = completeWithLeftZeros(b,difTamanho);
} else if (difTamanho < 0){
a = completeWithLeftZeros(a,-difTamanho);
}
//valor a ser incluido na proxima adicao
int carry = 0;
//soma em valores absolutos dos caracteres
int somaCaracteres = 0;
//variavel auxiliar para armazenar momentaneamente a soma
char temp = '0';
for (int i = a.length()-1; i >= 0; i--) {
somaCaracteres = carry + a.charAt(i) + b.charAt(i)- 2*Z;
temp = (char)((somaCaracteres%10) + Z);
carry = (somaCaracteres/10);
soma.append(temp);
}
if (carry != 0) {
soma.append(carry);
}
soma.reverse();
return soma.toString();
}
É necessária a inclusão do valor unicode do caractere zero uma vez que os
caracteres ‘0’ até ‘9’ possuem valores seqüenciais de 48 a 57. A adição recebe duas
strings formadas apenas por números (caracteres de ‘0’ a ‘9’) e retorna uma nova
String representando a soma destes números.
3.5.2.2 SUBTRAÇÃO NATURAL
A subtração natural foi implementada segundo o Exemplo 12, Onde Z é a
constante
Number.CARACTERE_ZERO
e
subNaturalStrings
é
o
método
subtracaoNaturalStrings:
O método subtracaoNaturalStrings requer que a String “a” seja maior
que a String “b”, lembrando que este método é visível apenas dentro da classe
(private), outro método desta classe se preocupará em comparar [ver Seção 3.5.4]
se “a” é maior que “b” e chamará o método com os parâmetros na ordem correta.
O algoritmo da subtração inicia completando com zeros à esquerda da String
de menor comprimento até que as duas strings tenham a mesma quantidade de
caracteres.
41
Exemplo 12. Subtração Natural de Strings
private static String subNaturalStrings(String a, String b){
//variavel de retorno
String subtracao = "";
int dif = a.length() - b.length();
if (dif > 0) {
b = completeWithLeftZeros(b, dif);
} else if (dif < 0) {
a = completeWithLeftZeros(a, -dif);
}
//define se adicionaremos mais 1 ao subtraendo ou não
int carry = 0;
//valor de cada caractere resultado da subtracao
char temp = '0';
for (int i = a.length()-1; i >= 0; i--) {
if (a.charAt(i) < b.charAt(i) + carry) {
temp = (char)(a.charAt(i) + 10);
temp = (char)(temp - (b.charAt(i) + carry) + Z);
subtracao = temp + subtracao;
carry = 1;
} else {
temp = (char)(a.charAt(i) - (b.charAt(i) + carry) + Z);
subtracao = temp + subtracao;
carry = 0;
}
subtracao = Number.removeLeftZeros(subtracao);
return subtracao;
}
A cada iteração (Exemplo 13) é verificado se o valor unicode [UNICODE] do
caractere da String “a” na posição i, é menor que a soma do valor unicode do
caractere da String “b’, na posição i somado com o carry. O carry, é uma
variável utilizada no algoritmo de subtração, para indicar se na próxima iteração deve
ser adicionada ou não, uma unidade ao subtraendo. O carry é iniciado com o valor
zero, pois inicialmente não é necessária a adição de uma unidade ao subtraendo”.
Exemplo 13. Inicialização do carry e avaliação do uso da dezena emprestada.
int carry = 0;
for (int i = a.length()-1; i >= 0; i--) {
if (a.charAt(i) < b.charAt(i) + carry) {
Se o resultado da comparação:
a.charAt(i) < b.charAt(i) + carry (Exemplo 12, 13) (I)
for verdade: ao caractere da posição i da String “a” é adicionado 10 unidades
(dezena emprestada Exemplo 14 e 12) e armazenada na variável temp. Desta
variável (temp) é subtraído o caractere da posição i da String “b” somado com o
carry. Sempre se deve adicionar o valor unicode do caractere zero para que a
42
variável “temp” guarde o caractere numérico resultante da subtração. À String
“subtracao”, que armazena os caracteres da operação subtrair, é atribuído o valor
da concatenação (+) entre ela e a variável “temp”. Neste caso atribui-se ao carry o
valor 1 para indicar um futuro acréscimo a subtração da próxima iteração devido ao
uso da dezena emprestada utilizada na iteração corrente (Exemplo 14 e 12).
Exemplo 14. Calculo do caractere da diferença com uso da dezena emprestada.
temp = (char)(a.charAt(i) + 10);
temp = (char)(temp - (b.charAt(i) + carry) + Z);
subtracao = temp + subtracao;
carry = 1;
Caso o resultado da comparação definida em (I) seja verdadeiro, deve-se
subtrair do caractere situado na posição i da String “a”, o caractere da posição i da
String “b” adicionado do carry. Seguindo a mesma filosofia da adição, adiciona-se a
esta diferença o valor unicode do caractere zero. Em seguida atribui-se ao carry o
valor zero, uma vez que não houve a necessidade de se utilizar uma dezena
emprestada (Exemplo 15).
Exemplo 15.Calculo do caractere da iteração sem o uso da dezena emprestada.
temp = (char)(a.charAt(i) - (b.charAt(i) + carry) + Z);
subtracao = temp + subtracao;
carry = 0;
3.5.2.3 MULTIPLICAÇÃO NATURAL
O algoritmo da multiplicação possui complexidade O(n2) pois é necessário
multiplicar todos os caracteres das strings parcelas. Sejam A e B strings, M o
resultado da multiplicação parcial e C o carry, no laço mais interno (Figura 11) um
caractere da segunda String multiplica todos os caracteres da primeira String.
C0,j = 0
Ci,j+1 = (Aj x Bi + Ci,j) div 10
Mi,j = (Aj x Bi + Ci,j) % 10
Figura 11. Resolução do carry e do caractere resultante por iteração.
No Laço mais interno Figura 11 (Exemplo 18). um carry (dezena da
multiplicação anterior, que inicialmente é zero) é avaliado para ser somado a próxima
43
multiplicação. Concatenam-se as unidades da soma (soma mod 10) do carry com o
resultado da multiplicação de A e B a String que armazena a soma parcial (variável
subproduto).
O algoritmo completo está descrito no Exemplo 16, onde Z é a constante
CARACTERE_ZERO, mult a variável multiplicacaoCaracteres e multNaturalString
é o método multiplicacaoNaturalStrings:
Exemplo 16. Multiplicação Natural de Strings
private static String multNaturalStrings(String a, String b){
//produto da multiplicacao
String produto = "0";
StringBuffer subProduto = new StringBuffer("");
b = removeLeftZeros(b);
a = removeLeftZeros(a);
//valor a ser incluido na proxima multiplicacao
int carry = 0;
//soma em valores absolutos dos caracteres
int mult = 0;
//variável para armazenar momentaneamente o produto
char temp = '0';
StringBuffer deslocamento = new StringBuffer("");
for (int i = b.length()-1; i >= 0; i--) {
carry = 0;
subProduto = new StringBuffer(deslocamento.toString());
for (int j = a.length()-1; j >= 0; j--) {
mult = carry +((a.charAt(j) - Z) * (b.charAt(i)-Z)) ;
temp = (char)((mult%10) + Z);
carry = mult/10;
subProduto.append(temp);
}
if (carry != 0) {
subProduto.append(carry);
}
subProduto.reverse();
String subProd = subProduto.toString();
produto = Number.adicaoNaturalStrings(produto, subProd);
deslocamento = deslocamento.append('0');
}
return produto;
}
Inicialmente são removidos, se houverem, os zeros à esquerda das Strings a e
b. Um laço duplo percorre ambas as Strings e realiza a multiplicação de todos os
caracteres de A pelos caracteres de B (Exemplo 17, laço externo). À variável
“deslocamento” é acrescentado um zero a cada iteração para que as somas
parciais contemplem a ordem de grandeza (dezena, centena, milhar,...) que está
44
sendo multiplicada na iteração. Na variável subproduto estão sendo armazenadas
as multiplicações parciais. Onde a cada iteração um caractere da string “b” é
multiplicado pela String “a”. As somas dos produtos parciais são armazenadas na
variável produto que no final da execução do algoritmo (Exemplo 17) conterá o valor
da multiplicação.
Exemplo 17. Laço mais externo do algoritmo da Multiplicação Natural.
StringBuffer deslocamento = new StringBuffer("");
for (int i = b.length()-1; i >= 0; i--) {
…
subProduto = new StringBuffer(deslocamento.toString());
for (int j = a.length()-1; j >= 0; j--) {
…
}
String subProd = subProduto.toString();
produto = Number.adicaoNaturalStrings(produto, subProd);
deslocamento = deslocamento.append('0');
}
No Laço mais interno (Exemplo 18, Figura 11), inicialmente é calculado o valor
da multiplicação do caractere da String A pelo caractere da String B, mais uma
vez é preciso subtrair cada caractere numérico do caractere zero unicode (variável Z).
Na terceira linha, é atribuída a variável temp o valor do caractere a ser concatenado
às multiplicações das iterações anteriores que formarão o “produto parcial”
(armazenado na variável subProduto, operação de append). Na quarta linha
calcula-se o carry para a próxima iteração (Exemplo 18).
Exemplo 18 Laço mais interno do algoritmo da Multiplicação Natural
for (int j = a.length()-1; j >= 0; j--) {
mult = carry +((a.charAt(j) - Z) * (b.charAt(i)-Z)) ;
temp = (char)((mult%10) + Z);
carry = mult/10;
subProduto.append(temp);
}
45
3.5.2.4 DIVISÃO NATURAL
O algoritmo da divisão natural recebe como parâmetro dois strings e retorna
um array de Strings de duas posições. Na primeira posição está o quociente e na
segunda posição é atribuído o resto. A partir do resto e do quociente é possível
calcular a divisão racional [ver seção 3.5.2.7]. Duas funções auxiliares foram criadas
para ajudar o calculo da divisão natural (Figura 12):
Figura 12. Elementos utilizados no algoritmo da divisão
•
acharSubstringDividendoMaiorQueDivisor:
Este
método
recebe
como parâmetro o dividendo e o divisor e como o nome já diz tem como
responsabilidade procurar no dividendo o primeiro substring (subconjunto de
uma String) do dividendo, que seja maior que o divisor. A relação de
precedência é definida pelo método que compara Strings descrito na Seção
3.5.4. O algoritmo está descrito no Exemplo 19.
Exemplo 19. Encontra a maior substring do dividendo maior que o divisor
private static String ex19 (String dividendo, String divisor){
String aux = "";
boolean cond = true;
for (int i=0; i <= dividendo.length() && cond; i++) {
aux = dividendo.substring(0,i);
if (compareStrings(aux,divisor) > 0) {
cond = false;
}
}
return aux;
}
46
•
“acharMaiorMultiploMenorQueSubstringDividendo”: Este método
auxiliar procura o maior múltiplo do divisor que não seja maior que uma
determinada substring do dividendo. Esta substring é obtida pelo método
descrito no Exemplo 20. O método recebe como parâmetro uma substring do
dividendo e o divisor, retornando um array de strings com duas posições. Na
primeira posição é colocado o múltiplo do divisor que queremos encontrar. E
na segunda posição é colocado o caractere (de ‘1’ a ‘9’) que multiplica o divisor
e dá como resultado o múltiplo colocado na primeira posição do array. O
algoritmo (Exemplo 20) inicia a busca utilizando o caractere “5”, ou seja,
multiplica o caractere ‘5’ pelo divisor e avalia se a multiplicação foi maior ou
menor que o dividendo:
o Se a multiplicação for maior: Subtrai-se uma unidade do caractere
multiplicador (utilizar-se-á o caractere “4” na próxima iteração). E voltase a multiplicar o substring do dividendo pelo novo caractere. Até que
encontremos um múltiplo que seja menor que a substring do dividendo.
Este então é o múltiplo que desejamos e o caractere multiplicador fará
parte do quociente da divisão.
o Se o produto foi menor: Incrementa-se de uma unidade o caractere
multiplicador. E volta-se a multiplicar o dividendo pelo novo caractere.
Este procedimento se repete até quando encontrarmos um múltiplo que
seja maior que a substring do dividendo. Devemos então retornar este
caractere subtraído de uma unidade, pois queremos um múltiplo menor
que o substring, e o múltiplo relativo a este caractere.
o Se o produto for igual: retorna-se o caractere multiplicador e a
multiplicação deste com o divisor.
O procedimento é ilustrado no Exemplo 20 onde multNaturalStrings é o
método multiplicacaoNaturalStrings e subNaturalStrings é o método
subtracaoNaturalStrings.
47
Exemplo 20 Calcula o maior múltiplo do divisor menor que a substring do dividendo
private static String[] ex20 (String sub, String divisor){
String retorno[] = null;
String initialNumber ="5";
String condAnterior = "";
String produto = "";
boolean cond = true;
produto = multNaturalStrings(divisor,initialNumber);
while (cond) {
if (compareStrings(produto,subDividendo) > 0) {
if (condAnterior.equals("menor")) {
cond = false;
retorno = new String [] {
subNaturalStrings(produto,divisor),
subNaturalStrings(initialNumber,"1")
};
} else {
condAnterior = "maior";
initialNumber =subNaturalStrings(initialNumber,"1");
produto = subNaturalStrings(produto,divisor);
}
} else if (compareStrings(produto,subDividendo) < 0) {
if (condAnterior.equalsIgnoreCase("maior")){
retorno = new String[]{produto,initialNumber};
cond = false;
} else {
condAnterior = "menor";
initialNumber = adicaoNaturalStrings(initialNumber,"1");
produto = adicaoNaturalStrings(produto, divisor);
}
} else {
cond = false;
retorno = new String[]{produto,initialNumber};
}
}
return retorno;
}
Com os métodos do Exemplo 19 e 20 construímos o algoritmo que divide uma
cadeia de caracteres por outra (Exemplo 21), onde remLZeros é o método
removeLeftZeros, div é o dividendo, subDiv é o substringDividendo,
divIncial é o dividendoInicial, ex19 é o método acharSubstringDividendoMaiorQueDivisor, ex20 é o método acharMaiorMultiploMenorQueSubstringDividendo e subtracao é o método subtracaoNaturalStrings.
48
Exemplo 21. Divisão Natural de Strings
private static String[] divisaoRestoNaturalStrings(String div,
String divisor) throws ExcecaoDivisaoPorZero {
String quociente = "";
String resto = "";
if (igualdadeStrings(divisor,"0")) {
throw new ExcecaoDivisaoPorZero(dividendo, divisor);
}
int comparacao = compareStrings(dividendo, divisor);
if (comparacao < 0) {
quociente = "0";
resto = dividendo;
} else if (comparacao == 0){
quociente = "1";
resto = "0";
} else {
String divInicial = ex19(div,divisor);
String subDiv = div.replaceFirst(divInicial,"");
String[] multiplo = ex20(divInicial, divisor);
quociente = quociente + multiplo[1];
String diferenca = subtracao(divInicial,multiplo[0]);
while(subDiv.length() > 0) {
diferenca = diferenca + subDiv.charAt(0);
subDiv = subDiv.substring(1,subDiv.length());
if (compareStrings(diferenca, divisor) < 0) {
quociente = quociente + "0";
} else {
multiplo = ex20(diferenca, divisor);
quociente = quociente + multiplo[1];
diferenca = subtracao (diferenca, multiplo[0]);
}
}
resto = diferenca;
}
return new String[]{remLZeros(quociente),remLZeros(resto)};
}
Inicialmente
é
verificado
se
o divisor
é
igual
a
zero,
se sim
a
“ExcecaoDivisaoPorZero” é lançada. Em seguida, compara-se o dividendo com o
divisor: Se o dividendo for menor que o divisor, o quociente é igual a zero e ao resto é
atribuído o dividendo. Se o dividendo for igual ao divisor: atribui-se 1 ao quociente e o
resto será igual a zero.
Se nenhum dos casos anteriores ocorrer (Exemplo 22): executa-se o algoritmo
do Exemplo 20 que acha a primeira substring do dividendo maior que o divisor. Esta
String é armazenada na variável “divInicial”. Na variável “subDiv” é atribuído a
chamada do método “replaceFirst”. O método “replaceFirst” da classe
String substitui a primeira ocorrência de uma String (no caso “divInicial”) por
outra String passada como parâmetro (String vazia ou “”). Desta forma, da
49
variável “div” (do dividendo) é retirado a String encontrada no algoritmo do
Exemplo 20 (menor substring do dividendo maior que o divisor). Em seguida utilizase o algoritmo do Exemplo 21 para achar o maior múltiplo do divisor que ainda seja
menor que esta substring do dividendo. O algoritmo do Exemplo 21 nos retorna um
caractere multiplicador e o múltiplo do divisor, sendo seu retorno atribuído a variável
“multiplo”. Este caractere multiplicador vai ser atribuído a String que formará o
“quociente”. Para encontramos o próximo caractere do quociente é preciso subtrair
do substring do dividendo (divInicial) o múltiplo encontrado (multiplo[0]).
Esta subtração é armazenada na String ”diferenca”.
Exemplo 22. Caso em que o dividendo é maior que o divisor
} else {
String divInicial = ex19(div,divisor);//substring
String subDiv = div.replaceFirst(divInicial,"");
String[] multiplo = ex20(divInicial, divisor);//multiplo
quociente = quociente + multiplo[1];
String diferenca = subtracao(divInicial,multiplo[0]);
...
}
O algoritmo do Exemplo 23 segue iterativamente até acabar os
caracteres do dividendo. O algoritmo verifica se a concatenação da variável
diferenca com o próximo caractere do dividendo é maior que o divisor.
•
Se for menor, um caractere zero é concatenado ao quociente.
•
Caso contrário (Exemplo 23), executa-se o algoritmo do Exemplo 20 (achar o
maior múltiplo do divisor menor que a substring do dividendo). Concatena-se
ao quociente o caractere multiplicador, e é realizada a subtração entre a
variável “diferenca” e o múltiplo encontrado.
Exemplo 23.Laço principal da divisão natural.
while(subDiv.length() > 0) {
diferenca = diferenca + subDiv.charAt(0);
subDiv = subDiv.substring(1,subDiv.length());
if (compareStrings(diferenca, divisor) < 0) {
quociente = quociente + "0";
} else {
multiplo = ex20(diferenca, divisor);
quociente = quociente + multiplo[1];
diferenca = subtracao (diferenca, multiplo[0]);
}
}
resto = diferenca;
50
Quando
os
caracteres
do
dividendo
tiverem
chagado
ao
fim
(subDiv.length() > 0) a variável “quociente” conterá o quociente da divisão e
a variável “resto”, o resto da divisão. Na primeira posição do array de retorno é
colocado o quociente e na segunda posição o resto (Exemplo 21) removendo-se
os zeros à esquerda.
3.5.2.5 MÁXIMO DIVISOR COMUM (MDC)
O algoritmo do “mdc” é muito importante nesta biblioteca, pois possibilita a
simplificações entre numeradores e denominadores e o cálculo do mínimo múltiplo
comum (mmc) que é utilizado na adição de frações com denominadores diferentes. O
algoritmo de Euclides foi utilizado, ele é recursivo e segue a indução descrita na
Figura 13.
mdc(0,n) = n
mdc(m,n) = mdc ( n mod m, m) se m > 0
Figura 13. Definição mdc
Exemplo 24. Algoritmo para cálculo do mdc
public static String mdc(String m, String n){
String mdc = "1";
if (m.equals("1") || n.equals("1")) {
mdc = "1";
} else {
mdc = mdcRecursivo(m,n);
}
return mdc;
}
private
static
String
mdcRecursivo(String
m,
String
ExcecaoDivisaoPorZero {
String mdc = "";
if (m.equals("0")) {
mdc = n;
} else {
if (compareStrings(n,m) >=0) {
mdc = mdcRecursivo(restoDivisaoNaturalStrings(n,m),m);
} else {
mdc = mdcRecursivo(restoDivisaoNaturalStrings(m,n),n);
}
}
n)
throws
51
A função “restoDivisaoNaturalStrings” utiliza a divisão natural de
Strings [ver Seção 3.5.2.4] e retorna apenas o resto da divisão. O algoritmo para
calcular o mdc está descrito no Exemplo 24.
3.5.2.6 MÍNIMO MULTIPLO COMUM (MMC)
O mmc é calculado a partir do mdc e é encontrado através da relação descrita
na Figura 14:
mmc (m,n) = (m * n) / mdc (m, n)
Figura 14. Definição mmc
No exemplo 25 está descrito algoritmo do mmc que utiliza a multiplicação
natural de Strings [ver Seção 3.5.2.3], onde EDPZ é a ExcecaoDivisaoPorZero.
Exemplo 25. Algoritmo para calcular o mmc
public static String mmc (String a, String b) throws EDPZ{
return divisaoNaturalStrings(
multiplicacaoNaturalStrings(a,b),
mdc(a,b)
);
}
3.5.2.7 DIVISÃO RACIONAL DESCONSIDERANDO O SINAL
A divisão Racional utiliza a divisão natural [ver Seção 3.5.2.4] como ponto de
partida. O algoritmo da divisão [referencia link] está dividido nos Exemplos 26 a 31.
Esta divisão ainda não leva em consideração o sinal, esta avaliação será feita na
operação de divisão que será vista na Seção 3.5.3.3. O método que implementa a
divisão recebe como parâmetro o dividendo, o divisor, uma variável boleana que
assumindo o valor “verdadeiro” (true) coloca o resultado da divisão em forma
fracionária. Por ultimo um parâmetro que indica a precisão. Este último parâmetro só
será usado, se o indicador de formato fracionário estiver com valor “falso” (false)
atribuído.
O
método
retorna
um
tipo
Number
e
lança
a
exceção
ExcecaoDivisaoPorZero se o divisor for igual a zero. A assinatura do método está
descrita no Exemplo 26:
52
Exemplo 26. Assinatura método da Divisão Racional
private static Number
divisaoRacionalStrings(String dividendo,
String divisor,
boolean fracionar,
int casasDecimais)
throws ExcecaoDivisaoPorZero;
Inicialmente (Exemplo 27) calcula-se o mdc [ver Seção 3.5.2.5], se este existir,
entre o dividendo e o divisor, a fim de simplificar suas mantissas e menos caracteres
serem processados. Depois é calculada a divisão natural, obtendo-se quociente e
resto.
Exemplo 27. simplificando as mantissas do dividendo e do divisor
String mdc = mdc(dividendo,divisor);
if (!mdc.equals("1")) {
dividendo = divisaoNaturalStrings(dividendo,mdc);
divisor = divisaoNaturalStrings(divisor,mdc);
}
String [] divisaoInteira = divisaoRestoNaturalStrings(dividendo, divisor);
Sendo o resto da divisão inteira igual zero (String “0”, Exemplo 28), não há
necessidade de realizar a divisão pelo resto. Portanto é atribuída a mantissa do
objeto resultado, o valor do quociente da divisão inteira. Quando o resto da divisão
inteira é diferente de Zero na divisão racional, o algoritmo continua dividindo o resto
até que uma dízima periódica seja identificada (Figura 16) o dividendo da iteração
seja igual a zero (Figura 15) ou a precisão desejada seja alcançada.
Figura 15. Dividendo da Iteração igual a Zero
53
Uma “Hashtable” (java.util.Hashtable) tabela hash que armazena uma
tupla (chave, valor) é utilizada para identificar a existência de dízimas periódicas. O
conjunto das chaves do Hashtable é formado pelo resto da divisão natural e dos
resultados das subtrações dos dividendos das interações pelo maior múltiplo do
divisor (Figura 16). Na tupla (chave, valor) é atribuído ao valor o comprimento da
String que representa o quociente da divisão do resto na iteração corrente. Este
comprimento quando armazenado possibilita identificar onde começa o período da
dízima. Na Figura 16, por exemplo os valores 30 e 80 são colocados como chave da
hashtable, na terceira iteração, o número 30 reaparece nas subtrações sucessivas,
caracterizando uma periodicidade:
Figura 16 - Identificação da Periodicidade de uma Divisão
. No algoritmo do Exemplo 28, zeros são concatenados a variável “resto” até
que o resultado destas concatenações seja maior que o divisor. O primeiro zero
atribuído não é contabilizado na variável quocienteDepoisVirgula, somente a
partir
do
segundo
zero,
um
zero
é
concatenado
a
variável
quocienteDepoisVirgula.
54
Exemplo 28.Núcleo do algoritmo da Divisão desconsiderando o sinal
Hashtable ht = new Hashtable();
divisaoInteira[1] = removeLeftZeros(divisaoInteira[1]);
Number resultado = new Number();
if (divisaoInteira[1].equalsIgnoreCase("0")) {
resultado.sinal = SINAL_POSITIVO;
resultado.mantissa = divisaoInteira[0];
} else {
//guarda o quociente da divisao calculado apos a virgula
String quocienteDepoisVirgula = "";
int inicioDizima = 0;
String resto = divisaoInteira[1];
String multiplo[] = new String[2];
boolean cond = true;
String quocienteAvaliado = "";
boolean isparteInteiraZero = divisaoInteira[0].equalsIgnoreCase("0");
while (cond && !removeLeftZeros(resto).equalsIgnoreCase("0")
&&(fracionar || quocienteAvaliado.length() < casasDecimais + 1)){
resto = resto + "0";
while (compareStrings(resto, divisor) < 0 ) {
resto = resto + "0";
quocienteDepoisVirgula = quocienteDepoisVirgula + "0";
}
if (!ht.containsKey(resto)) {
ht.put(resto, "" + quocienteDepoisVirgula.length());
multiplo = ex20(resto, divisor);
quocienteDepoisVirgula = quocienteDepoisVirgula + multiplo[1];
resto = subtracaoNaturalStrings(resto,multiplo[0]);
} else {
cond = false;
inicioDizima = Integer.parseInt((String)ht.get(resto));
}
quocienteAvaliado = removeLeftZeros(quocienteDepoisVirgula);
}
...
}
A divisão segue como na divisão natural determina-se o maior múltiplo do
divisor que seja menor que o dividendo (neste caso o dividendo no laço é a variável
“resto”) [ver Exemplo 21, Seção 3.5.2.4]. À variável quocienteDepoisVirgula é
concatenado o caractere multiplicador e da variável resto subtrai-se o múltiplo. Este
processo continua até atingirmos o critério de parada do laço principal (While
(cond && !RLZ(resto).equalsIgnoreCase("0")), Figuras 15 e 16 ou a
precisão ser atingida quando escolhe-se não fracionar (&&(fracionar
||
quocienteAvaliado.length() < casasDecimais + 1)). Onde a variável
cond indica existência de periodicidade, RLZ é o método removeLeftzeros e o
método equalsIgnoreCase verifica a igualdade entre strings. Se foi identificada a
periodicidade, cond é atribuída o valor false e a variável inicioDizima passa a
guardar o índice na String quocienteDepoisVirgula onde começa a parte
55
periódica. A precisão é definida pela variável quocienteAvaliado que é utilizada
para garantir a flutuação do ponto quando o resultado da divisão natural for igual a
zero.
Se ao sairmos do laço (Exemplo 29) a variável resto (que guarda o último
dividendo de iteração) for igual a zero, significa que não encontramos uma dízima
periódica, e criamos um objeto do tipo Number como resultado da operação.
Exemplo 29. Construção do tipo Number resultado quando não existe período
if (removeLeftZeros(resto).equalsIgnoreCase("0")){
resultado = new Number(
divisaoInteira[0] + quocienteDepoisVirgula,
SINAL_POSITIVO,
SINAL_NEGATIVO,
"" + quocienteDepoisVirgula.length(),
null);
} else {
…
}
Como ainda não estamos levando em consideração o sinal, foi atribuído o sinal
positivo ao tipo Number “resultado”, a mantissa é atribuída à concatenação do
quociente da divisão inteira com o valor da variável quocienteDepoisVirgula. O
tipo Number guarda a mantissa sem levar em conta a posição da vírgula. A vírgula é
determinada pelo comprimento da String do expoente. O sinal do expoente neste
caso é sempre negativo, pois estamos concatenando a mantissa caracteres depois
da virgula. O valor do expoente é dado pela quantidade de casas depois da vírgula
que é determinado pelo comprimento da cadeia de caracteres da variável
quocienteDepoisVirgula. Como não existe um objeto denominador é passado
null no ultimo parâmetro do construtor.
Se foi encontrada uma dízima periódica, ou seja, a variável resto do Exemplo
31 é diferente da String Zero, utilizaremos a indicação da variável boleana
fracionar (Exemplo 26) passada como parâmetro. A fração geratriz que é a função
que gera a dízima periódica sendo calculada da seguinte forma:
1.Suponha uma dízima periódica simples m formada por n algarismos:
m = 0, k1k2k3...kn....
multiplicando-se os dois membros por 10n, temos:
56
10n • m = k1k2k3...kn , k1k2k3... kn...., Subtraindo m de cada lado da equação:
10n • m - m= k1k2k3...kn , k1k2k3... kn - 0, k1k2k3... kn … ,Este cálculo nos leva a
m = (k1k2k3...kn ) / (10n - 1) .
Por exemplo, m = 0,67896789.... ,n = 4. logo m = 6789/(1000-1) = 6789/9999
2. Dada uma dízima periódica m composta:
m = 0,BK , onde B é a parte não periódica com p algarismos e K a parte
periódica com q algarismos. Multiplicando-se os dois membros por 10p+q e 10p
temos:
10p+q • m = BK,K (I) e 10p • m = B,K (II); subtraindo (II) de (i) temos:
(10p+q - 10p) • m = BK – B, assim temos a fórmula geral:
m = (BK – B) / ((10q-1) 10p). (Regra Geral)
para m = 0,322222…. K = 2, B = 3, p = 1 e q = 1 logo m = (32 - 3)/((10-1)•10)
Portanto m = 29/90.
O algoritmo do Exemplo 30 implementa a fórmula geral para calcular a fração
geratriz. Na primeira linha é obtida a parte periódica da dízima através do índice que
determina seu início (inicioDizima). A variável do tipo String mantissaDividendo armazena o numerador da fração geratriz que é dado pela subtração da
parte
não
periódica
concatenada
com
a
parte
periódica
da
dízima
(divisaoInteira[0] + quocienteDepoisVirgula + dizima) pela parte
após a virgula (BK-B da Regra Geral) .
A função CZDFG é o método calcularZerosDenominadorFracaoGeratriz. Ele recebe como parâmetro uma String com a mantissa do numerador da
fração geratriz (mantissaDividendo) e a quantidade de casas decimais após a
vírgula até a primeira ocorrência do período da dízima (quocienteDepoisVirgula.length()). Este método retorna um array de String de duas posições:
•
Na posição 0 é retornada a mantissa normalizada do numerador da
fração geratriz, sem os zeros que estariam à direita depois da vírgula,
estes zeros surgem após a subtração que determina o dividendo da
fração geratriz (BK - K) e não devem ser incluídos na mantissa.
57
•
Na posição 1 é colocada uma String com zeros a serem concatenados
no denominador da fração geratriz (10p da regra Geral).
Exemplo 30. Coloca o resultado na forma fracionária.
String dizima = quocienteDepoisVirgula.substring(
inicioDizima, quocienteDepoisVirgula.length());
if (fracionar) {
String mantissaDividendo = subtracaoNaturalStrings(
divisaoInteira[0] + quocienteDepoisVirgula + dizima,
divisaoInteira[0] + quocienteDepoisVirgula);
String[] calcularDizima =
CZDFG (mantissaDividendo, (quocienteDepoisVirgula).length());
mantissaDividendo = calcularDizima[0];
String denominadorFracao =
subtracaoNaturalStrings(completeWithRightZeros("1",
dizima.length()),"1");
denominadorFracao = denominadorFracao + calcularDizima[1];
if (compareStrings(denominadorFracao,divisor) < 0) {
resultado.sinal = SINAL_POSITIVO;
resultado.mantissa = mantissaDividendo;
resultado.denominador = new Number();
resultado.denominador.sinal = SINAL_POSITIVO;
resultado.denominador.mantissa = denominadorFracao;
} else {
resultado.sinal = SINAL_POSITIVO;
resultado.mantissa = dividendo;
resultado.denominador = new Number();
resultado.denominador.sinal = SINAL_POSITIVO;
resultado.denominador.mantissa = divisor;
}
} else {...}
No Exemplo 30, a mantissa do denominador da fração geratriz da dízima
(variável denominadorFracao) é determinada pela formula ((10q
- 1
) 10p). O valor
10q é calculado a partir da parte periódica da dízima (variável dizima). O cálculo do
denominador utiliza a função completeWithRightZeros que concatena zeros à
direita de uma String numérica. A quantidade de zeros a serem concatenados a
String numérica “1” é determinada por (10q) que é o comprimento da parte periódica
(dizima.length()).
Deve-se subtrair o resultado da concatenação (concatenar
zeros a uma String numérica é o mesmo que multiplicar por potências de 10) por 1
(10q
-
1)
e
depois
concatenar
os
zeros
retornados
(10p)
pelo
método
calcularZerosDenominadorFracaoGeratriz ao resultado.
58
Exemplo 31. Resultado na forma não fracionária, com precisão especificada.
if (fracionar) {
…
} else {
if (casasDecimais <= quocienteDepoisVirgula.length()) {
String arredondamento = null;
if (!isparteInteiraZero){
arredondamento = arredondar(divisaoInteira[0] +
quocienteDepoisVirgula + dizima.charAt(0),
casasDecimais + divisaoInteira[0].length());
} else {
arredondamento = arredondar(quocienteAvaliado, casasDecimais);
}
resultado = new Number(
arredondamento,
SINAL_POSITIVO,
SINAL_NEGATIVO,(casasDecimais +
(quocienteDepoisVirgula.length() - quocienteAvaliado.length())) + "",
null);
} else {
int numeroCaracteresAntesDizima = quocienteDepoisVirgula.length() dizima.length();
int repeticoesDizima = (casasDecimais –
numeroCaracteresAntesDizima)/dizima.length();
int tamanhoSubStringDizima =
(casasDecimais - numeroCaracteresAntesDizima) % dizima.length();
String resultadoString =
quocienteDepoisVirgula.replaceFirst(dizima,"");
for (int i = 0; i < repeticoesDizima; i++) {
resultadoString = resultadoString + dizima;
}
resultadoString = resultadoString +
dizima.substring(0,tamanhoSubStringDizima + 1);
resultadoString = arredondar(resultadoString, casasDecimais);
resultadoString = divisaoInteira[0] + resultadoString;
resultado = new Number(
resultadoString,
SINAL_POSITIVO,
SINAL_NEGATIVO,
"" + casasDecimais,
null);
}
}
O algoritmo (Exemplo 30) que coloca a dízima na forma fracionária avalia se o
denominador da dízima seria menor que o divisor passado como parâmetro se isso
acontecer o denominador da dízima será utilizado, caso contrário é colocado na
forma fracionária os parâmetros de entrada, sendo o dividendo o numerador e o
divisor o denominador.
59
Se foi encontrada uma dízima periódica e a variável boleana fracionar
tenha o valor false, é preciso usar o parâmetro casasDecimais (Exemplo 26) para
determinar com que precisão será retornada, o resultado da divisão. O Exemplo 31
apresenta o algoritmo utilizado para recuperar o resultado da divisão com uma
precisão especifica
O algoritmo verifica se a quantidade de casas decimais depois da virgula é
menor do que a precisão desejada. Caso o resultado da divisão inteira seja zero,
arredonda-se a variável quocienteAvaliado que despreza os zeros anteriores ao
primeiro algarismo diferente de zero. Isto é necessário para garantir a flutuação do
ponto.
Caso
o
resultado
da
divisão
inteira
seja
diferente
de
zero,
um tipo Number é criado tendo como mantissa o arredondamento da concatenação
do
quociente
da
divisão
natural
com
o
quociente
da
divisão
do
resto
(quocienteDepoisVirgula). O arredondamento utilizado é o arredondamento
para o mais próximo. A mantissa do expoente neste caso é dada pelo tamanho de
casas decimais especificado.
Se a quantidade de casas decimais especificadas for maior do que a
quantidade de caracteres da parte não periódica da dizima concatenada com a parte
periódica (quocienteDepoisVirgula Exemplo 31): Na determinação da mantissa
do resultado, é preciso calcular quantas vezes a parte periódica da dízima se repete a
fim
de
retornar
o
resultado
da
divisão
com
a
precisão
desejada
(repeticoesDizima). Também é preciso calcular até qual caractere da substring da
parte
periódica
da
dízima
é
atingida
a
precisão
especificada
(tamanhoSubStringDizima). São concatenadas a parte não periódica da dízima
(quocienteDepoisVirgula.replaceFirst(dizima,"")),
a
quantidade
de
repetições da parte periódica e a substring da parte periódica que atinge a precisão.
Por ultimo arredonda-se este valor para a precisão desejada.
3.5.3 MÉTODOS ARITMÉTICOS PÚBLICOS DO TIPO NUMBER
Os métodos públicos do tipo Number utilizam os métodos das seções
anteriores como base para realizar operações aritméticas com suas mantissas e
expoentes que são formados por cadeia de caracteres (strings). As operações no
conjunto dos números racionais requerem a resolução de sinais e denominadores de
60
números fracionários. Estas regras estão definidas nas operações racionais descritas
nas próximas sessões.
3.5.3.1 ADIÇÃO E SUBTRAÇÃO RACIONAL
As operações de adição e subtração no conjunto dos números racionais têm
que verificar se os denominadores dos números a serem adicionados são iguais, e
avaliar os sinais dos mesmos. Se os sinais forem opostos uma operação de
subtração deverá ser realizada, sinais iguais indicam que uma adição será efetuada.
Na operação de subtração antes de se avaliar os sinais inverte-se o sinal do
subtraendo. A adição e a subtração racional utilizam o método do Exemplo 32 para
calcular seus resultados:
Exemplo 32. Adição ou Subtração Racional
private static Number adicaoOuSubtracaoRacional(Number a, Number b,
boolean isAdicao) {
Number r = new Number();
colocarMesmaBaseDenominadores(a,b);
r = somaOuSubtracaoSemDenominadores(a,b,isAdicao);
if (a.denominador != null) {
r.denominador = a.denominador.clonar();
}
return r;
}
O método adicaoOuSubtracaoRacional recebe como parâmetro dois
objetos do tipo Number e uma variável boleana que ao assumir o valor verdadeiro
indica adição, e caso contrário indica subtração. O método retorna um tipo Number
com
o
resultado
da
adição
ou
da
subtração.
O
método
colocarMesmaBaseDenominadores (Exemplo 33) calcula o mínimo múltiplo
comum [ver Seção 3.5.2.6] dos denominadores (se estes existirem) e faz as devidas
multiplicações nos numeradores para manter a proporcionalidade da fração. O
método adicaoOuSubtracaoSemDenominadores (Exemplo 38) é responsável por
avaliar os sinais dos números, normalizar (adicionar zeros à direita) a mantissa do
tipo Number de maior expoente e realizar uma adição ou subtração das mantissas
dos números com os parâmetros na ordem correta. A normalização é necessária para
que se possa efetuar as operações com as mantissas sob a mesma ordem de
grandeza. O ultimo passo do método adicaoOuSubtracaoRacional é atribuir ao
61
denominador do Objeto Number resultado, o mmc calculado a partir dos
denominadores dos parâmetros de entrada. O método clonar é responsável por
retornar uma nova instância (diferente referência na memória) do objeto a ser copiado
ou “clonado”, contendo os mesmos valores para todos os atributos.
O método colocarMesmaBaseDenominadores (Exemplo 33) é responsável
por calcular o mmc dos denominadores e ajustar (Exemplo 36, método
ajustarParaNovaBase) os numeradores dos parâmetros para a novo denominador
(Figura 17). O método começa avaliando o atributo denominador dos números
passados como parâmetro. Se o atributo denominador de um dos números for nulo
(nulo significa que o denominador é igual ao número “1”), é atribuído o denominador
do outro.
Exemplo 33. Colocar dois números sob a mesma base de denominadores
private static void colocarMesmaBaseDenominadores(Number a, Number b){
if (a.denominador != null && b.denominador == null) {
ajustarParaNovaBase(a,b);
} else if (a.denominador == null && b.denominador != null) {
ajustarParaNovaBase(b,a);
} else if (a.denominador != null && b.denominador != null){
//elimina sinal e potencia dos denominadores
eliminaExpoenteSinalDenominador(a);
eliminaExpoenteSinalDenominador(b);
//calcula mmc
String mmc = mmc(a.denominador.mantissa, b.denominador.mantissa);
//calcula o resultado da divisao do mmc pelo denominador atual
String fatorMultiplicativoA =
divisaoNaturalStrings(mmc,a.denominador.mantissa);
String fatorMultiplicativoB =
divisaoNaturalStrings(mmc,b.denominador.mantissa);
//atribui ao denominador o mmc
a.denominador.mantissa = mmc;
b.denominador.mantissa = mmc;
//calcula o dividendo
a.mantissa = multiplicacaoNaturalStrings(fatorMultiplicativoA,a.mantissa);
b.mantissa = multiplicacaoNaturalStrings(fatorMultiplicativoB,b.mantissa);
}
62
 a × mmc(b, d ) c × mmc(b, d ) 


a c 
b
d

f , =
,
mmc(b, d ) 
 b d   mmc(b, d )




Figura 17. Colocando sob a mesma base de denominadores
Caso os atributos denominadores (denominadores) sejam diferentes é preciso
calcular seu mmc. Para otimizar a busca pelo mmc e utilizar o mesmo algoritmo da
Seção
3.5.2.6
foi
criado
o
método
eliminaExpoenteSinalDenominador
(Exemplo 34) que retira, caso se aplique, o sinal negativo do expoente e iguala o
expoente a zero. subtraindo o expoente do numerador pelo expoente do denominador
(Figura 18).
 834784 × 1013  − 834784 × 10 4
=
f 
9 
4647
 − 4647 × 10 
Figura 18. Exemplo do efeito do método que elimina o expoente e o sinal do denominador
Exemplo 34. Resolve o denominador eliminando ordem de grandeza e sinal negativo
private static void eliminaExpoenteSinalDenominador(Number m) {
String [] expoenteA =
avaliaSinaisRealizaOperacaoAdicaoOuSubtracao(m.expoente,
m.denominador.expoente, m.sinalExpoente, -m.denominador.sinalExpoente);
m.denominador.expoente = "0";
m.denominador.sinalExpoente = SINAL_ZERO;
m.sinalExpoente = Integer.parseInt(expoenteA[0]);
m.expoente = expoenteA[1];
if (m.denominador.sinal == SINAL_NEGATIVO){
m.sinal = -m.sinal;
m.denominador.sinal = SINAL_POSITIVO;
}
}
O método do Exemplo 34 inverte o sinal do objeto Number (numerador), se o
sinal do denominador for negativo e coloca o sinal do denominador como positivo. O
método também precisa subtrair o expoente do numerador pelo expoente do
denominador, e para isto utiliza a função avaliaSinaisRealizaOperacaoAdicaoOuSubtracao (Exemplo 35) que avalia os sinais passados como parâmetro
e decide se será realizada uma Adição ou subtração. Esta função retorna um array
com duas posições. Na primeira posição é retornado o sinal do resultado da operação
e na segunda posição é retornada uma String numérica representando a mantissa
do resultado. O algoritmo do Exemplo 35 assume que será sempre realizada uma
63
Adição, portanto os métodos que utilizam este algoritmo desejando calcular uma
subtração, invertem um dos sinais passados como parâmetro. Este algoritmo
(Exemplo 35) pode ser visto como a Adição no conjunto dos Inteiros.
No Exemplo 35 se na avaliação dos sinais forem identificados sinais iguais o
sinal da variável resultado (result) será igual a este sinal e uma operação de
Adição natural [ver Seção 3.5.2.1] será efetuada. Caso os sinais sejam diferentes,
verifica-se qual das mantissas é maior através do método compareStrings [ver
Seção 3.5.1]. Realiza-se a subtração da maior mantissa pela menor mantissa. O sinal
retornado, neste caso, é o sinal relativo a maior mantissa. Caso o retorno do método
compareStrings indique igualdade entre as mantissas (retorno do método igual a
zero) é atribuido ao resultado a String numérica “0” (zero).
Exemplo 35. Avalia mantissas e sinais calculando uma adição ou subtração.
private static String[] avaliaSinaisRealizaOperacaoAdicaoOuSubtracao(
String mantissaA, String mantissaB,
int sinalA, int sinalB) {
String result [] = new String[2];
if (sinalA == sinalB) {
result[0] = sinalA + "";
result[1] = adicaoNaturalStrings(mantissaA, mantissaB);
} else {
if (compareStrings(mantissaA, mantissaB) > 0) {
result[0] = sinalA + "";
result[1] = subtracaoNaturalStrings(mantissaA, mantissaB);
} else if (compareStrings(mantissaA, mantissaB) < 0){
result[0] = sinalB + "";
result[1] = subtracaoNaturalStrings(mantissaB, mantissaA);
} else {
result[0] = SINAL_ZERO + "";
result[1] = "0";
}
}
return result;
}
O método ajustarParaNovaBase (Exemplo 36) é utilizado na operação que
iguala os denominadores de dois números que serão somados ou subtraídos
(Exemplo 33, colocarMesmaBaseDenominadores). Este método (ajustarParaNovaBase) é usado quando um dos objetos do tipo Number envolvidos na operação
de adição ou subtração tem o atributo denominador igual a null. Ter um
denominador igual a null é o mesmo que ter um denominador de fração igual a 1.
Portanto, no ajuste para um mínimo múltiplo comum (mmc), o mmc é igual ao mmc
64
do atributo denominador não-nulo. Simplificando a operação descrita na Figura 17
temos a operação do Exemplo 37 definida na Figura 19.
a b  a×d b 
f , =
, 
1 d   d d 
Figura 19. Ajuste de um tipo Number com denominador null para a realização de uma adição
ou subtração com outro tipo Number com denominador não-nulo.
Exemplo 36. Iguala o denominador e faz o ajuste no numerador
private static void ajustarParaNovaBase(Number m, Number n) {
Number produto =
multiplicacaoDesconsiderandoDenominador(n, m.denominador);
n.sinal = produto.sinal * m.denominador.sinal;
n.mantissa = produto.mantissa;
n.expoente = produto.expoente;
n.sinalExpoente = produto.sinalExpoente;
n.denominador = m.denominador.clonar();
}
Para realizar o ajuste do Exemplo 36 foi preciso criar um método
(multiplicacaoDesconsiderandoDenominador, Exemplo 37) que recebe dois
objetos do tipo Number e realiza a multiplicação entre as mantissas (numeradores)
desconsiderando os denominadores (denominadores).
Exemplo 37. Multiplicação desconsiderando o denominador
private static Number multiplicacaoDesconsiderandoDenominador(Number
Number b){
Number n = new Number();
n.sinal = a.sinal * b.sinal;
String expoente [] =
avaliaSinaisRealizaOperacaoAdicaoOuSubtracao(
a.expoente, b.expoente, a.sinalExpoente, b.sinalExpoente);
n.sinalExpoente = Integer.parseInt(expoente[0]);
n.expoente = expoente[1];
n.mantissa = multiplicacaoNaturalStrings(a.mantissa, b.mantissa);
return n;
}
a,
O método descrito no Exemplo 37 calcula a soma dos expoentes dos tipos
Number passados como parâmetro através do método avaliaSinaisRealizaOperacaoAdicaoOuSubtracao definido no Exemplo 35. Ele utiliza a multiplicação
natural [ver Seção 3.5.2.3] para calcular a multiplicação entre as mantissas. O tipo
Number retornado desconsidera o denominador. isto é, calcula multiplicações
considerando apenas mantissas, sinais, expoentes e sinais dos expoentes. A
65
operação que considera o denominador será vista mais a frente e utiliza o método do
Exemplo 38.
O Exemplo 32 usa o método adicaoOuSubtracaoSemDenominadores do
Exemplo 38. Este método adiciona apenas os numeradores dos tipos Number
passados como parâmetro desconsiderando o denominador, assim como o método
do Exemplo 37. Este método é sempre usado após ser efetuada o ajuste dos
denominadores ou cálculo do mmc para os denominadores. O método recebe como
parâmetro os números a serem adicionados e um boleano que indica se verdadeiro
uma adição, caso contrário uma subtração. Se uma subtração for escolhida inverte-se
o sinal do segundo número passado como parâmetro.
Exemplo 38. Adição ou Subtração desconsiderando os denominadores
private static Number adicaoOuSubtracaoSemDenominadores(Number a, Number b,
boolean isAdicao){
Number n = new Number();
if (!isAdicao) {
b.sinal = -b.sinal;
}
normalizarNumerosDesconsiderandoDenominador(a,b);
String [] soma = avaliaSinaisRealizaOperacaoAdicaoOuSubtracao(
a.mantissa, b.mantissa, a.sinal, b.sinal);
n.sinalExpoente = a.sinalExpoente;
n.expoente = a.expoente;
n.sinal = Integer.parseInt(soma[0]);
n.mantissa = soma[1];
if (!isAdicao) {
b.sinal = -b.sinal;
}
return n;
}
Antes de avaliar os sinais e o tamanho das mantissas para efetuar a Adição ou
subtração é preciso coloca-las sob a mesma ordem de grandeza, isto é, igualar os
expoentes adicionando zeros à esquerda da mantissa do número de maior expoente.
O método normalizarNumerosDesconsiderandoDenominador do Exemplo 39
realiza este procedimento. Inicialmente o método avalia os sinais dos expoentes, se
os sinais forem iguais uma subtração entre as mantissas será realizada para calcular
a quantidade de zeros que serão concatenados a mantissa de maior expoente.
Se os expoentes tiverem sinais diferentes, uma adição será realizada para a
determinar quantidade de zeros a serem concatenados. Os zeros sempre são
66
concatenados a mantissa do número que contem o maior expoente e os expoentes
igualados. A Figura 20 ilustra esta operação:
(
) (
f 103 × 10 −5 ,45 × 10 3 = 103 × 10 −5 ,4500000000 × 10 −5
)
Figura 20 - Operação de normalização que iguala os expoentes de dois números
Exemplo 39. Coloca dois números sob a mesma ordem de grandeza
private static void normalizarNumerosDesconsiderandoDenominador(Number a,
Number b){
boolean aMaior = false;
//mesmos sinais
if (a.sinalExpoente == b.sinalExpoente) {
String diferencaExpoentes = "0";
if ((compareStrings(a.expoente, b.expoente) > 0)) {
diferencaExpoentes = subtracaoNaturalStrings(a.expoente, b.expoente);
aMaior = true;
} else if (compareStrings(a.expoente, b.expoente) < 0){
diferencaExpoentes = subtracaoNaturalStrings(b.expoente, a.expoente);
}
if ((a.sinalExpoente == SINAL_POSITIVO && aMaior) ||
(a.sinalExpoente == SINAL_NEGATIVO && !aMaior)) {
a.expoente = b.expoente;
a.mantissa =
completeWithRightZeros(a.mantissa,diferencaExpoentes);
} else {
b.expoente = a.expoente;
b.mantissa =
completeWithRightZeros(b.mantissa,diferencaExpoentes);
}
} else {
//sinais diferentes
String somaExpoentes = "";
somaExpoentes = adicaoNaturalStrings(a.expoente,b.expoente);
if (a.sinalExpoente > b.sinalExpoente) {
a.sinalExpoente = b.sinalExpoente;
a.expoente = b.expoente;
a.mantissa = completeWithRightZeros(a.mantissa, somaExpoentes);
} else if (b.sinalExpoente > a.sinalExpoente){
b.sinalExpoente = a.sinalExpoente;
b.expoente = a.expoente;
b.mantissa = completeWithRightZeros(b.mantissa, somaExpoentes);
}
}
}
3.5.3.2 MULTIPLICAÇÃO RACIONAL
O algoritmo da multiplicação (Exemplo 40) multiplica inicialmente os
denominadores através do algoritmo do Exemplo 37 que desconsidera os
denominadores, verifica se o denominador de algum dos denominadores é igual a
67
null. Se algum deles for nulo: o denominador do resultado será igual ao denominador
não-nulo. Se ambos forem não-nulos o denominador do resultado é calculado pela
multiplicação dos denominadores utilizando-se o algoritmo do Exemplo 37.
Exemplo 40. Algoritmo da Multiplicação.
public static Number multiplicar (Number a, Number b) {
Number r = multiplicacaoDesconsiderandoDenominador(a,b);
if (a.denominador == null && b.denominador != null) {
r.denominador = b.denominador.clonar();
} else if (a.denominador != null && b.denominador == null) {
r.denominador = a.denominador.clonar();
} else if (a.denominador != null && b.denominador != null) {
r.denominador = multiplicacaoDesconsiderandoDenominador(a.denominador,
b.denominador);
}
return r;
}
3.5.3.3 DIVISÃO RACIONAL
Este algoritmo utiliza a operação de divisão racional que desconsidera o sinal
[ver Seção 3.5.2.7]. O algoritmo recebe como parâmetro o dividendo, o divisor, uma
variável boleana que indica se o resultado da operação permanecerá na forma de
fracionária ou não e a quantidade de casas decimais (precisão), caso se decida por
um resultado na forma não fracionária.
O primeiro passo do algoritmo da divisão racional é verificar se os
denominadores são nulos, se isto for verdade, é atribuído ao denominador o número
“1”. Multiplica-se então o numerador do dividendo pelo denominador do divisor e é
atribuído ao numerador da variável de retorno (r) este resultado. Em seguida, é
multiplicado o denominador do dividendo pelo numerador do divisor e este resultado é
atribuído ao denominador da variável de retorno. Para diminuir a quantidade de
caracteres
no
denominador
utiliza-se
o
método
eliminaExpoenteSinal-
Denominador do Exemplo 34.
Se a variável que decide se o resultado permanecerá na forma fracionária foi
atribuído o valor falso,
o algoritmo realiza a operação de divisão racional
desconsiderando o sinal [ver Seção 3.5.2.7] entre as mantissas do numerador e do
denominador da variável de retorno.
O resultado desta divisão retorna um tipo
Number que ainda precisa ser multiplicado pela ordem de grandeza da variável de
retorno, determinada pelo seu expoente que não foi considerado na divisão. A
68
multiplicação entre as ordens de grandeza é calculada através do método
avaliaSinaisRealizaOperacaoAdicaoOuSubtracao definido no Exemplo 35.
Exemplo 41. Algoritmo da Divisão.
public static Number dividir (Number a, Number b, boolean fracionar,
int casasDecimais) throws ExcecaoDivisaoPorZero {
if (a.denominador == null) {
try {
a.denominador = new Number("1");
} catch (Exception e) {}
}
if (b.denominador == null) {
try {
b.denominador = new Number("1");
} catch (Exception e) {}
}
Number r = multiplicacaoDesconsiderandoDenominador(a,b.denominador);
r.denominador = multiplicacaoDesconsiderandoDenominador(a.denominador,b);
eliminaExpoenteSinalDenominador(r);
if (!fracionar) {
Number divisaoMantissas = divisaoRacionalStrings(
r.mantissa,r.denominador.mantissa,false, casasDecimais);
r.mantissa = divisaoMantissas.mantissa;
String [] expoente = avaliaSinaisRealizaOperacaoAdicaoOuSubtracao(
r.expoente, divisaoMantissas.expoente, r.sinalExpoente,
divisaoMantissas.sinalExpoente);
r.sinalExpoente = Integer.parseInt(expoente[0]);
r.expoente = expoente[1];
r.denominador = null;
} else {
simplificarMantissaDenominadores(r);
}
return r;
}
Caso se deseje manter o resultado da divisão na forma fracionária, simplificase a mantissa do denominador e do numerador da variável resultado se for possível
(mdc > 1), calculando-se o mdc [ver Seção 3.5.2.5] entre o numerador e o
denominador e dividindo estes últimos pelo seu mdc.
3.5.4 COMPARAÇÃO ENTRE TIPOS NUMBER
O algoritmo da comparação recebe dois tipos Number e define a relação de
ordem entre o primeiro parâmetro e o segundo parâmetro. Se o primeiro parâmetro
for maior que o segundo o método retorna um inteiro (int)
igual a 1 . Se os
parâmetros forem iguais retorna o valor 0 e se o primeiro parâmetro for menor que o
segundo retorna o valor –1. o algoritmo da comparação está definido no Exemplo 42.
69
O método comparar (Exemplo 42) clona os dois parâmetros de entrada e os
atribui a duas variáveis diferentes pois modificações em seus atributos podem ser
efetuadas no decorrer da operação. Para determinar a ordem de precedência
inicialmente avaliam-se os sinais dos números. Caso os sinais sejam iguais é preciso
igualar
seus
denominadores
utilizando
o
método
colocarMesmaBase-
Denominadores (Exemplo 33). Depois de igualar os denominadores é necessário
colocar os numeradores sob a mesma ordem de grandeza através do método
normalizarNumerosDesconsiderandoDenominador (Exemplo 39). Estando
números sob a mesma ordem de grandeza comparam-se suas mantissas com o
método compareStrings (Exemplo 10). Se os sinais dos números passados como
parâmetro forem negativos, o resultado da comparação de mantissas precisa ser
invertido.
Exemplo 42. Algoritmo de Comparação
public static int comparar (Number a, Number b) {
int ret = 0;
Number m = a.clonar();
Number n = b.clonar();
if (m.sinal > n.sinal){
ret = 1;
} else if (m.sinal < n.sinal) {
ret = -1;
} else { //sinais iguais
colocarMesmaBaseDenominadores(m,n);
m.denominador = null;
n.denominador = null;
normalizarNumerosDesconsiderandoDenominador(m,n);
ret = compareStrings(m.mantissa,n.mantissa);
if (m.sinal == SINAL_NEGATIVO) {
ret = -ret;
}
}
return ret;
}
70
4. RESULTADOS
Neste Capítulo serão apresentados os resultados obtidos com a biblioteca
desenvolvida nesta dissertação. Para validar estes resultados foi utilizado o JDK 1.4.2
da Sun [ref] rodando num Sistema Operacional Windows XP com processador
Celeron da Intel de 1.8 GHz.
Para validação dos resultados da biblioteca intervalar Java neste trabalho
desenvolvida foram realizados cálculos utilizando as operações definidas nas seções
anteriores e os resultados foram comparados com o software IntpakX [INTPAKX], que
é uma extensão intervalar do Maple [MAPLE] e com a biblioteca JAVA-XSC [JAVAXSC] em alguns casos. A escolha da ferramenta Maple foi motivada por sua ampla
divulgação na literatura. A versão do software escolhida foi a 9.0.
Para realização dos testes foram criados nas próprias classes da biblioteca
métodos estáticos (assinatura public static void main(String args[]))
para execução das funções implementadas. Ou seja, para cada operação
desenvolvida eram realizados testes unitários, cujos valores eram comparados com
os resultados do Maple. Os Algoritmos de teste estão em vermelho e os resultados da
saída padrão estão destacados em azul.
4.1 OPERAÇÕES ARITMÉTICAS COM STRINGS
Os resultados desta seção envolvem operações com cadeias de caracteres
numéricos portanto, cadeias de números naturais. Têm-se então as operações
aritméticas, o máximo divisor comum e o mínimo múltiplo comum. Em cada
subseção, é comparado, quando possível,
o resultado utilizando a técnica de
processamento de Strings utilizando as operações do tipo Number, o tipo double, o
tipo java.math.BigDecimal e o resultado do Maple.
4.1.1 ADIÇÃO
Esta seção mostra como foi validada a operação de adição de Strings [ver
Seção 3.5.2.1].
71
Java
private void testeAdicao(){
String a = "9857433243498432837248972948792837424";
String b = "932480923482343248923897492734987324879832";
System.out.println("adicao: " + Number.adicaoNaturalStrings(a,b));
double ad = 9857433243498432837248972948792837424.;
double bd = 932480923482343248923897492734987324879832.;
System.out.println("adicao double: " + (ad + bd));
BigDecimal ab =
new BigDecimal(9857433243498432837248972948792837424.);
BigDecimal bb =
new BigDecimal(932480923482343248923897492734987324879832.);
System.out.println("adicao BigDecimal: " + (ab.add(bb)));
}
adicao
: 932490780915586747356734741707936117717256
adicao double
: 9.324907809155868E41
adicao BigDecimal: 932490780915586727029735553981435325972480
Maple
> 9857433243498432837248972948792837424 +
932480923482343248923897492734987324879832;
932490780915586747356734741707936117717256
4.1.2 SUBTRAÇÃO
Esta seção mostra como foi validada a operação de subtração de Strings [ver
Seção 3.5.2.2].
Java
private static void testeSubtracao(){
String a = "100000000000000000000004343494";
String b = "10000000000000234343443430000000";
System.out.println("subtracao: " +
Number.subtracaoNaturalStrings(b,a));
double ad = 100000000000000000000004343494.;
double bd = 10000000000000234343443430000000.;
System.out.println("subtracao double
: " + (bd - ad));
BigDecimal ab = new BigDecimal(100000000000000000000004343494.);
BigDecimal bb = new BigDecimal(10000000000000234343443430000000.);
System.out.println("subtracao BigDecimal: " + (bb.subtract(ab)));
}
subtracao
: 9900000000000234343443425656506
subtracao double
: 9.900000000000233E30
subtracao BigDecimal: 9900000000000233831643767373824
72
Maple
>
10000000000000234343443430000000
100000000000000000000004343494;
-
9900000000000234343443425656506
4.1.3 MULTIPLICAÇÃO
Esta seção mostra como foi validada a operação de multiplicação de Strings
[ver Seção 3.5.2.3].
Java
private static void testeMultiplicacao(){
String a = "9864959123598765767463";
String b = "19875897459485";
System.out.println("mult: " +
Number.multiplicacaoNaturalStrings(b,a));
double ad = 9864959123598765767463.;
double bd = 19875897459485.;
System.out.println("mult double
: " +
(bd * ad));
BigDecimal ab = new BigDecimal(9864959123598765767463.);
BigDecimal bb = new BigDecimal(19875897459485.);
System.out.println("mult BigDecimal: " + (bb.multiply(ab)));
}
mult
: 196074915982660080627999427973736555
mult double
: 1.9607491598266007E35
mult BigDecimal: 196074915982660079675725304792350720
Maple
> 9864959123598765767463 * 19875897459485;
196074915982660080627999427973736555
4.1.4 DIVISÃO INTEIRA
Para a operação de divisão inteira [ver Seção 3.5.2.4] não foram considerados
os tipos BigDecimal e double pois não existe a operação definida para estes tipos
em java. Os tipos long e int que possuem a operação de divisão inteira, trabalham
com poucas casas decimais e não foram levados em consideração. A função evalf do
maple avalia a expressão que o Maple consideraria como uma fração e converte-a
para ponto-flutuante com a precisão especificada entre colchetes([]);
73
Java
private static void testaDivisaoInteira () {
String a = "97435987459375743";
String b = "3427598782398423894983248973284324324";
System.out.println("div: " + Number.divisaoNaturalStrings(b,a));
}
div: 35177955001764642450
Maple
> evalf[20](3427598782398423894983248973284324324 /
97435987459375743);
4.1.5 RESTO DA DIVISÃO INTEIRA
Para validar o resto da divisão inteira [ver Seção 3.5.2.4] não foram levados
em consideração, assim como na validação da divisão inteira, os tipos BigDecimal,
double, long e int pelos mesmos motivos.
Java
private static void testaRestoDivisaoInteira () {
String a = "97435987459375743";
String b = "3427598782398423894983248973284324324";
System.out.println("resto: " +
Number.restoDivisaoNaturalStrings(b,a));
}
resto: 9201886686233974
Maple
> 3427598782398423894983248973284324324 mod 97435987459375743;
9201886686233974
4.1.6 MÁXIMO DIVISOR COMUM
Para validar o mdc [ver Seção 3.5.2.5] foi utilizado o método gcd (greatest
common divisor) do Maple que calcula o máximo divisor comum.
Java
private static void testeMDC() {
String a = "8726346348765843765";
String b = "982734873297492837432";
System.out.println("mdc: " + Number.mdc(b,a));
}
mdc: 3
Maple
> gcd (8726346348765843765, 982734873297492837432);
3
74
4.1.7 MÍNIMO MULTIPLO COMUM
Para validar o mmc [ver Seção 3.5.2.6] foi utilizado o método lcm (least
common multiple) do Maple que calcula o máximo divisor comum.
Java
private static void testeMMC() {
String a = "50400";
String b = "1230390";
System.out.println("mmc: " + Number.mmc(b,a));
}
mmc: 98431200
Maple
> lcm (50400, 1230390);
98431200
4.2 OPERAÇÕES ARITMÉTICAS COM O TIPO NUMBER
A partir das operações com cadeias de caracteres, o passo seguinte no
desenvolvimento da biblioteca desta dissertação foi implementar as operações com
números racionais, as quais são realizadas com o tipo Number definidos no Capítulo
3. O inverso aditivo não foi implementado como um método, pois é facilmente
calculado através da troca do sinal algébrico.
4.2.1 ADIÇÃO
Esta seção mostra como foi validada a operação de adição de tipos Number
[ver Seção 3.5.3.1]. o método toScientificNotation(int)retorna uma String
com a representação do objeto Number na notação científica, e o inteiro passado
como parâmetro especifica a quantidade de casas após a virgula que devem ser
mostradas. No método abaixo foram escolhidas 20 casas.
Java
private static void testeAdicaoNumber(){
Number a = new Number("-98123713E234/123123E123");
Number b = new Number("-9123923829/12");
System.out.println(“=” +
Number.adicionar(a,b).toScientificNotation(20));
}
=-7,96956807420222054368E113
75
Maple
> eval[20]((-98123713E234/123123E123) + (-9123923829/12));
4.2.2 SUBTRAÇÃO
Esta seção mostra como foi validada a operação de subtração de tipos
Number [ver Seção 3.5.3.1].
Java
private static void testeSubtracaoNumber(){
Number a = new Number("34765783465E3");
Number b = new Number("8123923829345435/12");
System.out.println("=" +
Number.subtrair(a,b).toScientificNotation(20));
}
=-6,42227868980452916667E14
Maple
> 34765783465E3 - 8123923829345435/12;
4.2.3 MULTIPLICAÇÃO
Esta seção mostra como foi validada a operação de multiplicação de tipos
Number [ver Seção 3.5.3.2].
Java
private static void testeMultiplicaoNumber(){
Number a = new Number("762354762534765324/-987");
Number b = new Number("-25647864356254874/32E-2");
System.out.println("=" +
Number.multiplicar(a,b).toScientificNotation(10));
}
=6,1907204727E31
Maple
> (762354762534765324/(-987)) * (-25647864356254874/32E-2);
4.2.4 DIVISÃO
Esta seção mostra como foi validada a operação de divisão de tipos Number
[ver Seção 3.5.3.3]. Três possíveis tipos de retorno foram testados utilizando a
notação científica, a representação interna e a forma fracionária.
76
Java
private static void testeDivisaoNumber(){
Number a = new Number("9873892173983/234342");
Number b = new Number("3489756345345/49538457");
System.out.println("=" +
Number.dividir(a,b,true,1).toScientificNotation(10));
System.out.println("=" + Number.dividir(a,b,false,10));
System.out.println("=" + Number.dividir(a,b,true,10));
}
=5,9811627215E2
=5981162721534E-10
=54348598098165929359/(90866275720093110)
Maple
>eval[10]((9873892173983/234342)/(3489756345345/49538457));
4.2.5 INVERSO MULTIPLICATIVO
Esta seção mostra como foi validada a operação de inverso multiplicativo de
um tipo Number. No Maple o símbolo “^” significa elevar o numero a um expoente.
Java
private static void testeInversoMultiplicativoNumber(){
Number a = new Number("436587436756/1234");
System.out.println("=" + Number.inversoMultiplicativo(a));
System.out.println("=" +
Number.inversoMultiplicativo(a).toScientificNotation(10));
}
=617/(218293718378)
=2,8264670398E-9
77
Maple
> evalf[10]((436587436756/1234)^(-1));
4.3 OPERAÇÕES INTERVALARES
As operações Intervalares utilizam o tipo Intervalo desenvolvido neste trabalho
[ver Seção 3.1]. Os resultados das operações serão comparados com o Maple
utilizando sua biblioteca intervalar (intpakX [INTPAKX]) e a biblioteca Java-XSC
[JAVA-XSC] que implementa operações intervalares utilizando o tipo primitivo double.
4.3.1 ADIÇÃO
Esta seção compara os resultados das operações de adição intervalar. [ver
Seção 2.6.3.1].
Java com strings
private static void testeAdicaoIntervalar(){
Intervalo a = new Intervalo("822/1234","833/1234");
Intervalo b = new Intervalo("-12E-3","1582E-2");
System.out.println("=" + new Adicao
(a,b).toScientificNotation(10));
}
=[6,5412641815E-1 , 1,6495040519E1]
Java-XSC
private static void adicaoJavaXSC(){
double a1 = 822./1234.;
double a2 = 833./1234.;
double b1 = -12E-3;
double b2 = 1582E-2;
Interval a = new Interval(a1,a2,10);
Interval b = new Interval(a1,a2,10);
System.out.println("=" + IntervalElementary.add(a,b));
}
=[0.6541264181, 16.4950405187]
Maple
> a:=construct(822/1234,833/1234);type(a,interval);
> b:=construct(-12E-3,1582E-2);type(b,interval);
> a&+b;
78
4.3.2 SUBTRAÇÃO
Esta seção compara os resultados das operações de subtração intervalar. [ver
Seção 2.6.3.3], que usa o pseudo-inverso aditivo [ver Seção 2.6.3.2].
Java com strings
private static void testeSubtracaoIntervalar(){
Intervalo a = new Intervalo("-199121212","-199121212.1");
Intervalo b = new Intervalo("-199121211.77","-199121211.7");
System.out.println("=" + new Subtracao
(a,b).toScientificNotation(10));
}
=[-4E-1 , -2,3E-1]
Java-XSC
private static void subtracaoJavaXSC(){
double a1 = -199121212;
double a2 = -199121212.1;
double b1 = -199121211.77;
double b2 = -199121211.7;
Interval a = new Interval(a1,a2,10);
Interval b = new Interval(b1,b2,10);
System.out.println(“=” + IntervalElementary.sub(a,b));
}
=[-0.400000006, -0.2299999892]
Maple
>a:=construct(-199121212,-199121212.1);type(a,interval);
>b:=construct(-199121211.77,-199121211.7);type(b,interval);
> a&-b;
4.3.3 MULTIPLICAÇÃO
Esta seção compara os resultados das operações de multiplicação intervalar.
[ver Seção 2.6.3.4].
79
Java com strings
private static void testeMultiplicacaoIntervalar(){
Intervalo a = new Intervalo("-0.1212212","0.32456");
Intervalo b = new Intervalo("1.00034444","1.00000345");
System.out.println("=" +
new Multiplicacao (a,b).toScientificNotation(10));
}
=[-1,2126295343E-1 , 3,2467179145E-1]
Java-XSC
private static void multiplicacaoJavaXSC(){
double a1 = -0.1212212;
double a2 = 0.32456;
double b1 = 1.00034444;
double b2 = 1.00000345;
Interval a = new Interval(a1,a2,10);
Interval b = new Interval(b1,b2,10);
System.out.println("=" + IntervalElementary.mult(a,b));
}
=[-0.1212629535, 0.3246717915]
Maple
> a:=construct(-0.1212212,0.32456);type(a,interval);
> b:=construct(1.00034444,1.00000345);type(b,interval);
> a&*b;
4.3.4 INVERSO MULTIPLICATIVO
Esta seção compara os resultados das operações de Inverso multiplicativo
Intervalar (ou recíproco). [ver Seção 2.6.3.5].
Java com Strings
private static void testeInversomultiplicativo(){
Intervalo a = new Intervalo("98383838","98383849");
System.out.println("=" + new
InversoMultiplicativo(a).toScientificNotation(10));
}
=[1,016426995E-8 , 1,0164271087E-8]
80
Java-XSC
private static void reciprocoJavaXSC(){
double a1 = 98383838.;
double a2 = 98383849.;
Interval a = new Interval(a1,a2,10);
System.out.println("=" + a.reciprocal());
}
=[1.01E-8, 1.02E-8]
Maple
> a:=construct(98383838,98383849);type(a,interval);
> Interval_reciprocal(a);
4.3.5 DIVISÃO
Esta seção compara os resultados das operações de divisão intervalar. [ver
Seção 2.6.3.6].
Java com strings
private static void testeDivisaoIntervalar(){
Intervalo a = new Intervalo("-2010220.2234","-2010220.9899");
Intervalo b = new Intervalo("50021","50224.23");
System.out.println("=" + new
Divisao(a,b).toScientificNotation(10));
}
=[-4,0187541031E1 , -4,0024908762E1]
Java-XSC
private static void divisaoJavaXSC(){
double a1 = -2010220.2234;
double a2 = -2010220.9899;
double b1 = 50021;
double b2 = 50224.23;
Interval a = new Interval(a1,a2,10);
Interval b = new Interval(b1,b2,10);
System.out.println("=" + IntervalElementary.div(a,b));
}
=[-40.1875410308, -40.0249087621]
81
Maple
> a:=construct(-2010220.2234,-2010220.9899);
type(a,interval);
> b:=construct(50021, 50224.23);type(b,interval);
> a&/b;
4.3.6 INTERSECÇÃO
Esta seção compara os resultados entre da operação Intersecção de intervalos
[ver Seção 3.6.4.1];
Java com strings
private static void testeInterseccaoIntervalar(){
Intervalo a = new Intervalo("230203","-2010220.9899");
Intervalo b = new Intervalo("87686","-120215.23");
System.out.println("=" + new
Interseccao(a,b).toScientificNotation(10));
}
=[-1,2021523E5 , 8,7686E4]
Java-XSC
private static void intersecacaoJavaXSC() {
Interval a = new Interval(230203., -2010220.9899,10);
Interval b = new Interval(87686.,-120215.23,10);
System.out.println("=" + IntervalSet.interscetion(a,b));
}
=[-120215.23, 87686.0]
Maple
> a:=construct(230203,-2010220.9899);type(a,interval);
> b:=construct(87686, -120215.23);type(b,interval);
> a &intersect b;
82
4.3.7 UNIÃO
Esta seção compara os resultados entre da operação União de intervalos [ver
Seção 3.6.4.2];
Java com strings
private static void testeUniaoIntervalar(){
Intervalo a = new Intervalo("-0.120120","1.12122112");
Intervalo b = new Intervalo("0.0001212","-1.234434");
System.out.println("=" + new Uniao(a,b).toScientificNotation(10));
}
=[-1,234434 , 1,12122112]
Java-XSC
private static void uniaoJavaXSC() {
Interval a = new Interval(-0.120120,1.12122112,10);
Interval b = new Interval(0.0001212,-1.234434,10);
System.out.println("=" + IntervalSet.union(a,b));
}
=[-1.234434, 1.12122112]
Maple
> a:=construct(-0.120120,1.12122112);type(a,interval);
> b:=construct(0.0001212,-1.234434);type(b,interval);
> a &union b;
4.3.8 DISTÂNCIA
Esta seção compara os resultados entre da operação Distância de intervalos
[ver Seção 3.6.5.1]; O Maple não implementa esta operação.
Java com strings
private static void testeDistanciaIntervalar(){
Intervalo a = new Intervalo("-0.3726483838","1.293874");
Intervalo b = new Intervalo("0.39393993933","-3.983274");
System.out.println("=" +
Intervalo.getDistancia(a,b).toScientificNotation(10));
}
=3,6106256162
83
Java-XSC
private static void distanciaJavaXSC(){
Interval a = new Interval(-0.3726483838,1.293874,10);
Interval b = new Interval(0.39393993933,-3.983274,10);
System.out.println("=" + IntervalUtil.distance(a,b));
}
=3.6106256162
4.3.9 DIÂMETRO
Esta seção compara os resultados entre da operação diâmetro de intervalos
[ver Seção 3.6.5.2];
Java com strings
private static void testeDiametro(){
Intervalo a = new Intervalo("873264263.12341243","873264263.2948");
System.out.println("=" +
a.getDiametro().toScientificNotation(16));
}
=1,7138757E-1
Java-XSC
private static void diametroJavaXSC(){
Interval a = new Interval(873264263.12341243,873264263.2948, 16);
System.out.println("=" + a.width());
}
=0.1713876724243164
Maple
>a:=construct(873264263.12341243,873264263.2948);
type(a,interval);
> width(a);
4.3.10 PONTO MÉDIO
Esta seção contém os resultados da operação ponto médio de um intervalo
[ver Seção 3.6.5.3]; Apenas a biblioteca desenvolvida neste sistema implementa esta
operação.
84
Java com strings
private static void testePontoMedio(){
Intervalo a = new Intervalo("873264263.12341243","873264263.2948");
System.out.println("=" +
a.getPontoMedio(10).toScientificNotation(10));
}
=8,7326426321E8
4.3.11 VALOR ABSOLUTO
Esta seção compara os resultados entre da operação valor absoluto de um
intervalo [ver Seção 3.6.5.4]; O Maple não implementa esta operação.
Java com strings
private static void testeValorabsoluto(){
Intervalo a = new Intervalo("873264263.12341243","873264263.2948");
System.out.println("=" +
a.valorAbsoluto().toScientificNotation(15));
}
=8,732642632091062E8
Java-XSC
private static void valorabsolutoJavaXSC(){
Interval a = new Interval(873264263.12341243,873264263.2948,15);
System.out.println("=" +
IntervalUtil.abs(a));
}
=8.732642632948E8
4.4 COMPARAÇÃO ENTRE RESULTADOS
A Tabela 1 na seqüência apresenta os resultados obtidos na Seção 4.3
usando a biblioteca desenvolvida nesta dissertação, Java-XSC [JAVA-XSC] e o
Maple Intervalar [INTPAKX]. S é o resultado das operações com Strings, J o resultado
com Java-XSC e M o resultado com o Maple Intervalar. Sendo |S - J| e |S - M| o valor
absoluto das distâncias [Ver Seção 2.6.5.1] intervalares entre os resultados.
85
Tabela 1 - Comparação entre resultados Obtidos com Java com Strings, Java-XSC e o Maple Intervalar
Operação
Adição
Subtração
Multiplicação
Recíproco
Divisão
União
Intersecção
Java com Strings
Java-XSC
Maple Intervalar
[6,5412641815E-1
[0.6541264181,
[0.6541264180,
,1,6495040519E1]
16.4950405187]
16.49504053]
[-0.400000006, -
[-.4000000001, -
0.2299999892]
.1999999999]
[-1,2126295343E-1,
[-0.1212629535,
[-.1212629535,
3,2467179145E-1]
0.3246717915]
0.3246717915]
[-4E-1 , -2,3E-1]
[1,016426995E-8,
1,0164271087E-8]
[1.01E-8,1.02E-8]
[1.016426994E-8,
1.016427110E-8]
| S – J|
|S - M|
3E-10
11E-9
108E-10
300000001E10
7E-11
7E-11
6426995E-17
13E-18
2E-10
39E-9
[-4,0187541031E1 , -
[-40.1875410308, -
[-40.18754107, -
4,0024908762E1]
40.0249087621]
40.02490873]
[-1,234434, 1,12122112]
[-1,234434, 1,12122112]
[-1,234434, 1,12122112]
0
0
[-120215.23, 87686.0]
[-1.2021523E5, 87686]
0
0
0
-
[-1,2021523E5 ,
8,7686E4]
Distância
3,6106256162
3.6106256162
-
Diâmetro
1,7138757E-1
0.1713876724243164
0.2
8,7326426321E8
-
-
-
-
8,732642632091062E8
8.732642632948E8
-
856938E-7
-
Ponto Médio
Valor Absoluto
1024243164E16
2861243E-8
4.5 COMPARAÇÃO DO DESEMPENHO
A Tabela 2 a seguir apresenta uma comparação entre o desempenho das
operações realizadas processando-se Strings e Java-XSC, através da métrica tempo.
A comparação não envolve o Maple intervalar, uma vez que a ferramenta para
mensurar o tempo não seria a mesma (JBuilder 7, JDK 1.4.2, o cálculo do tempo é
realizado através da medição do tempo do relógio da máquina antes e depois da
operação ser efetuada). Foram realizadas 1000 execuções, comentando-se as
operações de I/O (System.out.println); o maior tempo de execução, t, é o que foi
considerado nesta tabela, assim como o tempo total, T, para realizar 1000 vezes a
operação.
Tabela 2 - Análise do desempenho entre Java com Strings e Java-XSC
Operação
Java com Strings (ms)
Java-XSC (ms)
t
T
t
T
Adição
40
821
10
70
Subtração
31
121
10
80
Multiplicação
30
120
11
71
Recíproco
20
60
30
60
Divisão
30
130
31
71
União
20
100
10
40
Intersecção
20
80
10
60
Distância
30
130
10
60
Diâmetro
20
100
10
60
Ponto Médio
40
110
-
-
Valor Absoluto
30
120
30
110
87
Com os resultados das Tabelas 1 e 2 pode-se observar que o Maple e a API
Java-XSC apresentam pequenas diferenças em relação a técnica de processamento
de Strings envolvendo operações com baixa precisão. A velocidade das operações
com Strings por serem feitas via software são intrinsecamente mais lentas, porem
estão com um bom desempenho.
88
5 CONCLUSÕES E TRABALHOS FUTUROS
Este
trabalho
implementou
uma
biblioteca
intervalar
em
Java
via
processamento de Strings. A principal motivação para o desenvolvimento deste foi a
busca por cálculos numéricos com altas precisão e exatidão. As principais conclusões
foram:
A representação de números racionais processados através de strings permite
que se trabalhe com precisão e exatidão superiores à Java-XSC e o Maple Intervalar
(Tabela 1), sendo o custo desta exatidão refletido no tempo das operações (Tabela
2).
Enfatiza-se que, para qualquer uma das operações, executando 1000
repetições, o tempo de processamento é menor do que 1 segundo (Tabela2).
Quando o fator tempo estiver envolvido, como nos sistemas de tempo real, é
aconselhável um estudo prévio daqueles (sistemas) para assegurar que a biblioteca
desenvolvida nesta dissertação realize os cálculos no limite aceitável de tempo para o
uso seguro do sistema.
Este trabalho propôs uma alternativa ao sistema de ponto-flutuante
implementado em Java o qual possui problemas numéricos, como mostrado nos
Capítulos 1 e 2. A comunidade usuária de Java terá acesso à biblioteca que está
disponível em (www.cin.ufpe.br/~javaxsc) em código aberto, possibilitando o
desenvolvimento de possíveis extensões, como as propostas na Seção 5.1.
5.1 TRABALHOS FUTUROS
Como trabalhos futuros sugere-se:
•
Implementação das Funções Potência e Raiz
•
Implementação das Funções Exponencial e Logarítmica.
•
Implementação das Funções Trigonométricas.
Utilização de Expressões Regulares para a construção do tipo Number. Para
tornar o código mais legível e melhorar a manutenibilidade do sistema, serão usadas
89
expressões regulares que permitem definir um autômato e casamento de padrões em
Strings.
Desenvolvimento de um tipo CadeiaDeCaractere. Este tipo é uma alternativa
ao tipo String estendendo o tamanho das mantissas ao limite da memória da
máquina. O tamanho máximo de uma String está limitado ao tamanho de um tipo
inteiro (int).
Analisar quais as operações que consomem mais tempo de processamento
quando comparadas com as de Java-XSC.
90
REFERÊNCIAS
BOOCH, G., JACOBSON, I., RUMBAUGH, J. The Unified Modeling Language
Reference Manual. Addison-Wesley, 1999.
CAMPOS, M. A; FIGUEIREDO, P. L., Introdução ao Tratamento da Informação
nos Ensinos Fundamental e Médio, 2005.
CAMPOS M. A., Implementing the Type Interval and Proving its Fundamental
properties in the OBJ3 System. Exame de Qualificação. Departamento de
Informática, UFPE.1995.
CHOUDHARI, P., Java Advantages & Disadvantages, 2001. Disponível em:
http://arizonacommunity.com/articles/java_32001.shtml Acesso em: 12/06/2005.
DIVÉRIO, T. A.; OLIVEIRA, P. W; CLAUDIO, D. M. Fundamentos da Matemática
Intervalar. Porto Alegre: Sagra-Luzzatto, 1997. 93p. (Série Matemática da
Computação e Processamento Paralelo, v.1, Instituto de Informática da UFRGS,
Projeto ArInPar-ProTeM-CNPq) ISBN 85-241-0515-1
DUTRA, Enéas Montenegro Dutra. Java XSC: Uma Biblioteca Java para
Computações Intervalares. Dissertação de Mestrado em Ciências da Computação,
Departamento de Informática e Matemática Aplicada, Universidade Federal do Rio
Grande do Norte, Natal, 2000.
GAJSKI, D.D., Principles of Digital Design, Prentice Hall, 1997
GOSLING, J.; JOY, B., STEELE G., (1996), The Java Language Specification,
Disponível em: http://java.sun.com/docs/books/jls/second_edition/html/j.title.doc.html.
Acesso em 31/07/2007
HÖLBIG, C.A. Sistema de Ponto-flutuante e Padrão IEEE-754. 2006.
HOPCROFT, E.J., ULLMAN J.D. Introduction to Automata Theory, Languages
and Computation, Addison-Wesley publishing company, 1979. ISBN:020102988
91
INTPAKX. Disponível em http://www.math.uni-wuppertal.de/~xsc/software/intpakX/.
Acesso em: 31/07/2007.
JAVA COM STRINGS, Uma Biblioteca Intervalar baseada em Processamento de
Strings, Diponível em http://www.cin.ufpe.br/~javaxsc. Acesso em 31/07/2007.
JAVA-XSC. Java para Computação científica com Controle automático do Erro.
Disponível em http://www.cin.ufpe.br/~javaxsc. Acesso em 31/07/2007.
KULISCH, U. W; A new aritmetic for Scientific Computation. Em U. W Kulisch abd
W.L. Miranker, editors. A new Approach to Scientific Computation, pages 1 – 26.
Proceeding of Symposium held at IBM Researche Center, Torktown Heights, N. Y,
Academic Press, 1983. (ok)
MACAULAY INSTITUTE, Float Point Arithmetic and Java, 2004. Disponível em
http://macaulay.ac.uk/fearlus/float-point/javabugs.html acesso em: 31/07/2007
MAPLE, MapleSoft.com. Disponível em: http://www.maplesoft.com/. Acesso em
31/07/2007.
MOORE, R. E. Methods and Aplications of Intervals Analysis. Society for
Industrial and Applied Mathematics, Philadhelpia, PA, USA, 1979.
SUNAGA, Theory of an Interval Algebra and its Application to Numerical
Analysis, RAAG Memoirs, 1958 (ok)
SUN MICROSYSTEMS, Java Language Overview, Java 2 Platform, Standard
Edition, White Papers. Disponível em: http://java.sun.com/docs/overviews/java/javaoverview-1.html. Acesso em: 31/07/2007.
UNICODE, Unicode Home Page, 1991-2007 Disponível em http://unicode.org
Acesso em: 31/07/2007.
VUIK, K. Some Disasters caused by Numerical Errors. Disponível em
http://ta.twi.tudelft.nl/users/vuik/wi211/disasters.html . Acesso 31/07/2007.
92
93
Download