Tehnički članak

Keš vrednosti Excel formula u Delphi-ju bez rekalkulacije

HotXLS, nativna Delphi i C++Builder Excel biblioteka, čita vrednost koju je Excel već upisao pored formule kroz TryGetCachedFormulaValue i IXLSFormulaCacheReader. Nijedna ulazna tačka ne poziva kalkulator, ne dekompajlira tokene formule, ne ažurira dirty stanje i ništa ne upisuje nazad u model, pa radna sveska koju samo čitate ostaje tačno onakva kakvu ste je otvorili

Scenario koji ovo tera je dosadan i krajnje čest. Noćni posao otvori par stotina radnih sveski koje je proizveo neko drugi, izvuče iz svake po jednu kolonu zbirova i gurne brojeve u skladište podataka. Zbirovi već sede u fajlovima — Excel ih je izračunao i sačuvao. A onog trenutka kad posao zamoli ćeliju formule za vrednost, biblioteka koja na to pitanje ima samo jedan odgovor gradi graf zavisnosti i evaluira ceo list, i posao koji je trebalo da bude ograničen I/O-om postaje takmičenje u računanju

Zašto čitanje ćelije formule košta punu rekalkulaciju?

Jer je getter vrednosti na ćeliji formule zahtev da se vrednost proizvede, a jedini univerzalno ispravan način da se proizvede je evaluacija formule. To je prava podrazumevana vrednost za aplikaciju koja uređuje radne sveske, i pogrešna za procesnu liniju koja ih izvlači. Gore od toga, evaluacija nije bez neželjenih efekata: upisuje rezultate nazad u ćelije, okreće dirty flagove, i može se razrešiti drugačije od aplikacije koja je proizvela fajl kad funkcija nije podržana ili je spoljna referenca slomljena. Posao koji ste operativnom timu opisali kao read-only tiho proizvodi radnu svesku koja se više ne poklapa sa onom na disku, i ako je bilo šta kasnije sačuva, menja se i fajl na disku

Čitanje keširanih vrednosti je druga polovina ugovora. Ono odgovara na uži question — šta je aplikacija koja je proizvela fajl ovde upisala? — i odbija da odgovori na bilo šta drugo. Kad zaista želite sveže brojeve, HotXLS i dalje nudi inkrementalnu rekalkulaciju vođenu grafom zavisnosti; poenta je da izvlačenje i evaluacija budu dva različita poziva, a ne jedan poziv sa dva raspoloženja

Tri ortogonalne činjenice o jednoj ćeliji

Prvo zaključak: keširana vrednost formule nosi tri nezavisne činjenice, i njihovo stapanje u jedan Variant gubi informacije koje treba da imate. TXLSFormulaCacheInfo ih drži odvojene kao State, Kind i Value. TXLSFormulaCacheState beleži poreklo kroz pet slučajeva — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated i xlfcsInvalidated — dok TXLSFormulaCacheValueKind klasifikuje sadržaj kao xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean ili xlfcvError. Ta separacija je ono što prisustvu dozvoljava da se prijavi iskreno: keširan prazan, keširan prazan string, keširan False, keširana nula i keširana greška sve su stvarne vrednosti, pa se prisustvo nikad ne sme izvesti iz VarIsEmpty ili VarIsNull. TryGetCachedFormulaValue vraća True samo za xlfcsLoaded i xlfcsCalculated, i i tada kada vraća False popunjava dijagnostifikabilno stanje

HotXLS zapis TXLSFormulaCacheInfo drži tri ortogonalne činjenice o jednoj ćeliji formule odvojene: poreklo State kroz pet slučajeva, sadržaj Kind kroz šest i Variant Value, pa keširan prazan ili False nikad nije uzet za odsutan keš
Poreklo, tip sadržaja i vrednost sadržaja ostaju odvojeni, i to je jedini način da se keširan prazan, nula, prazan string ili greška prijavi kao stvarna vrednost koja jeste
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row i Col su ovde svi jedan-bazirani
    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 keširana vrednost nedostaje?

Postoji tačno četiri razloga da TryGetCachedFormulaValue vrati False, i stanje vam kaže koji važi. xlfcsNotFormula znači da ćelija drži literal ili ništa, i koordinate van opsega se slijevaju u isti odgovor. xlfcsMissing znači da ćelija zaista jeste formula ali proizvođač nije upisao sadržaj vrednosti za nju — čest ishod kad generator piše formule i pušta Excel da dopuni rezultate pri prvom otvaranju. xlfcsInvalidated znači da je tekst formule zamenjen posle učitavanja, pa vrednost koja je tamo bila opisuje izraz koji više ne postoji. xlfcsCalculated je, nasuprot tome, slučaj uspeha: obeležava vrednost koju je vaš kod ili HotXLS evaluator proizveo tokom ove sesije, nasuprot xlfcsLoaded, koja je došla iz fajla

Iskrenost o nedostajućem kešu znači više od prekrivanja. HotXLS odbija da izmisli vrednost, i pri čuvanju je podjednako strog — samo xlfcsLoaded i xlfcsCalculated ispisuju keširanu vrednost, dok xlfcsMissing i xlfcsInvalidated upisuju samu formulu umesto da zamrznu zastareli broj u fajl. To vam ostavlja tri razumna odgovora u procesnoj liniji: preskočite red i zabeležite prazninu, namerno rekalkulišite tu jednu radnu svesku i prihvatite trošak, ili evaluirajte i uskladite. Ako se evaluirani broj ne slaže sa onim što bi aplikacija proizvođač upisala, tracer evaluacije formula je alat za pronalaženje gde su se dva računa razišli, umesto nagađanja iz rezultata

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

Procesna linija ne bi trebalo da marči da li je fajl koji je upravo otvorila bio BIFF, OOXML ili ODF. IXLSFormulaCacheReader je jedina read-only ulazna tačka za sva tri: i TXLSWorkbook.CreateFormulaCacheReader i TXLSXWorkbook.CreateFormulaCacheReader vraćaju lagan adapter preko retkog pretraživanja ćelija koje svaki motor već koristi, sa identičnim jedan-baziranim koordinatama lista, reda i kolone. Klase radne sveske namerno same ne implementiraju interfejs — interfejs referenca na radnu svesku bi menjala njenu semantiku vlasništva i pustila pozivaoce da prokiju pored doživotnog najma. Umesto toga, uništenje radne sveske briše sirovi pokazivač unutar tog najma, i svaki čitač koji vaš kod još drži baca EXLSFormulaCacheReaderInvalidated na sledećem upitu umesto da dereferencira oslobođenu memoriju. To je fail-fast provera životnog veka, ne garancija konkurentnosti

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, nijedan dirty flag se nije pomerio, Book je nepromenjen
end;

Gde keširani bajtovi zaista žive

Za klasične .xls fajlove keš je polje FormulaValue Formula zapisa, osam bajtova opisanih u [MS-XLS] §2.5.133. Kada je gornja reč jednaka $FFFF, sadržaj nije IEEE 754 double već označena varijanta, i raspored je lako suptilno pokvariti: tip varijante sedi u val[0] a boolean ili BErr sadržaj u val[2], sa val[1] nedefinisanim. HotXLS je ranije čitao sadržaj iz val[1], što je vrsta off-by-one greške koja ispliva samo na specifičnim fajlovima koji keširaju boolean ili grešku umesto broja. Čitač i pisac deljenih formula se sada slažu oko istih ofseta, pa keširano TRUE preživi učitavanje i čuvanje netaknuto umesto da truli u šum

Osam bajtova polja FormulaValue klasičnog XLS Formula zapisa kako ga HotXLS čita: IEEE 754 double osim ako gornja reč nije jednaka FFFF, u kom slučaju tip varijante sedi u val nula a Boolean ili greška sadržaj u val dva
Kad je gornja reč FFFF polje je označena varijanta, i sadržaj sedi u val[2] sa val[1] nedefinisanim, što je baš bajt koji je čitač ranije uzimao

Vernost tipova u paketnim formatima je poseban problem sa svojom zamkom. U OOXML keširana vrednost visi na c elementu kao <v>, sa t atributom koji imenuje tip po ECMA-376 Part 1 §18.3.1.4. HotXLS čita t="e" pravo u varError Variant i vraća ga na standardni tekst greške pri čuvanju, pa greške nikad ne glume obične celobrojne vrednosti — ali Delphi RTL vam ovde ne pomaže, jer VarAsType(Integer, varError) baca izuzetak konverzije. Radna konstrukcija postavlja TVarData.VType i TVarData.VError direktno. Datumi slede istu disciplinu u suprotnom smeru: t="d" i ODF date tip vrednosti su eksplicitne deklaracije tipa i postaju varDate, dok BIFF numerički keš uopšte ne nosi datumski flag i zato ostaje Double. HotXLS nikad ne pogađa datum iz numeričkog formata ćelije, jer je format prikaza a keš je podatak. ODF dodaje još jedan slučaj vredan znanja — office:value-type="void" izražava keš koji je prisutan ali ne nosi vrednost, i pošto ODF nema tip vrednosti za grešku, tekst koji liči na grešku čuva se kao tekst umesto da bude unapređen 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;

Da li deljene formule dele svoje keširane vrednosti?

Ne, i pretpostavka suprotnog je način na koji pretraga završi prijavljujući isti broj za celu kolonu. OOXML deljena formula deli samo izraz formule i optimizaciju smeštaja; svaka članska ćelija i dalje poseduje svoj <v>. HotXLS zato nikad ne propagira keš korena na pratiloca koji je stigao bez vrednosti, i pratilac učitan kao xlfcsMissing i dalje prijavljuje xlfcsMissing posle čuvanja i ponovnog otvaranja. Ako proučavate kako se grupa uopšte smešta i širi, mehanika si atributa deljene formule i njegovo širenje pokrivena je odvojeno; za čitanje keša pravilo se svodi na jedan red — pitajte svaku ćeliju, ne verujte ništa što niste pitali

HotXLS pogled na OOXML grupu deljenih formula u kojoj si atribut deli samo izraz i raspored smeštaja, dok svaka članska ćelija poseduje svoju keširanu vrednost, pa pratilac učitan bez nje i dalje prijavljuje xlfcsMissing
Grupa deli izraz, ne brojeve, pa se keš korena nikad ne propagira i član koji je stigao bez vrednosti i dalje prijavljuje tu prazninu

Čitanje keširanih vrednosti, objedinjeni čitač preko motora i motor rekalkulacije koji možete odlučiti da ne pozovete svi stižu uz standardni HotXLS Delphi Spreadsheet Component za Delphi i C++Builder, bez zavisnosti od Excel-a ili bilo kog OLE automation servera; stranica proizvoda nosi kompletnu API referencu za ulazne tačke radne sveske i čitača prikazane ovde