Technischer Artikel

AGGREGATE-Optionen und Nested-Gates in HotXLS für Delphi

Vier Zellen genügen, damit =AGGREGATE(9,7,A1:A4) in einem handgestrickten Evaluator 20 zurückgibt, während Excel 40 zeigt. 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), ein Gruppen-Subtotal im Wert von 30. Option 7 bedeutet: versteckte Zeilen überspringen, Fehler überspringen, verschachtelte Subtotals als Werte zählen – Excel addiert also 10 und 30. Die 20 erscheint, wenn die Einstellung „versteckte Zeilen überspringen“ des äußeren Aufrufs noch eingeschaltet ist, während A4 seinen eigenen Wert berechnet, was A4 auf 10 schrumpfen lässt. Auf 40 zu kommen verlangt zwei Dinge: die drei Optionsbits an den richtigen Positionen zu dekodieren und sicherzustellen, dass eine referenzierte Formel nie die Optionen der Formel sieht, die sie referenziert

Was wählen die AGGREGATE-Optionen 0 bis 7 tatsächlich aus?

Das Options-Argument ist eine Drei-Bit-Matrix, und die Bits sind voneinander unabhängig. Bit 0 (Wert 1) überspringt versteckte Zeilen, Bit 1 (Wert 2) überspringt Fehlerwerte, und Bit 2 (Wert 4) hört auf, Zellen zu überspringen, deren eigene Formel ein SUBTOTAL oder AGGREGATE ist, 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, also ist AGGREGATE(9,1,...) die gefilterte Summe und AGGREGATE(9,2,...) die fehlertolerante. Und die Nested-Policy läuft entgegengesetzt zu den anderen beiden: Nur die Codes 4 bis 7 behandeln eine verschachtelte Aggregation als gewöhnliche Zahl. 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; AGGREGATE, in OOXML unter dem _xlfn.-Präfix gespeichert, faltet diese Splitzung ins Options-Argument, und die Tabelle, die Microsoft für die Funktion veröffentlicht, ist der Kontrakt, den eine Engine erfüllen muss

OptionVersteckte ZeilenFehlerwerteVerschachtelte SUBTOTAL / AGGREGATE
0eingeschlossenpropagiertignoriert
1ignoriertpropagiertignoriert
2eingeschlossenignoriertignoriert
3ignoriertignoriertignoriert
4eingeschlossenpropagierteingeschlossen
5ignoriertpropagierteingeschlossen
6eingeschlossenignorierteingeschlossen
7ignoriertignorierteingeschlossen

Jeder Code außerhalb von 0 bis 7 wird mit #VALUE! zurückgewiesen, und genau das tut Excel auch

Warum das Dekodieren gerade an Einzel-Bit-Codes scheitert

Ein Dekodieren, das aus einer Paraphrase der Tabelle statt aus der Tabelle selbst geschrieben wurde, sieht typischerweise so aus: Fehler ab Code 4 aufwärts ignorieren, versteckte Zeilen für 2, 3, 6 und 7 überspringen, und die Nested-Policy komplett weglassen. Diese Version überlebt lange in Produktion. Die Kombination, die die meisten tatsächlich nutzen – versteckte Zeilen plus Fehler –, landet unter beiden Lesarten bei den Codes 3 und 7, also zeigt sich der Tausch nur an einem Einzel-Bit-Code. AGGREGATE(9,1,A1:A4) liefert dann die ungefilterte Summe, und AGGREGATE(9,2,...) überspringt versteckte Zeilen, während es weiterhin #DIV/0! propagiert. Echte Workbooks nutzen die Codes 1 und 2 selten allein, und genau deshalb findet ein Review gegen die veröffentlichte Tabelle mehr als das Warten auf eine Kundendatei

HotXLS dekodiert die Optionen als drei Mengenzugehörigkeitstests, einen pro Spalte der Tabelle, und weist alle drei Ergebnisse zu, wenn der AGGREGATE-Aufruf startet, und stellt beim Ende wiederher, was vorher in Kraft war. Die Zuweisung ist absichtlich bedingungslos. Ein als „wenn die Option es verlangt, schalte das Flag ein“ geschriebenes Dekodieren ließe ein AGGREGATE mit Code 4, das in einem SUBTOTAL(109, ...) sitzt, die äußere Versteckt-Zeilen-Einstellung erben, statt sie zu löschen. Die Skizze unten zeigt dieselbe Regel für alle, die einen eigenen Evaluator schreiben oder reviewen

// Illustrarisches Dekodieren für Ihren eigenen Evaluator: TAggPolicy und
// DecodeAggregateOptions sind Namen, die Sie selbst vergeben, keine HotXLS-API
type
  TAggPolicy = record
    IgnoreHidden, IgnoreErrors, SkipNested: Boolean;
  end;

function DecodeAggregateOptions(OptCode: Integer; out P: TAggPolicy): Boolean;
begin
  Result := (OptCode >= 0) and (OptCode <= 7);   // alles andere ist #VALUE!
  if not Result then
    Exit;
  P.IgnoreHidden := OptCode in [1, 3, 5, 7];       // Bit 0
  P.IgnoreErrors := OptCode in [2, 3, 6, 7];       // Bit 1
  P.SkipNested   := OptCode in [0, 1, 2, 3];       // Bit 2 gelöscht
end;
Zwei AGGREGATE-Options-Dekodierungen im Vergleich: Ein Paraphrase-Dekodieren bewaffnet das Versteckt-Zeilen-Gate für die Codes 2, 3, 6, 7 und ignoriert Fehler ab 4 aufwärts ohne Nested-Policy, während das HotXLS-Dekodieren versteckte Zeilen in 1, 3, 5, 7, Fehler in 2, 3, 6, 7 und Nested-Skips in 0 bis 3 testet
Nur Einzel-Bit-Codes entlarven den Tausch, weil die beliebte Versteckt-plus-Fehler-Kombination in beiden Tabellen auf die Codes 3 und 7 fällt, und Codes außerhalb 0 bis 7 geben #VALUE! zurück, exakt so, wie Excel sie zurückweist

Die SUBTOTAL-Seite derselben Versteckt-Zeilen-Regeln – auch, wie sich gefilterte Zeilen und manuell versteckte Zeilen verhalten – ist in dem Artikel zu SUBTOTAL und AGGREGATE bei versteckten Zeilen beschrieben

Warum greift ein äußeres AGGREGATE in die Formeln ein, die es referenziert?

Weil der naheliegende Weg, „versteckte Zeilen überspringen“ an sechs oder sieben Zellablauf-Schleifen weiterzureichen, ein Feld am Calculator-Objekt ist, und ein einziger Calculator jede Formel in einem Neuberechnungsdurchlauf evaluiert. Dieses Design trägt so lange, wie alles, was läuft, während das Feld gesetzt ist, zur Aggregation gehört, die es gesetzt hat. Es hört an einem konkreten Moment auf: dem Zell-Fetch. Wenn der Walker einen Zellwert anfordert und die Zelle eine Formel ohne gecachtes Ergebnis hält, kompiliert das Workbook diese Formel und evaluiert sie auf der Stelle, auf demselben Calculator, mit den äußeren Einstellungen noch in Kraft

Spielen Sie das Eröffnungsbeispiel durch eine solche Engine durch. AGGREGATE mit Code 7 schaltet das Überspringen versteckter Zeilen ein und startet bei A1. A1 liefert 10, A2 ist versteckt und wird übersprungen, A3 ist ein Fehler und wird übersprungen. Dann erreicht der Walker A4, das noch nicht berechnet wurde. A4s SUBTOTAL(9,A1:A2) läuft, und eine SUBTOTAL-Implementierung, die das Überspringen versteckter Zeilen nur für die Codes 101 bis 111 einschaltet (ein völlig vernünftiger Weg, sie zu schreiben), schaltet es für Code 9 nie aus. Sie findet die Einstellung schon eingeschaltet vor, überspringt A2 und liefert 10. Die äußere Summe wird 10 + 10 = 20. Nichts in A4s Formel erwähnt versteckte Zeilen

Wie ein äußeres AGGREGATE ohne Isolierung in seine Präzedenzen greift: Mit eingeschaltetem Überspringen versteckter Zeilen für Code 7 erreicht der Durchlauf das ungecachede A4 mit SUBTOTAL 9 über A1:A2, der Zell-Fetch evaluiert es auf demselben Calculator, das verschachtelte SUBTOTAL erbt die Einstellung und liefert 10 statt 30, also meldet die Summe 20, wo Excel 40 liefert
Die Nested-Einstellung leakt auch in die andere Richtung: Ein SUBTOTAL, das das Überspringen verschachtelter Zellen beim Austritt abschaltet, statt den vorgefundenen Zustand zurückzuschreiben, entwaffnet die äußere Policy für jede Zelle nach einem mitten im Durchlauf erreichten ungecacheden Subtotal

Die Nested-Aggregate-Einstellung leakt in beide Richtungen. Bei den Codes 0 bis 3 ist sie eingeschaltet, und ein generischer Range-Walker, der sie beehrt, würde eine Präzedenz wie =SUM(B1:B3) B2 fallen lassen, falls B2 zufällig ein SUBTOTAL enthält. In die andere Richtung schaltet ein SUBTOTAL, das das Überspringen verschachtelter Zellen am Ende abschaltet, statt den früheren Zustand wiederherzustellen, die Policy des äußeren AGGREGATE für den Rest seines Durchlaufs ab. Jeder Evaluator, der ein transientes Feld nutzt, um Kontext in einen rekursiven Durchlauf zu reichen, hat diese Form; das Feld ist korrekt für den Rahmen, der es setzt, und falsch für jeden Rahmen, der es erbt

Die Policy an der Fetch-Grenze abschneiden

HotXLS legt eine Grenze um jeden Punkt, an dem AGGREGATE einen Wert liest, den es nicht selbst berechnet hat. Vor dem Fetch werden beide Einstellungen gesichert und gelöscht, danach in einem finally-Block wiederhergestellt. 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. Die referenzierte Formel läuft ohne jede Policy, was zu Excel passt. Alle drei Walker-Familien gehen durch diese Grenze: die SUM-Familie (SUM, AVERAGE, COUNT, COUNTA, MAX, MIN), die geschlossenen Reduktionen (PRODUCT, STDEV, VAR) und die Formen mit einem k-Argument (MEDIAN, LARGE, SMALL, die Perzentile und Quartile)

// Illustrarischer Walker für Ihren eigenen Evaluator. FPolicy, FetchCell,
// IsRowHidden und IsSubtotalCell sind Member, die Sie selbst definieren
function TMyEvaluator.FetchForAggregate(Sheet, Row, Col: Integer;
  var V: Variant): Integer;
var
  Saved: TAggPolicy;
begin
  Saved := FPolicy;
  FPolicy := Default(TAggPolicy);     // die referenzierte Formel besitzt ihre eigene Policy
  try
    Result := FetchCell(Sheet, Row, Col, V);
  finally
    FPolicy := Saved;
  end;
end;

// innerhalb des Range-Durchlaufs: die äußere Policy wird um den Fetch herum angewendet
for R := R1 to R2 do
begin
  if FPolicy.IgnoreHidden and IsRowHidden(Sheet, R) then
    Continue;
  for C := C1 to C2 do
  begin
    if FPolicy.SkipNested and IsSubtotalCell(Sheet, R, C) then
      Continue;
    Code := FetchForAggregate(Sheet, R, C, V);
    // ... die Fehler-Policy auf Code / V anwenden, dann akkumulieren ...
  end;
end;
Die HotXLS-Isolation: Vor jedem Zell-Fetch werden beide Selektionseinstellungen gesichert und gelöscht, der Wert gelesen, und beide in einem finally-Block wiederhergestellt, also evaluiert eine Präzedenzformel ohne jede Policy, während der äußere Walker weiterhin Versteckt-Zeilen- und Nested-Cell-Tests um den Fetch herum anwendet
Berechnete Array-Argumente bekommen dieselbe Grenze und behalten einen Fehler pro Element für jede Zelle, die scheiterte, während ein Resource-Limit-Ergebnis unter den Ignore-Errors-Optionen nie als ignorierbarer Fehler behandelt wird

Argumente, die keine schlichten Ranges sind, brauchen mehr als gelöschte Flags. Ein Argument wie A1:A4/(B1:B4-20) ist ein berechnetes Array, und seine Elementform muss überleben, damit Option 6 genau die Elemente fallen lassen kann, die durch null teilten. HotXLS verwandelt jeden referenzierten Range durch dieselbe Grenze in ein zweidimensionales Array, speichert eine gescheiterte Zelle als Fehler-Element, statt abzubrechen, und wendet die arithmetischen und Vergleichsoperatoren Element für Element an. Ein Range über mehrere Sheets oder einer mit invertierten Ecken gibt #VALUE! zurück. Ein Range, der zu groß für TXLSCalculator.FormulaArrayMemoryLimit ist (standardmäßig 64 MB), liefert ein Resource-Limit-Ergebnis, und dieses Ergebnis liegt bewusst außerhalb der Fehlermenge, die die Optionen 2, 3, 6 und 7 überspringen dürfen. Das eigene Out-of-Memory-Signal der Engine zu verschlucken, weil die Formel bat, #N/A zu ignorieren, würde eine souveräne, falsche Zahl produzieren, also scheitert die Formel stattdessen

Welchen Fehler gibt AGGREGATE zurück, wenn es Fehler nicht ignoriert?

Den ersten, dem es begegnet, unverändert. Jeden Fehler zu #VALUE! zusammenzufalten ist der bequeme Kurzschluss, und er lässt AGGREGATE(9,4,A1:A3) über eine #DIV/0!-Zelle von Excel abweichen, das #DIV/0! durchreicht. HotXLS mappt sowohl echte Fehlervarianten als auch die sieben Fehler-Strings (#NULL!, #DIV/0!, #VALUE!, #REF!, #NAME?, #NUM!, #N/A) auf ihre eigenen Codes zurück, und jeder Walker gibt den ersten Code zurück, den er notiert. Eine Zelle, deren Formel nie berechnet wurde, liefert ihren Fehler als Rückgabecode aus dem Fetch statt als gecachter Wert; sie propagiert sich exakt wie eine gecachte

Zwei Zählfunktionen folgen SUBTOTAL statt SUM. Für Funktion 2, COUNT, wird eine Fehlerzelle nie gezählt und nie propagiert, ganz gleich welcher Optionscode, denn COUNT zählt nur Zahlen. Für Funktion 3, COUNTA, ist eine Fehlerzelle ein nicht-leerer Wert und zählt als 1, außer der Optionscode überspringt Fehler. Diese Asymmetrie gilt auch außerhalb von AGGREGATE, denn so behandelt Excel COUNT und COUNTA ebenfalls, und eine pauschale „wenn Fehler, dann propagiere“-Regel bekommt beide falsch

Alle acht Optionen gegen ein einziges Arbeitsblatt prüfen

Die vier Eröffnungszellen ergeben eine kompakte Prüfung über jeden Code: Für jeden Code von 0 bis 7 evaluiert man die SUM-Form und die MEDIAN-Form über A1:A4 und vergleicht mit von Hand ausgerechneten Werten. Die Codes 0, 1, 4 und 5 müssen das #DIV/0! aus A3 propagieren, denn keiner von ihnen überspringt 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 – und das ist der Code, den eine Engine ohne Fetch-Grenze als 20 meldet. Die Prüfung ist nur aussagekräftig, wenn A4 beim Lauf des AGGREGATE ungecached ist, also evaluieren Sie jeden Fall einmal mit bereits berechneten Präzedenzen und einmal ohne

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 und 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       20 bedeutet, eine Einstellung leakt in A4
    Sheet.Cells[10, 1].Formula := '=AGGREGATE(12,6,A1:A4)'; // 20     MEDIAN von 10, 20, 30
    Book.Recalculate;
    Book.SaveAs('aggregate-options.xlsx');
  finally
    Book.Free;
  end;
end;

Öffnet man die gespeicherte Datei in Excel und drückt Strg+Alt+F9, gibt das eine zweite Meinung zu denselben Zellen. Für einen breiteren Sweep lassen Sie jede Funktionsnummer von 1 bis 19 gegen alle acht Codes laufen

Die Callbacks verdrahten, wenn Sie den Calculator selbst bauen

Beide Einstellungen hängen an zwei Callbacks. TXLSCalculator.Create nimmt nach dem Value-Getter einen TXLSIsRowHidden und einen TXLSIsSubtotalCell entgegen, und beide defaulten auf nil. Fehlt einer von ihnen, greift die entsprechende Spalte der Tabelle nie, also schließt jeder Optionscode versteckte Zeilen, verschachtelte Aggregationen oder beides ein. Die XLSX- und Classic-XLS-Workbook-Objekte reichen ihre eigenen Implementierungen in ihren Konstruktoren durch; Code, der einen TXLSCalculator über seine eigene Datenquelle von Hand anlegt, muss beide liefern. Die Workbooks erkennen eine verschachtelte Aggregation an einer Formel, die mit SUBTOTAL(, AGGREGATE( oder _xlfn.AGGREGATE( beginnt, mit oder ohne führendes Gleichheitszeichen, und eine eigene Quelle sollte dieselbe Regel verwenden

// TMySheetSource und FormulaAt sind Ihre eigenen Typen; die drei Methoden-
// signaturen entsprechen TXLSGetValue, TXLSIsRowHidden und TXLSIsSubtotalCell
// (SheetIndex 1-basiert, Row und Col nullbasiert, wie die Workbooks sie übergeben)
function TMySheetSource.IsSubtotalCell(SheetIndex, Row, Col: Integer): Boolean;
var
  F: string;
begin
  F := FormulaAt(SheetIndex, Row, Col);
  if (F <> '') and (F[1] = '=') then
    Delete(F, 1, 1);
  Result := SameText(Copy(F, 1, 9), 'SUBTOTAL(') or
            SameText(Copy(F, 1, 10), 'AGGREGATE(') or
            SameText(Copy(F, 1, 16), '_xlfn.AGGREGATE(');
end;

// Extern ist die Sheet-Namen-Tabelle, die Ihr Host bereits führt
Calc := TXLSCalculator.Create(Extern, Source.GetCellValue,
  Source.IsRowHidden, Source.IsSubtotalCell);

Die Präfix-Regel hat eine Konsequenz für generierte Workbooks. Ein Generator, der =ROUND(SUBTOTAL(9,B2:B9),2) schreibt, produziert eine Zelle, die der Präfix-Test nicht erkennt, also zählen die Codes 0 bis 3 sie, und die Gruppe landet doppelt in der Gesamtsumme. Halten Sie den SUBTOTAL- oder AGGREGATE-Aufruf ganz außen und verlagern Sie das Runden ins Zahlenformat. Ein volles Recalculate evaluiert Präzedenzen vor Abhängigen, also lesen Aggregationen normalerweise gecachte Werte, und dieselbe Ordnung gilt unter der inkrementellen Neuberechnung über den Dependency Graph. Wenn eine Summe immer noch falsch aussieht und der Formeltext richtig aussieht, zeigt die Schritt-für-Schritt-Verfolgung der Evaluation, ob eine Präzedenz on demand innerhalb des Durchlaufs lief oder ob ein Callback nie angeschlossen wurde

Der Options-Decoder, die Fetch-Grenze und die hier beschriebenen Callback-Hooks kommen mit der HotXLS Delphi spreadsheet component, die XLS-, XLSX- und ODS-Workbooks aus Delphi- und C++Builder-Code liest, schreibt und neu berechnet, ohne dass Excel auf der Maschine installiert ist