Teknisk artikel

AGGREGATE-optionsmatrice og gate-leak i HotXLS til Delphi

HotXLS, den native Excel-regnearkskomponent til Delphi og C++Builder, shippede to relaterede AGGREGATE-fixes i september 2026. Version 2.382.0 korrigerede options-argumentet, så koderne 1/3/5/7 ignorerer skjulte rækker, 2/3/6/7 ignorerer fejl, og 0 til 3 ignorerer nestede SUBTOTAL- og AGGREGATE-celler, præcis som Microsoft dokumenterer. Version 2.382.3 stoppede derefter de valgflags fra at lække ind i evalueringen af netop de celler, funktionen refererer. Den første defekt er pinlig på den måde, tabelafskrivningsbugs altid er: bitpositionerne var byttet om, så hver formel, der brugte en optionskode forskellig fra nul, fik en politik, dens forfatter ikke bad om. Den anden er mere interessant, for det er en form, du vil møde i enhver evaluator, der bruger et transient felt til at give kontekst med ind i et rekursivt gennemløb. En ydre aggregation armerer et flag, gennemløber et range og trækker en celle, hvis formel endnu ikke er beregnet. Den formel kører på samme lommeregner, ser samme armerte flag og aggregerer i stilhed de forkerte rækker og producerer et tal, der er forkert med en mængde, ingen kan forklare ud fra formelteksten alene

Hvad vælger AGGREGATE-options 0 til 7 faktisk?

Options-argumentet til AGGREGATE er en tre-bit matrice, og de tre bits er uafhængige. Bit 0 (værdi 1) betyder ignorér skjulte rækker, bit 1 (værdi 2) betyder ignorér fejlværdier, og bit 2 (værdi 4) betyder stop med at ignorere nestede SUBTOTAL- og AGGREGATE-celler, for at springe dem over er default for de lave koder. To ting ved dette er lette at bytte om. Hidden-row-bitten er den lave bit, ikke den midterste, så AGGREGATE(9,1,...) er den filtrerede totalform, og AGGREGATE(9,2,...) er den fejltolerante. Og nested-aggregate-politikken er invers i forhold til de andre to: kun koderne 4 til 7 behandler en celle, hvis egen formel er en SUBTOTAL eller AGGREGATE, som en almindelig værdi. ECMA-376 Part 1 §18.17.7 definerer SUBTOTAL med samme include-eller-ekskluder hidden-row-split på tværs af koderne 1-11 og 101-111, og AGGREGATE, gemt i OOXML-filer under _xlfn.-præfikset, generaliserer det split ind i options-argumentet, så tabellen, Microsoft publicerer for AGGREGATE-funktionen, er den kontrakt, en engine skal opfylde snarere end en bekvemmelighed

OptionSkjulte rækkerFejlværdierNested SUBTOTAL / AGGREGATE
0inkluderetpropageretignoreret
1ignoreretpropageretignoreret
2inkluderetignoreretignoreret
3ignoreretignoreretignoreret
4inkluderetpropageretinkluderet
5ignoreretpropageretinkluderet
6inkluderetignoreretinkluderet
7ignoreretignoreretinkluderet

Hvorfor havde HotXLS AGGREGATE-options omvendt?

Fordi den originale TXLSCalculator.CalcAggregateFunc var skrevet ud fra en parafrase af tabellen snarere end tabellen. Den beregnede ignoreErrors := (optCode >= 4) and (optCode <= 7) og armerede hidden-row-gaten for koderne 2, 3, 6 og 7, mens nested-aggregate-politikken slet ikke var implementeret. Den tidligere artikel om SUBTOTAL- og AGGREGATE-hidden rows listede det hul som en åben grænse og beskrev den gamle mapping, som den da shippede; beskrivelsen var korrekt om koden og forkert om Excel, og ingen bemærkede det i lang tid, for de to politikker, de fleste kombinerer, hidden plus errors, lander på koderne 3 og 7 under begge tabeller. Kun en single-bit-kode udstillede byttet: AGGREGATE(9,1,A1:A4) returnerede den ufiltrerede sum, og AGGREGATE(9,2,...) sprang skjulte rækker over, mens den stadig propagerede #DIV/0!. Defekten kom frem ved en statisk gennemgang af lxCalc.pas, logget som HXLS-008 i projektets known-issues-register, ikke fra en kundefil, hvilket siger noget om, hvor sjældent single-bit-koderne optræder i produktionsworkbooks. Version 2.382.0 omskrev dekodningen som tre set-medlemskabstests og tilføjede en anden gate til nested-politikken, koblet gennem et nyt TXLSIsSubtotalCell-callback, som workbook'en leverer ved siden af TXLSIsRowHidden

HotXLS AGGREGATE-options-dekodningen før og efter v2.382.0: den originale CalcAggregateFunc armerede hidden-row-gaten for koderne 2, 3, 6, 7 og ignorerede fejl fra 4 og op uden nested-politik, mens den korrigerede dekodning tester skjulte rækker i 1, 3, 5, 7, fejl i 2, 3, 6, 7 og nested skips i 0 til 3
Kun single-bit-koder udstillede byttet, for den populære hidden-plus-errors-kombination lander på koderne 3 og 7 under begge tabeller, og koder uden for 0 til 7 returnerer nu lxErrorValue præcis som Excel afviser dem
// TXLSCalculator.CalcAggregateFunc, v2.382.3-form
if (optCode < 0) or (optCode > 7) then
begin
  Result := lxErrorValue;            // Excel afviser koder uden for 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
  // ... map function_num til den indre iftab, gennemløb ref1..refN ...
finally
  FIgnoreHiddenRows := prevIgnoreHidden;
  FIgnoreSubtotalCells := prevIgnoreSubtotal;
end;

Bemærk, at de to flags tildeles betingelsesløst snarere end kun sættes, når optionen beder om det. Versionen i v2.382.0 brugte stadig if ... then FIgnoreHiddenRows := True, hvilket betød, at en AGGREGATE med kode 4 nested inde i en SUBTOTAL(109, ...) arvede den ydre hidden-row-gate i stedet for at rydde den. At tildele den dekodede værdi ved indgang og gendanne den forrige værdi i finally-blokken gør, at hvert AGGREGATE-kald ejer sin politik i gennemløbets varighed og intet mere. Version 2.382.0 gjorde også array-formen ærlig: når et argument evaluerer til et én- eller todimensionelt Variant-array, gennemløber CalcAggregateFunc nu hvert element og anvender fejlpolitikken pr. element, hvor den gamle kode kun testede for en NaN double og ellers gav hele arrayet til ExcelSum

Hvorfor lækker en ydre AGGREGATE ind i de formler, den refererer?

Fordi FIgnoreHiddenRows og FIgnoreSubtotalCells er felter på lommeregneren, og lommeregneren deles af hver formel, der evalueres under én genberegning. Gaterne var designet som scratch-felter præcis for, at seks celle-gennemløbsløkker kunne konsultere dem uden at træde en parameter igennem hver signatur, og det design er sundt, så længe alt, hvad der kører, mens en gate er armert, tilhører den aggregation, der armerede den. Antagelsen bryder ved ét specifikt punkt: FGetValue. Når en walker beder workbook'en om en celleværdi, og den celle holder en formel uden cached resultat, kompilerer workbook'en formlen og evaluerer den på stedet, på den samme TXLSCalculator, med de ydre gates stadig sat. Regression-fixturen i HotXLS.WorkbookApiTests.pas viser fejlen med fire celler. A1 holder 10, A2 holder 20 på en skjult række, A3 holder =1/0, og A4 holder =SUBTOTAL(9,A1:A2), hvis korrekte værdi er 30. Evaluér nu =AGGREGATE(9,7,A1:A4): ignorér skjulte rækker, ignorér fejl, tæl den nestede subtotal som en værdi. Excel returnerer 10 + 30 = 40. Med A4 uncached armerede pre-2.382.3-engine hidden-row-gaten, gennemløb til A4, udløste dens evaluering, og CalcSubtotalFunc for kode 9 arvede den armerte gate, for den sætter kun flaget for koderne 101 til 111 og rydder det aldrig. A4 evaluerede til 10 i stedet for 30, og den ydre total kom tilbage som 20. Intet i nogen af formlerne nævner skjulte rækker på den vej, der producerede det forkerte tal

Hvordan en ydre HotXLS AGGREGATE lækkede ind i sine precedents: med FIgnoreHiddenRows armeret for kode 7 når gennemløbet uncached A4, der holder SUBTOTAL 9 over A1:A2, FGetValue evaluerer den på samme lommeregner, CalcSubtotalFunc arver gaten og returnerer 10 i stedet for 30, så totalen rapporterer 20, hvor Excel returnerer 40
Den nestede gate lækkede også den anden vej, og CalcSubtotalFunc nulstillede FIgnoreSubtotalCells ved exit i stedet for at gendanne den, hvilket afvæbnede den ydre politik for hver celle, efter en uncached subtotal blev nået midt i gennemløbet

Nested-aggregate-gaten lækkede på samme måde i den anden retning. Med koderne 0 til 3 er FIgnoreSubtotalCells armeret, og den generiske range-walker i GetValueItemRange ærer den, så en precedent, hvis formel er =SUM(B1:B3), ville lydløst droppe B2, hvis B2 tilfældigvis indeholdt en SUBTOTAL. Værre, CalcSubtotalFunc nulstiller FIgnoreSubtotalCells til False ved exit i stedet for at gendanne den forrige værdi, så en uncached SUBTOTAL-precedent, nået midt i gennemløbet, afvæbnede den ydre gate for hver celle efter den. Projektets known-issues-register filer dette under HXLS-008 som nested selection state leakage, og det er det rigtige navn for bug-klassen: et globalt transient flag, der er korrekt for den ramme, der satte det, og forkert for hver ramme, der arver det

Hvordan AggregateGetCellValue og AggregateGetItemValue isolerer gennemløbet

Fixet i v2.382.3 sætter en grænse omkring hvert punkt, hvor AGGREGATE læser en værdi, den ikke selv beregnede. TXLSCalculator.AggregateGetCellValue pakker det rå FGetValue-kald ind: den gemmer begge flags, rydder dem, udfører fetch'en og gendanner dem i en finally-blok. Den ydre aggregation anvender stadig sin egen politik på cellen, den lige fetchede, for hidden-row- og nested-cell-testene sker i walkeren omkring fetch'en, men precedent-formlen selv kører uden nogen politik overhovedet, hvilket er, hvad Excel gør

Isoleringen i HotXLS v2.382.3: AggregateGetCellValue gemmer begge gate-flags, rydder dem, fetcher gennem FGetValue og gendanner dem i en finally-blok, så en precedent-formel evaluerer uden nogen politik, mens den ydre walker stadig anvender hidden-row- og nested-cell-tests omkring fetch'en
AggregateGetItemValue gør det samme for beregnede array-argumenter og mapper fetch-fejl til VarAsError, mens en resource-limit-kode bevidst aldrig behandles som en ignorérbar fejl under ignore-errors-options
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;        // en precedent-formel ejer sin egen politik
  FIgnoreSubtotalCells := False;
  try
    Result := FGetValue(SheetIndex, Row, Col, Value, OutOfRange);
  finally
    FIgnoreHiddenRows := Hidden;
    FIgnoreSubtotalCells := Nested;
  end;
end;

AggregateGetItemValue gør det samme for ikke-range-argumenter, og den må gøre mere end at rydde flags, for et argument som A1:A4/(B1:B4-20) er et beregnet array, hvis elementform skal overleve. Wrapperen materialiserer et almindeligt range til et todimensionelt Variant-array gennem AggregateGetCellValue og mapper en celle, der returnerede en fejlkode, til VarAsError, så fejlpolitikken stadig kan anvendes pr. element, og den rekurrerer gennem de binære og unære operator-knuder (SA_ADD, SA_DIV, SA_UNARMINUS og resten) med ApplyArrayBinaryOp og ApplyArrayUnaryOp; alt andet falder igennem til den normale GetValueItem. To værn sidder foran materialiseringen: et range større end EffectiveFormulaArrayMemoryLimit returnerer lxErrorResourceLimit, og et multi-sheet eller inverteret range returnerer #VALUE!. En resource-limit-kode behandles bevidst ikke som en ignorerbar cellefejl, selv under options 2/3/6/7, da en engine, der slugte sit eget out-of-memory-signal, fordi brugeren bad om at springe #N/A over, ville lyve. Alle tre AGGREGATE-walkers, AggregateCollectRange til SUM-familien, AggregateReduceVariance til STDEV, VAR og PRODUCT, og AggregateReduceWithK til MEDIAN og kvantilformerne, blev skiftet fra FGetValue og GetValueItem til de to wrappers, og hver fik nested-cell-testen gennem FIsSubtotalCell

Hvilken fejl returnerer AGGREGATE, når den ikke ignorerer fejl?

Den originale, siden v2.382.3. Version 2.382.0 detekterede fejlceller korrekt men kollapsede hver af dem til lxErrorValue, så AGGREGATE(9,4,A1:A3) over en #DIV/0!-celle returnerede #VALUE!, hvor Excel propagerer den første fejl, den møder, uændret. Erstatnings-helperen AggregateErrorCode mapper en Variant til den matchende lxError*-kode, uanset om Varianten er en ægte varError eller én af de syv error-strenge, og AggregateValueIsError er nu blot en test for et resultat forskelligt fra nul. Hver walker registrerer den første fejlkode, den ser, og returnerer den kode, hvilket også betyder, at en celle, hvis formel aldrig blev beregnet, og hvis fejl derfor ankommer som en returkode fra FGetValue snarere end som en cached Variant, propagerer på samme måde som en cached. To tællefunktioner får særlig behandling inde i AggregateCollectRange, og behandlingen matcher SUBTOTAL snarere end SUM. For indre funktion 0, COUNT, tælles og propageres en fejlcelle aldrig uanset optionskoden, for COUNT tæller kun tal. For indre funktion 169, COUNTA, er en fejlcelle en ikke-tom værdi og tæller som 1, medmindre optionskoden ignorerer fejl, i hvilket tilfælde den springes over. Den asymmetri er, hvordan Excel behandler COUNT og COUNTA uden for AGGREGATE også, og det er den slags detalje, en generisk "hvis fejl så propager"-regel i stilhed tager fejl af

Hvad den otte-options regressionsmatrice verificerer

Fixturen beskrevet ovenfor øves som en fuld matrice i AggregateFunc_OptionMatrixCoversHiddenErrorsAndNestedAggregates: for hver optionskode fra 0 til 7 evaluerer den både SUM-formen og MEDIAN-formen over A1:A4 og tjekker resultatet op mod en håndudledt forventning. Koderne 0, 1, 4 og 5 skal propagere #DIV/0! fra A3, da ingen af dem ignorerer fejl. Kode 2 giver SUM 30 og MEDIAN 15, ud af 10 og 20 med den nestede A4 sprunget over. Kode 3 giver 10 og 10. Kode 6 giver 60 og 20, for de 30 i A4 tæller nu. Kode 7 giver 40 og 20, hvilket er det tilfælde, der returnerede 20 før leak-fixet. Det bredere acceptance-run, der er registreret i known-issues-registeret, dækker alle nitten funktionsnumre mod alle otte koder, med hver precedent både cached og uncached, for 304 scenarier på Win32 og 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)';   // gruppe-subtotal = 30
    Sheet.RowHidden[2] := True;

    Sheet.Cells[6, 1].Formula := '=AGGREGATE(9,1,A1:A4)'; // #DIV/0!  skjulte sprunget over, fejlen propagerer
    Sheet.Cells[7, 1].Formula := '=AGGREGATE(9,3,A1:A4)'; // 10       skjult + fejl + nested sprunget over
    Sheet.Cells[8, 1].Formula := '=AGGREGATE(9,6,A1:A4)'; // 60       kun fejl sprunget over
    Sheet.Cells[9, 1].Formula := '=AGGREGATE(9,7,A1:A4)'; // 40       var 20 før v2.382.3
    Book.Recalculate;
    Book.SaveAs('aggregate-options.xlsx');
  finally
    Book.Free;
  end;
end;

Hvor grænsen stadig er

Tre grænser er værd at kende, før du bygger videre på dette. For det første er nested-aggregate-prædikatet tekstligt. TXLSXWorkbook.GetCalcIsSubtotalCell og dets classic-engine-twin svarer True, når en celles formel starter med SUBTOTAL(, AGGREGATE( eller _xlfn.AGGREGATE(, med eller uden det indledende lighedstegn, så en formel som =IF(C1,SUBTOTAL(9,B1:B9),0) eller =SUBTOTAL(9,B1:B9)*2 genkendes ikke som nested og vil blive dobbelttalt af koderne 0 til 3, hvor Excel ville springe den over; en generator, der emitterer beregnede subtotals, bør holde aggregation-kaldet i hovedet af formlen. For det andet bor isoleringen i de tre AGGREGATE-walkers. CalcSubtotalFunc gennemløber stadig gennem GetValueItemRange, CollectRangeValues og SubtotalReduceVariance, som kalder FGetValue direkte, så en SUBTOTAL(109, ...), hvis range indeholder en uncached precedent-formel, kan stadig give sin hidden-row-gate videre til den precedent. En fuld Recalculate evaluerer precedents før dependents, så den cachede vej tages, og gaten aldrig arves; eksponeringen er begrænset til ad hoc-evaluering gennem Calculate og til workbooks loadet uden cachede værdier, og stoler du på inkrementel genberegning over afhængighedsgrafen for at holde store modeller responsive, er samme rækkefølgegaranti det, der holder dette leak dvælende. For det tredje er begge gates betinget af Assigned(FIsRowHidden) og Assigned(FIsSubtotalCell). Begge workbook-facades kobler callbacks i deres konstruktorer, men kode, der bygger en TXLSCalculator i hånden med kun de to originale argumenter, får legacy include-everything-adfærden for hver optionskode, lydløst. Når en total ser forkert ud, og formelteksten ser rigtig ud, er at spore evalueringen trin for trin den hurtigste måde at se, om en precedent blev evalueret under en arvet gate, eller om et callback simpelthen aldrig blev koblet

Beregningsmotoren beskrevet her, options-dekoderen, de isolerede fetch-wrappers og regressionsmatricen, der låser dem fast, følger alle med som kildekode med HotXLS Delphi spreadsheet-komponenten, som læser, skriver og genberegner XLS-, XLSX- og ODS-workbooks i Delphi og C++Builder uden en Excel-installation