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
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ïf | Défaut implicite | Mauvaise lecture | Où cela se voit |
|---|---|---|---|
Value = '1' | false | true devient false | sheetFormatPr customHeight, le <v> d'une cellule t="b" |
Value <> '0' | true dès que présent | false devient true | colonne hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false est ignoré, le défaut reste actif | sheetView showGridLines, commutateurs de permission sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true est coupé en t | bordures 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
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"/>:CustomHeightimprime 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 rapportethickBot=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
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