Delphi 7 y la pérdida de la cifra más significativa en el pie de DevExpress: ¡el culpable era la RTL!

Me encontré con un total incorrecto en el pie de DevExpress: mostraba 1,00 en lugar de 11,00. El error solo aparecía con ciertos datos y parecía un problema de visualización, como si hubiera desaparecido la primera cifra. No era la primera vez: ya me había pasado de vez en cuando hace varios años, pero no encontré ninguna actualización para la antigua Delphi 7. Al final incluso escribí una solución alternativa que calculaba la suma manualmente y la mostraba como texto. Ya lo había contado en mi chuleta de DevExpress cxGrid y Delphi 7.

Esta vez decidí probar si la IA podía encontrar la raíz del problema. Quien use DevExpress sabe lo enorme que es: muchos miles de archivos de código fuente, en los que uno puede pasarse muchísimo tiempo buscando. Sin embargo, para mi sorpresa, después de revisarlos la IA concluyó que el error estaba más abajo, en los componentes básicos de Delphi 7, concretamente en la RTL instalada. La solución consistió en corregir dos líneas, y comprobamos el resultado tanto fuera de la interfaz como en formularios reales de la aplicación.

Un ejemplo mínimo sin base de datos ni componentes

Para reproducir el problema basta con un proyecto de consola de Delphi 7 y la unidad estándar FMTBcd. Los ceros de la parte decimal de las cadenas son intencionados: establecen la escala con la que apareció el fallo.

program BcdFooterRepro;

{$APPTYPE CONSOLE}

uses
  SysUtils, Variants, FMTBcd;

function DecimalValue(const S: string): Variant;
begin
  Result := VarFMTBcdCreate(
    StrToBcd(StringReplace(S, '.', DecimalSeparator, [])));
end;

var
  Total: Variant;
begin
  Total := DecimalValue('4.00000');
  Total := Total + DecimalValue('5.00000');
  Writeln('4 + 5 = ', VarToStr(Total));

  Total := Total + DecimalValue('2.00000');
  Writeln('4 + 5 + 2 = ', VarToStr(Total));
end.

Con la biblioteca defectuosa, el segundo resultado es 1; después de la corrección, es 11.

Dónde se perdía la cifra más significativa

La cadena de cálculo resultó ser la siguiente:

suma integrada de DevExpress
  → suma de Variant BCD
  → TFMTBcdData.DoAdd
  → BcdAdd
  → NormalizeBcdPair

BCD almacena dígitos decimales. Precision describe la precisión total de la representación, mientras que la escala es el número de posiciones decimales. Para 9.00000, con una precisión de 6 y una escala de 5, queda espacio para una cifra en la parte entera. 11.00000 ya necesita una segunda. Antes de sumar, BcdAdd llamaba a:

NormalizeBcdPair(bcdIn1, bcdIn2, bcd1, bcd2, 1);

El último argumento, ExtraDigits, solicitaba espacio adicional para un acarreo. Sin embargo, en la versión de NormalizeBcdPair que investigamos, ese argumento no se tenía en cuenta al elegir la precisión necesaria. Los operandos cabían en la representación elegida, pero el resultado con acarreo ya no. En nuestro ejemplo se perdía la cifra más significativa.

Esto también explica por qué el error parecía aleatorio. Dependía de la escala, de la precisión disponible y de la aparición de un acarreo en una secuencia concreta de sumas.

El parche: dos líneas

En NormalizeBcdPair sustituimos la condición que aumenta la precisión. El resto del algoritmo quedó igual:

   while (MaxDigits < (SizeOf(Bcd1.Fraction) * 2)) and
-        ((MaxDigits - MaxScale < Bcd1.Precision - BcdScale(Bcd1)) or
-         (MaxDigits - MaxScale < Bcd2.Precision - BcdScale(Bcd2))) do
+        ((MaxDigits - MaxScale < NumberOfDigits(Bcd1) - BcdScale(Bcd1) + ExtraDigits) or
+         (MaxDigits - MaxScale < NumberOfDigits(Bcd2) - BcdScale(Bcd2) + ExtraDigits)) do
     Inc(MaxDigits, 2);

Ahora la comprobación tiene en cuenta tanto las cifras que realmente necesitan los operandos como la cifra adicional solicitada por la función que realiza la llamada. Aquí NumberOfDigits es importante: si en cada suma se añade margen a la precisión ya reservada, esta puede seguir creciendo aunque el número en sí no crezca. Al calcular un total de miles de filas, también hay que comprobar este caso.

Conservamos la aritmética BCD. No hizo falta convertir los valores a Double, cambiar el SQL ni sustituir la suma integrada por un manejador personalizado. DevExpress tampoco se modificó. El parche exacto, FMTBcd.patch, y el script que lo aplica, Apply-FmtBcdFix.ps1, están disponibles en un archivo aparte. La recompilación debe actualizar dos archivos:

Delphi7/Lib/FMTBcd.dcu
Delphi7/Lib/Debug/FMTBcd.dcu

Así quedó resuelto un problema que me había molestado durante años. Por curiosidad busqué si alguien más tenía este parche. No encontré esa corrección concreta, pero sí aproveché para ver qué siguen publicando los entusiastas para las versiones antiguas de Delphi.

ProyectoFinalidad y forma de integración
VCL Fix Pack 1.4, Andreas HausladenCorrecciones de VCL y RTL que se aplican al iniciar la aplicación. Compatible con Delphi 7; incluye correcciones del foco de las ventanas MDI, los menús contextuales y la visualización de algunos controles. Se incorpora añadiendo una sola unidad al proyecto.
Synopse Enhanced RTL, Arnaud BouchezCorrecciones y optimizaciones del código fuente para Delphi 7 y 2007, incluidas unidades básicas de RTL y algunas unidades de VCL. Modifica la biblioteca de forma más amplia que una sola unidad añadida al proyecto.
FastCodeSustituciones optimizadas de funciones concretas de RTL: procesamiento de cadenas, búsqueda, comparación y operaciones con memoria. Su objetivo principal es el rendimiento.
DelphiSpeedUp 3.1Mejoras de velocidad y correcciones del entorno de desarrollo, también para Delphi 7. Actúa en otro nivel: las mejoras del IDE no deben considerarse correcciones de la aritmética de la aplicación compilada.

Es decir, todavía hay maneras de mejorar las bibliotecas antiguas, pero cada vez quedamos menos: estos parches tampoco se actualizan desde hace bastante tiempo. De estas herramientas, yo instalé DelphiSpeedUp 3.1 y FastMM4.

FastMM4 como mejora del entorno de ejecución estándar

FastMM4 es un gestor de memoria de terceros creado por Pierre le Riche. En sentido estricto, sustituye el gestor de memoria; no es un paquete de correcciones de VCL. Pero en un proyecto antiguo cumple una función práctica parecida: mejora un mecanismo básico del entorno de ejecución sin obligarnos a reescribir el código de la aplicación.

Se encarga de reservar, liberar y reasignar memoria, y está diseñado para trabajar con varios hilos y reducir la fragmentación. Ofrece herramientas para detectar fugas y errores de memoria, cuyo comportamiento depende de la configuración. El autor indica compatibilidad con Delphi 4 y versiones posteriores, incluida Delphi 7. Véase la descripción del proyecto. La última actualización fue el 10 de agosto de 2021. Ya había mencionado esta mejora de rendimiento en mi chuleta de DevExpress cxGrid y Delphi 7.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *