Quando a matemática do Access não "bate"
por Luke Chung
Presidente da FMS
FMSInc
Todos os direitos reservados
Publicado originalmente em Smart Access Outubro 1997
Pinnacle Publishing, Inc.
P.O. Box 888
Kent, WA 98035-0888
Tel 206-251-1900, Fax 206-251-5057
Pinnacle Publishing
Traduzido por Osmar José Correia Júnior
OsmarJr
Seria de se imaginar que, em 1997, programas como o Microsoft Access executassem cálculos matemáticos corretamente. Na maioria das vezes o Access e o VBA o fazem, mas existem diversas áreas onde eles geram resuktados inesperados. Na maioria dos casos as discrepâncias são menores; entretanto, se você estiver tentando determinar se dois valores são idênticos, procurando por pequenas diferenças entre valores, ou efetuando cálculos múltiplos, estes erros podem divergir significativamente da resposta correta. Encontrei estes problemas ao escrever o programa Total Access Statistics.
Rapidamente descobri problemas em cálculos utilizando Séries de Taylor e outros cálculos iterativos, bem como em códigos que tentavam pegar a divisão por zero. Um pouco de pesquisa revelou mais do que "erros de arredondamento".
Erros de arredondamento não são incomuns em programas pois os computadores armazenam e executam cálculos matemáticos utilizando a representação binária de números decimais. Isto causa erros de arredondamento em cálculos não exatos como a divisão. Poderíamos desculpar o Access por problemas de exatidão mo 15º dígito significativo. Isto seria esperado. O que não esperava, entretanto, são erros muito maiores e mais óbvios que devem ser corrigidos através da utilização de código VBA.
Problemas de Subtração
A subtração é uma área onde não deveriam ocorrer erros de arredondamento. Por definição, o resultado de uma subtração não pode ter mais casas decimais que qualquer dos número iniciais. Se os resultados matemáticos binários resultarem em um número incorreto, o Access deveria arredondar o resultado de acordo. Em muitos casos isto não ocorre.
Um exemplo simples revela o problema. Examine a diferença entre 100,8 e 100,7. A solução esperada seria 0,1, mas veja o que acontece quando testamos isto na janela de depuração:
? 100.8 - 100.7
9.99999999999943E-02
O erro ocorre no 14º dígito mais significativo.
O erro fica pior se o número for maior:
? 10000.8 - 10000.7
9.99999999985448E-02
O erro agora está no 11º dígito. Podemos imaginar por quê um erro no 11º dígito seria um problema. Na maioria dos casos não o seria. Entretanto, este erro é significativo o bastante para causar resultados inesperados. Com certeza não podemos ter algo como:
Const X = 100.8
Const Y = 100.7
If X - Y = .1 Then ...
Uma regra geral ao trabalhar com cálculos que utilizam números reais é evitar comparar diretamente dois números double (ou single) para ver se são iguais. Devido a problemas de arredondamento, podemos esperar diferenças a partir do 15º dígito. Assim sendo, usaríamos uma função semelhante a apresentada a seguir para testar a identidade de dois números double:
Function IsEqual (dblValue1 As Double, dblValue2 As Double) As Integer
Const dblSmall = .000000000000001
If Abs(dblValue1 - dblValue2) <= dblSmall Then
IsEqual = True
Else
IsEqual = False
End If
End Function
Calculando a diferença absoluta entre dois valores, podemos considerar dois valores equivalentes de a diferença for menor que um número minúsculo, não zero (neste caso 10-15). Mas isto vai falhar se o Access introduzir diferenças maiores.
Por exemplo, se utilizarmos esta função para comparar a diferença entre 100,8 e 100,7 a 0,1:
IsEqual(100.8-100.7, .1)
Inesperadamente o resultado será Falso no Access, o que causa problemas. Para evitar isso precisaremos aumentar o valor de dblSmall na função IsEqual(). Mas o valor pode ser aumentado tanto que dois números que não são iguais retornarão Verdadeiro.
Não ser capaz de determinar se dois números são iguais pode ser especialmente problemático em divisões. Aqui está uma função simples que tenta evitar a divisão por zero:
Function CalcDivision (dblNumerator As Double, dblDenominator As Double) As Double
If IsEqual(dblDenominator, 0) Then
' Deveria ser indefinido.
CalcDivision = 0
Else
CalcDivision = dblNumerator / dblDenominator
End If
End Function
Para demosntrar este problema, digite o seguinte na janela de depuração:
? CalcDivision(10, 100.8 - 100.7)
100.000000000006
Mesmo não sendo completamente correto, fica razoavelmente próximo com o erro no último dígito. Mas vejamos o que acontece quando ocorre uma divisão por valor próximo de zero:
? CalcDivision(10, 100.8 - 100.7 - .1)
-1.757502293608E+15
A função CalcDivision deveria ter pego a diferença entre 100,8 - 100,7 - 0,1 como zero. Em vez disso, ela utilizou o minúsculo erro como divisor e gerou um resultado imenso (10 à 15ª potência!).
Estes problemas emanam das inexatidões das subtrações do Access para números com decimais. Por sorte existe uma forma de contornar este problema. Como a diferença entre dois números não pode ter mais casas decimais que qualquer dos números iniciais, podemos escrever um procedimento que efetue a subtração corretamente. Esta função está incluída na nova biblioteca de códigos da FMS Total Access Sourcebook (http://www.fmsinc.com/products/sourcebook/index.html).
Function Subtract_TSB (dblValue1 As Double, dblValue2 As Double) As Double
Dim dblResult As Double
Dim intDecimals1 As Integer
Dim intDecimals2 As Integer
Dim intMaxDecimals As Integer
' Variável temporária para evitar problemas de arredondamento em Int()
Dim dblTemp As Double
dblResult = dblValue1 - dblValue2
' Pega o número de decimais no valor 1
If InStr(dblValue1, ".") = 0 Then
intDecimals1 = 0
Else
intDecimals1 = Len(dblValue1 & "") - InStr(dblValue1, ".")
End If
' Pega o número de decimais em valor 2
If InStr(dblValue2, ".") = 0 Then
intDecimals2 = 0
Else
intDecimals2 = Len(dblValue2 & "") - InStr(dblValue2, ".")
End If
If intDecimals1 + intDecimals2 > 0 Then
' Arredonda os valores ao número máximo de decimais
intMaxDecimals = IIf(intDecimals1 > intDecimals2, intDecimals1, intDecimals2)
dblTemp = dblResult * 10 ^ intMaxDecimals + .5
dblResult = Int(dblTemp) / (10 ^ intMaxDecimals)
End If
Subtract_TSB = dblResult
End Function
No Access 97 podemos usar a função CDec:
Function Subtract_TSB (dblValue1 As Double, dblValue2 As Double) As Double
Subtract_TSB = CDec(dblValue1) - CDec(dblValue2)
End Function
Com esta função vamos obter o resultado correto:
? Subtract_TSB(100.8, 100.7).1
Aplicando-a à divisão, veremos que identificamos corretamente a divisão por zero:
Sub TestDivision ()
' Perform: CalcDivision(10, 100.8-100.7-.1) which returned -1.757502293608E+15
' due to subtraction inaccuracies
Dim dblDiff As Double
' Rather than setting dblDiff to 100.8-100.7-.1
' use the Subtract_TSB proc for subtraction
dblDiff = Subtract_TSB(100.8, 100.7)
dblDiff = Subtract_TSB(dblDiff, .1)
Debug.Print (CalcDivision(10, dblDiff))
End Sub
Com certeza, não desejamos chamar uma função especial cada vez que executamos uma subtração. Isto é necessário apenas quando calculamos a diferença entre dois números que incluem decimais, e seja necessário comparar o resultado com outro número ou usá-lo como divisor.
Problemas de Tipos de Dados
Outro problema numérico comum está assocoado a expressões passadas a funções do Access. O procedimento abaixo é uma função comum utilizada para arredondar números à casa decimal mais próxima. Números na fronteira, *,5, deveriam ser arredondados para cima, mas são, na verdade, arredondados para baixo pelo Access:
Function RoundBad (dblNumber As Double, intDecimals As Integer) As Double
Dim dblFactor As Double
dblFactor = 10 ^ intDecimals
RoundBad = Int(dblNumber * dblFactor + .5) / dblFactor
End Function
A execução da função gera os seguintes resultados:
? RoundBad(100.06, 1) ' Correamente devolve 100.1
? RoundBad(100.04, 1) ' Corretamente devolve 100.0
? RoundBad(100.05, 1) ' Incorretamente devolve 100.0 em vez de of 100.1
O problema surge pela forma com que a função Int() processa a expressão passada para ela. Na verdade ela arredonda o número antes de aplicar a conversão para Integer. Vamos isolar o problema usando a janela de depuração:
Como esperado, o resultado é, corretamente, apresentado como 1001:
? 100.05 * 10 + .5
1001
Entretando, usar a mesma expressão dentro da função Int() fornece um resultado errado:
? Int(100.05 * 10 + .5)
1000
Assim sendo, é absolutamente imperativo que evitemos passar expressões envolvendo números reais para funções do Access. Se a expressão recebe um valor e, em seguida, é passada à função Int(), normalmente o resultado será correto. Entretanto às vezes ocorre de também falhar e acrescentar uma string nula a ela resolve o problema. Eis a forma correta de executar o arredondamento, novamente do Total Access Sourcebook:
Function Round_TSB (dblNumber As Double, intDecimals As Integer) As Double
Dim dblFactor As Double
Dim dblTemp As Double
dblFactor = 10 ^ intDecimals
dblTemp = dblNumber * dblFactor + .5
Round_TSB = Int("" & dblTemp) / dblFactor
End Function
No Access 97 podemos alterar a última linha para:
Round_TSB = Int(CDec(dblTemp)) / dblFactor
Com a simples adição da variável dblTemp, a função Round_TSB() devolve o resultado correto:
? Round_TSB(100.05, 1) ' Correctly returns 100.1
Conclusão
Mesmo nestes dias não podemos assumir que a matemática é exata. Os problemas discutidos neste artigo impactam não apenas nos códigos de módulos mas também em cálculos exercitados em consultas. Enquanto alguns destes problemas podem ser considerados erros de arredondamento menores, espero ter demonstrado que este nem sempre é o caso. Não tomar cuidados com estes erros pode causar problemas imensos em aplicativos que exigem este tipo de cálculos. Reconhecendo onde e como tais problemas ocorrem, podemos atuar para cerceá-los ou evitá-los.
Luke Chung é o presidente e fundador da FMS, Inc., uma companhia de desenvolvimento de suplementos para o Access. Ele participou ativamente na criação de todos os produtos da FMS e escreveu grande parte de Total Access Analiyzer, Total Access Code Tools, Total Access Detective, Total Access Statistics e o recente Total Access Sourcebook.
703-356-4700
http://www.fmsinc.com/index.html
Luke Chung
|