Öffnet man eine Arbeitsmappe, die LibreOffice Calc gespeichert hat, kann eine Spalte, als hidden="false" geschrieben, versteckt zurückkommen, obwohl nichts in der Datei falsch ist. Unter xsd:boolean, dem Typ hinter diesem Attribut, sind true, false, 1 und 0 alle legal, mit oder ohne umgebende Leerzeichen; HotXLS für Delphi liest jede dieser Schreibweisen, wo immer ein XLSX-Schema ein Boolean deklariert
Die Hälfte von xsd:boolean, die Excel nie schreibt
W3C XML Schema Teil 2 §3.2.2 definiert boolean mit vier legalen Literalen, true, false, 1 und 0, und fixiert seine whiteSpace-Facette auf collapse, was bedeutet, dass customHeight=" 1 " genauso gültig ist wie customHeight="1". Derselbe Abschnitt nennt true und false als die kanonischen Formen. SpreadsheetML tut nichts, das einzuengen: sml.xsd deklariert customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix und Dutzende ihrer Nachbarn als plain xsd:boolean, die meisten mit explizitem Default false und einige, etwa showGridLines, mit Default true. Excel schreibt zufällig nur die nicht-kanonischen Ziffern. LibreOffice und manche andere Producer schreiben die Wörter, also trägt ein Paket von Calc ein <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/>, wo Excel customHeight="1" geschrieben hätte
Diese Asymmetrie ist der Grund, warum Ein-Ziffern-Vergleiche jahrelang in OOXML-Readern überleben. Baut man ein Testkorpus aus Excel-Output, ist jeder Boolean, den man je sehen wird, ein 0 oder 1. Der Vergleich besteht jeden Test. Dann erreicht eine Calc-Datei einen Nutzer. Packen Sie mindestens ein paar von LibreOffice gespeicherte Pakete in jedes XLSX-Korpus, das Reader-Tests speist — seine Schreibweise von Booleans ist einer von mehreren Orten, an denen sich die beiden Producer unterscheiden, dieselbe Kluft, die in Formeln und Regeln auftaucht, wenn man Arbeitsmappen über ODS zwischen Excel und LibreOffice bewegt. Ein weiteres Detail des Datentyps zählt für das Parser-Design: Die Literale sind case sensitive, also ist TRUE überhaupt kein gültiges xsd:boolean, ein Punkt, der zurückkommt, wenn man entscheidet, wie nachsichtig der eigene Reader sein soll
Was ein Ein-Zeichen-Vergleich falsch macht
Ein Reader, der ein Attribut gegen eine einzelne Ziffer vergleicht, scheitert in die eine oder die andere Richtung. Welche Richtung, hängt vom Default ab, den der Vergleich stillschweigend einbaut. Die Tabelle listet die Gestalten, die in handgeschriebenem OOXML-Code auftauchen, was jede mit den Wort-Schreibweisen macht, und einige Attribute, an denen der Schaden sichtbar wird. Sie lässt showDropDown aus, dessen Problem sein Name ist und nicht seine Schreibweise: Der Schema-Default ist false, doch showDropDown="1" auf einer Listen-Validation versteckt den In-Cell-Pfeil, also exponiert TXLSXDataValidation.ShowDropDown die nutzerseitige Bedeutung (True zeigt den Pfeil) und speichert das invertierte OOXML-Flag
| Naiver Test | Stillschweigender Default | Missdeutet | Wo es sich zeigt |
|---|---|---|---|
Value = '1' | false | true wird false | sheetFormatPr customHeight, das <v> einer t="b"-Zelle |
Value <> '0' | true, sobald vorhanden | false wird true | Spalten-hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false wird ignoriert, der Default bleibt an | sheetView showGridLines, sheetProtection-Permission-Schalter |
Copy(Raw, P + 5, 1) = '1' | false | true wird auf t gekappt | ChartML-Datentabellen-Ränder, Serien-smooth |
Die mittlere Zeile produziert den sichtbarsten Schaden, denn hidden="false" auf einem <col>-Element ist eine völlig normale Sache für einen Producer, der eine Breite für eine sichtbare Spalte aufzeichnet, und ein Reader, der jeden Nicht-null-Text als gesetzt behandelt, versteckt diese Spalte beim Laden. HotXLS führt jedes dieser Attribute durch eine einzige xsd:boolean-Konversion, die umgebende Leerzeichen trimmt, die Ziffern und die Wörter akzeptiert und für alles andere den eigenen Default des Attributs zurückgibt. Dieser letzte Zweig spiegelt eine Design-Entscheidung, die man aussprechen sollte: Ein Wert wie yes ist eine malformed Datei — aber eine Arbeitsmappe wegen eines schlechten Sichtbarkeits-Flags nicht zu öffnen würde den Nutzer für den Fehler des Producers bestrafen, also behält der Reader einen festen Fallback pro Attribut und macht weiter
Ein LibreOffice-artiges Worksheet mit HotXLS lesen
Die Flags auf Worksheet-Ebene erscheinen als gewöhnliche Properties auf TXLSXWorksheet: CustomHeight, ThickTop und ThickBottom kommen aus <sheetFormatPr>, während ColHidden[], RowHidden[], RowThickTop[] und RowThickBottom[] 1-basierte Spalten- und Zeilennummern nehmen. Eine Boolean-Zelle kommt als Boolean-Variante in TXLSXCell.Value an, welche Schreibweise das <v>-Element auch benutzt hat. Die Routine unten druckt sie alle für das erste Sheet, und die Liste danach gibt die erwartete Ausgabe für Calc-artiges Markup
uses
SysUtils, Variants, lxHandleX;
procedure DumpSheetBooleans(const FileName: string);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Flag: Variant;
Col, Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Book.Sheets.Count = 0 then
raise Exception.Create('Workbook has no sheets');
Sheet := Book.Sheets[1]; // der Sheets-Indexer ist 1-basiert
Writeln('Default row height is custom: ', Sheet.CustomHeight);
Writeln('Rows default to thick top / bottom: ',
Sheet.ThickTop, ' / ', Sheet.ThickBottom);
for Col := 1 to 8 do
if Sheet.ColHidden[Col] then
Writeln('Column ', Col, ' is hidden');
for Row := 1 to 20 do
if Sheet.RowHidden[Row] or Sheet.RowThickTop[Row] or
Sheet.RowThickBottom[Row] then
Writeln('Row ', Row, ': hidden=', Sheet.RowHidden[Row],
' thickTop=', Sheet.RowThickTop[Row],
' thickBot=', Sheet.RowThickBottom[Row]);
Flag := Sheet.Cells[2, 1].Value;
if VarType(Flag) = varBoolean then
Writeln('A2 holds the boolean ', Boolean(Flag));
finally
Book.Free;
end;
end;
<sheetFormatPr defaultRowHeight="12.8" customHeight="true"/>:CustomHeightdruckt TRUE<col min="3" max="3" width="20" hidden="false"/>: Spalte 3 wird nicht gelistet<row r="5" thickBot="true">: Zeile 5 meldetthickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 hält TRUE
Der Zeilen-Hinweis ist der Ort, an dem das Schema selbst unbeholfen wird. Eine Idee, zwei Schreibweisen — CT_SheetFormatPr nennt seine Attribute thickTop und thickBottom, während CT_Row thickTop und thickBot benutzt, also findet ein Reader, der auf einer Zeile nach thickBottom sucht, nichts. HotXLS schreibt jede Form auf ihrem eigenen Element zurück, und die Hinweise überleben ein Speichern zusammen mit dem Rest des Pakets, ganz wie unberührte Teile in einem verlustfreien XLSX-Round-Trip
Wie soll ein Chart ein Element ohne val lesen?
Ein ChartML-Switch ohne val-Attribut ist an. DrawingML Charts deklariert diese Switches mit CT_Boolean in dml-chart.xsd, dessen val ein optionales xsd:boolean mit Default true ist, also bedeuten <c:plotVisOnly/> und <c:showHorzBorder/> dasselbe wie ein val="1". Dieser Default ist das Gegenteil der meisten SpreadsheetML-Attribute, und er bricht den verbreiteten Shortcut, Chart-XML als Text zu lesen: das Element beim Namen finden, dann vorwärts nach dem nächsten val=" suchen. Lässt das Element val weg, läuft diese Suche über das Ende des Start-Tags hinaus und leiht sich den Wert von dem Element, das später im Part kommt, vielleicht ein val="0" zwei Geschwister weiter. Treffs nichts, gibt PosEx 0 zurück, und Code, der bei P + 5 schneidet, liest am Ende ab Zeichen 5 des Parts, was das l von <?xml ist. Keiner der Fehler wirft irgendetwas; die Chart-Property widerspricht nur der Datei
Quotes sind die andere Hälfte des Problems. XML 1.0 erlaubt, einen Attributwert mit doppelten oder einfachen Quotes zu begrenzen (die AttValue-Produktion in §2.3), also ist <c:majorTickMark val='out'/> genauso legitim wie die doppelt-quotierte Form, und eine Suche nach val=" überspringt sie komplett. HotXLS bewahrt das originale ChartML auf, damit es Excel-Charts bearbeiten kann, ohne Markup zu verlieren, das es nicht modelliert, und die aus diesem Text gelesenen Switches werden nur innerhalb des eigenen Start-Tags des Elements gesucht, mit beiden Quote-Stilen akzeptiert und einem fehlenden val, das als CT_Boolean-Default gelesen wird. Die öffentlichen Chart-Properties spiegeln dieses Lesen
procedure DumpChartSwitches(Sheet: TXLSXWorksheet);
const
TickNames: array[TXLSXChartTickMark] of string =
('none', 'in', 'out', 'cross');
var
I: Integer;
Chart: TXLSXChart;
begin
for I := 0 to Sheet.Charts.Count - 1 do // Charts ist 0-basiert
begin
Chart := Sheet.Charts[I];
Writeln(Chart.Name, ': plotVisOnly=', Chart.PlotVisibleOnly,
' dTable showHorzBorder=', Chart.ShowDataTableHorzBorder);
if Chart.ValueAxis <> nil then
Writeln(' value axis majorTickMark=',
TickNames[Chart.ValueAxis.MajorTickMark]);
if Chart.SeriesCount > 0 then
Writeln(' first series smooth=', Chart.Series[0].Smooth);
end;
end;
Erwartet für einen Chart-Part, der ein <c:plotVisOnly/> enthält, später gefolgt von einem Element mit val="0", einer Datentabelle mit <c:showHorzBorder/><c:showVertBorder val="0"/>, und einer Wertachse mit <c:majorTickMark val='out'/>: PlotVisibleOnly und ShowDataTableHorzBorder sind beide TRUE, und das Tick-Mark druckt out
Ein begrenzter xsd:boolean-Reader für Ihren eigenen OOXML-Code
Wenn Sie ein Paket selbst nachverarbeiten, etwa einen Custom-Part, den Ihre Anwendung hinzufügt, oder einen schnellen Scan des Chart-XMLs in einem Build-Schritt, gelten dieselben zwei Regeln: mit dem vollen lexikalischen Raum konvertieren, und eine Attributsuche nie das Start-Tag ihres Elements verlassen lassen. Die Funktionen unten tun beides. ChartSwitch erwartet, dass TagStart auf das < des Elements zeigt, und wendet den CT_Boolean-Default an, wenn val fehlt
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: Padding ist legal
Result := True;
if (S = '1') or (S = 'true') then
Value := True
else if (S = '0') or (S = 'false') then
Value := False
else
Result := False; // TRUE, yes, on: kein xsd:boolean
end;
function ReadXsdBoolean(const Literal: string; Default: Boolean): Boolean;
begin
if not TryParseXsdBoolean(Literal, Result) then
Result := Default;
end;
function AttrInStartTag(const Xml: string; TagStart: Integer;
const AttrName: string; out Value: string): Boolean;
var
TagEnd, P, Q: Integer;
Quote: Char;
begin
Result := False;
Value := '';
TagEnd := PosEx('>', Xml, TagStart);
if TagEnd = 0 then
Exit;
// der Name muss ein Token eröffnen, nicht einen anderen Attributnamen beenden
P := PosEx(AttrName + '=', Xml, TagStart);
while (P > 0) and (P < TagEnd) and
not CharInSet(Xml[P - 1], [' ', #9, #10, #13]) do
P := PosEx(AttrName + '=', Xml, P + 1);
if (P = 0) or (P >= TagEnd) then
Exit;
Q := P + Length(AttrName) + 1;
Quote := Xml[Q];
if (Quote <> '"') and (Quote <> '''') then
Exit;
P := PosEx(Quote, Xml, Q + 1);
if (P = 0) or (P > TagEnd) then
Exit;
Value := Copy(Xml, Q + 1, P - Q - 1);
Result := True;
end;
function ChartSwitch(const Xml: string; TagStart: Integer): Boolean;
var
V: string;
begin
if not AttrInStartTag(Xml, TagStart, 'val', V) then
Exit(True); // CT_Boolean: ein fehlendes val bedeutet true
Result := ReadXsdBoolean(V, True);
end;
Der strenge Zweig in TryParseXsdBoolean folgt dem Datentyp bis aufs Wort, und dort gehen Ihr Reader und HotXLS etwas auseinander: Die HotXLS-Konversion vergleicht die Wörter ohne Blick auf die Schreibweise, also liest TRUE als true. Wir akzeptieren lieber eine Schreibweise, die das Schema verbietet, als ein Flag abzuschalten, weil irgendein Producer es groß geschrieben hat, denn kein Producer schreibt TRUE, um false zu meinen; ein Validator dagegen sollte beim strengen Zweig bleiben und den Wert melden. Der Text-Scanner nimmt außerdem zwei Abkürzungen, die für ChartML-Switches sicher sind und für XML im Allgemeinen falsch. Ein > ist innerhalb eines Attributwerts legal, also kann das Anhalten am frühesten einen Tag kaputtschneiden, dessen Werte ihn enthalten, und XML erlaubt Whitespace um das =, was die Suche nach AttrName + '=' nicht sieht. ChartML-Switch-Werte sind kurze Tokens ohne > darin, und Leerzeichen um das Gleichheitszeichen zu überspringen ist eine Zwei-Zeilen-Erweiterung, aber für alles jenseits einer Handvoll bekannter Switches entfernt ein XML-Reader, der Ihnen die Attribute eines Elements nach dem anderen liefert, beide Probleme, und er ist es, was HotXLS für Worksheet-Parts benutzt
Lädt eine Calc-Arbeitsmappe mit anderen versteckten Spalten oder Zeilenhöhen als ihr Excel-Zwilling, entpacken Sie beide und sehen Sie nach, wie die Flags geschrieben sind; hidden="false" gegen hidden="0" erklärt die Kluft meist innerhalb einer Minute. Beide Schreibweisen, gepolstert oder nackt, auf einen Satz von Worksheet-, Zell- und Chart-Properties abzubilden ist die Arbeit, die die HotXLS Delphi Spreadsheet-Komponente bei jedem Laden leistet