Technisch artikel

Excel schrijft 1, LibreOffice schrijft true: XLSX-booleans

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

HotXLS xsd:boolean-attribuutparsing in Delphi: Excel schrijft customHeight="1" terwijl LibreOffice Calc customHeight="true" schrijft, beide binnen de vier-literal lexicale ruimte van true, false, 1 en 0 met whitespace collapse, gelezen via één HotXLS-conversie
Excel geeft alleen ooit de cijfers uit en LibreOffice Calc de woorden, maar beide spellingen zitten binnen één vier-literal datatype dat een reader volledig moet accepteren

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 toetsGeïmpliceerde defaultLeest verkeerdWaar het zichtbaar wordt
Value = '1'falsetrue wordt falsesheetFormatPr customHeight, de <v> van een cel t="b"
Value <> '0'true zodra aanwezigfalse wordt truekolom hidden, definedName hidden, alignment wrapText
Value = '0'truefalse wordt genegeerd, de default blijft aansheetView showGridLines, permission-schakelaars van sheetProtection
Copy(Raw, P + 5, 1) = '1'falsetrue wordt afgekapt tot tChartML-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

HotXLS XLSX boolean-attribuutlezer vergeleken met naieve toetsen Value = '1', Value <> '0', Value = '0' en Copy(Raw, P + 5, 1) in Delphi, met per toets welke woordspellingen hij verkeerd leest voor customHeight, kolom hidden, showGridLines en ChartML smooth
Elke toets op één teken slaagt voor 1 en 0 en faalt op precies één woordspelling, wat is waarom een Excel-alleen-testcorpus hem nooit vangt

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"/>: CustomHeight drukt TRUE af
  • <col min="3" max="3" width="20" hidden="false"/>: kolom 3 staat er niet bij
  • <row r="5" thickBot="true">: rij 5 meldt thickBot=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

HotXLS ChartML CT_Boolean-schakelaarlezing in Delphi: een vooruitse val="-zoektocht leent een latere val="0", mist val='out' en snijdt <?xml aan als PosEx 0 teruggeeft, terwijl HotXLS alleen de start-tag leest voor PlotVisibleOnly en ShowDataTableHorzBorder
De val-opzoeking begrenzen op de eigen start-tag van het element herstelt in één keer drie stille chart-foutlezingen: een geleende waarde, een overgeslagen enkel-gequote waarde en een snede vanaf de XML-declaratie
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