Техническа статия

Кеширани стойности на формули в Delphi без преизчисление

HotXLS, нативната Excel библиотека за Delphi и C++Builder, прочита стойността, която Excel вече е запазил до формула, чрез TryGetCachedFormulaValue и IXLSFormulaCacheReader. Нито една от двете входни точки не извиква калкулатора, не декомпилира формулни токени, не обновява dirty състояние и не пише нищо обратно в модела, така че работна книга, която само четете, остава точно в състоянието, в което сте я отворили

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

Защо четенето на формулна клетка струва пълно преизчисление?

Защото getter за стойност на формулна клетка е заявка да произведе стойност, а единственият универсално коректен начин да се произведе такава е да се оцени формулата. Това е правилното поведение по подразбиране за приложение, което редактира работни книги, и грешното за конвейер, който ги извлича. По-лошо, оценяването не е без странични ефекти: тя пише резултатите обратно в клетки, обръща dirty флагове и може да се разреши различно от произвеждащото приложение, когато функция не се поддържа или външна препратка е счупена. Задача, която сте описали на операционния си екип като само за четене, тихо произвежда работна книга, която вече не съответства на тази на диска, а ако нещо по-късно я запази, файлът на диска също се променя

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

Три ортогонални факта за една клетка

Първо изводът: една кеширана формулна стойност носи три независими факта, а сгъването им в един Variant губи информация, която ви трябва. TXLSFormulaCacheInfo ги държи разделени като State, Kind и Value. TXLSFormulaCacheState записва произхода в пет случая — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated и xlfcsInvalidated — докато TXLSFormulaCacheValueKind класифицира payload-а като xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean или xlfcvError. Именно това разделяне позволява присъствието да се докладва честно: кеширана празна клетка, кеширан празен низ, кеширано False, кеширана нула и кеширана грешка са всички реални стойности, така че присъствието никога не може да се изведе от VarIsEmpty или VarIsNull. TryGetCachedFormulaValue връща True само за xlfcsLoaded и xlfcsCalculated, а и при връщане на False пак попълва състояние, което може да се диагностицира

Записът TXLSFormulaCacheInfo на HotXLS държи разделени три ортогонални факта за една формулна клетка: произхода State в пет случая, вида на payload-а Kind в шест и Variant стойността Value, така че кеширана празна клетка или False никога не се бърка с липсващ кеш
Произходът, видът на payload-а и стойността му остават разделени, което е единственият начин кеширана празна клетка, нула, празен низ или грешка да бъдат докладвани като реалната стойност, която са
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 означава, че клетката наистина е формула, но производителят не е съхранил payload със стойност за нея — чест резултат, когато генератор пише формули и оставя Excel да попълни резултатите при първо отваряне. xlfcsInvalidated означава, че формул текстът е бил заменен след зареждането, така че стойността, която някога е била там, описва израз, който вече не съществува. xlfcsCalculated, напротив, е успешен случай: той маркира стойност, която вашият собствен код или оценителят на HotXLS е произвел по време на тази сесия, за разлика от xlfcsLoaded, която идва от файла

Честността за липсващ кеш тежи повече от зашиването му. HotXLS отказва да измисли стойност, а при запис е еднакво строг — само xlfcsLoaded и xlfcsCalculated излъчват кеширана стойност, докато xlfcsMissing и xlfcsInvalidated записват само формулата, вместо да замразяват остаряла цифра във файла. Това ви оставя три разумни отговора в конвейер: прескочете реда и запишете пролуката, преизчислете съзнателно точно тази работна книга и приемете цената, или оценете и съгласувайте. Ако оцененото число не се съгласува с това, което произвеждащото приложение би записало, трасьорът на оценяване на формули е инструментът, с който да разберете къде двете изчисления се разминават, вместо да гадаете от резултата

Един четец за класическите, OOXML и ODF двигатели

Конвейерът не бива да се интересува дали току-що отвореният файл е BIFF, OOXML или ODF. IXLSFormulaCacheReader е единствената входна точка само за четене и за трите: както TXLSWorkbook.CreateFormulaCacheReader, така и TXLSXWorkbook.CreateFormulaCacheReader връщат лек адаптер върху оскъдното търсене на клетки, което всеки двигател вече използва, с идентични базирани на едно координати за лист, ред и колона. Класовете на работната книга умишлено не имплементират интерфейса сами — интерфейсна препратка към работната книга би променила семантиката на притежание и би позволила на извикващите да се промъкнат покрай lease-а за време на живот. Вместо това унищожаването на работната книга изчиства суровия указател вътре в този lease, а всеки четец, все още държан от кода ви, вдига 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);
  // Никакъв калкулатор не се изпълни, никакъв dirty флаг не мръдна, Book е непроменена
end;

Къде всъщност живеят кешираните байтове

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

Осембайтовото поле FormulaValue на класически XLS Formula запис, както HotXLS го чете: IEEE 754 double, освен ако горната дума не е FFFF, в който случай типът на варианта седи в val нула, а булевият или грешковият payload в val две
Когато горната дума е FFFF, полето е маркиран вариант, а payload-ът седи в val[2] с val[1] неопределен — точно байтът, който четецът някога вземаше

Типовата точност в пакетните формати е отделен проблем със собствен капан. В OOXML кешираната стойност виси от елемента c като <v>, като атрибутът t назовава типа съгласно ECMA-376 Part 1 §18.3.1.4. HotXLS чете t="e" директно в varError Variant и го картографира обратно към стандартния текст за грешка при запис, така че грешките никога не се маскират като обикновени цели числа — но Delphi RTL няма да ви помогне тук, защото 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 справка за входните точки на работната книга и четеца, показани тук