Tehnični članak

Formule v Delphiju: predpomnjene vrednosti brez izračuna

HotXLS, izvorna knjižnica Excel za Delphi in C++Builder, prebere vrednost, ki jo je Excel že shranil ob formuli, prek TryGetCachedFormulaValue in IXLSFormulaCacheReader. Noben od vstopnih točk ne pokliče kalkulatorja, ne dekompilira žetonov formul, ne posodobi stanja umazanosti in ne zapiše ničesar nazaj v model, zato delovni zvezek, ki ga le berete, ostane točno tak, kot ste ga odprli

Scenarij, ki to poganja, je dolgočasen in izjemno pogost. Nočno opravilo odpre nekaj sto delovnih zvezkov, ki jih je izdelal nekdo drug, iz vsakega izvleče en stolpec vsot in številke potisne v podatkovno skladišče. Vsote že ležijo v datotekah — Excel jih je izračunal in shranil. A v trenutku, ko opravilo vpraša celico s formulo po vrednosti, knjižnica, ki ima za to vprašanje samo en odgovor, zgradi graf odvisnosti in vrednoti cel list, opravilo, ki bi moralo biti vezano na I/O, pa se spremeni v merilnik izračunov

Zakaj branje celice s formulo stane poln ponovni izračun?

Ker je pridobivalnik vrednosti na celici s formulo prošnja, da se vrednost izdela, edini splošno pravilen način izdelave pa je vrednotenje formule. To je prava privzeta izbira za aplikacijo, ki ureja delovne zvezke, in napačna privzeta izbira za cevovod, ki jih izvleče. Še huje, vrednotenje ni brez stranskih učinkov: rezultate zapiše nazaj v celice, prevrne zastavice umazanosti in se lahko razreši drugače kot pri proizvajajoči aplikaciji, kadar funkcija ni podprta ali je zunanja referenca pokvarjena. Opravilo, ki ste ga svoji ekipi za obratovanje opisali kot samo za branje, tiho izdela delovni zvezek, ki se ne ujema več z tistim na disku, če ga kasneje karkoli shrani, pa se spremeni tudi datoteka na disku

Branje predpomnjenih vrednosti je druga polovica dogovora. Odgovarja na ožje vprašanje — kaj je proizvajajoča aplikacija shranila tukaj? — in zavrača odgovoriti na karkoli drugega. Ko resnično želite sveže številke, vam HotXLS vseeno ponudi inkrementalni ponovni izračun, voden z grafom odvisnosti; bistvo je, da sta izvleka in vrednotenje dva različna klica in ne en klic z dvema razpoloženjema

Trije ortogonalni podatki o eni celici

Najprej sklep: predpomnjena vrednost formule nosi tri neodvisne podatke, njihovo zloženje v en sam Variant pa izgubi informacijo, ki jo potrebujete. TXLSFormulaCacheInfo jih ločuje kot State, Kind in Value. TXLSFormulaCacheState beleži izvor čez pet primerov — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated in xlfcsInvalidatedTXLSFormulaCacheValueKind pa razvršča tovor kot xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean ali xlfcvError. Prav ta ločitev omogoča pošteno poročanje o prisotnosti: predpomnjena prazna celica, predpomnjen prazen niz, predpomnjen False, predpomnjena ničla in predpomnjena napaka so vse prave vrednosti, prisotnosti torej nikoli ni mogoče sklepati iz VarIsEmpty ali VarIsNull. TryGetCachedFormulaValue vrne True le za xlfcsLoaded in xlfcsCalculated, ob False pa vseeno izpolni stanje, iz katerega je razvidna diagnoza

Zapis HotXLS TXLSFormulaCacheInfo ločuje tri ortogonalne podatke o eni celici s formulo: izvor State čez pet primerov, tovor Kind čez šest in Variant Value, tako da predpomnjena prazna vrednost ali False nikoli ne zamenjata manjkajočega predpomnilnika
Izvor, vrsta tovoru in vrednost tovoru ostanejo ločeni, in to je edini način, da se predpomnjena prazna vrednost, ničla, prazen niz ali napaka poročajo kot prava vrednost, ki so
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row in Col so tu vsi eniško osnovani
    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;

Zakaj predpomnjena vrednost manjka?

Ravno štirje so razlogi, da TryGetCachedFormulaValue vrne False, stanje pa vam pove, kateri velja. xlfcsNotFormula pomeni, da celica nosi literál ali sploh nič, koordinate izven območja pa se zložijo v isti odgovor. xlfcsMissing pomeni, da je celica res formula, proizvajalec pa zanjo ni shranil tovora vrednosti — pogost izid, ko generator zapiše formule in pusti Excelu, da rezultate dopolni ob prvem odpiranju. xlfcsInvalidated pomeni, da je besedilo formule po nalaganju zamenjano, vrednost, ki je bila nekdaj tam, pa opisuje izraz, ki ne obstaja več. xlfcsCalculated pa je nasprotno primer uspeha: označuje vrednost, ki jo je v tem zasedanju izdelala vaša koda ali vrednotitelj HotXLS, v nasprotju s xlfcsLoaded, ki je prišla iz datoteke

Iskrenost glede manjkajočega predpomnilnika je pomembnejša od prekrivanja luknj. HotXLS noče izmisliti vrednosti, pri shranjevanju pa je enako strog — predpomnjeno vrednost izdajata le xlfcsLoaded in xlfcsCalculated, xlfcsMissing in xlfcsInvalidated pa zapišeta samo formulo, namesto da bi zamrznila zastarel podatek v datoteko. V cevovodu vam to pusti tri smiselne odgovore: preskočite vrstico in zabeležite vrzel, namenoma ponovno izračunajte tisti en zvezek in sprejmite ceno, ali pa vrednotite in uskladite. Če izračunana številka ne ustreza temu, kar bi zapisala proizvajajoča aplikacija, je sledilnik vrednotenja formul orodje za ugotavljanje, kje se izračuna razideta, in ne ugibanje iz rezultata

En bralnik čez klasični, OOXML in ODF pogon

Cevovodu ne bi smelo biti mar, ali je bila pravkar odprta datoteka BIFF, OOXML ali ODF. IXLSFormulaCacheReader je ena sama vstopna točka, namenjena branju, za vse tri: TXLSWorkbook.CreateFormulaCacheReader in TXLSXWorkbook.CreateFormulaCacheReader vračata lahkoten vmesnik nad razpršenim iskanjem celic, ki ga vsak pogon že uporablja, z enakimi koordinatami lista, vrstice in stolpca, osnovanimi na ena. Razredi delovnih zvezkov vmesnika namenoma ne izvajajo sami — referenca vmesnika na delovni zvezek bi spremenila njegovo semantiko lastništva in klicateljem dovolila zdrsiti mimo najema življenjske dobe. Namesto tega uničenje delovnega zvezka počisti surovi kazalec znotraj tega najema, vsak bralnik, ki ga vaša koda še drži, pa ob naslednji poizvedbi sproži EXLSFormulaCacheReaderInvalidated, namesto da bi dereferenciral sproščen pomnilnik. To je hitro odpovedoče preverjanje življenjske dobe, ne zagotovilo sočasnosti

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);
  // Kalkulator ni tekel, zastavica umazanosti se ni premaknila, Book je nespremenjen
end;

Kjer predpomnjeni bajti resnično živijo

Za klasične datoteke .xls je predpomnilnik polje FormulaValue zapisa Formula, osem bajtov, opisanih v [MS-XLS] §2.5.133. Ko je zgornja beseda enaka $FFFF, tovor ni IEEE 754 double, temveč označena varianta, postavitev pa je lahko subtilno narobe: vrsta variante leži v val[0], tovor boolean ali BErr pa v val[2], val[1] pa je nedefiniran. HotXLS je tovor prej bral iz val[1], kar je tista vrsta off-by-one, ki pride na dan le na posebnih datotekah, ki predpomnijo boolean ali napako namesto številke. Bralnik in zapisovalnik skupnih formul se zdaj strinjata o istih odmikih, zato predpomnjen TRUE preživi nalaganje in shranjevanje nedotaknjen, namesto da bi razpadel v šum

Osem bajtov dolgo polje FormulaValue zapisa Formula klasičnega XLS, kot ga bere HotXLS: IEEE 754 double, razen če je zgornja beseda enaka FFFF; takrat vrsta variante leži v val nič, tovor Boolean ali napake pa v val dva
Ko je zgornja beseda FFFF, je polje označena varianta, tovor leži v val[2], val[1] pa je nedefiniran — točno ta bajt je bralnik nekdaj vzel

Zvestoba vrst v paketnih formatih je ločen problem s svojo pastjo. V OOXML visi predpomnjena vrednost na elementu c kot <v>, atribut t pa poimenuje vrsto po ECMA-376 Part 1 §18.3.1.4. HotXLS prebere t="e" naravnost v Variant varError in ga pri shranjevanju preslika nazaj v standardno besedilo napake, zato se napake nikoli ne izdajajo za navadna cela števila — RTL Delphi pa vam tu ne pomaga, ker VarAsType(Integer, varError) sproži izjemo pretvorbe. Delujoča konstrukcija nastavi TVarData.VType in TVarData.VError neposredno. Datumi sledijo istemu redu v obratni smeri: t="d" in vrsta vrednosti datuma ODF sta izrecni izjavi vrste in postaneta varDate, številski predpomnilnik BIFF pa ne nosi nobene zastavice datuma in zato ostane Double. HotXLS nikoli ne uganjuje datuma iz oblike števila celice, ker je oblika števila predstavitev, predpomnilnik pa podatki. ODF doda še en primer, vreden poznavanja — office:value-type="void" izraža predpomnilnik, ki je prisoten, a ne nosi vrednosti, ker ODF nima vrste vrednosti za napako, pa se besedilo, ki spominja na napako, ohrani kot besedilo in ne povzdigni v napako

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;

Ali skupne formule delijo svoje predpomnjene vrednosti?

Ne, in domneva nasprotnega je način, kako pretres na koncu poroča isto številko za cel stolpec. Skupna formula OOXML deli le izraz formule in optimizacijo shranjevanja; vsaka članska celica še vedno lasti svoj <v>. HotXLS zato nikoli ne razširi predpomnilnika korenskega člana na sledilca, ki je prispel brez vrednosti; sledilec, naložen kot xlfcsMissing, pa še po shranjevanju in ponovnem odpiranju poroča xlfcsMissing. Če razglabljate, kako je skupina sploh shranjena in razširjena, so mehanika atributa si skupne formule in njena razširitev obravnavana ločeno; za branje predpomnilnika se pravilo skrči na eno vrstico — vprašajte vsako celico, ne zaupajte ničemer, česar niste vprašali

Pogled HotXLS na skupino skupnih formul OOXML, v kateri atribut si deli le izraz in shranjevalno postavitev, vsaka članska celica pa lasti svojo predpomnjeno vrednost, tako da sledilec, naložen brez nje, še naprej poroča xlfcsMissing
Skupina deli izraz, ne številk, zato se predpomnilnik korena nikoli ne razširi in član, ki je prispel brez vrednosti, še naprej poroča to vrzel

Branje predpomnjenih vrednosti, poenoteni bralnik med pogoni in pogon ponovnega izračuna, ki ga lahko izberete, da ga ne pokličete, vsi pridejo v standardni HotXLS Delphi Spreadsheet Component za Delphi in C++Builder, brez odvisnosti od Excela ali kateregakoli strežnika avtomatizacije OLE; stran izdelka nosi celotno referenco API za vstopni točki delovnega zvezka in bralnika, prikazana tukaj