mcFileDialog Cuadro de diálogo Abrir archivo
28/11/2021Dim WithEvents
13/12/2021Problemas de precisión con las variables de coma flotante
Las variables de coma flotante (Double y Single) tarde o temprano van a dar problemas de redondeo o de comparación. Es posible que nunca te haya tocado, o que nunca te hayas fijado, pero prueba lo siguiente:
¿Te has fijado en los resultados?
Cuando escribimos números en el editor de VB, el tipo predeterminado es Double y es la falta de precisión de estos números de coma flotante el origen del problema.
Las variables de tipo Currency lo solucionan, pero sólo si no necesitas más de cuatro decimales; sin embargo, en ocasiones, necesitamos más; por ejemplo, la norma de Facturae dice que el valor del producto puede tener hasta 8 decimales.
El tipo de datos Decimal
No existe una variable tipo Decimal, pero existe un subtipo del tipo Variant que es Decimal. Además, tenemos el tipo de datos Decimal, que puede almacenar en una tabla valores de precisión Decimal.
La forma de que una variable Variant se convierta al subtipo Decimal es adquiriendo un valor que ya es Decimal (por ejemplo, al leerlo de la tabla), o convirtiéndola a Decimal usando la función cDec().
Es decir, si queremos usar algo equivalente a una variable Decimal, es decir un Variant de subtipo Decimal, primero debemos dimensionarla como Variant y luego transformarla con cDec():
Una clase como un tipo de variable
A mí me gustaría disponer de un tipo de variable como el que se define en el xsd de Facturae, DoubleUpToEightDecimalType, o sea Decimal y hasta 8 decimales y, como no existe, me lo invento.
De momento, creo una clase que se limita a hacer la conversión con cDec y guardar el valor en una variable Variant privada:
Option Compare Database
Option Explicit
Private v_Value As Variant
Public Property Get Value() As Variant
Value = (v_Value)
End Property
Public Property Let Value(ByVal NewValue As Variant)
If Not IsNumeric(NewValue) Then
Err.Raise 104, , "No coinciden los tipos. Sólo se admiten valores numéricos"
Else
v_Value = CDec(NewValue)
End If
End Property
Pero también me gustaría que redondeara a 8 decimales si es que tiene más.
La única función que redondea correctamente en Access, evitando el redondeo bancario, es Format(). Si tenías alguna función personalizada que use Int(), ya has visto en los ejemplos de más arriba lo que te puede ocurrir.
Basta con cambiar una línea, y nos queda así:
Option Compare Database
Option Explicit
Private v_Value As Variant
Public Property Get Value() As Variant
Value = (v_Value)
End Property
Public Property Let Value(ByVal NewValue As Variant)
If Not IsNumeric(NewValue) Then
Err.Raise 104, , "No coinciden los tipos. Sólo se admiten valores numéricos"
Else
v_Value = CDec(Format(NewValue, "0.00000000"))
End If
End Property
Todavía no está a mi gusto. Empezamos con que para dimensionar la variable tenemos que usar New, cosa que no ocurre con las variables de verdad, y, además tenemos que usar Value para asignarle u obtener el valor.
Con lo de New nos tendremos que quedar, pero podemos evitar la molestia de tener que usar Value para todo.
Especificar el miembro predeterminado de una clase
Cuando en VBA nos referimos, por ejemplo, al valor de un cuadro de texto de un formulario, no necesitamos especificar la propiedad Value del mismo, pues sabemos que Value es la propiedad predeterminada. Lo mismo podemos hacer en nuestras propias clases y definir cuál es su propiedad predeterminada.
Para ello debemos añadir el atributo VB_UserMemId = 0 en el Property Get del miembro que queremos que sea el predeterminado de la clase, en este caso Value. Es importante que lo escribamos en la línea inmediatamente después de la de Public Property Get, pues si dejamos una línea en blanco no va a funcionar.
Es algo que no podemos hacer directamente en VBE, pues ese atributo, aunque exista, no será visible. Debemos exportar el módulo de clase, editarlo con un editor de texto y volver a importarlo:
Si te fijas, en el archivo de texto que hemos importado existen otros Attribute que no se ven en el editor de VB. Busca en internet acerca de ellos. Son interesantes.
Probar la clase
Después de importar de nuevo el archivo .cls, nuestro módulo de clase se comportará casi como un tipo de variable.
Eso sí, debemos tener en cuenta que el subtipo Decimal es un tipo que ocupa en memoria el doble que otro tipo de datos numérico, lo que le hace lento en cálculos masivos; si, además, cada vez que se le invoca, estamos ejecutando código y convirtiendo variables, el cálculo será mucho más lento. Sin embargo, en la mayoría de los usos de gestión, por ejemplo, generar una Factura Electrónica, esa lentitud será completamente imperceptible.
También debemos tener en cuenta que, puesto que los números que escribamos en el editor de VBE son Double, puede haber valores con muchos decimales que no podemos escribir directamente en el editor, pues los trunca o los transforma. En su lugar debemos escribir el valor como cadena de texto; al ser un Variant, no hay problema en meter un texto y, como luego lo convierte con cDec(), al final tenemos un subtipo decimal.