技術記事

HotXLSによるDelphiでのライブなブック間数式

HotXLSは=[Rates.xlsx]FX!B4*C10のような数式を、古びたキャッシュ値ではなく、メモリ上に開かれている別のワークブックに対して評価できる。RegisterExternalWorkbookで名前を指定して2つ目のワークブックを登録すると、再計算のたびに参照先のセルがそのオブジェクトから直接読み取られる。登録を解除すると、評価はファイルがもともと保持している外部リンクキャッシュにフォールバックする

これは特有の、そして苛立たしいギャップを埋めるものである。Excelワークブックは頻繁に互いを参照する:レートテーブル、勘定科目表、別チームが管理する価格表などである。そのようなファイルがExcelではなくライブラリによって開かれると、それらの参照は通常、Excelが最後に再計算した時にキャッシュされた値に解決され、それは数か月前のものである可能性があり、何度再計算してもデータそのものが存在しないため直らない

HotXLSのDelphiにおけるブック間数式解決経路の図。RegisterExternalWorkbookで登録されたワークブックはRecalculate時に最新のセル値を供給し、未登録のブックは外部リンクキャッシュにフォールバックし、キャッシュもない場合はセルが最後の計算値を保持し続けることを示す
登録すればライブなオブジェクトからセルを読み取り、キャッシュへのフォールバックは登録解除を安全に保ち、解決できない参照は最後の正常な値をそのまま残す

ファイル内部でブック間参照はどう見えるか

外部参照は、数式バーに見えるテキストとして保存されているわけではない。ワークブックは参照先ブックのテーブルを保持し、各数式トークンはそのテーブル内のインデックスを指し、別のキャッシュ済み値のパートが各参照セルの最後に既知の結果を保持する。これが、対になるファイルなしで開かれたファイルでも数値が表示される理由である

テキスト形式の解析には独自の微妙さがある。字句解析器は角括弧で囲まれたブック名を1つの識別子の一部として扱うため、[Book.xlsx]Sheet!A1は釣り合わない括弧に続くシート参照ではなく、1つのトークンになる。名前はその後、検索用に正規化される:引用符、パスの接頭辞、拡張子、数値インデックス形式はすべて大文字化されたベース名へと畳み込まれるため、'C:\Rates\[Rates.xlsx]FX'!B4と[rates.xlsx]FX!B4は同じ登録済みブックに解決される

ワークブックの登録と再計算

登録は名前で行い、その名前は参照する側のワークブックの数式に実際に現れているものである

uses
  lxHandleX;

var
  Main, Rates: TXLSXWorkbook;
begin
  Main := TXLSXWorkbook.Create;
  Rates := TXLSXWorkbook.Create;
  try
    if (Main.Open('valuation.xlsx') <> 1) or
       (Rates.Open('rates.xlsx') <> 1) then
      Exit;

    // valuation.xlsx内の[Rates.xlsx]...を含むformulaは今や
    // cached external valueでなくlive objectから読む
    Main.RegisterExternalWorkbook('Rates.xlsx', Rates);
    Main.Recalculate;

    Writeln(VarToStr(Main.Sheets[0].Cells[10, 3].Value));

    // sourceを変更して再計算すると、新しいvalueが流れる
    Rates.Sheets[0].Cells[4, 2].Value := 1.0842;
    Main.Recalculate;
    Writeln(VarToStr(Main.Sheets[0].Cells[10, 3].Value));
  finally
    Main.UnregisterExternalWorkbook('Rates.xlsx');
    Rates.Free;
    Main.Free;
  end;
end;

この登録は弱参照を保持する。HotXLSは登録されたワークブックの所有権を取得せず、それを解放することもない。つまりライフタイムの管理は自分の責任であり、順序が重要になる:参照元のワークブックを解放する前に登録を解除するか、少なくとも解放後には再計算しないこと

ブックが登録されていない場合はどうなるか

評価はファイルに保存された外部リンクキャッシュにフォールバックする。これはまさに、対になるファイルなしで開かれたワークブックの挙動である。このフォールバックがあるからこそ、登録解除は破壊的ではなく安全な操作になる

登録解除後の挙動は正確に述べておく価値がある。人を驚かせることがあるからだ。参照が登録済みブックからもキャッシュからも解決できない場合、評価はエラーを生成するが、そのエラーはセルに書き戻されない。セルは最後に計算された値を保持したままになる。ソースを再登録して再計算すれば、値は再び更新される

この「失敗時は保持する」というルールは意図的なものである。代わりにソースが利用できなくなった瞬間にすべての依存セルへエラー値を書き込むという方法を取れば、一時的に見つからないレートテーブルが、後の再計算成功時に修復しなければならないエラーマーカーだらけのワークブックへと変わってしまう。最後の正常な値を保持することで、中断されたパイプラインが残すドキュメントも、依然として意味の通る内容を保つ

ダーティ伝播はワークブックの境界で止まる

登録されたソース内のセルを編集しても、メインワークブック内の依存セルはダーティとしてマークされない。増分再計算は1つのワークブック内の依存関係を追跡するのであり、境界をまたいで監視することはない。Excelも同じ挙動をする。だからこそ、その外部参照は継続的にではなく、開いた時や明示的な更新時に更新される

実務上の帰結として、ソースの値を変更した場合、上の例のように参照側のワークブックを明示的に再計算する必要がある。アプリケーションがループの中で両方のブックを編集する場合は、編集のたびにではなく最後に一度だけ再計算するとよい。そうすれば増分再計算と依存関係グラフで説明されている増分エンジンが、各ワークブック内で必要最小限の作業だけを行う

この機能が当てはまる3つの形

統合が最も分かりやすい例である。親ワークブックが12個の月次ファイルを参照する場合、その12個すべてを登録し、一度だけ再計算し、Excelを一切開くことなく統合された数値を読み取ればよい。それぞれの登録名は親の数式が実際に示しているものと一致していなければならず、ファイル名と一致すると決めつけるのではなく、数式のテキストに照らして確認する価値がある

シナリオ分析が2つ目である。ソースを登録し、一連の前提条件をそこに書き込み、依存するモデルを再計算し、出力を記録し、それを繰り返す。ソースはライブなオブジェクトであるため、反復のたびにファイルが書き出されることはなく、100通りのシナリオも100回の保存・再読み込みサイクルではなく100回の再計算で済む

納品前の検証が3つ目であり、最も分かりにくい。ワークブックを開き、その相手先を登録し、再計算し、結果をファイルがもともと保持していた値と比較する。不一致はキャッシュされた値が古いことを意味し、クライアントがそのファイルを受け取る前に捕まえる価値のある欠陥である。この比較は、2つのExcelワークブックの差分比較で説明されているワークブックの差分比較と自然に組み合わせられる

HotXLSのDelphiにおけるライブなブック間評価の3つの形を示す図。12個の月次ファイルを統合するケース、実行の合間に一切ファイルを書き出さないシナリオ分析のケース、再計算した結果をファイルが保持するキャッシュ値と比較する検証のケース
統合はすべての月次ファイルを登録して一度だけ再計算し、シナリオのループはディスクに一切触れず、検証はクライアントが受け取るはずだったキャッシュと最新の結果を比較する

頼る前の参照の健全性チェック

まず数式解析パスを実行すること。1回の走査で欠落した関数、外部参照、循環依存の構成要素を報告してくれるため、ファイルを探しに行く前に、そのドキュメントがどのブック名を必要としているかが正確にわかる

var
  Issues: TArray<TXLSXFormulaIssue>;
  I: Integer;
begin
  Issues := Main.AnalyzeFormulaIssues;
  for I := 0 to High(Issues) do
    if Issues[I].Kind = xlsxFormulaIssueExternalReference then
      Writeln(Format('sheet %d R%dC%d needs [%s] (%s)',
        [Issues[I].Sheet, Issues[I].Row, Issues[I].Col,
         Issues[I].ExternalWorkbook, Issues[I].SourceFileName]));
end;

他にも時間の節約になる習慣が2つある。登録名は正規の形に保つこと——プレーンなファイル名で登録し、数式内の引用符付きやパス接頭辞付きの変種は正規化処理に任せればよい。そして、供給できないファイルを参照するワークブックは読み取り専用の成果物として扱うこと:再計算してもキャッシュされた値は保持されるが、依存セルを変更するどんな編集も、一部の値は最新で一部は過去のものという状態のドキュメントを残し、ファイル内にはどちらがどちらかを示すものが何もない。ワークブック間で数式をコピーすることは、独自の再バインドルールを持つ別の操作であり、ブック間コピーと数式の再バインドで扱われている。名前付き範囲はさらに別の層を加えるものであり、定義された名前とシート間数式で説明されている

ライブなブック間評価、増分再計算、数式解析はいずれも、DelphiおよびC++Builder向けの同じエンジン上で動作する。機能一覧の全体はHotXLS Delphiスプレッドシートコンポーネントページにある