Article technique

Excel écrit 1, LibreOffice écrit true : les booléens XLSX

Ouvrez un classeur enregistré par LibreOffice Calc et une colonne écrite hidden="false" peut revenir cachée, bien que rien dans le fichier ne soit faux. Sous xsd:boolean, le type derrière cet attribut, true, false, 1 et 0 sont tous légaux, avec ou sans blancs autour ; HotXLS pour Delphi lit chacune de ces orthographes partout où un schéma XLSX déclare un booléen

La moitié de xsd:boolean qu'Excel n'en écrit jamais

W3C XML Schema Partie 2 §3.2.2 définit boolean avec quatre littéraux légaux, true, false, 1 et 0, et fixe sa facette whiteSpace à collapse, ce qui fait que customHeight=" 1 " est aussi valide que customHeight="1". La même section nomme true et false comme formes canoniques. SpreadsheetML ne restreint rien de cela : sml.xsd déclare customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix et des dizaines de leurs voisins comme du xsd:boolean simple, la plupart avec un défaut explicite de false et quelques-uns, comme showGridLines, par défaut à true. Excel se trouve n'écrire que les chiffres non canoniques. LibreOffice et d'autres producteurs écrivent les mots, si bien qu'un paquet venu de Calc porte <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> là où Excel aurait écrit customHeight="1"

Cette asymétrie explique pourquoi des comparaisons à un chiffre survivent des années dans les lecteurs OOXML. Construisez un corpus de tests depuis la sortie d'Excel et chaque booléen que vous verrez jamais est 0 ou 1. La comparaison passe chaque test. Puis un fichier Calc atteint un utilisateur. Mettez au moins quelques paquets enregistrés par LibreOffice dans tout corpus XLSX qui pilote les tests de lecteurs — son orthographe des booléens est un des plusieurs endroits où les deux producteurs diffèrent, le même écart qui apparaît dans les formules et règles quand vous déplacez des classeurs entre Excel et LibreOffice via ODS. Un autre détail du type de donnée compte pour la conception de parseur : les littéraux sont sensibles à la casse, si bien que TRUE n'est pas du tout un xsd:boolean valide, un point qui revient quand vous décidez de la tolérance de votre propre lecteur

Parsing des attributs xsd:boolean par HotXLS en Delphi : Excel écrit customHeight="1" tandis que LibreOffice Calc écrit customHeight="true", tous deux dans l'espace lexical à quatre littéraux de true, false, 1 et 0 avec collapse des blancs, lus par une seule conversion HotXLS
Excel n'émet jamais que les chiffres et LibreOffice Calc émet les mots, pourtant les deux orthographes se trouvent dans un seul type à quatre littéraux qu'un lecteur doit accepter en entier

Ce qu'une comparaison à un caractère rate

Un lecteur qui compare un attribut à un seul chiffre échoue dans un sens ou dans l'autre. Le sens dépend du défaut que la comparaison encode en silence. La table liste les formes qui apparaissent dans du code OOXML écrit à la main, ce que chacune fait aux orthographes en mots et quelques attributs où le dégât se voit. Elle laisse de côté showDropDown, dont le problème est le nom plutôt que l'orthographe : le défaut du schéma est false, pourtant showDropDown="1" sur une validation de liste cache la flèche dans la cellule, si bien que TXLSXDataValidation.ShowDropDown expose le sens vu par l'utilisateur (True montre la flèche) et stocke le drapeau OOXML inversé

Test naïfDéfaut impliciteMauvaise lectureOù cela se voit
Value = '1'falsetrue devient falsesheetFormatPr customHeight, le <v> d'une cellule t="b"
Value <> '0'true dès que présentfalse devient truecolonne hidden, definedName hidden, alignment wrapText
Value = '0'truefalse est ignoré, le défaut reste actifsheetView showGridLines, commutateurs de permission sheetProtection
Copy(Raw, P + 5, 1) = '1'falsetrue est coupé en tbordures de table de données ChartML, smooth de série

La ligne du milieu produit le dégât le plus visible, parce que hidden="false" sur un élément <col> est une chose parfaitement normale à écrire pour un producteur qui enregistre une largeur pour une colonne visible, et un lecteur qui traite tout texte non nul comme posé cache cette colonne au chargement. HotXLS route chacun de ces attributs à travers une seule conversion xsd:boolean qui rogne les blancs autour, accepte les chiffres et les mots, et renvoie le défaut propre de l'attribut pour tout le reste. Cette dernière branche reflète un choix de conception digne d'être énoncé : une valeur comme yes est un fichier mal formé — mais refuser d'ouvrir un classeur pour un mauvais drapeau de visibilité punirait l'utilisateur de l'erreur du producteur, donc le lecteur garde un repli fixe par attribut et continue

Lecteur d'attributs booléens XLSX de HotXLS comparé aux tests naïfs Value = '1', Value <> '0', Value = '0' et Copy(Raw, P + 5, 1) en Delphi, montrant quelles orthographes en mots chacun lit mal pour customHeight, colonne hidden, showGridLines et smooth ChartML
Chaque test à un caractère passe sur 1 et 0 et échoue sur exactement une orthographe en mot, voilà pourquoi un corpus de tests Excel seul ne l'attrape jamais

Lire une feuille de style LibreOffice avec HotXLS

Les drapeaux de niveau feuille apparaissent comme des propriétés ordinaires sur TXLSXWorksheet : CustomHeight, ThickTop et ThickBottom viennent de <sheetFormatPr>, tandis que ColHidden[], RowHidden[], RowThickTop[] et RowThickBottom[] prennent des numéros de colonnes et de lignes à partir de un. Une cellule booléenne arrive comme un variant Boolean dans TXLSXCell.Value, quelle que soit l'orthographe qu'a employée l'élément <v>. La routine ci-dessous imprime tout cela pour la première feuille, et la liste ensuite donne la sortie attendue pour un balisage style Calc

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];   // l'indexeur Sheets est base 1

    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 imprime TRUE
  • <col min="3" max="3" width="20" hidden="false"/> : la colonne 3 n'est pas listée
  • <row r="5" thickBot="true"> : la ligne 5 rapporte thickBot=TRUE
  • <c r="A2" t="b"><v>true</v></c> : A2 porte TRUE

L'indice de ligne est l'endroit où le schéma lui-même devient gênant. Une idée, deux orthographes — CT_SheetFormatPr nomme ses attributs thickTop et thickBottom, tandis que CT_Row emploie thickTop et thickBot, si bien qu'un lecteur qui cherche thickBottom sur une ligne ne trouve rien. HotXLS réécrit chaque forme sur son propre élément, et les indices survivent à un enregistrement avec le reste du paquet, à peu près comme les parties intactes dans un aller-retour XLSX sans perte

Comment un graphique doit-il lire un élément sans val ?

Un commutateur ChartML sans attribut val est actif. DrawingML Charts déclare ces commutateurs avec CT_Boolean dans dml-chart.xsd, dont le val est un xsd:boolean optionnel par défaut à true, si bien que <c:plotVisOnly/> et <c:showHorzBorder/> signifient la même chose qu'écrire val="1". Ce défaut est l'inverse de la plupart des attributs SpreadsheetML, et il casse le raccourci habituel pour lire du XML de graphique comme texte : trouver l'élément par nom, puis chercher en avant le prochain val=". Quand l'élément omet val, cette recherche court au-delà de la fin de la balise ouvrante et emprunte la valeur de n'importe quel élément venant plus tard dans la partie, peut-être un val="0" deux frères plus loin. Quand rien ne correspond du tout, PosEx renvoie 0, et du code qui découpe à P + 5 finit par lire depuis le caractère 5 de la partie, qui est le l de <?xml. Aucun des deux échecs ne lève quoi que ce soit ; la propriété du graphique se contente de contredire le fichier

Les quotes sont l'autre moitié du problème. XML 1.0 permet de délimiter une valeur d'attribut par des quotes doubles ou simples (la production AttValue au §2.3), si bien que <c:majorTickMark val='out'/> est aussi légitime que la forme quotée en doubles, et une recherche de val=" la saute entièrement. HotXLS conserve le ChartML d'origine pour pouvoir éditer des graphiques Excel sans perdre le balisage qu'il ne modélise pas, et les commutateurs lus depuis ce texte sont cherchés dans la seule balise ouvrante propre de l'élément, avec les deux styles de quotes acceptés et un val absent lu comme le défaut CT_Boolean. Les propriétés publiques de graphique reflètent cette lecture

Lecture des commutateurs CT_Boolean ChartML par HotXLS en Delphi : une recherche val=" en avant emprunte un val="0" ultérieur, rate val='out' et découpe depuis <?xml quand PosEx renvoie 0, tandis que HotXLS ne lit que la balise ouvrante pour PlotVisibleOnly et ShowDataTableHorzBorder
Borner la recherche de val à la balise ouvrante propre de l'élément répare trois mauvaises lectures silencieuses de graphique à la fois : une valeur empruntée, une valeur quotée en simples sautée et une découpe depuis la déclaration XML
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 est base 0
  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;

Attendu pour une partie de graphique contenant <c:plotVisOnly/> suivi plus loin d'un élément avec val="0", une table de données avec <c:showHorzBorder/><c:showVertBorder val="0"/>, et un axe de valeurs avec <c:majorTickMark val='out'/> : PlotVisibleOnly et ShowDataTableHorzBorder sont tous deux TRUE, et la marque de graduation imprime out

Un lecteur xsd:boolean borné pour votre propre code OOXML

Quand vous post-traitez un paquet vous-même, disons une partie personnalisée qu'ajoute votre application ou un balayage rapide du XML de graphique dans une étape de build, les deux mêmes règles s'appliquent : convertir avec l'espace lexical complet, et ne jamais laisser une recherche d'attribut quitter la balise ouvrante de l'élément auquel elle appartient. Les fonctions ci-dessous font les deux. ChartSwitch attend que TagStart pointe le < de l'élément et applique le défaut CT_Boolean quand val manque

uses
  SysUtils, StrUtils;

function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
  S: string;
begin
  S := Trim(Literal);            // whiteSpace=collapse : le bourrage est légal
  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 : pas du 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;
  // le nom doit commencer un jeton, pas finir un autre nom d'attribut
  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 : un val absent signifie true
  Result := ReadXsdBoolean(V, True);
end;

La branche stricte de TryParseXsdBoolean suit le type de donnée à la lettre, et c'est là que votre lecteur et HotXLS divergent un peu : la conversion HotXLS compare les mots sans égard à la casse, si bien que TRUE se lit true. Nous préférons accepter une orthographe que le schéma interdit plutôt que d'éteindre un drapeau parce qu'un producteur l'a mise en capitales, puisque aucun producteur n'écrit TRUE pour dire false ; un validateur, au contraire, devrait garder la branche stricte et signaler la valeur. Le scanneur de texte prend aussi deux raccourcis qui sont sûrs pour les commutateurs ChartML et faux pour le XML en général. Un > est légal dans une valeur d'attribut, donc stopper au premier peut tronquer une balise dont les valeurs en contiennent, et le XML permet des blancs autour du =, que la recherche AttrName + '=' ne voit pas. Les valeurs de commutateurs ChartML sont des jetons courts sans > dedans, et sauter les blancs autour du signe égal est une extension de deux lignes, mais pour quoi que ce soit au-delà d'une poignée de commutateurs connus, un lecteur XML qui vous rend les attributs d'un élément à la fois supprime les deux problèmes, et c'est ce qu'HotXLS emploie pour les parties de feuille

Quand un classeur Calc se charge avec des colonnes cachées ou des hauteurs de ligne différentes de son jumeau Excel, dézippez les deux et regardez comment les drapeaux sont orthographiés ; hidden="false" contre hidden="0" explique l'écart en général en une minute. Mapper les deux orthographes, bourrées ou nues, sur un seul jeu de propriétés de feuille, de cellule et de graphique est le travail que fait le composant tableur Delphi HotXLS à chaque chargement