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