Odborný článok

Kopírovanie naprieč zošitmi a znovunaviazanie vzorcov v HotXLS pre Delphi

Metóda AddCopy v HotXLS kopíruje hárok z jedného zošita Excelu do druhého tak, že dekompiluje každý vzorec na tomto hárku do textu v štýle A1 a znova skompiluje tento text vnútri cieľového zošita, namiesto toho, aby priamo skopírovala skompilovaný strom vzorca, pretože odkazy na sériu grafu, indexy písiem formátovaného textu a číslovanie externých odkazov sa vo vnútri každého súboru zošita priraďujú nezávisle

Zlyhanie sa prejaví presne v takom zošite, ako by ste čakali: koncomesačná úloha, ktorá vytiahne jeden hárok z reportu každej pobočky a pripojí ho k súhrnnému súboru. Otvorte výsledok a graf medzisúčtu vykresľuje čísla úplne inej pobočky, poznámka, ktorá bola v zdroji tučná a červená, je späť na obyčajnom čiernom texte, a vzorec, ktorý kedysi ťahal daňovú sadzbu zo sprievodného vyhľadávacieho zošita, teraz zobrazuje zamrznuté číslo, ktoré nikto nedokáže vysvetliť. Nič tu nevyvolá výnimku — súbor sa otvorí, čísla vyzerajú vierohodne, a škoda tam sedí, kým si niekto nevšimne graf so zlým titulkom sediaci vedľa

Prečo AddCopy nemôže jednoducho skopírovať skompilovaný strom vzorca?

AddCopy nemôže presunúť skompilovaný strom vzorca nezmenený, pretože skompilovaný vzorec BIFF nie je samostatný text — je to postupnosť tokenov, a niekoľko z týchto tokenov sú malé celé čísla, ktoré sa správne vyriešia iba vnútri zošita, ktorý ich vyprodukoval. 3D odkaz ako Sheet2!A1:A10 po skompilovaní nenesie doslovný názov Sheet2; nesie pole, ktoré špecifikácia BIFF nazýva ixti (HotXLS drží tú istú hodnotu vo vlastnom skompilovanom strome pod názvom poľa FExternID), index do vlastnej súkromnej tabuľky EXTERNSHEET tohto zošita, číslovaný podľa toho, v akom poradí tento konkrétny zošit náhodou registroval svoje hárky a externé knihy. Presuňte token nezmenený do zošita, ktorého tabuľka EXTERNSHEET bola zostavená v inom poradí, a index 3 už neznamená Sheet2 — znamená čokoľvek, čo tam náhodou obsadzuje slot 3, a Excel nemá žiadny spôsob, ako túto chybu označiť, pretože pokiaľ ide o formát súboru, vzorec je dokonale správne zostavený. Presne toto je zlyhanie, ktorému TXLSWorksheets.AddCopy existuje, aby sa mu vyhla: volaná z vlastnej kolekcie hárkov ktoréhokoľvek zo zošitov v kóde Delphi alebo C++Builder, kopíruje hárok — hodnoty buniek, formáty, vzorce, grafy, komentáre, zlúčenia, nastavenia strany a viac — zo zdrojového zošita, ktorý nemusí byť tým, na ktorom ju voláte, a pripojí výsledok do cieľa pod menom podľa vášho výberu, alebo jednoznačnou kópiou originálu

var
  Summary, Branch: IXLSWorkbook;   // interface-counted: do not Free
begin
  Summary := TXLSWorkbook.Create;
  Branch := TXLSWorkbook.Create;
  Branch.Open('branch-east.xls');

  // Appends a copy of Branch's first sheet onto Summary, renamed to
  // stay unique inside the destination workbook
  Summary.Sheets.AddCopy(Branch.Sheets[1], 'East Detail');
  Summary.SaveAs('consolidated.xls');
end;

Oprava: dekompilovať do textu, znova skompilovať v cieli

HotXLS rieši problém indexovania tým, že nikdy nedovolí samotnému skompilovanému stromu prekročiť hranicu zošita. Pre každú bunku so vzorcom pri kopírovaní naprieč zošitmi AddCopy dekompiluje zdrojový vzorec do toho istého textu v štýle A1, aký by používateľ videl v paneli vzorcov Excelu, a potom tento text odovzdá cieľovému zošitu, ktorý ho znova naparsuje do stromu pomocou vlastných tabuliek od nuly — odkaz kvalifikovaný hárkom, ako Data!D2:D100, je v tom bode iba reťazec, a reťazec znamená to isté v akomkoľvek zošite, takže ak cieľ už má hárok pomenovaný Data, odkaz sa vyrieši správne bez akéhokoľvek prekladu indexov, pretože nikdy nebol vo vzduchu žiadny surový index na preklad. HotXLS platí za túto cestu tam a späť iba vtedy, keď musí: kopírovanie hárka vnútri toho istého zošita ide lacnejšou cestou, kde sa skompilovaný strom jednoducho duplikuje v pamäti, keďže každý index v ňom je už platný tam, kde zostáva, a obchádzka cez text beží iba vtedy, keď AddCopy zistí, že zdroj a cieľ sú naozaj odlišné inštancie zošita. Oplatí sa byť presný aj v tom, čím toto prepísanie nie je. Nemá nič spoločné s posúvaním riadkov a stĺpcov, ktoré beží, keď vkladáte alebo odstraňujete riadky vnútri jedného hárka, čo do hĺbky pokrýva sprievodný článok — tento engine prepisuje text A1 na mieste, aby sledoval bunky, ktoré sa posunuli o niekoľko riadkov hore alebo dole v rámci jedného zošita, zatiaľ čo tento beží, keď vzorec opustí zošit, ktorý ho skompiloval, úplne, kde posunuté riadky nie sú problémom a súkromné číslovanie zošita je

// Conceptually, this is what AddCopy does for each formula cell: turn
// the compiled tree back into text using the source workbook's own
// tables, then let the destination workbook parse that text back into
// a tree using its own tables, from scratch
FormulaText := SourceBook.GetUnCompiledFormula(SourceFormula, Row, Col, SourceSheetID);
DestFormula := DestBook.GetCompiledFormula(FormulaText, DestSheetID);

Čo ak cieľ ešte nemá tento hárok, alebo tento názov?

Opätovná kompilácia AddCopy uspeje iba vtedy, keď cieľový zošit už má všetko, na čo sa text vzorca odkazuje, a dve medzery, ktoré sa v praxi objavujú, sú rovnako pomenovaný hárok, ktorý ešte nebol v tejto dávke skopírovaný, a definovaný názov v rozsahu zošita, ktorý v cieli nikdy neexistoval. HotXLS nevyvolá výnimku, keď opätovná kompilácia uprostred kopírovania hárka zlyhá — priradenie Value bunky namiesto toho ticho uloží text vzorca ako obyčajný reťazec, zámerný, inšpekovateľný režim zlyhania namiesto tichého, keďže bunka so vzorcom, ktorá neočakávane zobrazuje doslovný text ako =SUM(Q1!B2:B12) namiesto vypočítaného čísla, je príznak, že sa niečo vyššie v kopírovaní nevyriešilo. Ešte pred vzdaním sa AddCopy vyskúša jednu opravu: prejde strom syntaxe zlyhaného vzorca a zozbiera každé ID definovaného názvu, ktorého sa vzorec dotýka, a pre každý názov v rozsahu zošita, ktorý existuje v zdroji, no ešte nie v cieli, skopíruje názov naprieč a znova skompiluje ten istý text druhýkrát. Názvy v rozsahu hárka ležia mimo toho, čo táto oprava dokáže vyriešiť, keďže názov viditeľný iba pre vzorce na jednom hárku zdrojového zošita nemá žiadny ekvivalentný slot, do ktorého by mohol migrovať, a cieľ, ktorý už vlastní názov s rovnakým znením, sa ponechá nedotknutý namiesto prepísania, na základe predpokladu, že názov, ktorý volajúci zámerne vopred vytvoril, je ten, ktorý chce rešpektovať. Vnútri jedného zošita vyhľadávanie názvu medzi-hárkového vzorca prechádza z rozsahu hárka nahor do rozsahu zošita automaticky, čo je mechanizmus, aký pokrýva článok HotXLS o definovaných názvoch a medzi-hárkových vzorcoch; prekročenie skutočnej hranice zošita túto poistku úplne odstráni, a názov musí byť zámerne prenesený naprieč, inak sa vzorec, ktorý naň spolieha, degraduje na text

Odkazy série grafu potrebujú tú istú opravu, no odlišnú cestu kódu

Séria grafu HotXLS, ktorá vykresľuje rozsah buniek, naráža presne na ten istý problém číslovania ako obyčajný vzorec bunky, pretože odkaz na dátový rozsah grafu je tiež skompilovaný prúd tokenov vzorca — špecifikácia BIFF nazýva záznam, ktorý ho nesie, BRAI ([MS-XLS] sekcia 2.4.51) — no AddCopy ho nemôže opraviť opätovným použitím normálnej cesty načítania grafu, pretože práve táto cesta je to, čo vytvára chybu. Keď sa záznam grafu naparsuje z disku v bežnom priebehu otvárania súboru, jeho strom vzorca sa zostaví prekladom surových bajtov cez akúkoľvek inštanciu kalkulačky, ktorá parsovanie vykonáva; nakŕmte surové bajty BRAI zdrojového grafu namiesto toho cez bežný načítač záznamov cieľového zošita, a ixti vložené do týchto bajtov sa vyrieši voči tabuľke EXTERNSHEET cieľa, takže séria ticho ukazuje na čokoľvek, čo tam obsadzuje tento slot — rovnaká trieda chyby ako kopírovanie skompilovaného stromu bunky nezmeneného, len ťažšie si ju všimnúť, pretože nikto nečíta vzorce sérií grafu tak, ako číta vzorce buniek. HotXLS sa tejto pasci vyhýba namiesto toho vyhradenou cestou klonovania: TXLSCustomChart.AssignFrom skopíruje vlastné hlavičkové bajty každého záznamu grafu bez vzorca doslovne, a potom znova zostaví pripojený rozsah cez ten istý primitív dekompilácie-a-rekompilácie, aký sa používa pre obyčajné bunky, takže nový strom sa zostaví voči tabuľke EXTERNSHEET cieľa od nuly namiesto toho, aby sa voči nej dodatočne znova interpretoval

Ten istý problém číslovania, jeden index písma naraz

Nie každé lokálne číslo zošita vnútri grafu alebo bunky s formátovaným textom je vzorec, a index písma je ten istý problém v malom. Behy formátovaného textu, spolu s dvomi ďalšími typmi záznamov grafu, ktoré nesú titulok alebo písmo osi, ukladajú odkaz na písmo ako surový celočíselný index do vlastnej tabuľky písiem vlastniaceho zošita, a tento index neznamená nič v tabuľke iného zošita — mohol by tam rovnako ľahko ukazovať na úplne inú typografiu, veľkosť, alebo farbu. HotXLS to rieši podľa hodnoty, nie podľa čísla: vyhľadá skutočné atribúty písma na tomto indexe v zdrojovej tabuľke, nájde alebo vytvorí zodpovedajúci záznam v tabuľke písiem cieľa, a prepíše uložený index tak, aby ukazoval na tento nový slot. Jedna zvláštnosť formátu robí samotné vyhľadávanie chúlostivým — index číslovaný v súbore preskakuje slot 4, medzera v číslovaní, ktorú dokumentuje [MS-XLS] sekcia 2.5.339, takže kód musí index posunúť o jednu dole ešte pred porovnaním písiem a späť o jednu hore ešte pred zápisom výsledku

// The file-numbered font index skips slot 4 (MS-XLS section 2.5.339);
// shift into the in-memory slot, migrate the font by value if the
// destination differs, then shift back before writing the result
if Ifnt >= 5 then
  Dec(Ifnt);
if DestFonts.Key[Ifnt] <> SourceFonts.Key[Ifnt] then
  Ifnt := DestFonts.SetKey(0, SourceFonts.Key[Ifnt]);
if Ifnt >= 4 then
  Inc(Ifnt);

Čo sa stane so vzorcom, ktorý už ukazuje mimo zošita?

Vzorec, ktorý siaha do tretieho zošita ešte pred tým, než vôbec zavoláte AddCopy, je ten jeden prípad, ktorý textová cesta tam a späť nedokáže preniesť, pretože vlastný dekompilátor vzorca-na-text v HotXLS zámerne nesyntetizuje text v zátvorkách [Book]Sheet! pre externý odkaz, a kompilátor na druhom konci ani nepríma túto syntax ako vstup — takže tento jeden prípad beží cez druhý mechanizmus, ktorý sa textu vôbec nedotýka. Keď oprava migrácie názvov opísaná vyššie stále ponechá bunku ako reťazec, a zdrojový zošit má skutočný názov súboru, AddCopy prepne stratégiu: hlboko skopíruje samotný skompilovaný strom vzorca namiesto jeho textu, a potom odovzdá kópiu vyhradenému prechodu znovunaviazania, RebindExternRefsInTree, ktorý ho prechádza uzol po uzle. Pre každý odkaz na rozsah, ktorý nájde, tento prechod vyrieši záznam EXTERNSHEET zdroja späť do dvojice názvov hárkov, a zaregistruje, alebo znova použije, ekvivalentný záznam vo vlastných tabuľkách externých odkazov cieľa, pričom vytvorí úplne nový odkaz na externý zošit, ak cieľ ešte nikdy neodkazoval na tento zdrojový súbor

Práve tu je problém lokálneho číslovania zošita najdoslovnejší, pretože token externého odkazu zväzuje tri samostatné súradnice do jedného poľa, a každá z nich je súkromná pre zošit, ktorý ju zapísal: ktorý externý zošit, slot vo vlastnom zozname externých kníh cieľa priradený v akomkoľvek poradí, v akom ich tento zošit náhodou zaregistroval; ktorý hárok vnútri vlastného zoznamu hárkov tohto externého zošita, uložený ako index počítaný od 1 v rozsahu špecificky pre danú externú knihu, úplne odlišná číselná doména od vlastných interných ID hárkov cieľa; a samotný rozsah buniek, obyčajné súradnice riadkov a stĺpcov, ktoré nepotrebujú žiadny preklad, pretože nikdy neboli relatívne voči zošitu. Pomýlite jedno z prvých dvoch a Excel súbor stále otvorí, stále zobrazí vzorec, a vyhodnotí ho voči nesprávnym externým bunkám bez sťažovania sa. Jeden druh uzla porazí aj toto znovunaviazanie na úrovni stromu: odkaz na definovaný názov, index do vlastnej súkromnej tabuľky názvov vlastného zošita presne tak, ako je index hárka súkromný pre vlastný EXTERNSHEET, bez akejkoľvek ekvivalentnej opravy na úrovni stromu k dispozícii — vo chvíli, keď prechod znovunaviazania kdekoľvek v strome narazí na odkaz na názov, opustí celý vzorec namiesto toho, aby zapísal čiastočne správny. Aj keď sa znovunaviazanie podarí, cieľová bunka nezobrazí čerstvo prepočítané číslo; zobrazí hodnotu, akú zdrojová bunka už držala v čase kopírovania, uloženú vo vyrovnávacej pamäti rovnako, ako si samotný Excel ukladá poslednú známu hodnotu akéhokoľvek externého odkazu, kým výslovne neobnovíte odkazy, čo je správna predvolená hodnota, keďže prepočítanie cez živý odkaz do iného súboru je presne taký druh operácie, akú chcete spustiť raz, zámerne, nie pri každom otvorení

Čo tento návrh stojí

Mašinéria dekompilácie-a-rekompilácie v AddCopy nie je zadarmo, a s touto cenou sa oplatí počítať ešte pred tým, než naskriptujete veľkú konsolidačnú úlohu, nie po tom. Kopírovanie hárka vnútri toho istého zošita ide lacnou cestou, priamou duplikáciou skompilovaného stromu v pamäti, pretože každý index v ňom je už platný v zošite, v ktorom zostáva; kopírovanie naprieč zošitmi namiesto toho platí za skutočné parsovanie pri každej bunke so vzorcom, dekompilovať do textu a potom tento text znova skompilovať odznova, a hoci sa rozdiel neoplatí merať na hárku s pár desiatkami vzorcov, zdrojový zošit s desiatkami tisíc buniek so vzorcami, skopírovaný ako jeden hárok medzi tuctami v dávkovej úlohe, by mal očakávať, že opätovná kompilácia bude dominovať v čase behu namiesto vstupno-výstupných operácií súboru okolo nej. Poradie kopírovania záleží aj z druhého dôvodu popri rýchlosti: vzorec, ktorý odkazuje na hárok, ku ktorému sa AddCopy v tejto dávke ešte nedostala, zlyhá pri opätovnej kompilácii z rovnakého dôvodu, ako zlyhá vzorec odkazujúci na naozaj neexistujúci hárok, takže úloha, ktorá skopíruje hárok B pred hárkom A, na ktorom vzorec závisí, uvidí tento vzorec degradovať presne tak, ako je opísané vyššie, na text reťazca, alebo na náhradu externým odkazom ukazujúcim rovno späť na zdrojový súbor, z ktorého práve pochádza. A pretože každý zdrojový zošit v konsolidačnej dávke je zvyčajne autorovaný nezávisle, oplatí sa výslovne otestovať práve ten jeden režim zlyhania, pred ktorým vás žiadny jednotlivý zdrojový súbor nikdy nemohol varovať — päť zošitov pobočiek, z ktorých každý súčtuje čísla partnerskej pobočky, sa môže skombinovať do skutočného cyklického odkazu vnútri súhrnného zošita bez toho, aby ktorýkoľvek jednotlivý zdrojový súbor niekedy obsahoval jeden, cyklus, ktorý existuje iba vtedy, keď každý hárok skončí na tom istom mieste a prepočet beží nad kombinovanou množinou

Kopírovanie hárkov naprieč zošitmi sa dodáva ako štandardné správanie AddCopy v komponente HotXLS Delphi Excel pre Delphi a C++Builder; stránka produktu nesie plnú referenciu API pre hárky a zošity, vrátane tu opísaného správania grafov, formátovaného textu, a externých odkazov