HotXLS, die native Excel-Spreadsheet-Komponente für Delphi und C++Builder, hat im September 2026 zwei verwandte AGGREGATE-Fixes ausgeliefert. Version 2.382.0 korrigierte das Options-Argument, sodass Codes 1/3/5/7 versteckte Zeilen ignorieren, 2/3/6/7 Fehler ignorieren und 0 bis 3 verschachtelte SUBTOTAL- und AGGREGATE-Zellen ignorieren – exakt so, wie Microsoft es dokumentiert. Version 2.382.3 hinderte dann diese Selektions-Flags daran, in die Auswertung genau der Zellen zu leaken, die die Funktion referenziert. Der erste Defekt ist peinlich auf die Art, wie Tabellen-Abschreib-Bugs es immer sind: Die Bitpositionen waren vertauscht, also bekam jede Formel mit einem Optionscode ungleich null eine Policy, die ihr Autor nie bestellt hat. Der zweite ist interessanter, denn es ist eine Form, der Sie in jedem Evaluator begegnen, der ein transientes Feld nutzt, um Kontext in einen rekursiven Durchlauf zu reichen. Eine äußere Aggregation bewaffnet ein Flag, läuft einen Range ab und zieht eine Zelle, deren Formel noch nicht berechnet wurde. Diese Formel läuft auf demselben Calculator, sieht dasselbe bewaffnete Flag und aggregiert stillschweigend die falschen Zeilen – das Ergebnis ist eine Zahl, die um einen Betrag danebenliegt, den niemand allein aus dem Formeltext erklären kann
Was wählen die AGGREGATE-Optionen 0 bis 7 tatsächlich aus?
Das Options-Argument von AGGREGATE ist eine Drei-Bit-Matrix, und die drei Bits sind unabhängig. Bit 0 (Wert 1) heißt versteckte Zeilen ignorieren, Bit 1 (Wert 2) heißt Fehlerwerte ignorieren, und Bit 2 (Wert 4) heißt aufhören, verschachtelte SUBTOTAL- und AGGREGATE-Zellen zu ignorieren, denn das Überspringen ist bei den niedrigen Codes der Default. Zwei Dinge daran sind leicht verdreht zu verstehen. Das Versteckt-Zeilen-Bit ist das niederwertige Bit, nicht das mittlere, also ist AGGREGATE(9,1,...) die Form für gefilterte Summen und AGGREGATE(9,2,...) die fehlertolerante. Und die Nested-Aggregate-Policy ist invertiert relativ zu den anderen beiden: Nur die Codes 4 bis 7 behandeln eine Zelle, deren eigene Formel ein SUBTOTAL oder AGGREGATE ist, als gewöhnlichen Wert. ECMA-376 Part 1 §18.17.7 definiert SUBTOTAL mit derselben Einschließen-oder-Ausschließen-Splitzung bei versteckten Zeilen über die Codes 1-11 und 101-111, und AGGREGATE, in OOXML-Dateien unter dem _xlfn.-Präfix gespeichert, verallgemeinert diese Splitzung ins Options-Argument, also ist die Tabelle, die Microsoft für die AGGREGATE-Funktion veröffentlicht, der Kontrakt, den eine Engine erfüllen muss, und keine Gefälligkeit
| Option | Versteckte Zeilen | Fehlerwerte | Verschachtelte SUBTOTAL / AGGREGATE |
|---|---|---|---|
| 0 | eingeschlossen | propagiert | ignoriert |
| 1 | ignoriert | propagiert | ignoriert |
| 2 | eingeschlossen | ignoriert | ignoriert |
| 3 | ignoriert | ignoriert | ignoriert |
| 4 | eingeschlossen | propagiert | eingeschlossen |
| 5 | ignoriert | propagiert | eingeschlossen |
| 6 | eingeschlossen | ignoriert | eingeschlossen |
| 7 | ignoriert | ignoriert | eingeschlossen |
Warum hatte HotXLS die AGGREGATE-Optionen verdreht?
Weil das ursprüngliche TXLSCalculator.CalcAggregateFunc aus einer Paraphrase der Tabelle geschrieben war statt aus der Tabelle. Es berechnete ignoreErrors := (optCode >= 4) and (optCode <= 7) und bewaffnete das Versteckt-Zeilen-Gate für die Codes 2, 3, 6 und 7, während die Nested-Aggregate-Policy überhaupt nicht implementiert war. Der frühere Artikel zu SUBTOTAL- und AGGREGATE-Versteckt-Zeilen listete diese Lücke als offene Grenze und beschrieb das alte Mapping, wie es damals ausgeliefert war; die Beschreibung traf auf den Code zu und nicht auf Excel, und niemand ist lange darauf aufmerksam geworden, weil die beiden Policies, die die meisten kombinieren – versteckt plus Fehler –, in beiden Tabellen auf die Codes 3 und 7 fallen. Nur ein Einzel-Bit-Code hat den Tausch entlarvt: AGGREGATE(9,1,A1:A4) lieferte die ungefilterte Summe, und AGGREGATE(9,2,...) übersprang versteckte Zeilen, während es weiterhin #DIV/0! propagierte. Der Defekt kam aus einer statischen Durchsicht von lxCalc.pas, geloggt als HXLS-008 im Known-Issues-Register des Projekts, nicht aus einer Kundendatei – was etwas darüber sagt, wie selten die Einzel-Bit-Codes in Produktions-Workbooks auftauchen. Version 2.382.0 schrieb das Dekodieren als drei Mengenzugehörigkeitstests um und ergänzte ein zweites Gate für die Nested-Policy, verdrahtet über einen neuen TXLSIsSubtotalCell-Callback, den das Workbook neben TXLSIsRowHidden bereitstellt
// TXLSCalculator.CalcAggregateFunc, Form ab v2.382.3
if (optCode < 0) or (optCode > 7) then
begin
Result := lxErrorValue; // Excel weist Codes außerhalb 0..7 zurück
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
// ... function_num auf das innere iftab mappen, ref1..refN ablaufen ...
finally
FIgnoreHiddenRows := prevIgnoreHidden;
FIgnoreSubtotalCells := prevIgnoreSubtotal;
end;
Beachten Sie, dass die beiden Flags bedingungslos zugewiesen werden statt nur gesetzt zu werden, wenn die Option es verlangt. Die Version v2.382.0 nutzte noch if ... then FIgnoreHiddenRows := True, was bedeutete, dass ein AGGREGATE mit Code 4, verschachtelt in einem SUBTOTAL(109, ...), das äußere Versteckt-Zeilen-Gate erbte, statt es zu löschen. Den dekodierten Wert beim Eintritt zuzuweisen und den vorherigen Wert im finally-Block wiederherzustellen lässt jeden AGGREGATE-Aufruf seine Policy für die Dauer seines Durchlaufs besitzen und sonst nichts. Version 2.382.0 machte auch die Array-Form ehrlich: Evaluiert ein Argument zu einem ein- oder zweidimensionalen Variant-Array, läuft CalcAggregateFunc jetzt über jedes Element und wendet die Fehler-Policy pro Element an, wo der alte Code nur auf ein NaN-Double testete und sonst das ganze Array an ExcelSum durchreichte
Warum leakt ein äußeres AGGREGATE in die Formeln, die es referenziert?
Weil FIgnoreHiddenRows und FIgnoreSubtotalCells Felder am Calculator sind, und der Calculator von jeder Formel geteilt wird, die während einer Neuberechnung evaluiert wird. Die Gates waren als Scratch-Felder entworfen, gerade damit sechs Zellablauf-Schleifen sie konsultieren konnten, ohne einen Parameter durch jede Signatur zu ziehen, und dieses Design ist solange stimmig, wie alles, was läuft, während ein Gate bewaffnet ist, zur Aggregation gehört, die es bewaffnet hat. Die Annahme bricht an einem konkreten Punkt: FGetValue. Fragt ein Walker das Workbook nach einem Zellwert und die Zelle hält eine Formel ohne gecachtes Ergebnis, kompiliert das Workbook die Formel und evaluiert sie auf der Stelle, auf demselben TXLSCalculator, mit den äußeren Gates noch gesetzt. Das Regression-Fixture in HotXLS.WorkbookApiTests.pas zeigt das Versagen mit vier Zellen. A1 hält 10, A2 hält 20 in einer versteckten Zeile, A3 hält =1/0, und A4 hält =SUBTOTAL(9,A1:A2), dessen korrekter Wert 30 ist. Evaluiert man nun =AGGREGATE(9,7,A1:A4): versteckte Zeilen ignorieren, Fehler ignorieren, das verschachtelte Subtotal als Wert zählen. Excel liefert 10 + 30 = 40. Mit ungecachedem A4 bewaffnete die Engine vor 2.382.3 das Versteckt-Zeilen-Gate, lief zu A4, löste dessen Evaluation aus, und CalcSubtotalFunc für Code 9 erbte das bewaffnete Gate, denn er setzt den Flag nur für die Codes 101 bis 111 und löscht ihn nie. A4 evaluierte zu 10 statt 30, und die äußere Summe kam als 20 zurück. Nichts in einer der beiden Formeln erwähnt versteckte Zeilen auf dem Pfad, der die falsche Zahl produziert hat
Das Nested-Aggregate-Gate leakte auf dieselbe Weise in die andere Richtung. Bei den Codes 0 bis 3 ist FIgnoreSubtotalCells bewaffnet, und der generische Range-Walker in GetValueItemRange ehrt es, also würde eine Präzedenz, deren Formel =SUM(B1:B3) ist, stillschweigend B2 fallen lassen, falls B2 zufällig ein SUBTOTAL enthielt. Schlimmer noch: CalcSubtotalFunc setzt FIgnoreSubtotalCells beim Austritt auf False, statt den vorherigen Wert wiederherzustellen, also entwaffnete eine ungecachede SUBTOTAL-Präzedenz, mitten im Durchlauf erreicht, das äußere Gate für jede Zelle nach ihr. Das Known-Issues-Register des Projekts führt das unter HXLS-008 als nested selection state leakage, und das ist der richtige Name für diese Bugklasse: ein globales transientes Flag, das für den Rahmen korrekt ist, der es setzt, und für jeden Rahmen falsch, der es erbt
Wie AggregateGetCellValue und AggregateGetItemValue den Durchlauf isolieren
Der Fix in v2.382.3 legt eine Grenze um jeden Punkt, an dem AGGREGATE einen Wert liest, den es nicht selbst berechnet hat. TXLSCalculator.AggregateGetCellValue hüllt den rohen FGetValue-Aufruf ein: Sie sichert beide Flags, löscht sie, führt den Fetch aus und stellt sie in einem finally-Block wieder her. Die äußere Aggregation wendet ihre eigene Policy weiterhin auf die Zelle an, die sie gerade gezogen hat, denn die Versteckt-Zeilen- und Nested-Cell-Tests passieren im Walker um den Fetch herum, aber die Präzedenzformel selbst läuft ohne jede Policy – genau wie bei Excel
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; // eine Präzedenzformel besitzt ihre eigene Policy
FIgnoreSubtotalCells := False;
try
Result := FGetValue(SheetIndex, Row, Col, Value, OutOfRange);
finally
FIgnoreHiddenRows := Hidden;
FIgnoreSubtotalCells := Nested;
end;
end;
AggregateGetItemValue tut dasselbe für Nicht-Range-Argumente, und es muss mehr tun als Flags zu löschen, denn ein Argument wie A1:A4/(B1:B4-20) ist ein berechnetes Array, dessen Elementform überleben muss. Der Wrapper materialisiert einen nackten Range über AggregateGetCellValue in ein zweidimensionales Variant-Array und mappt eine Zelle, die einen Fehlercode zurückgab, auf VarAsError, sodass die Fehler-Policy weiterhin pro Element angewendet werden kann, und er rekursiert durch die binären und unären Operator-Knoten (SA_ADD, SA_DIV, SA_UNARMINUS und die übrigen) mit ApplyArrayBinaryOp und ApplyArrayUnaryOp; alles andere fällt durch zum normalen GetValueItem. Zwei Guards sitzen vor der Materialisierung: Ein Range größer als EffectiveFormulaArrayMemoryLimit gibt lxErrorResourceLimit zurück, und ein Multi-Sheet- oder invertierter Range gibt #VALUE! zurück. Ein Resource-Limit-Code wird bewusst nicht als ignorierbarer Zellfehler behandelt, auch unter den Optionen 2/3/6/7, denn eine Engine, die ihr eigenes Out-of-Memory-Signal verschluckte, weil der Benutzer bat, #N/A zu überspringen, würde lügen. Alle drei AGGREGATE-Walker, AggregateCollectRange für die SUM-Familie, AggregateReduceVariance für STDEV, VAR und PRODUCT sowie AggregateReduceWithK für MEDIAN und die Quantilformen, wurden von FGetValue und GetValueItem auf die beiden Wrapper umgestellt, und jeder bekam den Nested-Cell-Test über FIsSubtotalCell
Welchen Fehler gibt AGGREGATE zurück, wenn es Fehler nicht ignoriert?
Den ursprünglichen, seit v2.382.3. Version 2.382.0 erkannte Fehlerzellen korrekt, kollabierte aber jede davon zu lxErrorValue, also lieferte AGGREGATE(9,4,A1:A3) über eine #DIV/0!-Zelle #VALUE!, wo Excel den ersten Fehler, dem es begegnet, unverändert propagiert. Der Ersatzhelfer AggregateErrorCode mappt ein Variant auf den passenden lxError*-Code, ob das Variant ein echtes varError ist oder einer der sieben Fehler-Strings, und AggregateValueIsError ist jetzt nur noch ein Test auf ein Ergebnis ungleich null. Jeder Walker notiert den ersten Fehlercode, den er sieht, und gibt diesen Code zurück, was auch bedeutet, dass eine Zelle, deren Formel nie berechnet wurde und deren Fehler deshalb als Rückgabecode von FGetValue ankommt statt als gecachtes Variant, sich genauso propagiert wie eine gecachte. Zwei Zählfunktionen bekommen in AggregateCollectRange eine Sonderbehandlung, und die Behandlung folgt SUBTOTAL statt SUM. Für die innere Funktion 0, COUNT, wird eine Fehlerzelle nie gezählt und nie propagiert, ungeachtet des Optionscodes, denn COUNT zählt nur Zahlen. Für die innere Funktion 169, COUNTA, ist eine Fehlerzelle ein nicht-leerer Wert und zählt als 1, außer der Optionscode ignoriert Fehler, in welchem Fall sie übersprungen wird. Diese Asymmetrie ist auch außerhalb von AGGREGATE, wie Excel COUNT und COUNTA behandelt, und sie ist die Sorte Detail, die eine generische „wenn Fehler dann propagiere“-Regel stillschweigend falsch bekommt
Was die Acht-Optionen-Regression-Matrix verifiziert
Das oben beschriebene Fixture wird als volle Matrix in AggregateFunc_OptionMatrixCoversHiddenErrorsAndNestedAggregates gefahren: Für jeden Optionscode von 0 bis 7 evaluiert es sowohl die SUM-Form als auch die MEDIAN-Form über A1:A4 und prüft das Ergebnis gegen eine von Hand abgeleitete Erwartung. Die Codes 0, 1, 4 und 5 müssen das #DIV/0! aus A3 propagieren, denn keiner von ihnen ignoriert Fehler. Code 2 liefert SUM 30 und MEDIAN 15, aus 10 und 20 mit übersprungenem verschachteltem A4. Code 3 liefert 10 und 10. Code 6 liefert 60 und 20, denn die 30 in A4 zählt jetzt. Code 7 liefert 40 und 20 – der Fall, der vor dem Leak-Fix 20 zurückgab. Der weitere Abnahme-Lauf, im Known-Issues-Register festgehalten, deckt alle neunzehn Funktionsnummern gegen alle acht Codes ab, mit jeder Präzedenz sowohl gecached als auch ungecached, für 304 Szenarien auf Win32 und 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)'; // Gruppen-Subtotal = 30
Sheet.RowHidden[2] := True;
Sheet.Cells[6, 1].Formula := '=AGGREGATE(9,1,A1:A4)'; // #DIV/0! versteckte übersprungen, Fehler propagiert
Sheet.Cells[7, 1].Formula := '=AGGREGATE(9,3,A1:A4)'; // 10 versteckt + Fehler + verschachtelt übersprungen
Sheet.Cells[8, 1].Formula := '=AGGREGATE(9,6,A1:A4)'; // 60 nur Fehler übersprungen
Sheet.Cells[9, 1].Formula := '=AGGREGATE(9,7,A1:A4)'; // 40 vor v2.382.3 war es 20
Book.Recalculate;
Book.SaveAs('aggregate-options.xlsx');
finally
Book.Free;
end;
end;
Wo die Grenze weiterhin liegt
Drei Grenzen sollte man kennen, bevor man darauf aufbaut. Erstens ist das Nested-Aggregate-Prädikat textuell. TXLSXWorkbook.GetCalcIsSubtotalCell und sein Classic-Engine-Zwilling antworten True, wenn die Formel einer Zelle mit SUBTOTAL(, AGGREGATE( oder _xlfn.AGGREGATE( beginnt, mit oder ohne führendes Gleichheitszeichen, also wird eine Formel wie =IF(C1,SUBTOTAL(9,B1:B9),0) oder =SUBTOTAL(9,B1:B9)*2 nicht als verschachtelt erkannt und von den Codes 0 bis 3 doppelt gezählt, wo Excel sie überspringen würde; ein Generator, der berechnete Subtotals emittiert, sollte den Aggregationsaufruf am Kopf der Formel halten. Zweitens lebt die Isolation in den drei AGGREGATE-Walkern. CalcSubtotalFunc läuft weiterhin durch GetValueItemRange, CollectRangeValues und SubtotalReduceVariance, die FGetValue direkt aufrufen, also kann ein SUBTOTAL(109, ...), dessen Range eine ungecachede Präzedenzformel enthält, sein Versteckt-Zeilen-Gate weiterhin in diese Präzedenz hineinreichen. Ein volles Recalculate evaluiert Präzedenzen vor Abhängigen, also wird der gecachte Pfad genommen, und das Gate wird nie geerbt; die Exposition beschränkt sich auf Ad-hoc-Evaluation über Calculate und auf Workbooks, die ohne gecachte Werte geladen wurden, und wenn Sie sich auf die inkrementelle Neuberechnung über den Dependency Graph verlassen, um große Modelle reaktionsschnell zu halten, ist dieselbe Ordnungsgarantie das, was dieses Leak schlafend hält. Drittens sind beide Gates an Assigned(FIsRowHidden) und Assigned(FIsSubtotalCell) geknüpft. Beide Workbook-Fassaden verdrahten die Callbacks in ihren Konstruktoren, aber Code, der einen TXLSCalculator von Hand mit nur den zwei ursprünglichen Argumenten baut, bekommt das Legacy-Verhalten alles einzuschließen für jeden Optionscode, stillschweigend. Wenn eine Summe falsch aussieht und der Formeltext richtig aussieht, ist die Evaluation Schritt für Schritt nachzuverfolgen der schnellste Weg zu sehen, ob eine Präzedenz unter einem geerbten Gate evaluiert wurde oder ob ein Callback schlicht nie angeschlossen wurde
Die hier beschriebene Berechnungs-Engine, der Options-Decoder, die isolierten Fetch-Wrapper und die Regression-Matrix, die alles festnagelt, kommen als Quelltext mit der HotXLS Delphi spreadsheet component daher, die XLS-, XLSX- und ODS-Workbooks in Delphi und C++Builder liest, schreibt und neu berechnet, ohne Excel-Installation