Open een werkboek dat LibreOffice Calc opslaat en een kolom weggeschreven als hidden="false" kan verborgen terugkomen, terwijl er niets in het bestand mis is. Onder xsd:boolean, het type achter dat attribuut, zijn true, false, 1 en 0 allemaal legaal, met of zonder omringende spaties; HotXLS for Delphi leest elk van die spellingen waar een XLSX-schema een boolean declareert
De helft van xsd:boolean die Excel nooit schrijft
W3C XML Schema Part 2 §3.2.2 definieert boolean met vier legale literals, true, false, 1 en 0, en zet zijn whiteSpace-facet vast op collapse, wat betekent dat customHeight=" 1 " net zo geldig is als customHeight="1". Dezelfde sectie benoemt true en false als de canonieke vormen. SpreadsheetML doet niets om dat te vernauwen: sml.xsd declareert customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix en tientallen van hun buren als kale xsd:boolean, de meeste met een expliciete default false en enkele, zoals showGridLines, met default true. Excel schrijft toevallig alleen de niet-canonieke cijfers. LibreOffice en sommige andere producenten schrijven de woorden, dus een pakket van Calc draagt <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> waar Excel customHeight="1" had geschreven
Die asymmetrie is waarom vergelijkingen op één cijfer jaren overleven in OOXML-readers. Bouw een testcorpus uit Excel-uitvoer en elke boolean die u ooit zult zien is 0 of 1. De vergelijking slaagt voor elke test. Dan bereikt een Calc-bestand een gebruiker. Stop minstens een paar door LibreOffice opgeslagen pakketten in elk XLSX-corpus dat readertests aandrijft — zijn spelling van booleans is een van meerdere plekken waar de twee producenten verschillen, hetzelfde gat dat in formules en regels opduikt wanneer u werkboeken tussen Excel en LibreOffice verplaatst via ODS. Nog één detail van het datatype telt voor parserontwerp: de literals zijn case sensitive, dus TRUE is helemaal geen geldige xsd:boolean, een punt die terugkomt wanneer u beslist hoe soepel uw eigen reader moet zijn
Wat een vergelijking op één teken verkeerd doet
Een reader die een attribuut tegen één enkel cijfer vergelijkt faalt in de ene of de andere richting. Welke richting hangt af van de default die de vergelijking stilletjes codeert. De tabel noemt de vormen die in handgeschreven OOXML-code opduiken, wat elk met de woordspellingen doet en een paar attributen waar de schade zichtbaar wordt. Hij laat showDropDown weg, wiens probleem zijn naam is in plaats van zijn spelling: de schema-default is false, toch verbergt showDropDown="1" op een lijstvalidatie de pijl in de cel, dus TXLSXDataValidation.ShowDropDown toont de gebruikersbetekenis (True toont de pijl) en slaat de geïnverteerde OOXML-vlag op
| Naieve toets | Geïmpliceerde default | Leest verkeerd | Waar het zichtbaar wordt |
|---|---|---|---|
Value = '1' | false | true wordt false | sheetFormatPr customHeight, de <v> van een cel t="b" |
Value <> '0' | true zodra aanwezig | false wordt true | kolom hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false wordt genegeerd, de default blijft aan | sheetView showGridLines, permission-schakelaars van sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true wordt afgekapt tot t | ChartML-datatabelfranden, series-smooth |
De middelste rij levert de zichtbaarste schade, want hidden="false" op een element <col> is iets heel normaals voor een producent die een breedte voor een zichtbare kolom registreert, en een reader die elke niet-nul-tekst als gezet behandelt verbergt die kolom bij het laden. HotXLS stuurt elk van deze attributen door één xsd:boolean-conversie die omringende spaties trimt, de cijfers en de woorden accepteert, en voor al het anders de eigen default van het attribuut teruggeeft. Die laatste tak belichaamt een ontwerpkeuze die het noemen waard is: een waarde als yes is een misvormd bestand — maar een werkboek weigeren om één slechte zichtbaarheidsvlag zou de gebruiker straffen voor de fout van de producent, dus de reader houdt een vaste per-attribuut-fallback aan en gaat door
Een werkblad in LibreOffice-stijl met HotXLS lezen
De vlaggen op werkbladniveau komen naar boven als gewone properties op TXLSXWorksheet: CustomHeight, ThickTop en ThickBottom komen uit <sheetFormatPr>, terwijl ColHidden[], RowHidden[], RowThickTop[] en RowThickBottom[] één-based kolom- en rijnummers nemen. Een boolean-cel arriveert als Boolean-variant in TXLSXCell.Value, welke spelling het element <v> ook gebruikte. De routine hieronder drukt ze allemaal af voor het eerste blad, en de lijst erna geeft de verwachte uitvoer voor Calc-stijl 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]; // de Sheets-indexeerder is 1-based
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"/>:CustomHeightdrukt TRUE af<col min="3" max="3" width="20" hidden="false"/>: kolom 3 staat er niet bij<row r="5" thickBot="true">: rij 5 meldtthickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 bevat TRUE
De rij-hint is waar het schema zelf ongemakkelijk wordt. Eén idee, twee spellingen — CT_SheetFormatPr benoemt zijn attributen thickTop en thickBottom, terwijl CT_Row thickTop en thickBot gebruikt, dus een reader die op een rij naar thickBottom zoekt vindt niets. HotXLS schrijft elke vorm terug op zijn eigen element, en de hints overleven een opslag samen met de rest van het pakket, zoals onaangeroerde delen dat doen in een verliesvrije XLSX-round trip
Hoe hoort een chart een element zonder val te lezen?
Een ChartML-schakelaar zonder attribuut val staat aan. DrawingML Charts declareert die schakelaars met CT_Boolean in dml-chart.xsd, waarvan val een optionele xsd:boolean is die default op true staat, dus <c:plotVisOnly/> en <c:showHorzBorder/> betekenen hetzelfde als val="1" schrijven. Die default is het omgekeerde van de meeste SpreadsheetML-attributen, en hij breekt de gebruikelijke korticut om chart-XML als tekst te lezen: vind het element op naam, en zoek dan vooruit naar de volgende val=". Laat het element val weg, dan loopt die zoektocht voorbij het einde van de start-tag en leent hij de waarde van welk element later in het onderdeel ook komt, misschien een val="0" twee siblings verder. Matcht er helemaal niets, dan geeft PosEx 0 terug, en code die op P + 5 snijdt belandt met lezen op teken 5 van het onderdeel, wat de l van <?xml is. Geen van beide falingen gooit iets omhoog; de chart-property verschilt gewoon met het bestand
Quotes zijn de andere helft van het probleem. XML 1.0 staat toe dat een attribuutwaarde door dubbele of enkele quotes wordt afgebakend (de productie AttValue in §2.3), dus <c:majorTickMark val='out'/> is net zo legitiem als de vorm met dubbele quotes, en een zoektocht naar val=" slaat hem helemaal over. HotXLS bewaart de originele ChartML zodat hij Excel-charts kan bewerken zonder markup te verliezen die hij niet modelleert, en de uit die tekst gelezen schakelaars worden uitsluitend binnen de eigen start-tag van het element opgezocht, met beide quotestijlen geaccepteerd en een afwezige val gelezen als de default van CT_Boolean. De publieke chart-properties weerspiegelen die lezing
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 is 0-based
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;
Verwacht voor een chart-onderdeel met <c:plotVisOnly/>, later gevolgd door een element met val="0", een datatabel met <c:showHorzBorder/><c:showVertBorder val="0"/>, en een waarde-as met <c:majorTickMark val='out'/>: PlotVisibleOnly en ShowDataTableHorzBorder zijn allebei TRUE, en de tick mark drukt out af
Een begrensde xsd:boolean-reader voor uw eigen OOXML-code
Post-processeert u een pakket zelf, bijvoorbeeld een custom part dat uw applicatie toevoegt of een snelle scan van chart-XML in een build-stap, dan gelden dezelfde twee regels: converteren met de volledige lexicale ruimte, en nooit een attribuut-opzoeking de start-tag van zijn eigen element laten verlaten. De functies hieronder doen beide. ChartSwitch verwacht dat TagStart naar de < van het element wijst en past de default van CT_Boolean toe wanneer val ontbreekt
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: opvulling is legaal
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: geen 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;
// de naam moet een token beginnen, niet het einde van een andere attribuutnaam zijn
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: een afwezige val betekent true
Result := ReadXsdBoolean(V, True);
end;
De strikte tak in TryParseXsdBoolean volgt het datatype op de letter, en daar gaan uw reader en HotXLS een beetje uit elkaar: de HotXLS-conversie vergelijkt de woorden ongeacht hoofdletters, dus TRUE leest als true. Liever accepteren we een spelling die het schema verbiedt dan een vlag uitzetten omdat een of andere producent hem in hoofdletters zette, want geen enkele producent schrijft TRUE om false te bedoelen; een validator daarentegen hoort de strikte tak aan te houden en de waarde te melden. De tekstscanner neemt ook twee shortcuts die voor ChartML-schakelaars veilig zijn en voor XML in het algemeen fout. Een > is legaal binnen een attribuutwaarde, dus stoppen bij de vroegste kan een tag afkappen wiens waarden hem bevatten, en XML staat witruimte rond de = toe, wat de zoektocht AttrName + '=' niet ziet. ChartML-schakelwaarden zijn korte tokens zonder >, en spaties rond het gelijkteken overslaan is een uitbreiding van twee regels, maar voor alles voorbij een handjevol bekende schakelaars haalt een XML-reader die u de attributen van één element per keer aanreikt beide problemen weg, en dat is wat HotXLS gebruikt voor werkblad-onderdelen
Laadt een Calc-werkboek met andere verborgen kolommen of rijhoogten dan zijn Excel-tweeling, pak dan beide uit en kijk hoe de vlaggen gespeld zijn; hidden="false" naast hidden="0" verklaart het gat meestal binnen een minuut. Beide spellingen, met opvulling of kaal, op één set werkblad-, cel- en chart-properties in kaart brengen is het werk dat de HotXLS Delphi spreadsheet component bij elke load doet