LibreOffice Calc が保存したブックを開くと、hidden="false" と書かれた列が隠れて返ってくることがあります。ファイルのどこも間違っていないのに、です。この属性の背後にある型 xsd:boolean では、true、false、1、0 がすべて合法で、前後の空白があってもなくても構いません。Delphi 用 HotXLS は、XLSX スキーマが真偽値を宣言している場所では、どの綴りも読みます
Excel が決して書かない xsd:boolean の半分
W3C XML Schema Part 2 の §3.2.2 は boolean を 4 つの合法リテラル、true、false、1、0 で定義し、whiteSpace ファセットを collapse に固定します。つまり customHeight=" 1 " は customHeight="1" と同じく正当です。同じ節は true と false を canonical form と名指します。SpreadsheetML はこれを狭めません。sml.xsd は customHeight、hidden、collapsed、showGridLines、wrapText、quotePrefix とその仲間数十個をただの xsd:boolean として宣言し、大半は false の明示的既定を持ち、showGridLines のような少数は既定が true です。Excel はたまたま非 canonical の数字しか書きません。LibreOffice と他のいくつかの作成者は単語を書きます。だから Calc 由来のパッケージは、Excel なら customHeight="1" と書いた場所に <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> を運びます
この非対称性ゆえに、1 文字の比較が OOXML リーダーの中で何年も生き延びます。テストコーパスを Excel の出力から作れば、目にする真偽値はすべて 0 か 1 で、比較は全試験を通ります。そして Calc のファイルがユーザーの手に届きます。リーダーの試験を支えるどんな XLSX コーパスにも、LibreOffice 保存のパッケージを最低数個は混ぜましょう。真偽値の綴りは 2 つの作成者が違う場所の 1 つで、ODS 経由で Excel と LibreOffice の間をブックが行き来するときに式や規則で顔を出すのと同じ溝です。データ型のもう 1 つの細部がパーサ設計に効きます。リテラルは大文字小文字を区別するので、TRUE はそもそも正当な xsd:boolean ではありません。自分のリーダーをどれだけ寛容にするかを決めるときに戻ってくる論点です
1 文字の比較が取り違えるもの
属性を 1 つの数字と比較するリーダーは、どちらかの向きに失敗します。どちらの向きかは、比較がこっそり組み込んだ既定で決まります。表は手書きの OOXML コードに現れる形、それぞれが単語の綴りに対して何をするか、被害が見える属性をいくつか列挙します。ただし showDropDown は外してあります。この問題は綴りではなく名前のほうにあります。スキーマ既定は false なのに、リスト入力規則の showDropDown="1" はセル内矢印を隠すのです。そこで TXLSXDataValidation.ShowDropDown はユーザーから見た意味(True で矢印を表示)を出し、反転した OOXML フラグを格納します
| 素朴な試験 | 暗黙の既定 | 取り違え | 現れる場所 |
|---|---|---|---|
Value = '1' | false | true が false になる | sheetFormatPr customHeight、t="b" セルの <v> |
Value <> '0' | 値があれば true | false が true になる | 列の hidden、definedName hidden、alignment wrapText |
Value = '0' | true | false が無視され、既定が立ったまま | sheetView showGridLines、sheetProtection の許可スイッチ |
Copy(Raw, P + 5, 1) = '1' | false | true が t に切り詰められる | ChartML のデータテーブル罫線、系列の smooth |
真ん中の行が最も見ていて痛い被害を作ります。可視列の幅を記録するとき、作成者が <col> 要素に hidden="false" と書くのはまったく普通のことで、ゼロ以外のテキストをすべて「設定あり」と扱うリーダーは、読み込みの時点でその列を隠してしまうからです。HotXLS はこれらの属性をすべて 1 つの xsd:boolean 変換へ回します。前後の空白を切り落とし、数字と単語の両方を受け入れ、それ以外には属性自身の既定を返します。最後の分岐には、明言する価値のある設計判断が込められています。yes のような値は不正形式のファイルです。それでも、1 つの壊れた表示フラグのためにブックを開くのを拒めば、ユーザーは作成者の過ちを罰せられます。リーダーは属性ごとの固定フォールバックを保って先へ進みます
HotXLS で LibreOffice 流のワークシートを読む
シートレベルのフラグは TXLSXWorksheet 上の普通のプロパティとして現れます。CustomHeight、ThickTop、ThickBottom は <sheetFormatPr> から来て、ColHidden[]、RowHidden[]、RowThickTop[]、RowThickBottom[] は 1 始まりの列番号と行番号を取ります。真偽値セルは TXLSXCell.Value に Boolean の variant として届きます。<v> 要素がどんな綴りを使っていても、です。下のルーチンは最初のシートについて全部を表示し、その後のリストが 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]; // Sheets のインデクサは 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は TRUE を表示<col min="3" max="3" width="20" hidden="false"/>:列 3 は一覧に現れない<row r="5" thickBot="true">:行 5 はthickBot=TRUEと報告<c r="A2" t="b"><v>true</v></c>:A2 は TRUE を保持
行のヒントは、スキーマ自身が不器用になる場所です。1 つの概念に 2 つの綴り。CT_SheetFormatPr は自分の属性を thickTop と thickBottom と名付け、CT_Row は thickTop と thickBot を使います。だから行に thickBottom を探すリーダーは何も見つけられません。HotXLS は各形をそれぞれの要素へ書き戻し、ヒントはパッケージの残りと一緒に保存を生き延びます。可逆 XLSX 往復で触らない部分が生き延びるのと同じように
val のない要素をチャートはどう読むべきか
val 属性のない ChartML スイッチはオンです。DrawingML Charts はこれらのスイッチを dml-chart.xsd の CT_Boolean で宣言し、その val は省略可能な xsd:boolean で既定は true。だから <c:plotVisOnly/> と <c:showHorzBorder/> は val="1" と書いたのと同じ意味です。この既定は大半の SpreadsheetML 属性と逆で、チャート XML をテキストとして読む定番の手抜きを壊します。名前で要素を見つけ、前へ val=" を探す、という手です。要素が val を省いていると、この探索は開始タグの終端を走り抜け、パート内で後から来る要素の値を借用します。2 つ隣の val="0" かもしれない。まったく一致しなければ PosEx は 0 を返し、P + 5 で切り出すコードはパートの 5 文字目から読むことになります。<?xml の l です。どちらの失敗も何も上げません。チャートのプロパティが黙ってファイルと食い違うだけです
引用符は問題の残り半分です。XML 1.0 は属性値を二重引用符でも単一引用符でも囲めると定めています(§2.3 の AttValue 生成規則)。だから <c:majorTickMark val='out'/> は二重引用符形と同じく正当で、val=" の探索はこれを丸ごと見過ごします。HotXLS は元の ChartML を保存しており、モデル化していないマークアップを失わずに Excel チャートを編集するためです。このテキストから読むスイッチは、要素自身の開始タグの中だけで探され、どちらの引用符も受け入れ、欠けた val は CT_Boolean の既定として読まれます。公開されているチャートプロパティはこの読みを反映します
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 は 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;
<c:plotVisOnly/> を含み、後から val="0" の要素が続き、データテーブルに <c:showHorzBorder/><c:showVertBorder val="0"/>、値軸に <c:majorTickMark val='out'/> があるチャートパートでの期待結果:PlotVisibleOnly と ShowDataTableHorzBorder はどちらも TRUE で、目盛りは out と表示されます
自前の OOXML コードのための閉じた xsd:boolean リーダー
パッケージを自分で後処理するとき——アプリケーションが追加するカスタムパートや、ビルド工程でのチャート XML の手早い走査など——には同じ 2 つの規則が効きます。語彙空間の全部で変換する、そして属性の探索を、その属性の属する要素の開始タグから外へ出さない。下の関数は両方をやります。ChartSwitch は TagStart が要素の < を指していることを前提とし、val が欠けるときは CT_Boolean の既定を適用します
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: パディングは合法
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 は 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;
// 名前はトークンの先頭でなければならない。別の属性名の末尾ではいけない
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: val がなければ true の意味
Result := ReadXsdBoolean(V, True);
end;
TryParseXsdBoolean の厳密な分岐はデータ型に文字どおり従います。そしてここで、あなたのリーダーと HotXLS は少しだけ道を分けます。HotXLS の変換は単語を大文字小文字を問わずに比較するので、TRUE は true として読めます。ある作成者が大文字で書いたからといってフラグを切るより、スキーマが禁じる綴りを受け入れるほうがましです。TRUE を false の意味で書く作成者はいないので。対照的にバリデータは厳密な分岐を保ち、値を報告すべきです。テキストスキャナは、ChartML スイッチには安全で、XML 一般には間違った手抜きを 2 つします。> は属性値の中でも合法なので、最初の 1 個で止まると、値にそれを含むタグを切り詰めかねません。また XML は = の前後の空白を許すのに、AttrName + '=' の探索はそれを見ません。ChartML のスイッチ値は > を含まない短いトークンで、等号の周りの空白を飛ばすのは 2 行の拡張です。しかしそれ以外の用途には、要素の属性を 1 個ずつ渡してくれる XML リーダーが両方の問題を取り除きます。HotXLS がワークシートパートに使っているのはそちらです
Calc のブックが、同じ Excel 版と違う隠し列や行の高さで読み込まれるときは、両方を展開してフラグの綴りを見ましょう。hidden="false" 対 hidden="0" は、たいてい 1 分で溝を説明してくれます。パディングあっても素でも、両方の綴りを 1 組のワークシート、セル、チャートのプロパティへ写像するのが、HotXLS Delphi spreadsheet component が読み込みのたびに行う仕事です