Articol tehnic

Citiți valorile formulelor cached din Excel în Delphi

HotXLS, biblioteca Excel nativă pentru Delphi și C++Builder, citește valoarea pe care Excel a stocat-o deja lângă o formulă prin TryGetCachedFormulaValue și IXLSFormulaCacheReader. Niciun punct de intrare nu apelează calculatorul, nu decompilează tokenuri de formulă, nu actualizează starea dirty și nu scrie nimic înapoi în model, astfel încât un registru pe care doar îl citiți rămâne exact așa cum l-ați deschis

Scenariul care motivează asta e plictisitor și extrem de comun. Un job nocturn deschide câteva sute de registre produse de altcineva, scoate o coloană de totaluri din fiecare și împinge numerele într-un warehouse. Totalurile stau deja în fișiere — Excel le-a calculat și le-a salvat. Însă în momentul în care jobul cere unei celule de formulă valoarea ei, o bibliotecă care are un singur răspuns la întrebarea asta construiește un graf de dependențe și evaluează întreaga foaie, iar un job care ar trebui să fie limitat de I/O se transformă într-un benchmark de calcul

De ce costă citirea unei celule de formulă o recalculare completă?

Pentru că un getter de valoare pe o celulă de formulă este o cerere de a produce o valoare, iar singura cale universal corectă de a produce una este să evaluați formula. Acesta e implicitul corect pentru o aplicație care editează registre și implicitul greșit pentru un pipeline care le extrage. Mai rău, evaluarea nu e lipsită de efecte secundare: scrie rezultatele înapoi în celule, schimbă flagurile dirty și se poate rezolva diferit față de aplicația producătoare când o funcție nu e suportată sau o referință externă e ruptă. Un job pe care i l-ați descris echipei de operațiuni ca read-only produce tăcut un registru care nu se mai potrivește cu cel de pe disc, iar dacă ceva îl salvează mai târziu, și fișierul de pe disc se schimbă

Citirea valorilor din cache e cealaltă jumătate a contractului. Răspunde la o întrebare mai îngustă — ce a stocat aici aplicația producătoare? — și refuză să răspundă la orice altceva. Când vreți cu adevărat numere proaspete, HotXLS vă oferă tot recalcularea incrementală condusă de un graf de dependențe; ideea e că extracția și evaluarea ar trebui să fie două apeluri diferite, nu un apel cu două dispoziții

Trei fapte ortogonale despre o celulă

Mai întâi concluzia: o valoare de formulă din cache poartă trei fapte independente, iar comprimarea lor într-un singur Variant pierde informații de care aveți nevoie. TXLSFormulaCacheInfo le ține separate ca State, Kind și Value. TXLSFormulaCacheState înregistrează proveniența în cinci cazuri — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated și xlfcsInvalidated — în timp ce TXLSFormulaCacheValueKind clasifică încărcătura ca xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean sau xlfcvError. Separarea asta e ce permite ca prezența să fie raportată onest: un blank din cache, un șir gol din cache, un False din cache, un zero din cache și o eroare din cache sunt toate valori reale, deci prezența nu poate fi niciodată dedusă din VarIsEmpty sau VarIsNull. TryGetCachedFormulaValue întoarce True doar pentru xlfcsLoaded și xlfcsCalculated, și tot completează o stare diagnosticabilă când întoarce False

Înregistrarea HotXLS TXLSFormulaCacheInfo ține separat trei fapte ortogonale despre o celulă de formulă: starea de proveniență State în cinci cazuri, tipul încărcăturii Kind în șase și valoarea Variant, astfel încât un blank sau False din cache să nu fie niciodată confundat cu un cache absent
Proveniența, tipul încărcăturii și valoarea ei rămân separate, iar asta e singura cale ca un blank, un zero, un șir gol sau o eroare din cache să fie raportate ca valoarea reală pe care o sunt
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // SheetIndex, Row și Col sunt toate cu bază unu aici
    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;

De ce lipsește valoarea din cache?

Există exact patru motive pentru care TryGetCachedFormulaValue întoarce False, iar starea vă spune care se aplică. xlfcsNotFormula înseamnă că celula conține un literal sau nimic, iar coordonatele în afara intervalului se prăbușesc în același răspuns. xlfcsMissing înseamnă că celula chiar e o formulă, dar producătorul nu a stocat nicio încărcătură de valoare pentru ea — un rezultat comun când un generator scrie formule și lasă Excel-ul să completeze rezultatele la prima deschidere. xlfcsInvalidated înseamnă că textul formulei a fost înlocuit după încărcare, deci valoarea care era acolo descrie o expresie care nu mai există. xlfcsCalculated, prin contrast, e un caz de succes: marchează o valoare produsă de codul dumneavoastră sau de evaluatorul HotXLS în această sesiune, spre deosebire de xlfcsLoaded, care a venit din fișier

Onestitatea față de un cache lipsă contează mai mult decât astuparea lui. HotXLS refuză să inventeze o valoare, iar la salvare e la fel de strict — doar xlfcsLoaded și xlfcsCalculated emit o valoare din cache, în timp ce xlfcsMissing și xlfcsInvalidated scriu doar formula în loc să înghețe un număr învechit în fișier. Asta vă lasă trei răspunsuri sănătoase într-un pipeline: săriți rândul și înregistrați golul, recalculați deliberat acel registru și acceptați costul, sau evaluați și reconciliați. Dacă numărul evaluat nu se potrivește cu ce ar fi scris aplicația producătoare, tracerul de evaluare a formulelor e unealta pentru a afla unde diverg cele două calculații, în loc să ghiciți din rezultat

Un singur cititor peste motoarele clasic, OOXML și ODF

Un pipeline nu ar trebui să-i pese dacă fișierul pe care tocmai l-a deschis era BIFF, OOXML sau ODF. IXLSFormulaCacheReader este singurul punct de intrare read-only pentru toate trei: atât TXLSWorkbook.CreateFormulaCacheReader, cât și TXLSXWorkbook.CreateFormulaCacheReader întorc un adaptor ușor peste căutarea sparse de celule pe care fiecare motor o folosește deja, cu coordonate identice de foaie, rând și coloană cu bază unu. Clasele de registru nu implementează deliberat ele însele interfața — o referință de interfață către registru i-ar schimba semanticile de proprietate și ar lăsa apelanții să treacă pe lângă lease-ul de durată de viață. În schimb, distrugerea registrului golește pointerul brut din interiorul acelui lease, iar orice cititor încă deținut de codul dumneavoastră ridică EXLSFormulaCacheReaderInvalidated la următoarea interogare în loc să dereferențieze memorie eliberată. E verificare fail-fast a duratei de viață, nu o garanție de concurență

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);
  // N-a rulat niciun calculator, niciun flag dirty nu s-a mișcat, Book e neschimbat
end;

Unde trăiesc de fapt octeții din cache

Pentru fișierele clasice .xls cache-ul este câmpul FormulaValue al înregistrării Formula, opt octeți descriși de [MS-XLS] §2.5.133. Când cuvântul înalt e egal cu $FFFF, încărcătura nu e un double IEEE 754, ci un variant etichetat, iar layoutul e ușor de greșit subtil: tipul variant stă în val[0], iar încărcătura boolean sau BErr stă în val[2], cu val[1] nedefinit. HotXLS citea înainte încărcătura din val[1], un off-by-one care iese la suprafață doar pe fișierele specifice care cache-uiesc un boolean sau o eroare în loc de un număr. Cititorul și scriitorul de formule partajate cad acum de acord pe aceleași offseturi, deci un TRUE din cache supraviețuiește intact unui ciclu de încărcare și salvare în loc să degenereze în zgomot

Câmpul de opt octeți FormulaValue al unei înregistrări XLS clasice Formula așa cum îl citește HotXLS: un double IEEE 754 exceptând cazul în care cuvântul înalt e egal cu FFFF, caz în care tipul variant stă în val zero și încărcătura boolean sau eroare în val doi
Când cuvântul înalt e FFFF, câmpul e un variant etichetat, iar încărcătura stă în val[2] cu val[1] nedefinit — exact octetul pe care îl lua cititorul înainte

Fidelitatea tipurilor în formatele de pachet e o problemă separată, cu propria capcană. În OOXML valoarea din cache atârnă de elementul c ca <v>, cu atributul t numind tipul conform ECMA-376 Part 1 §18.3.1.4. HotXLS citește t="e" direct într-un Variant varError și îl mapează înapoi la textul standard de eroare la salvare, deci erorile nu se dau niciodată drept întregi obișnuiți — dar RTL-ul Delphi nu vă ajută aici, pentru că VarAsType(Integer, varError) ridică o excepție de conversie. Construcția funcțională stabilește direct TVarData.VType și TVarData.VError. Datele urmează aceeași disciplină în direcția opusă: t="d" și tipul de valoare dată ODF sunt declarații explicite de tip și devin varDate, în timp ce un cache numeric BIFF nu poartă deloc un flag de dată și rămâne prin urmare un Double. HotXLS nu ghicește niciodată o dată din formatul numeric al unei celule, pentru că formatul numeric e prezentare, iar cache-ul e date. ODF adaugă încă un caz demn de știut — office:value-type="void" exprimă un cache care e prezent dar nu poartă nicio valoare, și cum ODF nu are tip de valoare eroare, textul cu aspect de eroare e păstrat ca text în loc să fie promovat la eroare

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;

Formulele partajate își partajează valorile din cache?

Nu, iar a presupune altfel e felul în care o baleiere ajunge să raporteze același număr pentru o coloană întreagă. O formulă partajată OOXML partajează doar expresia formulei și optimizarea de stocare; fiecare celulă membră deține în continuare propriul <v>. HotXLS nu propagă prin urmare niciodată cache-ul membrului rădăcină către un follower sosit fără valoare, iar un follower încărcat ca xlfcsMissing raportează în continuare xlfcsMissing după o salvare și redeschidere. Dacă lucrați la felul în care grupul e stocat și extins de la început, mecanica atributului si al formulei partajate și extinderea ei e tratată separat; pentru citirea din cache, regula se reduce la o linie — întrebați fiecare celulă și nu aveți încredere în nimic ce nu ați întrebat

O vedere HotXLS a unui grup de formule partajate OOXML în care atributul si partajează doar expresia și layoutul de stocare, în timp ce fiecare celulă membră deține propria valoare din cache, astfel încât un follower încărcat fără una continuă să raporteze xlfcsMissing
Grupul partajează expresia, nu numerele, deci cache-ul rădăcinii nu e niciodată propagat, iar un membru sosit fără valoare continuă să raporteze acel gol

Citirea valorilor din cache, cititorul unificat peste motoare și motorul de recalculare pe care puteți alege să nu îl apelați sosesc toate în HotXLS Delphi Spreadsheet Component standard pentru Delphi și C++Builder, fără nicio dependență de Excel sau de vreun server de automatizare OLE; pagina de produs poartă referința API completă pentru punctele de intrare de registru și cititor arătate aici