Tehnički članak

AGGREGATE opcije i curenje gatea u HotXLS-u za Delphi

HotXLS, izvorna Excel komponenta za proračunske tablice za Delphi i C++Builder, isporučio je u rujnu 2026. dva povezana AGGREGATE popravka. Verzija 2.382.0 ispravila je argument s opcijama tako da kodovi 1/3/5/7 ignoriraju skrivene retke, 2/3/6/7 ignoriraju greške, a 0 do 3 ignoriraju ugniježđene SUBTOTAL i AGGREGATE ćelije, točno onako kako Microsoft dokumentira. Verzija 2.382.3 zatim je zaustavila da se te zastavice odabira preliju u evaluaciju upravo onih ćelija koje funkcija referencira. Prva greška neugodna je na način na koji su greške u prepisivanju tablica uvijek neugodne: bit pozicije bile su zamijenjene, pa je svaka formula koja je koristila opcijski kod različit od nule dobila politiku koju njezin autor nije tražio. Druga je zanimljivija, jer je to oblik na koji ćete naići u svakom evaluatoru koji koristi prolazno polje za prenošenje konteksta u rekurzivni hod. Vanjska agregacija naoruža zastavicu, prođe kroz raspon i povuče ćeliju čija formula još nije izračunata. Ta formula izvršava se na istom kalkulatoru, vidi istu naoružanu zastavicu i tiho agregira pogrešne retke, dajući broj koji odstupa za iznos koji nitko ne može objasniti samo iz teksta formule

Što AGGREGATE opcije 0 do 7 zapravo odabiru?

Argument s opcijama funkcije AGGREGATE je matrica od tri bita, i ta tri bita neovisna su. Bit 0 (vrijednost 1) znači ignoriraj skrivene retke, bit 1 (vrijednost 2) znači ignoriraj vrijednosti grešaka, a bit 2 (vrijednost 4) znači prestani ignorirati ugniježđene SUBTOTAL i AGGREGATE ćelije, jer je njihovo preskakanje zadano za niske kodove. Dvije je stvari oko toga lako okrenuti naopako. Bit skrivenih redaka je niski bit, a ne srednji, pa je AGGREGATE(9,1,...) oblik filtriranog zbroja, a AGGREGATE(9,2,...) onaj tolerantan na greške. A politika ugniježđenih agregacija obrnuta je u odnosu na druge dvije: samo kodovi 4 do 7 tretiraju ćeliju čija je vlastita formula SUBTOTAL ili AGGREGATE kao običnu vrijednost. ECMA-376 Part 1 §18.17.7 definira SUBTOTAL s istom podjelom na uključivanje ili isključivanje skrivenih redaka kroz kodove 1-11 i 101-111, a AGGREGATE, spremljen u OOXML datotekama pod prefiksom _xlfn., tu podjelu generalizira u argument s opcijama, pa je tablica koju Microsoft objavljuje za funkciju AGGREGATE ugovor koji engine mora ispuniti, a ne pogodnost

OpcijaSkriveni reciVrijednosti grešakaUgniježđeni SUBTOTAL / AGGREGATE
0uključenipropagira seignoriraju se
1ignoriraju sepropagira seignoriraju se
2uključeniignoriraju seignoriraju se
3ignoriraju seignoriraju seignoriraju se
4uključenipropagira seuključeni
5ignoriraju sepropagira seuključeni
6uključeniignoriraju seuključeni
7ignoriraju seignoriraju seuključeni

Zašto je HotXLS imao AGGREGATE opcije naopako?

Zato što je izvorni TXLSCalculator.CalcAggregateFunc napisan prema parafrazi tablice, a ne prema tablici. Računao je ignoreErrors := (optCode >= 4) and (optCode <= 7) i naoružavao gate za skrivene retke za kodove 2, 3, 6 i 7, dok politika ugniježđenih agregacija nije bila implementirana uopće. Raniji članak o skrivenim recima u SUBTOTAL i AGGREGATE naveo je tu prazninu kao otvoreno ograničenje i opisao staro preslikavanje onako kako je tada bilo isporučeno; opis je bio točan o kodu, a pogrešan o Excelu, i nitko to dugo nije primijetio jer dvije politike koje većina ljudi kombinira, skriveno plus greške, padaju na kodove 3 i 7 pod obama tablicama. Samo je kod s jednim bitom razotkrio zamjenu: AGGREGATE(9,1,A1:A4) vraćao je nefiltrirani zbroj, a AGGREGATE(9,2,...) preskakao je skrivene retke dok je i dalje propagirao #DIV/0!. Greška je isplivala iz statičkog pregleda lxCalc.pas, evidentirana kao HXLS-008 u registru poznatih problema projekta, a ne iz datoteke kupca, što govori nešto o tome kako rijetko kodovi s jednim bitom pojavljuju u produkcijskim radnim knjigama. Verzija 2.382.0 prepisala je dekodiranje u tri testa pripadnosti skupu i dodala drugi gate za ugniježđenu politiku, spojen kroz novi TXLSIsSubtotalCell callback koji radna knjiga pruža uz TXLSIsRowHidden

Dekodiranje AGGREGATE opcija u HotXLS-u prije i nakon v2.382.0: izvorni CalcAggregateFunc naoružavao je gate za skrivene retke za kodove 2, 3, 6, 7 i ignorirao greške od 4 naviše bez ugniježđene politike, dok ispravljeno dekodiranje testira skrivene retke u 1, 3, 5, 7, greške u 2, 3, 6, 7 i ugniježđena preskakanja u 0 do 3
Samo su kodovi s jednim bitom razotkrili zamjenu jer popularna kombinacija skriveno plus greške pada na kodove 3 i 7 pod obama tablicama, a kodovi izvan 0 do 7 sada vraćaju lxErrorValue točno onako kako ih Excel odbija
// TXLSCalculator.CalcAggregateFunc, oblik iz v2.382.3
if (optCode < 0) or (optCode > 7) then
begin
  Result := lxErrorValue;            // Excel odbija kodove izvan 0..7
  Exit;
end;
ignoreErrors := optCode in [2, 3, 6, 7];
prevIgnoreHidden := FIgnoreHiddenRows;
prevIgnoreSubtotal := FIgnoreSubtotalCells;
FIgnoreHiddenRows := (optCode in [1, 3, 5, 7]) and Assigned(FIsRowHidden);
FIgnoreSubtotalCells := (optCode in [0, 1, 2, 3]) and Assigned(FIsSubtotalCell);
try
  // ... preslikaj function_num u unutarnji iftab, prođi ref1..refN ...
finally
  FIgnoreHiddenRows := prevIgnoreHidden;
  FIgnoreSubtotalCells := prevIgnoreSubtotal;
end;

Primijetite da se dvije zastavice dodjeljuju bezuvjetno, a ne samo postavljaju kad to opcija traži. Verzija 2.382.0 još je koristila if ... then FIgnoreHiddenRows := True, što je značilo da AGGREGATE s kodom 4 ugniježđen unutar SUBTOTAL(109, ...) nasljeđuje vanjski gate za skrivene retke umjesto da ga očisti. Dodjela dekodirane vrijednosti pri ulazu i vraćanje prethodne vrijednosti u finally bloku čine da svaki AGGREGATE poziv posjeduje svoju politiku za vrijeme trajanja svog hoda i ništa više. Verzija 2.382.0 učinila je i array oblik iskrenim: kad se argument evaluira u jednodimenzionalni ili dvodimenzionalni Variant array, CalcAggregateFunc sada prolazi kroz svaki element i primjenjuje politiku grešaka po elementu, dok je stari kod testirao samo na NaN double i inače predavao cijeli array funkciji ExcelSum

Zašto se vanjski AGGREGATE prelijeva u formule koje referencira?

Zato što su FIgnoreHiddenRows i FIgnoreSubtotalCells polja na kalkulatoru, a kalkulator dijeli svaka formula evaluirana tijekom jednog ponovnog izračuna. Gateovi su zamišljeni kao radna polja upravo zato da šest petlji za hod kroz ćelije može ih konzultirati bez provlačenja parametra kroz svaki potpis, i taj je dizajn ispravan dok god sve što se izvršava dok je gate naoružan pripada agregaciji koja ga je naoružala. Pretpostavka se lomi na jednoj konkretnoj točki: FGetValue. Kad walker zatraži od radne knjige vrijednost ćelije, a ta ćelija drži formulu bez cachiranog rezultata, radna knjiga kompajlira formulu i evaluira je odmah, na istom TXLSCalculatoru, s vanjskim gateovima još postavljenima. Regresijski uzorak u HotXLS.WorkbookApiTests.pas pokazuje kvar s četiri ćelije. A1 drži 10, A2 drži 20 na skrivenom retku, A3 drži =1/0, a A4 drži =SUBTOTAL(9,A1:A2), čija je ispravna vrijednost 30. Evaluirajte sada =AGGREGATE(9,7,A1:A4): ignoriraj skrivene retke, ignoriraj greške, računaj ugniježđeni subtotal kao vrijednost. Excel vraća 10 + 30 = 40. S necachiranim A4 engine prije v2.382.3 naoružao je gate za skrivene retke, došao do A4, pokrenuo njegovu evaluaciju, a CalcSubtotalFunc za kod 9 naslijedio je naoružani gate, jer on zastavicu postavlja samo za kodove 101 do 111 i nikad je ne čisti. A4 se evaluirao u 10 umjesto u 30, a vanjski zbroj vratio se kao 20. Ni jedna od tih dviju formula ne spominje skrivene retke na putu koji je proizveo pogrešan broj

Kako se vanjski HotXLS AGGREGATE prelio u svoje prethodnike: s naoružanim FIgnoreHiddenRows za kod 7 hod dolazi do necachiranog A4 koji drži SUBTOTAL 9 nad A1:A2, FGetValue evaluira ga na istom kalkulatoru, CalcSubtotalFunc nasljeđuje gate i vraća 10 umjesto 30, pa ukupan rezultat prijavljuje 20 ondje gdje Excel vraća 40
Ugniježđeni gate curio je i u drugom smjeru, a CalcSubtotalFunc resetirao je FIgnoreSubtotalCells na izlazu umjesto da ga vrati, razoružavajući vanjsku politiku za svaku ćeliju nakon necachiranog subtotala dosegnutog na pola hoda

Gate za ugniježđene agregacije curio je na isti način u drugom smjeru. S kodovima 0 do 3 FIgnoreSubtotalCells je naoružan, a generički walker raspona u GetValueItemRange ga poštuje, pa bi prethodnik čija je formula =SUM(B1:B3) tiho ispustio B2 ako bi B2 slučajno sadržavao SUBTOTAL. Još gore, CalcSubtotalFunc resetira FIgnoreSubtotalCells na False pri izlazu umjesto da vrati prethodnu vrijednost, pa je necachirani SUBTOTAL prethodnik dosegnut na pola hoda razoružao vanjski gate za svaku ćeliju nakon sebe. Registar poznatih problema projekta vodi to pod HXLS-008 kao curenje ugniježđenog stanja odabira, i to je pravo ime za tu klasu bugova: globalna prolazna zastavica koja je ispravna za okvir koji ju je postavio, a pogrešna za svaki okvir koji ju naslijedi

Kako AggregateGetCellValue i AggregateGetItemValue izoliraju hod

Popravak u v2.382.3 postavlja granicu oko svake točke gdje AGGREGATE čita vrijednost koju nije sam izračunao. TXLSCalculator.AggregateGetCellValue omata sirovi poziv FGetValue: sprema obje zastavice, čisti ih, obavlja dohvat i vraća ih u finally bloku. Vanjska agregacija i dalje primjenjuje svoju politiku na ćeliju koju je upravo dohvatila, jer se testovi skrivenih redaka i ugniježđenih ćelija događaju u walkeru oko dohvata, ali sama formula prethodnika izvršava se bez ikakve politike, što je ono što Excel radi

Izolacija u HotXLS-u v2.382.3: AggregateGetCellValue sprema obje gate zastavice, čisti ih, dohvaća kroz FGetValue i vraća ih u finally bloku, pa se formula prethodnika evaluira bez politike, dok vanjski walker i dalje primjenjuje testove skrivenih redaka i ugniježđenih ćelija oko dohvata
AggregateGetItemValue radi isto za izračunate array argumente i preslikava greške dohvata u VarAsError, dok se kod resursnog ograničenja namjerno nikad ne tretira kao greška koju se može ignorirati pod opcijama za ignoriranje grešaka
function TXLSCalculator.AggregateGetCellValue(SheetIndex, Row, Col: Integer;
  var Value: Variant; var OutOfRange: Boolean): Integer;
var
  Hidden, Nested: Boolean;
begin
  Hidden := FIgnoreHiddenRows;
  Nested := FIgnoreSubtotalCells;
  FIgnoreHiddenRows := False;        // formula prethodnika posjeduje vlastitu politiku
  FIgnoreSubtotalCells := False;
  try
    Result := FGetValue(SheetIndex, Row, Col, Value, OutOfRange);
  finally
    FIgnoreHiddenRows := Hidden;
    FIgnoreSubtotalCells := Nested;
  end;
end;

AggregateGetItemValue radi isto za argumente koji nisu rasponi, i mora učiniti više od čišćenja zastavica, jer je argument poput A1:A4/(B1:B4-20) izračunati array čiji oblik elemenata mora preživjeti. Wrapper materijalizira običan raspon u dvodimenzionalni Variant array kroz AggregateGetCellValue, preslikavajući ćeliju koja je vratila kod greške u VarAsError da se politika grešaka i dalje može primijeniti po elementu, i rekurzivno prolazi kroz čvorove binarnih i unarnih operatora (SA_ADD, SA_DIV, SA_UNARMINUS i ostale) s ApplyArrayBinaryOp i ApplyArrayUnaryOp; sve ostalo pada na uobičajeni GetValueItem. Dvije zaštite stoje pred materijalizacijom: raspon veći od EffectiveFormulaArrayMemoryLimit vraća lxErrorResourceLimit, a raspon preko više listova ili obrnut raspon vraća #VALUE!. Kod resursnog ograničenja namjerno se ne tretira kao greška ćelije koju se može ignorirati čak ni pod opcijama 2/3/6/7, jer bi engine koji proguta vlastiti signal o nedostatku memorije zato što je korisnik tražio da se preskoči #N/A lagao. Sva tri AGGREGATE walkera, AggregateCollectRange za obitelj SUM, AggregateReduceVariance za STDEV, VAR i PRODUCT, i AggregateReduceWithK za MEDIAN i kvantilne oblike, prebačena su s FGetValue i GetValueItem na ta dva wrappera, i svaki je dobio test ugniježđenih ćelija kroz FIsSubtotalCell

Koju grešku AGGREGATE vraća kad ne ignorira greške?

Izvornu, od v2.382.3. Verzija 2.382.0 detektirala je ćelije s greškom ispravno, ali ih je sve sažimala u lxErrorValue, pa je AGGREGATE(9,4,A1:A3) nad ćelijom s #DIV/0! vraćao #VALUE!, dok Excel propagira prvu grešku na koju naiđe nepromijenjenu. Zamjenski pomoćnik AggregateErrorCode preslikava Variant u odgovarajući lxError* kod, bio Variant pravi varError ili jedan od sedam stringova grešaka, a AggregateValueIsError sada je samo test na rezultat različit od nule. Svaki walker bilježi prvi kod greške koji vidi i vraća taj kod, što također znači da ćelija čija formula nikad nije izračunata, i čija greška zato stiže kao povratni kod iz FGetValue, a ne kao cachirani Variant, propagira se na isti način kao cachirana. Dvije funkcije brojanja dobivaju poseban tretman unutar AggregateCollectRange, i taj tretman odgovara SUBTOTALU, a ne SUMU. Za unutarnju funkciju 0, COUNT, ćelija s greškom nikad se ne broji i nikad se ne propagira bez obzira na opcijski kod, jer COUNT broji samo brojeve. Za unutarnju funkciju 169, COUNTA, ćelija s greškom je neprazna vrijednost i broji se kao 1, osim ako opcijski kod ignorira greške, u kojem se slučaju preskače. Ta asimetrija je način na koji Excel tretira COUNT i COUNTA i izvan AGGREGATEa, i to je vrsta pojedinosti koju generičko pravilo "ako je greška, propagiraj" tiho pogreši

Što provjerava regresijska matrica s osam opcija

Gore opisani uzorak vježba se kao puna matrica u AggregateFunc_OptionMatrixCoversHiddenErrorsAndNestedAggregates: za svaki opcijski kod od 0 do 7 evaluira i SUM oblik i MEDIAN oblik nad A1:A4 i provjerava rezultat prema ručno izvedenom očekivanju. Kodovi 0, 1, 4 i 5 moraju propagirati #DIV/0! iz A3, jer nijedan od njih ne ignorira greške. Kod 2 daje SUM 30 i MEDIAN 15, iz 10 i 20 uz preskočeni ugniježđeni A4. Kod 3 daje 10 i 10. Kod 6 daje 60 i 20, jer se 30 u A4 sada broji. Kod 7 daje 40 i 20, što je slučaj koji je vraćao 20 prije popravka curenja. Širi acceptance prolaz zabilježen u registru poznatih problema pokriva svih devetnaest brojeva funkcija protiv svih osam kodova, sa svakim prethodnikom i cachiranim i necachiranim, za 304 scenarija na Win32 i Win64

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Data');
    Sheet.Cells[1, 1].Value := 10;
    Sheet.Cells[2, 1].Value := 20;
    Sheet.Cells[3, 1].Formula := '=1/0';
    Sheet.Cells[4, 1].Formula := '=SUBTOTAL(9,A1:A2)';   // subtotal grupe = 30
    Sheet.RowHidden[2] := True;

    Sheet.Cells[6, 1].Formula := '=AGGREGATE(9,1,A1:A4)'; // #DIV/0!  skriveno preskočeno, greška se propagira
    Sheet.Cells[7, 1].Formula := '=AGGREGATE(9,3,A1:A4)'; // 10       skriveno + greška + ugniježđeno preskočeno
    Sheet.Cells[8, 1].Formula := '=AGGREGATE(9,6,A1:A4)'; // 60       preskočene samo greške
    Sheet.Cells[9, 1].Formula := '=AGGREGATE(9,7,A1:A4)'; // 40       bilo 20 prije v2.382.3
    Book.Recalculate;
    Book.SaveAs('aggregate-options.xlsx');
  finally
    Book.Free;
  end;
end;

Gdje je granica još danas

Tri ograničenja vrijedi znati prije nego na ovome gradite. Prvo, predikat ugniježđene agregacije je tekstualan. TXLSXWorkbook.GetCalcIsSubtotalCell i njegov blizanac u klasičnom engineu vraćaju True kad formula ćelije počinje s SUBTOTAL(, AGGREGATE( ili _xlfn.AGGREGATE(, sa ili bez vodećeg znaka jednakosti, pa se formula poput =IF(C1,SUBTOTAL(9,B1:B9),0) ili =SUBTOTAL(9,B1:B9)*2 ne prepoznaje kao ugniježđena i kodovi 0 do 3 će je brojiti dvaput ondje gdje bi je Excel preskočio; generator koji emitira izračunate subtotale trebao bi poziv agregacije držati na čelu formule. Drugo, izolacija živi u tri AGGREGATE walkera. CalcSubtotalFunc i dalje hoda kroz GetValueItemRange, CollectRangeValues i SubtotalReduceVariance, koji pozivaju FGetValue izravno, pa SUBTOTAL(109, ...) čiji raspon sadrži necachiranu formulu prethodnika još može prenijeti svoj gate za skrivene retke u tog prethodnika. Puni Recalculate evaluira prethodnike prije zavisnika, pa se ide cachiranim putem i gate se nikad ne nasljeđuje; izloženost je ograničena na ad hoc evaluaciju kroz Calculate i na radne knjige učitane bez cachiranih vrijednosti, a ako se oslanjate na inkrementalni ponovni izračun preko grafa ovisnosti da veliki modeli ostanu responzivni, upravo to jamstvo redoslijeda drži ovo curenje uspavanim. Treće, oba gatea uvjetovana su s Assigned(FIsRowHidden) i Assigned(FIsSubtotalCell). Oba facadea radne knjige spajaju callbackove u svojim konstruktorima, ali kod koji ručno gradi TXLSCalculator sa samo dva izvorna argumenta dobiva naslijeđeno ponašanje uključi-sve za svaki opcijski kod, tiho. Kad zbroj izgleda pogrešno, a tekst formule izgleda ispravno, praćenje evaluacije korak po korak najbrži je način da vidite je li prethodnik evaluiran pod naslijeđenim gateom ili callback jednostavno nikad nije bio priključen

Engine za izračun opisan ovdje, dekoder opcija, izolirani wrapperi za dohvat i regresijska matrica koja ih prikiva isporučuju se kao izvorni kod uz HotXLS Delphi komponentu za proračunske tablice, koja čita, zapisuje i ponovno izračunava XLS, XLSX i ODS radne knjige u Delphiju i C++Builderu bez instalacije Excela