Tehnički članak

HotXLS implicitna intersekcija definisanih imena u Delphiju

Definisano ime koje se odnosi na celu kolonu Excel čita kao jednu ćeliju kada se pojavi u skalarnoj poziciji: =Vertical+1 u redu 7 znači „ćelija imena Vertical u redu 7", a ne cela oblast. HotXLS Delphi Component primenjuje tu implicitnu intersekciju u v2.382.4 na dva nivoa, pri evaluaciji i pri izvlačenju zavisnosti, jer je predložak kredita sa 4805 formula pokazao da tačna vrednost nije dovoljna. Kada prolaz kroz zavisnosti razvije ime u punu oblast, formula nizvodno koja napaja bilo koju ćeliju te oblasti zatvara ciklus koji ne postoji, i TXLSXWorkbook.Recalculate odbija celu radnu svesku

Predložak o kojem je reč je radna sveska za amortizaciju kredita. Sa svakom keširanom vrednošću zatrovanom na 777 i punim Recalculate pokretanjem, obe arhitekture engine-a vratile su 23, što je lxErrorRef, kod za kružnu referencu. 3842 od 4805 formula nisu se poklopile sa nezavisnim očekivanjem, B18 je držao #VALUE!, E18 je i dalje bio 777, a broj rata u J7 pročitao je placeholdere u nedovršenoj koloni salda. Tri odvojena defekta krila su se iza jednog povratnog koda, i ovaj članak prolazi kroz svaki uz izvorni kod koji ga je ispravio

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

Zato što graf zavisnosti poznaje samo grane, a grana od formule do oblasti sa 480 redova je 480 grana, od kojih jedna vodi nazad kroz ćeliju koja zavisi od te formule. Uzmite =IF(TRUE,Vertical+1,0) u B1 sa Vertical definisanim kao Inputs!$A$1:$A$2, i =B1+1 u A2. Excel evaluira B1 kao A1+1 a A2 kao B1+1, pravi lanac. Prolaz koji zabeleži da B1 zavisi od A1:A2 čini A2 prethodnikom B1, A2 već navodi B1 kao prethodnika, i Kahn red koji pokreće inkrementalno preračunavanje u HotXLS-u nikad ne vidi nijedan čvor sa ulaznim stepenom nula. Od tog obrasca su napravljeni predlošci kredita: svaki red perioda referencira imenovane kolone za saldo, kamatu i broj rata, svako ime pokriva ceo plan otplate, a svaki red i upisuje u te kolone. Razvijte imena i graf je jedna ogromna jako povezana komponenta. Evaluirajte ih sa implicitnom intersekcijom i graf je skup kratkih lanaca, po jedan za svaki red, što je i ono što ECMA-376 Part 1 §18.17.2 opisuje za referentni operand koji se koristi tamo gde se traži jedna vrednost

Zašto je ime kolone zatvorilo lažni ciklus u HotXLS-u: sa Vertical definisanim kao Inputs!$A$1:$A$2 prolaz beleži B1 kao zavisno od A1:A2 dok A2 već navodi B1 kao prethodnika, pa se Kahn red nikad ne isprazni, dok intersekcija svodi B1 na ćeliju u redu A1 i čuva lanac po redu A2, B1, A1 koji Recalculate ordenuje
Razvijanje imena pretvorilo je graf u jednu ogromnu jako povezanu komponentu, a evaluacija istih formula sa implicitnom intersekcijom pretvara ga u kratke lance, po jedan za svaki red plana otplate
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 redu 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 trpi intersekciju, pa je ovo A2
    Sheet.Cells[2, 2].Formula := '=Alias';
    // Argument referentne klase: cela oblast se sabira, bez intersekcije
    Sheet.Cells[3, 2].Formula := '=SUM(Vertical)';
    // Red 6 leži izvan A1:A2, intersekcija je prazna i IFERROR je 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
      // Pre v2.382.4 ova grana je bila nedostupna: B1 -> A2 -> B1 bio je ciklus
    end;
  finally
    Book.Free;
  end;
end;

Kako HotXLS odlučuje da je argument skalaran?

HotXLS čita odgovor iz tabele funkcija, a ne iz oblika argumenta. Svaki unos u TXLSFormula.InitFuncHash registruje se preko THashFunc.SetValue sa opcionim 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 nivou funkcije. Nova TXLSFormula.FunctionArgumentClass(APtg, AArgument) izlaže taj bajt preko THashFuncEntry.ArgClass, a rezultat 1 znači klasu vrednosti. To su iste tri klase koje [MS-XLS] §2.2.2 dodeljuje tokenima operanada, i enkoder je već zavisio od njih: 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 fajl koji je napisao Excel čuva tu klasu u svakom referentnom tokenu, pa engine čija se tabela poklapa sa specifikacijom može da odgovori „da li je ovaj argument skalaran" bez gledanja u podatke. Srednji argument funkcije SUMIF je kriterijum, vrednost; prvi i treći su oblasti, reference. SUMPRODUCT je registrovan sa klasom 2 na nivou funkcije, array, i zato =SUMPRODUCT(Vertical,Vertical) i dalje množi celu oblast

Tri funkcije ne konsultuju sopstveni unos u tabeli za bilo šta posle prvog argumenta. IF (ptg 1), CHOOSE (ptg 100) i IFERROR (ptg 255) propuštaju kroz sebe ono što izaberu, pa njihovi argumenti grana nasleđuju klasu pozicije koju sama funkcija zauzima. To jedno pravilo omogućava da =CHOOSE(1,Vertical,0) u G2 razreši na A2 dok =SUMIF(Vertical,">0",Vertical) pored njega i dalje sabira oba reda, i to pravilo plan amortizacije najviše koristi, jer se njegove ćelije perioda oslanjaju na IF da provere da li je kredit još otvoren

Odakle HotXLS čita klase argumenata za implicitnu intersekciju: IF registruje 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 što daje PtgRef, PtgRefV i PtgRefA, a pass-through funkcije IF, CHOOSE i IFERROR nasleđuju klasu pozicije koju zauzimaju
Pošto se tabela klasa poklapa sa specifikacijom, engine može da odgovori da li je argument skalaran bez gledanja u podatke, a to što se CHOOSE razrešava na A2 pored SUMIF-a koji sabira oba reda sledi iz jednog pravila

Nošenje klase kroz prolaz kroz zavisnosti

Ekstraktor zavisnosti u lxCalc.pas je rekurzivni Walk nad kompajliranim sintaksnim stablom, i postoji dvaput, jednom u TXLSCalculator.ExtractDependencies za graf na nivou radne sveske i jednom u ExtractWorkspaceDependencies za graf između radnih sveska. v2.382.4 daje obama prolazima dva dodatna parametra. AScalar počinje kao True u korenu formule, ponovo se računa za svako dete funkcije iz FunctionArgumentClass, i prosleđuje se nepromenjen za argumente grane ptg 1, 100 i 255. ANameRoot postaje True samo kada prolaz zaroni u kompajliranu definiciju imena, i preživljava samo kroz SA_GROUP čvorove, zagrade, pa ime definisano kao =A1:A2+1 nije pomešano sa običnom oblašću. Kada su obe zastavice True u čvoru SA_RANGE, AddResolvedRange svodi oblast istim helper-om koji koristi i evaluator pre nego što zabeleži zavisnost. Helper je dovoljno kratak da se citira ceo

Odluka IntersectNamedScalarRange koja čuva zavisnosti imena u HotXLS-u: oblast koja je već jedna ćelija prolazi nepromenjeno, jedna kolona se svodi na red formule kada CurRow padne unutar nje, jedan red se svodi na kolonu formule, a sve ostalo, dvodimenzionalna oblast ili red van opsega, daje #VALUE! pri evaluaciji i ne beleži nikakvu zavisnost
I oba prolaza kroz zavisnosti i evaluator zovu isti helper, pa se vrednost koju formula čita i grana koju graf beleži nikad ne mogu razići oko imena na koje je primenjena intersekcija
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;                     // jedna kolona: uzmi ovaj red
    Exit(True);
  end;
  if (Row1 = Row2) and (CurCol >= Col1) and (CurCol <= Col2) then
  begin
    Col1 := CurCol; Col2 := CurCol;                     // jedan red: uzmi ovu kolonu
    Result := True;
  end;
end;

Sve što helper odbije, dvodimenzionalna oblast, referenca na više listova ili formula čiji red leži izvan imenovane kolone, daje #VALUE! na strani evaluacije i nikakvu zavisnost na strani grafa, što je i ono što Excel radi za praznu intersekciju. Strana evaluacije živi u TXLSCalculator.GetValueItemName: skida SA_GROUP omotače sa kompajlirane definicije, i ako je koren SA_RANGE, poziva GetRangeInfo, primenjuje intersekciju i dohvata tu jednu ćeliju preko FGetValue umesto da evaluira celu definiciju. Eksterne reference ostaju na staroj putanji, jer nema lokalnog reda sa kojim bi se presekle. Odakle inače dolaze smeštanje i opseg imena pokriveno je u članku o definisanim imenima i formulama između listova; ovde je poenta samo šta engine radi kada se ime razreši

Zašto je MATCH nad poluizračunatom kolonom pročitao 777?

Zato što je argument lookup-array funkcije MATCH scan referenca, a scan reference su namerno izuzete iz redosleda evaluacije. Članak o lookup scan-u uveo je TXLSDepRange.LookupScan i završio odeljkom o tome šta gubite izuzimanjem scan grana iz ordenacije: lookup formula može da se izvrši pre nego što se svaka ćelija u njenoj oblasti preračuna i da pročita zastarele vrednosti. U interaktivnoj sesiji to se slegne u sledećem prolazu. U batch preračunavanju zatrovanog predloška ne, i PaymentCount, definisan kao =MATCH(0.01,Balances,-1)+1, pročitao je 777 placeholdera koji su još stajali u koloni salda i vratio broj perioda koji nije mogao biti tačan

TXLSDepGraph.TopoOrder sada tretira scan grane kao meke grane ordenacije. Uz tvrdi ulazni stepen drži i niz ScanInDeg, brojeći prljave scan prethodnike po čvoru i umanjujući ga kako se ti prethodnici emituju, koristeći liste ScanPrecedents, ScanDependents i ScanPrecedentCount koje je prethodna izmena već čuvala. U svakoj iteraciji Kahn red pregleda svoj spremni prozor za prvim čvorom čiji je ScanInDeg nula i prebacuje ga na čelo; ako svaki spremni čvor još čeka scan prethodnika, čelo se skida u svom stabilnom redosledu. Scan grane nikad ne ulaze u tvrdi ulazni stepen, pa je samo-referencirajući VLOOKUP nad sopstvenom kolonom i dalje legalan, ali lookup koji je mogao da čeka prethodnika koji se može završiti sada i čeka. Regresija koja ovo prikiva, LookupScan_WaitsForDirtyFormulaValues, zatruje tri ćelije salda na 777 i očekuje da PaymentCount vrati 3, zatim obrće ulaz na nulu i očekuje da =IFERROR(PaymentCount,99) vidi #N/A i vrati 99

Odakle je došlo skraćivanje na četiri decimale?

Iz Delphi Variant aritmetike, i to samo u ugnježdenim pozicijama. Binarni operatori u TXLSCalculator.GetValueItem već su kopirali + ili - na vrhu u dve Double lokalne promenljive, pa je =B1-A1 bilo u redu. Unutar =IF(TRUE,B1-A1,0) isto oduzimanje izvršavalo se kao Value := Value - SubValue nad dva Variant-a, a kada je jedan operand bila Int64 vrednost ćelije a drugi Double, rezultat koji smo videli bio je Currency, fiksno-tačkasti tip sa četiri decimale, pa je 1066.1854641400994 minus 120 vraćeno skraćeno na četiri decimale. Kroz plan u kojem se svaka rata složi od prethodnog reda, ta greška prođe kroz stotine perioda pre nego što stigne do zbirova

// TXLSCalculator.GetValueItem, grana za binarnu aritmetiku (lxCalc.pas)
if VarIsNull(Value) then Value := 0;
if VarIsNull(SubValue) then SubValue := 0;
// Mešovita Int64/Double Variant aritmetika može da se promoviše u Currency.
// Spreadsheet aritmetika mora da zadrži preciznost pokretnog zareza.
if VarIsNumeric(Value) then Value := Double(Value);
if VarIsNumeric(SubValue) then SubValue := Double(SubValue);

Zaštita se izvršava pre SA_ADD, SA_SUB, SA_MUL i SA_DIV podjednako, a regresija Arithmetic_MixedInt64AndDoubleKeepsPrecision smešta Int64(120) u A1 i 1066.1854641400994 u B1, zatim proverava ugnježdenu razliku i zbir na 1E-10 a proizvod i količnik na 1E-8 i 1E-12. HotXLS ne tvrdi da zna svako pravilo promocije koje RTL primenjuje na mešovite Variant tipove kroz verzije kompajlera; tvrdi da je spreadsheet aritmetika IEEE double, i sada oba operanda čini double pre nego što ih operator vidi, čime pitanje uklanja

Šta ispravka garantuje, a šta ne

Posle v2.382.4 obe arhitekture engine-a vraćaju lxOk za zatrovani predložak, svih 4805 keširanih vrednosti poklapa se sa nezavisnim očekivanjem red po red unutar 1E-7, a tvrdnje da su keševi zaista bili zatrovani, da je hash izvora nepromenjen i da je svaka formula još prisutna sve stoje. Nijedna iteracija nije uključena i nijedan kod greške nije potisnut da bi se to postiglo. Pravi ciklus kroz ime, =B1 u A1 dok B1 još čita Vertical, i dalje vraća grešku, i test NamedScalarRanges_IntersectWithoutFalseCycles završava se upravo takvom tvrdnjom

Granice vredi reći jasno. Implicitna intersekcija primenjuje se samo na ime čija je kompajlirana definicija, po skidanju zagrada, oblast jedne kolone ili jednog reda na jednom listu; dvodimenzionalno ime u skalarnoj poziciji daje #VALUE!, kao u Excel-u, a funkcija koju tabela ne poznaje dobija klasu 0 iz FunctionArgumentClass, pa se njeni argumenti imena i dalje razvijaju u punoj meri. Meko ordeniranje je preferencija, ne garancija: ciklus samo od scan grana i dalje se evaluira u stabilnom redosledu i čita ono što je keširano, što je ponašanje koje je članak o lookup scan-u namerno prihvatio. A rezultat celog predloška verifikovan je prema nezavisnoj skripti očekivanja, ne prema drugom spreadsheet engine-u, jer referentni office paket nije završio preračunavanje originalnog predloška u budžetu od 60 sekundi. HotXLS je nativna Delphi i C++Builder spreadsheet komponenta koja čita, preračunava i piše XLS, XLSX, ODS i CSV bez instaliranog Excel-a; intersekcija imena, tabela klasa argumenata i meko scan ordeniranje primenjuju se na svaki format jer je engine za izračunavanje deljen, a trenutna pokrivenost funkcija navedena je na stranici HotXLS Delphi spreadsheet komponente