Műszaki cikk

Excel gyorsítótárazott képletértékek olvasása Delphiben

A HotXLS, a natív Delphi és C++Builder Excel-könyvtár, a TryGetCachedFormulaValue és az IXLSFormulaCacheReader útján olvassa azt az értéket, amelyet az Excel már a képlet mellett tárolt. Egyik belépési pont sem hívja a számítót, nem dekompilál képlettokéneket, nem frissíti a dirty állapotot, és nem ír semmit vissza a modellbe, így egy csak olvasott munkafüzet pontosan úgy marad, ahogy megnyitotta

A mögötte álló forgatókönyv unalmas és rendkívül gyakori. Egy éjszakai feladat néhány száz, mások által készített munkafüzetet nyit meg, mindegyikből egy összesítő oszlopot húz ki, és a számokat adattárházba nyomja. Az összesítők már a fájlokban ülnek — az Excel kiszámolta és elmentette őket. Mégis abban a pillanatban, amikor a feladat egy képletcellától az értékét kéri, egy olyan könyvtár, amely erre a kérdésre csak egy választ tud, függőséggrafikont épít és kiértékeli az egész munkalapot, és egy I/O-kötöttnek szánt feladat számítási teljesítménytesztté válik

Miért kerül egy képletcella olvasása teljes újraszámolásba?

Mert egy érték-lekérdező egy képletcellán kérés egy érték előállítására, és az egyetlen univerzálisan helyes módja az előállításnak a képlet kiértékelése. Ez a helyes alapértelmezés egy munkafüzeteket szerkesztő alkalmazás számára, és a rossz alapértelmezés egy kinyerő pipeline számára. Rosszabb, hogy a kiértékelés nem mentes a mellékhatásoktól: eredményeket ír a cellákba, dirty jelzőket billent át, és eltérően oldhat fel a készítő alkalmazáshoz képest, amikor egy függvény nem támogatott vagy egy külső hivatkozás sérült. Egy feladat, amelyet az üzemeltetési csapatának csak olvasóként írt le, csendben olyan munkafüzetet állít elő, amely már nem egyezik a lemezen lévővel, és ha bármi később menti, a lemezen lévő fájl is megváltozik

A gyorsítótárazott érték olvasása a szerződés másik fele. Szűkebb kérdésre felel — mit tárolt ide a készítő alkalmazás? —, és minden mást megtagad. Amikor valóban friss számokat akar, a HotXLS továbbra is ad függőséggráfon hajtott növekményes újraszámolást; a lényeg az, hogy a kinyerés és a kiértékelés két különböző hívás legyen, nem egy hívás két hangulattal

Három ortogonális tény egyetlen celláról

Először a következtetés: egy gyorsítótárazott képletérték három független tényt hordoz, és egyetlen Variant-be összecsukva elvész a szükséges információ. A TXLSFormulaCacheInfo külön tartja őket State, Kind és Value néven. A TXLSFormulaCacheState a származást rögzíti öt esetben — xlfcsNotFormula, xlfcsMissing, xlfcsLoaded, xlfcsCalculated és xlfcsInvalidated —, a TXLSFormulaCacheValueKind pedig a terhelést osztályozza xlfcvBlank, xlfcvNumber, xlfcvDateTime, xlfcvString, xlfcvBoolean vagy xlfcvError néven. Ez az elkülönítés az, amely lehetővé teszi a jelenlét őszinte jelentését: egy gyorsítótárazott üres cella, egy gyorsítótárazott üres sztring, egy gyorsítótárazott False, egy gyorsítótárazott nulla és egy gyorsítótárazott hiba mind valós értékek, így a jelenlét sosem következtethető a VarIsEmpty-ből vagy a VarIsNull-ből. A TryGetCachedFormulaValue csak xlfcsLoaded és xlfcsCalculated esetén ad True-t, és akkor is diagnosztizálható állapotot tölt ki, amikor False-ot ad

A HotXLS TXLSFormulaCacheInfo rekordja három ortogonális tényt tart szét egyetlen képletcelláról: a származás State-ét öt esetben, a terhelés Kind-ját hat esetben és a Variant Value-t, így egy gyorsítótárazott üres cellát vagy False-t sosem hisznek hiányzó gyorsítótárnak
A származás, a terhelés típusa és a terhelés értéke külön marad, ami az egyetlen módja annak, hogy egy gyorsítótárazott üres cella, nulla, üres sztring vagy hiba valódi értékként jelenjen meg
var
  Book: TXLSXWorkbook;
  Info: TXLSFormulaCacheInfo;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-model.xlsx');
    // a SheetIndex, Row és Col itt mind egy alapú
    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;

Miért hiányzik a gyorsítótárazott érték?

Pontosan négy oka van annak, hogy a TryGetCachedFormulaValue False-ot ad vissza, és az állapot megmondja, melyik érvényes. A xlfcsNotFormula azt jelenti, hogy a cella literált vagy semmit sem tartalmaz, és a tartományon kívüli koordináták ugyanabba a válaszba csuklanak össze. A xlfcsMissing azt jelenti, hogy a cella valóban képlet, de a készítő nem tárolt értékterhelést hozzá — gyakori kimenet, amikor egy generátor képleteket ír, és az Excel első megnyitáskor tölti ki az eredményeket. A xlfcsInvalidated azt jelenti, hogy a képletszöveg betöltés után cserélve lett, így a korábban ott lévő érték egy már nem létező kifejezést ír le. A xlfcsCalculated ezzel szemben sikeres eset: azt jelöli, amit a saját kódja vagy a HotXLS kiértékelője ebben a szekcióban állított elő, szemben a xlfcsLoaded-dal, amely a fájlból jött

Az őszinteség a hiányzó gyorsítótárral kapcsolatban fontosabb, mint azt elfedni. A HotXLS visszautasítja egy érték kitalálását, és mentéskor ugyanilyen szigorú — csak a xlfcsLoaded és a xlfcsCalculated bocsát ki gyorsítótárazott értéket, a xlfcsMissing és a xlfcsInvalidated pedig egyedül a képletet írja, elavult szám fájlba fagyasztása helyett. Ez három épelméjű választ hagy egy pipeline-ban: kihagyni a sort és feljegyezni a rést, szándékosan újraszámolni azt az egy munkafüzetet és vállalni a költséget, vagy kiértékelni és egyeztetni. Ha a kiértékelt szám nem egyezik azzal, amit a készítő alkalmazás írt volna, a képlet-kiértékelés nyomkövető az eszköz annak kiderítésére, hol válnak szét a két számítás, az eredményből találgatás helyett

Egyetlen olvasó a klasszikus, OOXML és ODF motorokon át

Egy pipeline nem foglalkozhat azzal, hogy az éppen megnyitott fájl BIFF, OOXML vagy ODF volt. Az IXLSFormulaCacheReader az egyetlen csak olvasó belépési pont mindháromhoz: a TXLSWorkbook.CreateFormulaCacheReader és a TXLSXWorkbook.CreateFormulaCacheReader egyaránt könnyűsúlyú adaptert ad vissza az egyes motorok által már használt ritka cellakeresésre, azonos egy alapú munkalap-, sor- és oszlopkoordinátákkal. A munkafüzet-osztályok szándékosan nem implementálják maguk az interfészt — egy munkafüzetre mutató interfészreferencia megváltoztatná annak tulajdonlási szemantikáját, és lehetővé tenné a hívóknak, hogy kicsússzanak az élettartami bérleményen. Ehelyett a munkafüzet megsemmisítése törli a nyers pointert a bérleményen belül, és a kódja által még tartott bármely olvasó EXLSFormulaCacheReaderInvalidated-t dob a következő lekérdezésén, felszabadított memória dereferálása helyett. Ez gyors hibázású élettartam-ellenőrzés, nem párhuzamosítási garancia

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);
  // Nem futott számító, nem mozdult dirty jelző, a Book változatlan
end;

Hol élnek valójában a gyorsítótárazott bájtok

Klasszikus .xls fájlok esetén a gyorsítótár a Formula rekord FormulaValue mezője, a [MS-XLS] §2.5.133 által leírt nyolc bájt. Amikor a magas szó $FFFF-fel egyezik, a terhelés nem IEEE 754 double, hanem címkézett variáns, és az elrendezést könnyű finoman elrontani: a variánstípus a val[0]-ban ül, a boolean vagy BErr terhelés a val[2]-ben, a val[1] definiálatlan. A HotXLS korábban a val[1]-ből olvasta a terhelést, ami az az off-by-one fajta, amely csak azokon a konkrét fájlokon kerül felszínre, amelyek szám helyett booleant vagy hibát gyorsítótáraznak. Az olvasó és a megosztott képlet író most azonos eltolásokon állapodik meg, így egy gyorsítótárazott TRUE sértetlenül él túl egy betöltésen és mentésen, zajmá romlás helyett

A klasszikus XLS Formula rekord nyolcbájtos FormulaValue mezője, ahogy a HotXLS olvassa: IEEE 754 double, kivéve ha a magas szó FFFF-fel egyezik, ekkor a variánstípus a val nullában ül, a Boolean vagy hiba terhelés a val kettőben
Amikor a magas szó FFFF, a mező címkézett variáns, és a terhelés a val[2]-ben ül, a val[1] definiálatlan, ami pontosan az a bájt, amelyet az olvasó korábban vett

A típushűség a csomagformátumokban külön probléma, saját csapdával. OOXML-ben a gyorsítótárazott érték a c elemen lóg <v>-ként, a t attribútum nevezi meg a típust az ECMA-376 Part 1 §18.3.1.4 szerint. A HotXLS a t="e"-t egyből varError Variant-be olvassa, és mentéskor vissza képezi a standard hibaszövegre, így a hibák sosem maszkírozzák magukat közönséges egész számoknak — de a Delphi RTL itt nem segít, mert a VarAsType(Integer, varError) konverziós kivételt dob. A működő konstrukció közvetlenül állítja a TVarData.VType-ot és a TVarData.VError-t. A dátumok ellentétes irányban követik ugyanezt a fegyelmet: a t="d" és az ODF dátum értéktípus explicit típusdeklarációk, és varDate-dé válnak, míg a BIFF numerikus gyorsítótár egyáltalán nem hordoz dátumjelzőt, és ezért Double marad. A HotXLS sosem találgat dátumot cellaszámformátumból, mert a számformátum prezentáció, a gyorsítótár pedig adat. Az ODF még egy, a tudásra érdemes esetet ad — az office:value-type="void" olyan gyorsítótárat fejez ki, amely jelen van, de nem hordoz értéket, és mivel az ODF-nek nincs hiba értéktípusa, a hibának tűnő szöveg szövegként őrződik meg, hibává léptetés helyett

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;

Megosztják-e a megosztott képletek a gyorsítótárazott értékeiket?

Nem, és a másik feltételezése az, ahogy egy átfogó olvasás egy teljes oszlopra ugyanazt a számot jelenti. Egy OOXML megosztott képlet csak a képlet kifejezést és a tárolási optimalizálást osztja meg; minden tagcella továbbra is a saját <v>-jét birtokolja. A HotXLS ezért sosem propagálja a gyökértag gyorsítótárát egy érték nélkül érkező követőre, és egy xlfcsMissing-ként betöltött követő mentés és újranyitás után is xlfcsMissing-et jelent. Ha azon dolgozik, hogy a csoport hogyan tárolódik és bontódik ki elsőként, a megosztott képlet si attribútumának és kibontásának mechanikája külön tárgyalt; a gyorsítótár olvasásához a szabály egy sorra zsugorodik — kérdezzen meg minden cellát, és semmiben ne bízzon, amit nem kért meg

Egy OOXML megosztott képletcsoport HotXLS nézete, amelyben a si attribútum csak a kifejezést és a tárolási elrendezést osztja meg, miközben minden tagcella a saját gyorsítótárazott értékét birtokolja, így egy nélküle betöltött követő továbbra is xlfcsMissing-et jelent
A csoport a kifejezést osztja meg, nem a számokat, így a gyökérgyorsítótár sosem propagálódik, és egy érték nélkül érkező tag továbbra is azt a rést jelenti

A gyorsítótárazott érték olvasása, az egységes motorokon átívelő olvasó és a kiértékelőmotor, amelyet úgy dönthet el, hogy nem hív meg, mind a standard HotXLS Delphi Spreadsheet Component részét képezi Delphi és C++Builder számára, Excel- vagy OLE automatizálási kiszolgálófüggés nélkül; a termékoldal hordozza a teljes API referenciát az itt bemutatott munkafüzet- és olvasó belépési pontokhoz