Tehnički članak

HotXLS implicit intersection za definirana imena u Delphiju

Definirano ime koje se odnosi na cijeli stupac Excel čita kao jednu ćeliju kada se pojavi u skalarnoj poziciji: =Vertical+1 u retku 7 znači "ćelija retka 7 u Vertical", a ne cijelo područje. HotXLS Delphi Component primjenjuje taj implicit intersection u v2.382.4 na dvije razine, tijekom evaluacije i tijekom izvlačenja ovisnosti, jer je predložak kredita s 4805 formula pokazao da ispravna vrijednost nije dovoljna. Kada dependency walker raširi ime na cijelo njegovo područje, nizvodna formula koja hrani bilo koju ćeliju tog područja zatvara ciklus koji ne postoji, pa TXLSXWorkbook.Recalculate odbija cijelu radnu knjigu

Predložak o kojem je riječ je standardna radna knjiga amortizacije kredita. Uz svaku keširanu vrijednost zatrovanu na 777 i puno Recalculate pokretanje, obje arhitekture enginea vratile su 23, što je lxErrorRef, kod kružne reference. 3842 od 4805 formula nisu se poklopile s nezavisnim očekivanjem, B18 je držao #VALUE!, E18 je i dalje bio 777, a broj plaćanja u J7 pročitao je placeholdere u nedovršenom stupcu stanja. Tri različita defekta krila su se iza jednog povratnog koda, a ovaj članak prolazi kroz svaki uz izvorni kod koji ga je popravio

Zašto skalarna referenca na ime stupca stvara lažni ciklus?

Zato što graf ovisnosti poznaje samo bridove, a brid od formule do područja od 480 redaka je 480 bridova, od kojih jedan pokazuje natrag kroz ćeliju koja ovisi o formuli. Uzmite =IF(TRUE,Vertical+1,0) u B1 s Vertical definiranim kao Inputs!$A$1:$A$2, i =B1+1 u A2. Excel evaluira B1 kao A1+1, a A2 kao B1+1, ravan lanac. Walker koji zabilježi B1 kao ovisan o A1:A2 čini A2 prethodnikom od B1, A2 već navodi B1 kao prethodnika, a Kahnov red koji pokreće inkrementalno preračunavanje u HotXLS-u nikad ne vidi ni jedan čvor s in-degreeom nula. To je uzorak od kojeg su predlošci kredita napravljeni: svaki redak razdoblja referencira imenovane stupce za stanje, kamatu i broj plaćanja, svako ime premošćuje cijeli raspored, a svaki redak i piše u te stupce. Raširite imena i graf je jedna golema jako povezana komponenta. Evaluirajte ih s implicit intersection i graf je skup kratkih lanaca, po jedan za svaki redak, što je i ono što ECMA-376 Part 1 §18.17.2 opisuje za referentni operand koji se troši tamo gdje se zahtijeva jedna vrijednost

Zašto je ime stupca zatvorilo lažni ciklus u HotXLS-u: s Vertical definiranim kao Inputs!$A$1:$A$2 walker bilježi B1 kao ovisan o A1:A2, dok A2 već navodi B1 kao prethodnika, pa se Kahnov red nikad ne isprazni, a siječenje sužava B1 na ćeliju retka A1 i čuva lanac po retku A2, B1, A1 koji Recalculate poreda
Raširivanje imena pretvorilo je graf u jednu golemu jako povezanu komponentu, a evaluacija istih formula s implicit intersection pretvara ga u kratke lance, po jedan za svaki redak rasporeda
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Inputs');
    Book.DefinedNames.Add('Vertical', 'Inputs!$A$1:$A$2');
    Book.DefinedNames.Add('Alias', '=Vertical');
    Sheet.Cells[1, 1].Value := 1;
    // Skalarna pozicija: Vertical se svodi na A1 jer je formula u retku 1
    Sheet.Cells[1, 2].Formula := '=IF(TRUE,Vertical+1,0)';
    Sheet.Cells[2, 1].Formula := '=B1+1';
    // Ime čija je definicija drugo ime i dalje siječe, pa je ovo A2
    Sheet.Cells[2, 2].Formula := '=Alias';
    // Argument referentne klase: zbraja se cijelo područje, bez siječenja
    Sheet.Cells[3, 2].Formula := '=SUM(Vertical)';
    // Redak 6 leži izvan A1:A2, siječenje je prazno i IFERROR ga hvata
    Sheet.Cells[6, 2].Formula := '=IFERROR(Vertical,42)';

    if Book.Recalculate = lxOk then
    begin
      // B1 = 2, A2 = 3, B2 = 3, B3 = 4, B6 = 42
      // Prije v2.382.4 ova grana bila je nedostižna: B1 -> A2 -> B1 bio je ciklus
    end;
  finally
    Book.Free;
  end;
end;

Kako HotXLS odlučuje da je argument skalaran?

HotXLS odgovor čita iz tablice funkcija, a ne iz oblika argumenta. Svaki unos u TXLSFormula.InitFuncHash registriran je kroz THashFunc.SetValue s neobaveznim stringom klase po argumentu: 'IF' nosi '100', 'SUMIF' nosi '010', 'VLOOKUP' nosi '1011', a 'SUM' ne nosi nijedan, pa svi njegovi argumenti padaju na klasu 0 na razini funkcije. Novi TXLSFormula.FunctionArgumentClass(APtg, AArgument) izlaže taj bajt kroz THashFuncEntry.ArgClass, a rezultat 1 znači vrijednosnu klasu. To su iste tri klase koje [MS-XLS] §2.2.2 dodjeljuje operand tokenima, i enkoder je o njima već ovisio: kada piše referencu, računa ptg kao $24 + $20 * aClass, što daje PtgRef za klasu 0, PtgRefV za klasu 1 i PtgRefA za klasu 2. BIFF datoteka koju je napisao Excel pohranjuje tu klasu u svakom referentnom tokenu, pa engine čija se tablica poklapa sa specifikacijom može odgovoriti "je li ovaj argument skalaran" bez gledanja u podatke. Srednji argument funkcije SUMIF je kriterij, vrijednost; prvi i treći su područja, reference. SUMPRODUCT je registriran s klasom 2 na razini funkcije, array, zato =SUMPRODUCT(Vertical,Vertical) i dalje množi cijelo područje

Tri funkcije ne konzultiraju vlastiti unos u tablici za bilo što nakon prvog argumenta. IF (ptg 1), CHOOSE (ptg 100) i IFERROR (ptg 255) propuštaju kroz sebe sve što odaberu, pa njihovi argumenti grana nasljeđuju klasu pozicije koju sama funkcija zauzima. To jedno pravilo omogućuje da se =CHOOSE(1,Vertical,0) u G2 razriješi na A2, dok =SUMIF(Vertical,">0",Vertical) pored njega i dalje zbraja oba retka, i to je pravilo koje raspored amortizacije najviše izvježba, jer se njegove ćelije razdoblja oslanjaju na IF da provjere je li kredit još otvoren

Odakle HotXLS čita klase argumenata za implicit intersection: IF registrira 100, SUMIF 010, VLOOKUP 1011, a SUM ništa pa njegovi argumenti padaju na klasu 0, enkoder piše referentne tokene kao ptg $24 plus $20 puta klasa dajući PtgRef, PtgRefV i PtgRefA, a pass-through funkcije IF, CHOOSE i IFERROR nasljeđuju klasu pozicije koju zauzimaju
Budući da se tablica klasa poklapa sa specifikacijom, engine može odgovoriti je li argument skalaran bez gledanja u podatke, a CHOOSE koji se razrješava na A2 pored SUMIF-a koji zbraja oba retka slijedi iz jednog pravila

Nošenje klase kroz dependency walk

Extractor ovisnosti u lxCalc.pas rekurzivni je Walk po kompiliranom sintaksnom stablu, i postoji dvaput, jednom u TXLSCalculator.ExtractDependencies za graf po radnoj knjizi i jednom u ExtractWorkspaceDependencies za graf preko radnih knjiga. v2.382.4 daje objema walkerima dva dodatna parametra. AScalar počinje kao True u korijenu formule, ponovno se računa za svako dijete funkcije iz FunctionArgumentClass, a dalje se prosljeđuje nepromijenjen za argumente grane ptg 1, 100 i 255. ANameRoot postaje True samo kada walker zaroni u kompiliranu definiciju imena, i preživljava samo kroz SA_GROUP čvorove, zagrade, pa ime definirano kao =A1:A2+1 nije zamijenjeno s običnim područjem. Kada su obje zastavice True na SA_RANGE čvoru, AddResolvedRange sužava područje istim pomoćnikom koji evaluator koristi prije nego zabilježi ovisnost. Pomoćnik je dovoljno kratak da ga citiramo u cijelosti

Odluka IntersectNamedScalarRange koja čuva ovisnosti o imenima u HotXLS-u: raspon koji je već jedna ćelija prolazi dalje, jedan stupac sužava se na redak formule kada CurRow padne unutar njega, jedan redak sužava se na stupac formule, a sve ostalo, dvodimenzionalno područje ili redak izvan raspona, daje #VALUE! tijekom evaluacije i ne bilježi nikakvu ovisnost
I oba dependency walkera i evaluator pozivaju istog pomoćnika, pa se vrijednost koju formula čita i brid koji graf bilježi nikad ne mogu razići oko siječenog imena
function IntersectNamedScalarRange(CurRow, CurCol: Integer;
  var Row1, Row2, Col1, Col2: Integer): Boolean;
begin
  Result := False;
  if (Row1 = Row2) and (Col1 = Col2) then Exit(True);   // već je ćelija
  if (Col1 = Col2) and (CurRow >= Row1) and (CurRow <= Row2) then
  begin
    Row1 := CurRow; Row2 := CurRow;                     // jedan stupac: uzmi ovaj redak
    Exit(True);
  end;
  if (Row1 = Row2) and (CurCol >= Col1) and (CurCol <= Col2) then
  begin
    Col1 := CurCol; Col2 := CurCol;                     // jedan redak: uzmi ovaj stupac
    Result := True;
  end;
end;

Sve što pomoćnik odbije, dvodimenzionalno područje, referenca na više listova ili formula čiji redak leži izvan imenovanog stupca, daje #VALUE! na strani evaluacije i nikakvu ovisnost na strani grafa, što je ono što Excel radi za prazno siječenje. Strana evaluacije živi u TXLSCalculator.GetValueItemName: skida SA_GROUP omote s kompilirane definicije, i ako je korijen SA_RANGE poziva GetRangeInfo, siječe, i dohvaća tu jednu ćeliju kroz FGetValue umjesto da evaluira cijelu definiciju. Vanjske reference ostaju na staroj putanji, jer nema lokalnog retka s kojim bi se sjekle. Odakle pohrana i doseg imena uopće dolaze obrađeno je u članku o definiranim imenima i formulama preko listova; ovdje je poanta samo ono što engine radi nakon što se ime razriješi

Zašto je MATCH nad napola izračunatim stupcem pročitao 777?

Zato što je lookup-array argument funkcije MATCH scan referenca, a scan reference bile su namjerno isključene iz redoslijeda evaluacije. Članak o lookup scanu uveo je TXLSDepRange.LookupScan i završio odjeljkom pod naslovom "Što gubite isključivanjem scan bridova iz poredanja": lookup formula može se izvršiti prije nego su sve ćelije u njenom rasponu preračunate i pročitati zastarjele vrijednosti. U interaktivnoj sesiji to se konvergira u sljedećem prolazu. U batch preračunavanju zatrovanog predloška ne, pa je PaymentCount, definiran kao =MATCH(0.01,Balances,-1)+1, pročitao 777 placeholdere koji su još sjedili u stupcu stanja i vratio broj razdoblja koji nije mogao biti točan

TXLSDepGraph.TopoOrder sada scan bridove tretira kao mekane bridove poredanja. Uz tvrdi in-degree drži i polje ScanInDeg, brojeći prljave scan prethodnike po čvoru i umanjujući ga kako se ti prethodnici emitiraju, koristeći liste ScanPrecedents, ScanDependents i ScanPrecedentCount koje je ranija promjena već spremala. U svakoj iteraciji Kahnov red pregledava svoj spremni prozor za prvim čvorom čiji je ScanInDeg nula i premješta ga na čelo; ako svi spremni čvorovi još čekaju scan prethodnika, čelo se ispisuje u svom stabilnom poretku. Scan bridovi nikad ne ulaze u tvrdi in-degree, pa je samoreferencirajući VLOOKUP nad vlastitim stupcem i dalje legalan, ali lookup koji bi mogao čekati dovršivog prethodnika sada i čeka. Regresija koja ovo prikiva, LookupScan_WaitsForDirtyFormulaValues, zatruje tri ćelije stanja na 777 i očekuje da PaymentCount vrati 3, zatim preokrene ulaz na nulu i očekuje da =IFERROR(PaymentCount,99) vidi #N/A i vrati 99

Odakle skraćivanje na četiri decimale?

Iz Delphi Variant aritmetike, i to samo u gniježđenim pozicijama. Binarni operatori u TXLSCalculator.GetValueItem već su kopirali + ili - s vrha u dvije Double lokalne varijable, pa je =B1-A1 bilo u redu. Unutar =IF(TRUE,B1-A1,0) isto se oduzimanje izvršilo kao Value := Value - SubValue nad dva Varianta, a kada je jedan operand bila Int64 vrijednost ćelije, a drugi Double, rezultat koji smo vidjeli bio je Currency, fiksni tip s četiri decimale, pa se 1066.1854641400994 minus 120 vratio skraćen na četiri decimale. Kroz raspored u kojem se svaka uplata složi iz prethodnog retka ta greška prođe stotine razdoblja prije nego stigne do ukupnih iznosa

// TXLSCalculator.GetValueItem, grana binarne aritmetike (lxCalc.pas)
if VarIsNull(Value) then Value := 0;
if VarIsNull(SubValue) then SubValue := 0;
// Miješana Int64/Double Variant aritmetika može promovirati u Currency.
// Spreadsheet aritmetika mora zadržati floating-point preciznost.
if VarIsNumeric(Value) then Value := Double(Value);
if VarIsNumeric(SubValue) then SubValue := Double(SubValue);

Zaštita se izvršava prije SA_ADD, SA_SUB, SA_MUL i SA_DIV jednako, a regresija Arithmetic_MixedInt64AndDoubleKeepsPrecision sprema Int64(120) u A1 i 1066.1854641400994 u B1, zatim provjerava gniježđenu razliku i zbroj na 1E-10 te umnožak i kvocijent na 1E-8 i 1E-12. HotXLS ne tvrdi da poznaje svako pravilo promocije koje RTL primjenjuje na miješane Variant tipove kroz verzije kompajlera; tvrdi da je spreadsheet aritmetika IEEE double, i sada oba operanda pretvara u double prije nego ih operator vidi, što to pitanje uklanja

Što popravak garantira, a što ne

Nakon v2.382.4 obje arhitekture enginea vraćaju lxOk za zatrovan predložak, svih 4805 keširanih vrijednosti poklapa se s nezavisnim očekivanjem redak po redak unutar 1E-7, a tvrdnje da su keševi doista bili zatrovani, da je hash izvora nepromijenjen i da je svaka formula još prisutna — sve vrijede. Nije omogućena nikakva iteracija niti je potisnut ijedan kod greške da bi se to postiglo. Stvarni ciklus kroz ime, =B1 u A1 dok B1 još čita Vertical, i dalje vraća grešku, a test NamedScalarRanges_IntersectWithoutFalseCycles završava upravo tom tvrdnjom

Granice valja reći otvoreno. Implicit intersection primjenjuje se samo na ime čija je kompilirana definicija, nakon skidanja zagrada, jednosstupčano ili jednoredno područje na jednom listu; dvodimenzionalno ime u skalarnoj poziciji daje #VALUE!, kao u Excelu, a funkcija koju tablica ne poznaje dobiva klasu 0 iz FunctionArgumentClass, pa se njeni argumenti imena i dalje raširuju u cijelosti. Mekano poredanje je preferencija, a ne garancija: ciklus samo od scan bridova i dalje se evaluira u stabilnom poretku i čita što je keširano, što je ponašanje koje je članak o lookup scanu namjerno prihvatio. A rezultat na razini cijelog predloška provjeren je protiv nezavisne skripte očekivanja, a ne protiv drugog spreadsheet enginea, jer referentni uredski paket nije završio preračunavanje izvornog predloška unutar budžeta od 60 sekundi. HotXLS je izvorna Delphi i C++Builder spreadsheet komponenta koja čita, preračunava i piše XLS, XLSX, ODS i CSV bez instaliranog Excela; siječenje imena, tablica klasa argumenata i mekano scan poredanje primjenjuju se na svaki format jer je engine za izračun zajednički, a trenutna pokrivenost funkcija navedena je na stranici proizvoda HotXLS Delphi spreadsheet komponente