Столкнулся с неправильной суммой в футтере DevExpress показывал 1,00 вместо 11,00. Ошибка появлялась лишь на части данных и напоминала проблему отрисовки: будто у числа исчезла первая цифра. И это не первый раз, периодически уже натыкался, несколько лет назад, но обновления под древнюю Delphi-7 не нашел, в результате даже писал обход бага ручным суммированием и выводом в виде текста. Про это я уже писал ранее в Шпаргалка по DevExpress cxGrid и Delphi 7 Теперь же решил вдруг ИИ найдет корень проблемы. Все кто использует DevExpress знает насколько монументальное это сооружение — исходников многие тысячи файлов и копаться в них можно очень долго. Однако на мое удивление ИИ порывшись в них пришел к выводу что ошибка глубже — в базовых компонентах Delphi 7, а именно в установленной RTL Delphi 7, где исправление довольно исправили две строки и проверили результат как отдельно от интерфейса, так и на настоящих формах.
Минимальный пример без базы и компонентов
Для воспроизведения достаточно консольного проекта Delphi 7 и стандартного модуля FMTBcd. Дробные нули в строках оставлены намеренно: они задают масштаб, на котором обнаружился дефект.
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.
С дефектной библиотекой второй результат — 1; после исправления — 11.
Где терялся разряд
Цепочка вычисления оказалась такой:
штатное суммирование DevExpress → сложение BCD Variant → TFMTBcdData.DoAdd → BcdAdd → NormalizeBcdPair
BCD хранит десятичные цифры. Precision описывает общую точность представления, а масштаб — количество дробных разрядов. Для 9.00000 при точности 6 и масштабе 5 остаётся один разряд целой части. Для 11.00000 нужен уже второй. Перед сложением BcdAdd вызывал:
NormalizeBcdPair(bcdIn1, bcdIn2, bcd1, bcd2, 1);
Последний аргумент, ExtraDigits, запрашивал запас для переноса. Однако в исследованной версии NormalizeBcdPair этот аргумент не учитывался при выборе необходимой точности. Операнды помещались в выбранное представление, а результат с переносом — уже нет. В нашем примере старший разряд терялся. Это также объясняет, почему ошибка казалась случайной. Она зависела от масштаба, доступной точности и появления переноса при конкретной последовательности сложений.
Патч: две строки
В NormalizeBcdPair заменили условие увеличения точности. Весь остальной алгоритм остался прежним:
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);
Теперь проверка учитывает и фактически необходимые цифры операндов, и дополнительный разряд, запрошенный вызывающей функцией. NumberOfDigits здесь важен: если на каждом сложении прибавлять запас к уже выделенной точности, она может расти даже без роста самого числа. Итог из тысяч строк требует проверки и этого сценария. Мы сохранили BCD-арифметику. Переводить значения в Double, менять SQL или подменять штатное суммирование пользовательским обработчиком не понадобилось. DevExpress также остался без изменений. Точный патч FMTBcd.patch и скрипт его применения Apply-FmtBcdFix.ps1 сохранены отдельно. Пересборка должна обновить два файла:
Delphi7/Lib/FMTBcd.dcu Delphi7/Lib/Debug/FMTBcd.dcu
Так решлась проблемма которая досаждала много лет. Из интереса я поискал есть ли у кого то этот патч, но именно такого исправления нигде не нашел, зато посмотрел что ещё выпускают энтузиасты для старой Delphi.
| Проект | Назначение и способ подключения |
|---|---|
| VCL Fix Pack 1.4, Andreas Hausladen | Исправления VCL и RTL при запуске приложения. Поддерживает Delphi 7; среди исправлений — фокус MDI-окон, контекстные меню и отрисовка некоторых контролов. Подключается одним модулем в проект. |
| Synopse Enhanced RTL, Arnaud Bouchez | Исправления и оптимизации исходников для Delphi 7 и 2007, включая базовые модули RTL и некоторые VCL-модули. Более широкое вмешательство в библиотеку, чем отдельный подключаемый модуль. |
| FastCode | Оптимизированные замены отдельных функций RTL: обработки строк, поиска, сравнения и работы с памятью. Основная задача — производительность. |
| DelphiSpeedUp 3.1 | Ускорение и исправления среды разработки, в том числе для Delphi 7. Это отдельный уровень: улучшения IDE не следует считать исправлениями арифметики собранного приложения. |
т.е. как то апгрейдить старые библиотеки можно но «наши ряды рядеют» — эти патчи тоже уже довольно давно не обновлялись. Из них я себе поставил DelphiSpeedUp 3.1, и FastMM4.
FastMM4 как улучшение стандартной среды выполнения
FastMM4 — сторонний менеджер памяти Pierre le Riche. Формально это замена менеджера памяти, а не пакет исправлений VCL. Но для старого проекта он выполняет похожую практическую роль: улучшает базовый механизм среды выполнения, не требуя переписывать прикладной код. Он обслуживает выделение, освобождение и перераспределение памяти, рассчитан на многопоточную работу и уменьшение фрагментации. Доступны средства диагностики утечек и ошибок памяти, поведение которых зависит от конфигурации. Автор указывает поддержку Delphi 4 и новее, включая Delphi 7. Описание проекта. Последнее обновление от 10 августа 2021 года. Про этот ускоритель я уже как то упоминал в Шпаргалка по DevExpress cxGrid и Delphi 7
