レガシーなExcelバイナリ形式のほぼすべての部分は、すっきりとした2バイトのタイプと2バイトの長さを持つ単一のレコードです。セルはLABELSSTまたはNUMBERです。結合された領域はMERGEDCELLSです。レコードを1つずつ辿り、タイプワードに基づいて振り分けることで、ワークシートの大部分を読み取ることができます。しかし、PivotTableはそのリズムを壊します。単一のピボットテーブルはレコードではなく、同じOLE複合ドキュメントストリーム内の2つの異なる場所に分散した数十の連携するレコードで構成される小さなプログラムであり、それらの関係は位置的、ビットパック型であり、わずかなズレも許されません。これはほとんどのBIFF8リーダーが完全にスキップするか、または不透明なバイトデータとしてそのまま保持する構造です。ゼロから書き出すことは、Excel自身が維持しているすべての相互参照を再現することを意味するからです。
ピボットテーブルの構築が難しい理由は、実際には2つの成果物が溶接されているためです。独自のサブストリームを持つソースデータの独立したスナップショットである「ピボットキャッシュ」と、どのフィールドがどの軸に配置されるかを示すレイアウトである「テーブルビュー」です。キャッシュとビューは互いにインデックスで参照し合っています。インデックスを1つでも間違えると、ファイルを開いたときに更新エラーが発生するか、警告なしでグリッドが空になります。
ピボットキャッシュは独自のサブストリーム
キャッシュは、ワークブックのグローバルストリーム内に完全なBIFFサブストリームとして存在し、ドキュメントタイプが0x0006(ワークブックの0x0005やワークシートの0x0010とは異なり、ピボットキャッシュを示す値)のBOFレコードで囲まれ、対応するEOFで閉じられます。そのフレーム内では構造が固定されています。SXDBレコードはキャッシュヘッダーです。これには、レコード数、キャッシュフィールドの数、およびテーブルビューが自身をこのキャッシュにバインドするために引用するストリーム識別子が含まれます。次に、各ソース列がSXFDBフィールド定義レコードを拠出し、その後にそれを分類するSXFDBTypeが続き、さらにその列が取ったユニークな値が、重複しない値ごとに1つの型付き項目レコードとして出力されます。
項目レコードこそが、キャッシュの本来の価値です。テキスト値はSXSTRINGになり、数値はSXNUMになり、論理値はSXBOOLEANになり、数式エラーはSXERRになります。キャッシュは元のグリッドを格納するのではなく、フィールドごとの一意の(重複しない)値と、レコードnについて各フィールドがどのアイテムをとったかを示すインデックステーブルを格納します。そのため、プログラムでピボットテーブルを構築することは、セルをコピーする単純な作業ではありません。ソース範囲をスキャンし、保持されている値から各フィールドの型を推測し、それらを型付きのアイテムリストに重複排除し、すべての行をアイテムインデックスのタプルとして記録する必要があります。HotXLSはまさにこれを実行します。すべて数値の列はSXNUMアイテムと共に出力され、混合テキスト列はSXSTRINGアイテムになり、日付は同じ数値パスを介してシリアル値として保持されます。
SXDBBと、処理を面白くしているビットパック
レコードごとのインデックステーブルは、構造全体の中で技術的に最も興味深い部分であり、SXDBBレコードに格納されています。素朴なエンコーディングであれば、各フィールドの項目インデックスを16ビットワードとして格納するでしょう。Excelはそうしません。各フィールドのインデックスを、そのフィールドの項目をアドレス指定するのに必要な正確なビット数のみにパックします。その幅はceil(log2(itemCount + 1))ビットです。+ 1が重要です。追加された値は「空白(このレコードのこのフィールドには値がないこと)」を意味するセンチネルであり、3つの一意の項目を持つフィールドは4つの状態を表現する必要があるため、3つの項目だけが示唆する1ビットではなく、2ビットを占有します。項目が全くないフィールドはゼロビットとなり、パック処理中に完全にスキップされます。
1つのレコードのビットはすべてのフィールドにわたって連結され、次のレコードは新しいバイト境界から開始されます。レコードは端から端までビットパックされるのではなくバイト配置(byte-aligned)されるため、各行に数ビットのパディングビットが必要になる代わりに、テーブルへのランダムアクセスが容易になります。バイト内でのパックは最下位ビットが先頭(least-significant-bit first)になります。これら2つのルールを受け入れれば、エンコーダーは単純なビットポンプであり、デコーダーはその鏡像です。
// Width of one field's index in the SXDBB stream.
// citmTotal distinct items need ceil(log2(citmTotal + 1)) bits,
// the +1 reserving a "blank" sentinel value.
function BitsForFieldItems(itemCount: Integer): Integer;
var
capacity: Integer;
begin
Result := 0;
if itemCount <= 0 then
Exit; // empty field contributes zero bits
Result := 1;
capacity := 2;
while capacity < itemCount + 1 do
begin
Inc(Result);
capacity := capacity * 2;
end;
end;
この詳細を無視できない理由は、単一のBIFFレコードにおける8,224バイトの天井にあります。ピボットレコードを含め、この形式のすべてのレコードはペイロードを最大8,224バイトに収める必要があり、数千のソース行を持つアクティブなピボットキャッシュは、すべての行を出力するはるか前にその制限を超えてしまいます。そのため、インデックステーブルは分割されます。HotXLSは、単一のSXDBBボディの上限を8,220バイト(8,224レコード制限からタイプと長さの4バイトのレコードヘッダーを引いたもの)に制限し、それをパックされた1つのレコードのバイト幅で除算して、いくつの行が収まるかを算出します。そして、行数が要求する数だけの継続SXDBBレコードを出力します。各継続レコードはレコード境界で綺麗に再開されるため、行が2つのレコードに分割されることはありません。レコードごとのビット幅を知っているリーダーは、すべてのSXDBBを1つの連続したビット配列であるかのように順番に読み進めることができます。
ビューのレイアウト:ボディ用のSXLIとページ用のSXPI
キャッシュが構築されると、テーブルビューが後半になります。その核となるのは軸のラインアイテムであり、テーブルが描画する行フィールドと列フィールドの値のすべての組み合わせを列挙するピボットボディの行です。これらはSXLIレコード(レコードタイプ0x00B5、[MS-XLS] §2.4.275で定義)に保持されます。1つのSXLIは多くのラインを保持しますが、ここでも8,224バイト制限によって新しいレコードが必要になります。これには小さな圧縮トリックが使用されています。すなわち、各ラインは上のラインとどのように異なるかのみを共通プレフィックス(接頭辞)カウントとして格納するため、深くネストされた軸がすべての行で外側のフィールド値を繰り返すことはありません。総合計行とレコードの最初の行は、常にそのプレフィックスカウントをゼロにリセットするため、リーダーはラインを再構築するためにレコード境界を超えてさかのぼる必要はありません。
ページ軸、すなわちピボットテーブルの上に配置されるフィルターのドロップダウンは、別のレコードです。SXPI(レコードタイプ0x00B6、[MS-XLS] §2.4.276)は、ページフィールドごとに1つの10バイトのエントリを保持します。ピボットフィールドインデックスisxvd、選択されたキャッシュアイテムiCache、位置ワードipos、およびレガシーなオブジェクトIDobjIdです。注意すべきはiCacheの値です。何もフィルターしない「(すべて)」を表示するページフィールドは、実際のアイテムインデックスではなく、センチネル0x7FFDを格納します。プログラムで構築されたピボットは、呼び出し側が事前にアイテムを選択するまで、すべてのページフィールドが「(すべて)」に設定された状態で開き、選択された時点でそのアイテムのキャッシュインデックスがセンチネルを置き換え、Excelはすでにフィルターが適用された状態で開きます。これらと並んで、個々のフィールドとそのフォーマットを記述するサポートレコード(フィールドビュー定義用のSXVDおよびSXVDEx、各軸を配置するフィールドインデックスリスト用のSXIVD、および数値フォーマット用のSXFormat)が存在し、それぞれがボディラインが参照するのと同じキャッシュにインデックスを返します。
1つに共存する2つのライター:生のバイナリ(Raw blobs)と型付きモデル
HotXLSがピボットテーブルを書き出すために完全に個別の2つのパスを用意しているのには構造的な理由があり、これは再現性(再現の忠実度)に対する要求に直結しています。ワークブックがディスクから読み取られる際、そのピボットレコードはExcelまたは他の生成ツールによって書き込まれたものであり、それらはサードパーティのライターが完全には再現できないレコードのバリエーション、順序の癖、または拡張レコードを使用している可能性があります。これらのバイトデータを処理する唯一の安全な方法は、変更せずにそのまま返すことです。したがって、ファイルから読み込まれたピボットテーブルはFromRawBlobs = Trueフラグが立てられ、保存時にライターは保持されたレコードのバイナリ(blobs)をそのまま再現します。何も再生成されず、何も再解釈されず、オープンと保存のラウンドトリップはバイト単位で安定します。
プログラムによって構築されたピボットテーブルは、逆のケースです。保持すべき元のバイトデータはなく、型付きオブジェクトモデル(フィールドとアイテムリストを持つTXLSPivotCache、および軸が割り当てられたTXLSPivotTable)のみが存在します。そのテーブルはFromRawBlobs = Falseフラグが立てられ、ライターは困難な方法でそれをシリアライズします。すなわち、新しいBOF = 0x0006キャッシュサブストリームを出力し、型付きモデルが保持するアイテムインデックスからSXDBBインデックステーブルをパックし、軸構成からSXLIおよびSXPIレコードを配置します。このフラグにより、両方の種類が1つのワークブックに共存できます。これがなければ、単一のライターは、読み込んだテーブルの再現性を犠牲にするか、新しいテーブルの生成を拒否するかのいずれかを迫られます。読み込まれたテーブルが保持していた生成元独自の拡張レコードは、テーブルのSupplementalRecordsリストからアクセス可能な追加レコードとして保持されるため、型付きモデルを介してテーブルを検査しても、モデルが記述していない部分が失われることはありません。
コードにおけるピボットテーブルの構築
上記のすべての機構は、1つの呼び出しの背後に収まっています。AddPivotTableは、A1参照形式のソース範囲、テーブルの左上隅が固定される宛先セル、および名前を受け取ります。範囲を解析し、スキャンしてフィールドの型を推測し、キャッシュを構築(すでに同じ範囲にバインドされている別のテーブルがある場合は既存のキャッシュを再利用)し、ソース列ごとに1つのフィールドを持ち、すべてのフィールドが最初は軸外に設定された型付きのTXLSPivotTableを返します。その後、フィールドを軸に配置し、集計方法を選択します。シグネチャはまさにこの通りであり、キャッシュ、SXDBBパック、およびビューレコードはすべて保存時に自動的に生成されます。
uses
lxHandle, lxPivot;
var
Book : TXLSWorkbook;
Sheet: IXLSWorkSheet;
Pivot: TXLSPivotTable;
begin
Book := TXLSWorkbook.Create;
try
Book.Open('Sales.xls');
Sheet := Book.Sheets[1];
// Source A1:E500 on 'Data'; anchor the pivot at row 3, col 1.
Pivot := Sheet.AddPivotTable('Data!$A$1:$E$500', 3, 1, 'SalesByRegion');
if Pivot <> nil then
begin
Pivot.AddRowField('Region');
Pivot.AddColumnField('Quarter');
Pivot.AddDataFieldByName('Revenue', xlpaSum);
end;
Book.SaveAs('Sales-Pivot.xls');
finally
Book.Free;
end;
end;
ソース範囲の最初の行はキャッシュフィールドに命名するヘッダーとして読み取られるため、AddRowField('Region')は位置ではなくヘッダーテキストによって列と一致します。返されるテーブルはFromRawBlobs = Falseを持つ型付きモデルであるため、ライターは新規作成(ゼロから作成する)パスを選択します。これは、更新時にソース範囲がまだ存在していることに依存しない独立したキャッシュを構築します。これは、元データを移動または削除する可能性のある受信者にピボットを送信する場合にまさに必要とされる特性です。
独自に作成したものではないファイルのピボットおよびキャッシュレコードの読み取りと調整(raw-blob保存パスを含む)については、ワークブック監査および変換ワークベンチのチュートリアルでカバーされています。ソース範囲が数万行に及び、SXDBBストリームが多数の継続レコードにまたがる場合、大規模ワークブックのパフォーマンスに関するメモの技術を使用すれば、キャッシュ構築処理が実行時間を占有するのを防ぐことができます。どちらも、このブログの他の場所で扱われているセル、数式、グラフ、および書式設定APIと並んで、Excelオートメーションを使用せずにXLSおよびXLSXの書き込みを行う、HotXLS spreadsheet componentの一部として提供されているピボットライターと深く結びついています。