Tehnički članak

Predmemorirane vrijednosti formula u Delphiju bez preračuna

HotXLS, izvorna Delphi i C++Builder Excel biblioteka, čita vrijednost koju je Excel već spremio uz formulu kroz TryGetCachedFormulaValue i IXLSFormulaCacheReader. Nijedna ulazna točka ne poziva kalkulator, ne dekompilira tokene formule, ne ažurira prljavo stanje ni ne zapisuje ništa natrag u model, pa radna knjiga koju samo čitate ostaje točno onakva kakvom ste je otvorili

Scenarij koji ovo pokreće dosadan je i izuzetno čest. Noćni posao otvori nekoliko stotina radnih knjiga koje je proizveo netko drugi, iz svake izvuče jedan stupac zbrojeva i gura brojeve u skladište podataka. Zbrojevi već leže u datotekama — Excel ih je izračunao i spremio. No trenutak kad posao upita ćeliju s formulom za njezinu vrijednost, biblioteka koja ima samo jedan odgovor na to pitanje gradi graf ovisnosti i vrednuje cijeli list, i posao koji je trebao biti vezan za I/O pretvori se u benchmark računanja

Zašto čitanje ćelije s formulom košta potpuni preračun?

Zato što je dohvatitelj vrijednosti na ćeliji s formulom zahtjev da se vrijednost proizvede, a jedini univerzalno ispravan način da se proizvede jest vrednovati formulu. To je pravo zadano ponašanje za aplikaciju koja uređuje radne knjige i krivo za cjevovod koji ih izvlači. Gore od toga, vrednovanje nije bez nuspojava: zapisuje rezultate natrag u ćelije, preokreće prljave zastavice i može se razriješiti drugačije od aplikacije koja je proizvela datoteku kada funkcija nije podržana ili je vanjska referenca slomljena. Posao koji ste operativnom timu opisali kao samo-čitanje tiho proizvodi radnu knjigu koja se više ne poklapa s onom na disku, i ako je bilo što kasnije spremi, datoteka na disku se također mijenja

Čitanje predmemoriranih vrijednosti druga je polovica ugovora. Odgovara na uže pitanje — što je aplikacija koja je proizvela datoteku ovdje spremila? — i odbija odgovoriti na bilo što drugo. Kada stvarno želite svježe brojeve, HotXLS i dalje nudi inkrementalni preračun vođen grafom ovisnosti; poanta jest da izvlačenje i vrednovanje budu dva različita poziva, a ne jedan poziv s dva raspoloženja

Tri ortogonalne činjenice o jednoj ćeliji

Najprije zaključak: predmemorirana vrijednost formule nosi tri neovisne činjenice, i njihovo stapanje u jedan Variant gubi informacije koje trebate. TXLSFormulaCacheInfo ih drži odvojene kao State, Kind i Value. TXLSFormulaCacheState bilježi podrijetlo kroz pet slučajeva — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated i xlfcsInvalidated — dok TXLSFormulaCacheValueKind klasificira sadržaj kao xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean ili xlfcvError. To razdvajanje jest ono što prisutnosti dopušta pošteno izvještavanje: predmemorirani prazan, predmemorirani prazan niz, predmemorirani False, predmemorirana nula i predmemorirana greška sve su stvarne vrijednosti, pa prisutnost nikada ne može biti zaključena iz VarIsEmpty ili VarIsNull. TryGetCachedFormulaValue vraća True samo za xlfcsLoaded i xlfcsCalculated, i i dalje puni dijagnostičko stanje kada vrati False

HotXLS zapis TXLSFormulaCacheInfo drži tri ortogonalne činjenice o jednoj ćeliji s formulom odvojene: podrijetlo State kroz pet slučajeva, vrstu sadržaja Kind kroz šest i Variant Value, pa se predmemorirani prazan ili False nikada ne zamijeni za odsutnu predmemoriju
Podrijetlo, vrsta sadržaja i vrijednost sadržaja ostaju odvojene, što je jedini način da se predmemorirani prazan, nula, prazan niz ili greška izvještaju kao stvarna vrijednost koja jest
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row i Col svi imaju bazu jedan ovdje
    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;

Zašto predmemorirana vrijednost nedostaje?

Postoji točno četiri razloga zašto TryGetCachedFormulaValue vraća False, i stanje vam kaže koji se primjenjuje. xlfcsNotFormula znači da ćelija drži literal ili ništa, i koordinate izvan raspona stape se u isti odgovor. xlfcsMissing znači da ćelija doista jest formula ali proizvođač nije spremio sadržaj vrijednosti za nju — čest ishod kada generator piše formule i pusti Excel da popuni rezultate pri prvom otvaranju. xlfcsInvalidated znači da je tekst formule zamijenjen nakon učitavanja, pa vrijednost koja je tamo nekad bila opisuje izraz koji više ne postoji. xlfcsCalculated, nasuprot tome, slučaj je uspjeha: označava vrijednost koju je vaš vlastiti kod ili HotXLS vrednovatelj proizveo tijekom ove sesije, za razliku od xlfcsLoaded, koji je došao iz datoteke

Iskrenost o nedostajućoj predmemoriji važnija je od njezina prekrivanja. HotXLS odbija izmisliti vrijednost, i pri spremanju je jednako strog — samo xlfcsLoaded i xlfcsCalculated ispisuju predmemoriranu vrijednost, dok xlfcsMissing i xlfcsInvalidated zapisuju samu formulu umjesto da zamrznu zastarjeli broj u datoteku. To vam ostavlja tri razumna odgovora u cjevovodu: preskočite redak i zabilježite prazninu, namjerno preračunajte tu jednu radnu knjigu i prihvatite trošak, ili vrednujte i uskladite. Ako se vrednovani broj ne slaže s onim što bi aplikacija koja je proizvela datoteku zapisala, tracer vrednovanja formula je alat za otkrivanje gdje su se dva računanja razdvojila, umjesto pogađanja iz rezultata

Jedan čitač za klasični, OOXML i ODF stroj

Cjevovod ne bi trebao mariti je li datoteka koju je upravo otvorio bila BIFF, OOXML ili ODF. IXLSFormulaCacheReader jest jedinstvena ulazna točka samo za čitanje za sve tri: i TXLSWorkbook.CreateFormulaCacheReader i TXLSXWorkbook.CreateFormulaCacheReader vraćaju lagan adapter nad rijetkim pretraživanjem ćelija koje svaki stroj već koristi, s identičnim koordinatama lista, retka i stupca s bazom jedan. Razredi radnih knjiga namjerno sami ne implementiraju sučelje — referenca sučelja na radnu knjigu promijenila bi njegovu semantiku vlasništva i pustila pozivatelje da prokližu pored najma trajanja. Umjesto toga, uništavanje radne knjige briše sirovi pokazivač unutar tog najma, i svaki čitač koji vaš kod još drži podiže EXLSFormulaCacheReaderInvalidated pri sljedećem upitu umjesto da dereferencira oslobođenu memoriju. To je fail-fast provjera trajanja, a ne jamstvo o istodobnosti

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);
  // Nijedan kalkulator nije radio, nijedna prljava zastavica se nije pomaknula, Book je nepromijenjen
end;

Gdje predmemorirani bajtovi stvarno žive

Za klasične .xls datoteke predmemorija je polje FormulaValue zapisa Formula, osam bajtova opisanih u [MS-XLS] §2.5.133. Kada je gornja riječ jednaka $FFFF, sadržaj nije IEEE 754 double nego označeni variant, i raspored je lako suptilno pokvariti: tip varianta sjedi u val[0] a boolean ili BErr sadržaj sjedi u val[2], uz nedefiniran val[1]. HotXLS je ranije čitao sadržaj iz val[1], što je vrsta pomaka baze koja izbije samo na specifičnim datotekama koje predmemoriraju boolean ili grešku umjesto broja. Čitač i pisac dijeljenih formula sada se slažu oko istih pomaka, pa predmemorirani TRUE preživi učitavanje i spremanje netaknut umjesto da truli u šum

Osmerobajtno polje FormulaValue klasičnog XLS zapisa Formula kako ga čita HotXLS: IEEE 754 double osim ako gornja riječ nije jednaka FFFF, u kojem slučaju tip varianta sjedi u val nula a Boolean ili greška sadržaj u val dva
Kada je gornja riječ FFFF polje je označeni variant, i sadržaj sjedi u val[2] uz nedefiniran val[1], što je točno bajt koji je čitač nekad uzimao

Vjernost tipova u formatima paketa zaseban je problem sa svojom zamkom. U OOXML-u predmemorirana vrijednost visi na elementu c kao <v>, uz atribut t koji imenuje tip prema ECMA-376 Part 1 §18.3.1.4. HotXLS čita t="e" izravno u varError Variant i preslikava ga natrag u standardni tekst greške pri spremanju, pa greške nikada ne glume obične cijele brojeve — ali Delphi RTL vam ovdje neće pomoći, jer VarAsType(Integer, varError) podiže iznimku konverzije. Radna konstrukcija postavlja TVarData.VType i TVarData.VError izravno. Datumi slijede istu disciplinu u suprotnom smjeru: t="d" i ODF tip vrijednosti datuma izričite su deklaracije tipa i postaju varDate, dok BIFF numerička predmemorija uopće ne nosi zastavicu datuma i zato ostaje Double. HotXLS nikada ne pogađa datum iz brojčanog formata ćelije, jer je brojčani format prezentacija a predmemorija su podaci. ODF dodaje još jedan slučaj vrijedan znanja — office:value-type="void" izražava predmemoriju koja je prisutna ali ne nosi vrijednost, i budući da ODF nema tip vrijednosti greške, tekst koji liči na grešku čuva se kao tekst umjesto da se unagradi u grešku

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;

Dijele li dijeljene formule svoje predmemorirane vrijednosti?

Ne, i pretpostavka suprotnog način je na koji pretraga završi javljajući isti broj za cijeli stupac. OOXML dijeljena formula dijeli samo izraz formule i optimizaciju spremanja; svaka članska ćelija i dalje posjeduje svoj vlastiti <v>. HotXLS zato nikada ne širi predmemoriju korijenskog člana na sljedbenika koji je stigao bez vrijednosti, i sljedbenik koji je učitan kao xlfcsMissing i dalje javlja xlfcsMissing nakon spremanja i ponovnog otvaranja. Ako proučavate kako se grupa uopće sprema i širi, mehanika si atributa dijeljene formule i njegovo širenje pokrivena je odvojeno; za čitanje predmemorije pravilo se svodi na jedan redak — pitajte svaku ćeliju, ne vjerujte ničemu što niste tražili

HotXLS prikaz OOXML grupe dijeljenih formula u kojoj si atribut dijeli samo izraz i raspored spremanja, dok svaka članska ćelija posjeduje svoju predmemoriranu vrijednost, pa sljedbenik učitan bez nje i dalje javlja xlfcsMissing
Grupa dijeli izraz, a ne brojeve, pa se predmemorija korijena nikada ne širi i član koji je stigao bez vrijednosti i dalje javlja tu prazninu

Čitanje predmemoriranih vrijednosti, objedinjeni čitač među strojevima i stroj preračuna koji možete odabrati da ne pozovete svi stižu sa standardnom HotXLS Delphi Spreadsheet Component komponentom za Delphi i C++Builder, bez ovisnosti o Excelu ili bilo kojem OLE automation poslužitelju; stranica proizvoda nosi potpunu API referencu za ulazne točke radne knjige i čitača prikazane ovdje