PDA

Ver la Versión Completa : Se quitan los ceros al multiplicar y dividir Variant BCD (TFmtBCDField) y Currency


Al González
30-03-2012, 20:48:27
Hola, para compartirles esto y de paso indagar más.

Me topé con cierto error de cálculo matemático en algo que estoy programando. Cuando el resultado tiene que ser 50, me arroja 5, cuando tiene que ser 800, me arroja 8, pero si tiene que ser 51 u 802, entonces sí lo respeta:
Debe ser Resulta
50 5 (incorrecto)
800 8 (incorrecto)
51 51 (correcto)
802 802 (correcto)
En pocas palabras, elimina los ceros significativos a la derecha del número. La operación matemática que realizo es por demás simple de entender:

X = (A * B) / B

Siendo A el número 50, 800, 51, 802, etcétera, y B una cantidad indeterminada. Si Multiplico A por B y al resultado le hago la operación inversa (dividirlo entre B), el resultado final será nuevamente en A, ¿cierto? Pero no sucede así en la versión 7 de Delphi cuando:

A es un entero que termina en 0 de tipo Currency,
la primera aparición de B es de tipo variante BCD,
la segunda aparición de B es de tipo Currency,
y toda la operación se hace en la misma sentencia

Es decir: X := (A_Currency * B_VariantBCD) / B_Currency

Lo he probado en Delphi 2007 también, con la fortuna de que ahí no ocurre el error de cálculo, por lo cual creo que se trata de un defecto corregido en alguna de las versiones posteriores a la 7.

Les agradecería si me ayudan ha probar este sencillo código con la versión de la que ustedes dispongan:
Uses
FmtBCD;

procedure TForm1.Button1Click(Sender: TObject);
Var
C1, C2, C3 :Currency;
V :Variant;
begin
C1 := 50;
C2 := 149.88;
V := VarFMTBcdCreate (149.88);

// (50 * 149.88) / 149.88 = 50

C3 := (C1 * V) / C2; // ¡Resulta 5!
ShowMessage (CurrToStr (C3));

// Una forma de evitar el problema es usar molde de tipo sobre el Variant
C3 := (C1 * Currency (V)) / C2; // Correcto, resulta 50
ShowMessage (CurrToStr (C3));

// Como la primera prueba pero con 51 en lugar de 50
C1 := 51;
C3 := (C1 * V) / C2; // Resulta 51, como debe ser
ShowMessage (CurrToStr (C3));

// Como la primera prueba pero con 800
C1 := 800;
C3 := (C1 * V) / C2; // ¡Resulta 8!
ShowMessage (CurrToStr (C3));

// Como la primera prueba pero con 802
C1 := 802;
C3 := (C1 * V) / C2; // Resulta 802, como debe ser
ShowMessage (CurrToStr (C3));
end;
Desconozco si este problema fue reportado en su momento, ya que no tuve suerte de encontrar información sobre ello. Pero puede que esta discusión (http://delphigroups.info/2/02/254388.html) del año 1902 (con razón dicen por ahí que Delphi está viejo :D) tenga alguna relación.

Algunos usuarios de versiones anteriores a la corrección (por lo menos desde la aparición de la unidad FmtBCD hasta la 7) se han de sentir despreocupados por esto al ver que el variante es obtenido con la función VarFmtBCDCreate, pues quizá no la usan. Pero ojo avizor, porque ese mismo tipo de variante es el que devuelve un típico campo monetario Numeric que en Delphi suele representarlo un objeto de clase TFmtBCDField. La recomendación entonces creo que sería usar siempre su propiedad AsCurrency en las versiones de Delphi que presenten ese problema. Además de la 7, ¿cuál otra?

Para mi caso actual implicaría cambiar muchas referencias tipo "ConjuntoDeDatos ['Campo']" por "ConjuntoDeDatos.FieldByName ('Campo').AsCurrency", pero creo lo mejor va a ser derivar una clase interpuesta de TFmtBCDField y redefinir su método virtual GetAsVariant, de manera que éste devuelva un variante Currency en lugar de un variante BCD. Cuando suba el proyecto a Delphi 2010 o XE2 (que les tengo tantas ganas como escasez de dinero) desecharé la clase interpuesta.

Un abrazo sin-cero.

Al González. :)

Casimiro Notevi
30-03-2012, 21:56:45
Vaya, sí que es viejo delphi :)
No puedo ayudar mucho, ahora tengo esas mismas versiones.

MartinS
30-03-2012, 22:06:30
Hola Al: Testeado en Delphi XE

C3 := (C1 * V) / C2; // ¡Resulta 5!
ShowMessage (CurrToStr (C3));

Muestra 50

C3 := (C1 * Currency (V)) / C2; // Correcto, resulta 50
ShowMessage (CurrToStr (C3));

muestra 50

C1 := 51;
C3 := (C1 * V) / C2; // Resulta 51, como debe ser
ShowMessage (CurrToStr (C3));

Muestra 51

C1 := 800;
C3 := (C1 * V) / C2; // ¡Resulta 8!
ShowMessage (CurrToStr (C3));

Muestra 800

C1 := 802;
C3 := (C1 * V) / C2; // Resulta 802, como debe ser
ShowMessage (CurrToStr (C3));

Muestra 802

Saludos

Al González
31-03-2012, 02:16:42
Gracias Casi, gracias MartinS.

Por lo dicho anteriormente suena lógico que tras Delphi 2007 no ocurra el problema, pero si alguien tiene alguna versión entre la 7 y la 2007, exclusive, o bien anterior a Delphi 7, con la que compile ese código, sería importante conocer los resultados.

Saludos.

Al González.

P.D.
* La palabra exclusive es nueva para mí, quizá sea más correcto decir "excluyendo ambas".
* Si alguien se anima a hacer más pruebas, evite los cabezazos. ;)

ecfisa
31-03-2012, 02:26:09
Hola Al.

No respondí antes por que al igual que vos tengo Delphi 7 y había entendido que sobre él hiciste las pruebas; con Delphi 7 obtuve los mismos resultados erróneos.
Lamento no tener otra versión disponible para aportarte una información más amplia.

Saludos. :)

Al González
31-03-2012, 03:20:28
No te preocupes, ecfisa. Al menos se va perfilando que esto viene de fábrica.

Saber en qué versiones ocurre será útil para quien pueda enfrentarse al mismo problema en el futuro y encuentre este hilo.

Delphius
31-03-2012, 20:14:14
Hola. Con D6 se obtiene:
5
50
51
8
802

Creo que esto confirma de que hay un problema con las versiones D7 e inferiores con dicha función.
Por cierto, en lo posible hay que evitar mezclar las operaciones con diferentes tipos. Aún cuando se suponga que éstos son los "más precisos".

Saludos,

egostar
31-03-2012, 20:22:05
Hola Alberto

Resultados con Turbo Delphi (D2006)

http://www.mexistemas.com/imagenes/FmtBCD.jpg

Saludos

egostar
31-03-2012, 20:24:51
Alberto

¿Te interesa saber lo que resulta en Delphi4 ?

Si te interesa, lo instalo para hacer la prueba.

Saludos

Delphius
31-03-2012, 20:57:18
¿Y no será que el problema está en la traducción de un TBCD al Variant? De la ayuda leo que existen estas funciones sobrecargadas:

procedure VarFMTBcdCreate(var aDest: Variant; const ABcd: TBcd); overload;
function VarFMTBcdCreate: Variant; overload;
function VarFMTBcdCreate(const ABcd: TBcd): Variant; overload;
function VarFMTBcdCreate(const AValue: string, Precision, Scale: SmallInt): Variant; overload;
function VarFMTBcdCreate(const AValue: Double; Precision: SmallInt = 18; Scale: SmallInt = 4): Variant; overload;

Pero en tu código tu haces simplemente esto:

V := VarFMTBcdCreate (149.88)

Por lo que utilizas esta:
function VarFMTBcdCreate(const AValue: Double; Precision: SmallInt = 18; Scale: SmallInt = 4): Variant;

Si tu idea es tener una representación TBCD desde un Double, ¿porqué no directamente emplear DoubleToBCD?. Nos evitamos trabajar con el Variant en formato TBCD:

function DoubleToBcd( const aValue: Double): TBcd; overload;
procedure DoubleToBcd( const aValue: Double, var bcd: TBcd; overload;

En el TFMTBCDField cuando uno trabaja con el AsCurrency internamente hace las conversiones desde el Double. Por ejemplo, al leer la propiedad se invoca a GetAsCurrency:

function TFMTBCDField.GetAsCurrency: Currency;
begin
Result := GetAsFloat;
end;

Lo que se vé que utiliza al tipo Double para hacer el trabajo:

function TFMTBCDField.GetAsFloat: Double;
var
bcd: TBcd;
begin
if not GetValue(bcd) then
Result := 0
else
Result := BcdToDouble(bcd);
end;

Por tanto.

Hasta el momento yo no he utilizado Variant. Me manejo con las propiedades AsTipoConcreto, como en este caso el que tu dices, AsCurrency. De todas formas a tener en cuenta esto.

Saludos,

Delphius
31-03-2012, 22:06:21
Aunque también se ha de tener ciertos cuidados si resulta ser que el problema estuviera en todas las funciones sobrecargadas que listé antes.

Si uno escribe en la propiedad AsCurrency tiene lugar esto:

procedure TFMTBCDField.SetAsCurrency(Value: Currency);
var
VMax, VMin: Variant;
FValue: TBcd;
begin
CurrToBcd(Value, FValue, MaxBcdPrecision, MaxBcdScale);
if FCheckRange then
begin
VMax := VarFMTBcdCreate(FMaxValue, Self.Precision, Self.Size);
VMin := VarFMTBcdCreate(FMinValue, Self.Precision, Self.Size);
if (Value < VMin) or (Value > VMax) then
BcdRangeError(Value, VMin, VMax);
end;
SetData(@FValue, False);
end;

Como se ve, si para el el campo se ha definido un rango se va a intentar comparar el valor contra el máximo y mínimo variant TBCD. Si esta versión sobrecargada sufre del mismo problema se nos viene abajo todo.

Al, Ya me hiciste poner en true mi propiedad ModeParanoid.

Saludos,

Al González
31-03-2012, 22:20:26
Eliseo: No es tan urgente averiguar si pasa lo mismo en Delphi 4.

Marcelo: Miraré de nuevo esas funciones y comento luego.

Muchas gracias a ambos.

Delphius
31-03-2012, 22:34:14
Eliseo: No es tan urgente averiguar si pasa lo mismo en Delphi 4.

Marcelo: Miraré de nuevo esas funciones y comento luego.

Muchas gracias a ambos.
No tienes que agradecer amigo. Ya tenía Delphi abierto así que no me costaba darme un tiempo a hacer esa prueba.
Menos mal que abriste el hilo porque estoy utilizando justamente la clase TFMTBDCDField que internamente todo pasa por el AsCurrency y ese SetAsCurrency ya me dio cosa.

Que yo recuerde no tuve problemas, pero si el defecto está presente en todo el uso del Variant TBDC es posible que truene en cualquier lado y momento.
Mantenos informados.

Saludos,

Delphius
24-04-2012, 19:32:38
¿Al, tuviste tiempo como para ver a que va el error o algunas novedades?

Saludos,

Al González
24-04-2012, 23:18:07
Hola Marcelo.

No he tenido tiempo de ver a detalle el problema (hoy se me fue la mitad del día buscando una casa de empeños que aceptara relojes Relic).

En su momento, para salir del paso, redefiní el método GetAsVariant en una clase interpuesta:


Function TFmtBCDField.GetAsVariant :Variant;
Begin
{ NOTA: Esta redefinición resuelve defecto presentado en Delphi 7 y
otras versiones anteriores a la 2007, relacionado con la división y
multiplicación de variantes BCD con valores Currency
(http://www.clubdelphi.com/foros/showthread.php?t=78225). }

Result := Inherited GetAsVariant;

{ Evitamos que el valor devuelto sea variante de tipo BCD,
sustituyéndolo por un variante de tipo Currency }
If Not VarIsNull (Result) Then
Result := System.Currency (Result);
End;

Eso me basta para el proyecto en cuestión, dado que todos los campos FmtBCD que utilizo son para cantidades monetarias. En otros casos, quizá podría añadirse a ese mismo If una verificación de la propiedad Currency (de tipo Boolean y que solamente tiene impacto en el formato de despliegue).

De todas formas, tengo los fuentes de Delphi 7 y 2007, así que sólo es cosa de encontrar un hueco de tiempo (un hueco sin preocupaciones "primitivas") para echar una mirada en las funciones involucradas y encontrar la diferencia, es decir, esa corrección que evidentemente trae Delphi al menos desde la versión 2007.

¿Los demás han podido adelantarse a ver algo de esto?

Saludos.

Delphius
25-04-2012, 00:33:46
Hola Al,
Yo tampoco puedo sacar tiempo como para estudiarlo. Hasta el momento yo me valgo del .AsCurrency y no he tenido problema; pero de todas formas no estoy totalmente confiado y me hace dudar hasta donde es bueno y certero el tener una mezcla de Double, Currency y TBCD porque son variables a las que utilizamos más que seguido.

Saludos,