Технічна стаття

Кешовані значення формул Excel у Delphi без перерахунку

HotXLS, нативна Excel-бібліотека для Delphi та C++Builder, читає значення, яке Excel уже зберіг поруч із формулою, через TryGetCachedFormulaValue та IXLSFormulaCacheReader. Жодна з точок входу не викликає калькулятор, не декомпілює токени формул, не оновлює брудний стан і не записує нічого назад у модель, тож книга, яку ви лише читаєте, залишається точно такою, як ви її відкрили

Сценарій, що рухає цим, нудний і надзвичайно поширений. Нічне завдання відкриває кілька сотень книжок, зроблених кимось іншим, витягує з кожної одну колонку підсумків і штовхає числа у сховище даних. Підсумки вже лежать у файлах — Excel їх обчислив і зберіг. Але щойно завдання питає клітинку з формулою про її значення, бібліотека, що має лише одну відповідь на це питання, будує граф залежностей і обчислює весь аркуш, і завдання, яке мало бути обмеженим вводом-виводом, перетворюється на бенчмарк обчислень

Чому читання клітинки з формулою коштує повний перерахунок?

Бо гетер значення на клітинці з формулою — це запит породити значення, а єдиний всезагально правильний спосіб породити його — обчислити формулу. Це правильна усталена поведінка для застосунку, що редагує книжки, і неправильна для конвеєра, що їх витягує. Гірше того, обчислення не вільне від побічних ефектів: воно записує результати назад у клітинки, перевертає брудні прапорці і може розвʼязатися інакше, ніж у застосунку-творці, коли функція не підтримується або зовнішнє посилання зламане. Завдання, яке ви описали своїй операційній команді як read-only, мовчки породжує книгу, що вже не збігається з файлом на диску, а якщо щось потім її збереже, файл на диску теж зміниться

Читання кешованих значень — друга половина контракту. Воно відповідає на вужче питання — що застосунок-творець тут зберіг? — і відмовляється відповідати на що-небудь інше. Коли ви справді хочете свіжі числа, HotXLS досі дає вам інкрементний перерахунок, керований графом залежностей; суть у тому, що витягування та обчислення мають бути двома різними викликами, а не одним викликом із двома настроями

Три ортогональні факти про одну клітинку

Спершу висновок: кешоване значення формули несе три незалежні факти, і злиття їх в один Variant втрачає потрібну вам інформацію. TXLSFormulaCacheInfo тримає їх окремо як State, Kind і Value. TXLSFormulaCacheState фіксує походження на пʼять випадків — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated і xlfcsInvalidated — тоді як TXLSFormulaCacheValueKind класифікує корисне навантаження як xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean або xlfcvError. Це розділення і дає можливість чесно звітувати про наявність: кешований бланк, кешований порожній рядок, кешований False, кешований нуль і кешована помилка — усе реальні значення, тож наявність ніколи не можна виводити з VarIsEmpty чи VarIsNull. TryGetCachedFormulaValue повертає True лише для xlfcsLoaded і xlfcsCalculated і все одно заповнює діагностований стан, коли повертає False

Запис HotXLS TXLSFormulaCacheInfo тримає окремо три ортогональні факти про одну клітинку з формулою: походження State на пʼять випадків, тип навантаження Kind на шість і Variant Value, тож кешований бланк чи False ніколи не сплутають з відсутнім кешем
Походження, тип навантаження і значення навантаження лишаються окремими — це єдиний спосіб звітувати кешований бланк, нуль, порожній рядок чи помилку як справжнє значення, яким воно є
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row і Col тут усі з одиниці
    if Book.TryGetCachedFormulaValue(1, 12, 5, Info) then
      Writeln('cached value: ', VarToStr(Info.Value))
    else
      Writeln('no usable cache, state ordinal ', Ord(Info.State));
  finally
    Book.Free;
  end;
end;

Чому кешованого значення бракує?

Рівно чотири причини змушують TryGetCachedFormulaValue віддати False, і стан підказує, котра з них застосовується. xlfcsNotFormula означає, що клітинка тримає літерал або взагалі нічого, а координати поза діапазоном згортаються в ту саму відповідь. xlfcsMissing означає, що клітинка справді формула, але творець не зберіг для неї корисного навантаження значення — типовий наслідок, коли генератор пише формули й лишає Excel заповнити результати при першому відкритті. xlfcsInvalidated означає, що текст формули замінили після завантаження, тож значення, що було там, описує вираз, якого більше не існує. xlfcsCalculated, навпаки, — випадок успіху: він позначає значення, яке ваш власний код чи обчислювач HotXLS породив у цій сесії, на противагу xlfcsLoaded, що прийшов із файлу

Чесність щодо відсутнього кешу важить більше, ніж замазування. HotXLS відмовляється вигадувати значення, і при збереженні так само строгий — лише xlfcsLoaded і xlfcsCalculated видають кешоване значення, тоді як xlfcsMissing і xlfcsInvalidated записують саму формулу, замість заморожувати застаріле число у файл. Це лишає вам три здорові відповіді в конвеєрі: пропустити рядок і зафіксувати прогалину, навмисно перерахувати саме ту книгу і прийняти вартість, або обчислити і звірити. Якщо обчислене число розходиться з тим, що написав би застосунок-творець, трасувальник обчислення формул — інструмент, щоб знайти, де дві обчислювальні гілки розходяться, а не вгадувати з результату

Один читач для класичного, OOXML та ODF рушіїв

Конвеєр не мусить цікавитися, чи був щойно відкритий файл BIFF, OOXML чи ODF. IXLSFormulaCacheReader — єдина точка входу лише для читання для всіх трьох: і TXLSWorkbook.CreateFormulaCacheReader, і TXLSXWorkbook.CreateFormulaCacheReader повертають легку адаптацію над розрідженим пошуком клітинок, який кожен рушій уже використовує, з ідентичними координатами аркуша, рядка й колонки з одиниці. Класи книжок свідомо не реалізують інтерфейс самі — посилання на інтерфейс до книги змінило б її семантику володіння і дозволило б викликачам прослизнути повз оренду часу життя. Натомість знищення книги очищує сирий вказівник усередині тієї оренди, і будь-який читач, усе ще утримуваний вашим кодом, кидає EXLSFormulaCacheReaderInvalidated на наступному запиті замість розіменування звільненої памʼяті. Це fail-fast перевірка часу життя, а не гарантія конкурентності

var
  Reader: IXLSFormulaCacheReader;
  Info: TXLSFormulaCacheInfo;
  Row, Missing, Errors: Integer;
  Total: Double;
begin
  Reader := Book.CreateFormulaCacheReader;
  Total := 0;
  Missing := 0;
  Errors := 0;
  for Row := 2 to LastRow do
    if Reader.TryGetCachedFormulaValue(1, Row, 7, Info) then
    begin
      case Info.Kind of
        xlfcvNumber: Total := Total + Double(Info.Value);
        xlfcvError:  Inc(Errors);
      end;
    end
    else if Info.State = xlfcsMissing then
      Inc(Missing);
  // Жоден калькулятор не запускався, жоден брудний прапорець не зсунувся, Book не змінена
end;

Де кешовані байти насправді живуть

Для класичних .xls файлів кеш — це поле FormulaValue запису Formula, вісім байтів, описаних у [MS-XLS] §2.5.133. Коли старше слово дорівнює $FFFF, навантаження — не IEEE 754 double, а мічений варіант, і макет легко тонко зіпсувати: тип варіанта сидить у val[0], а навантаження boolean чи BErr — у val[2], з невизначеним val[1]. HotXLS раніше читав навантаження з val[1] — той рід зсуву на одиницю, що спливає лише на конкретних файлах, які кешують boolean чи помилку, а не число. Читач і записувач спільних формул тепер узгоджені на тих самих зсувах, тож кешований TRUE виживає в циклі завантаження й збереження цілим, замість гнити в шум

Восьмибайтове поле FormulaValue класичного запису Formula в XLS, як його читає HotXLS: IEEE 754 double, якщо старше слово не дорівнює FFFF, а в цьому випадку тип варіанта сидить у val нуль, а навантаження Boolean чи помилки — у val два
Коли старше слово — FFFF, поле є міченим варіантом, навантаження сидить у val[2] з невизначеним val[1], і це рівно той байт, який читач раніше брав

Вірність типів у пакетних форматах — окрема проблема з власною пасткою. В OOXML кешоване значення висить на елементі <c> як <v>, а атрибут t називає тип за ECMA-376 Part 1 §18.3.1.4. HotXLS читає t="e" просто в Variant varError і відображає його назад у стандартний текст помилки при збереженні, тож помилки ніколи не маскуються під звичайні цілі числа — але RTL Delphi тут не допоможе, бо VarAsType(Integer, varError) кидає виняток конверсії. Робоча конструкція встановлює TVarData.VType і TVarData.VError напряму. Дати дотримуються тієї ж дисципліни у протилежний бік: t="d" і тип значення дати ODF — явні декларації типу і стають varDate, тоді як числовий кеш BIFF узагалі не несе прапорця дати і тому лишається Double. HotXLS ніколи не вгадує дату з числового формату клітинки, бо числовий формат — це презентація, а кеш — дані. ODF додає ще один випадок, вартий знання — office:value-type="void" виражає кеш, що присутній, але не несе значення, а оскільки ODF не має типу значення помилки, схожий на помилку текст зберігається як текст, а не підвищується до помилки

function DescribeCache(const Info: TXLSFormulaCacheInfo): string;
begin
  case Info.State of
    xlfcsNotFormula:  Result := 'not a formula cell';
    xlfcsMissing:     Result := 'formula stored with no cached value';
    xlfcsInvalidated: Result := 'formula replaced since load';
  else
    case Info.Kind of
      xlfcvError:    Result := 'error code ' + IntToStr(TVarData(Info.Value).VError);
      xlfcvDateTime: Result := DateTimeToStr(VarToDateTime(Info.Value));
      xlfcvBoolean:  Result := BoolToStr(Info.Value, True);
      xlfcvNumber:   Result := FloatToStr(Double(Info.Value));
      xlfcvString:   Result := VarToStr(Info.Value);
    else
      Result := 'present but blank';
    end;
  end;
end;

Чи діляться спільні формули своїми кешованими значеннями?

Ні, і припущення протилежного — це спосіб, яким масовий обхід закінчує звітом того самого числа для цілої колонки. Спільна формула OOXML ділиться лише виразом формули та оптимізацією зберігання; кожна клітинка-член досі володіє власним <v>. Тому HotXLS ніколи не розповсюджує кеш кореневого члена на послідовника, що прийшов без значення, і послідовник, завантажений як xlfcsMissing, досі звітує xlfcsMissing після збереження та повторного відкриття. Якщо ви розбираєтеся, як група взагалі зберігається й розгортається, механіка атрибута si спільної формули та її розгортання покрита окремо; для читання кешу правило зводиться до одного рядка — питайте кожну клітинку, не довіряйте нічому, про що не питали

Погляд HotXLS на групу спільної формули OOXML, де атрибут si ділиться лише виразом і макетом зберігання, тоді як кожна клітинка-член володіє власним кешованим значенням, тож послідовник, завантажений без нього, досі звітує xlfcsMissing
Група ділиться виразом, а не числами, тож кореневий кеш ніколи не розповсюджується, і член, що прийшов без значення, досі звітує ту прогалину

Читання кешованих значень, уніфікований міжрушійний читач і рушій перерахунку, який ви можете обрати не викликати, усе постачається зі стандартним HotXLS Delphi Spreadsheet Component для Delphi та C++Builder, без залежності від Excel чи будь-якого OLE-сервера автоматизації; продуктова сторінка несе повний довідник API точок входу книги та читача, показаних тут