予算ブックに FY2024 というシートがあり、集計セルが =FY2024!B7 と書いているのに、Excel がその式を読めない。FY2024 はセル番地でもあるので、名前には 'FY2024'!B7 のようにアポストロフィが必要です。HotXLS は Excel が保存時に適用するのと同じ規則でこれを書き込み、XLSX ブックではシートの名前変更や削除のときにすべての式を書き換えます
どのシート名が式の中で引用符を要るか
シート名にアポストロフィが要るのは、式パーサがそれをシート名以外——数値、真偽値、セル参照——として読み得るときです。ECMA-376 Part 1 の §18.17 の式文法は引用符付きと無しのシート名を両方扱いますが、実務上の基準は Excel が sheet1.xml の <f> 要素に書き込むテキストです。他のリーダーはすべて、このテキストに合わせて調整されてきたからです。Excel の COM の Range.Formula が返す文字列と保存した XML は、いくつかの名前で食い違います。R1. と A1.x は .Formula からは素のまま返り、ファイルの中ではアポストロフィを帯びます。COM の返りから導いた規則は、Excel 自身のファイルに否定されます。HotXLS は格納形に従います。引用符が要るのは:
A-Z、a-z、0-9、アンダースコアの外の文字すべて:空白、ハイフン、ドット、括弧- 先頭が数字:
2024、2nd_Half、3Dはどれも引用符付き - 大文字小文字を問わず
TRUEかFALSE。TRUE1とTRUEXは素のまま。どちらも真偽値には読めないので - グリッド内の A1 セルとして読める名前:
XFDまでの列になる 1〜3 文字の字母に、1〜1048576 の行が続き、先頭 0 も許される - R1C1 参照のように始まる名前:
R、C、RCの単独、またはその後に数字が続くもの - 引用符付きの名前の中ではアポストロフィが二重化される——
Bob's Dataは'Bob''s Data'!A1と書かれる
実務のブックが実際に突き当たるのは A1 規則です。ビジネスのシート名は、たまたま有効なセル番地であることが多すぎるからです。FY2024 は列 FY、行 2024。Jan2024 は列 JAN、行 2024 で、JAN は現代のシートが持つ 16,384 列の中に余裕で収まります。Q1、CR1、Ra1 もすべて番地で、3 つとも引用符が要ります。境界のケースは逆に外れます。A0 は行 0 がなく、XFE1 は端から 1 列外、XFD1048577 は 1 行外、ABCD1 は字母が 4 つで、Excel はどれも素のまま置きます。先頭 0 で名前は救われず、A01 はパーサが A1 と読むので今もアポストロフィを付けられます。名前の文字種だけを検査する writer は、年度レポートに FY2024!B7 を出力し、Excel はその式を読み戻せません
R1C1 類似名と、多く見積もって引用する理由
R1C1 の形をした名前には、Excel の出力からきれいな規則を復元する道がありません。Excel は R、C、RC、R1、C1、R1x、R1C1x、さらに実在しない列を名指す R1C16385 と C99999 まで引用します。それなのに R0、C0、RC0、R0C1、R1048577、Rx、Cx、R_1 は素のままです。グリッドの限界とトークンの形をいくら組み合わせても、両方のリストは再現できません。そこで HotXLS は上位集合を採ります。R、C、RC のいずれかである名前、またはその後に数字が続く名前は、アポストロフィを付けます。この上位集合は Excel が素のままにするいくつかの名前、R0 や R1048577 などを引用します。それでも構わない理由は単純です。どんなシート名でも引用符付きの綴りは正当な式構文で、式をパースするリーダーは皆これを受け入れます。一方、Excel に 1 個ずつ名前合わせしようとするリストは、誰も述べられない規則の上に立つことになります。Chart1、Region1、Cost2024 のような普通の名前は両試験の外に落ちて素のままです。R1C1 のテキストも扱うなら、HotXLS での R1C1 式記法の解説が、そもそもその参照がどうパースされるかを述べています
ASCII 外の字母を含む名前は、多めに引用されます。Excel はここで寛容です。アクセント付きや CJK の字母は、Excel のパーサでは名前文字として数えられるので、Données!A1 や 数据!A1 をアポストロフィなしで書きます。HotXLS はファイルを開くとき、Excel が作るのだからと、そうした素の qualifier を読みます。ただし書くときは 'Données'!A1 のように引用符付きにします。引用符付きの形はどこでも正当で、writer の試験は、全消費者が同意する必要のある Unicode の文字表ではなく、短く予測可能なリストのままで済みます
Delphi でシート qualifier を組み立てる
lxStandard.XlsSheetNameNeedsQuotes はこの規則の公開形で、XLSX writer もクラシックエンジンも、シート参照を出力するときに使う関数です。自分のコードがシート名から式テキストを組み立てるとき——地域ごと、年度ごとに 1 シートを指す集計シートなど——Name + '!' を連結する代わりに、各名前をこの関数へ通しましょう。関数は yes か no しか答えません。内側のアポストロフィの二重化は呼び出し側の仕事です。下のヘルパーがやっているように。XLSX のセル式は先頭の = を付けずに代入します
program SheetQualifiers;
{$APPTYPE CONSOLE}
uses
System.SysUtils, lxStandard, lxHandleX;
// 手組みの式テキスト用の qualifier。名前が必要とするときは
// アポストロフィを付け、名前内のアポストロフィは二重化する
function SheetQualifier(const Name: WideString): WideString;
begin
if XlsSheetNameNeedsQuotes(Name) then
Result := '''' + StringReplace(Name, '''', '''''', [rfReplaceAll]) + '''!'
else
Result := Name + '!';
end;
const
Candidates: array[0..10] of WideString = (
'Summary', '2024', 'FY2024', 'Q1', 'TRUE', 'TRUE1',
'R1C1', 'Region1', 'XFE1', 'Données', 'Bob''s Data');
var
Book: TXLSXWorkbook;
Map, Sheet: TXLSXWorksheet;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Map := Book.Sheets.Add('Map');
for I := Low(Candidates) to High(Candidates) do
begin
Sheet := Book.Sheets.Add(Candidates[I]);
Sheet.Cells[1, 1].Value := (I + 1) * 10;
// リクエストした名前ではなく、シートが実際に得た名前を使う
Map.Cells[I + 1, 1].Value := Sheet.Name;
Map.Cells[I + 1, 2].Formula := SheetQualifier(Sheet.Name) + 'A1';
Writeln(Format('%-12s %s', [Sheet.Name, Map.Cells[I + 1, 2].Formula]));
end;
Book.SaveAs('sheet-map.xlsx');
finally
Book.Free;
end;
end.
// 期待出力:
// Summary Summary!A1
// 2024 '2024'!A1
// FY2024 'FY2024'!A1
// Q1 'Q1'!A1
// TRUE 'TRUE'!A1
// TRUE1 TRUE1!A1
// R1C1 'R1C1'!A1
// Region1 Region1!A1
// XFE1 XFE1!A1
// Données 'Données'!A1
// Bob's Data 'Bob''s Data'!A1
XLSX ファイルのワークシート名には : \ / ? * [ ] を含められず、アポストロフィで始めたり終わったりできず、31 文字で打ち止め、大文字小文字を問わず一意でなければならず、Excel が予約する History は使えません。TXLSXSheets.Add も TXLSXWorksheet.Name の setter も同じく強制します。禁じられた文字はアンダースコアになり、衝突には (2) の接尾が付き、予約名や空名は Sheet に落ちます。したがって代入した値と読み戻した値は違いうるし、リクエストした名前から組み立てた qualifier は、実在しないシートを指します。代入のたびに Name を読み戻し、それを引用しましょう
Sheet := Book.Sheets.Add('Totals');
Sheet.Name := 'Q1/Q2 Sales'; // シート名には '/' を使えない
Writeln(Sheet.Name); // Q1_Q2 Sales
Writeln(SheetQualifier(Sheet.Name) + 'C9'); // 'Q1_Q2 Sales'!C9
シート削除のあと、Excel は何を格納するか
シートを削除すると、Excel はそれへの参照を #REF! でまるごと置き換えます。qualifier とセルや範囲が一緒にです。SUM(Data!A1:B2)+1 は SUM(#REF!)+1 と格納され、COUNT(Data!C:C) は COUNT(#REF!)、Data!$A$1:$B$2 を指していた defined name はただの #REF!、チャート系列の範囲は <c:f>#REF!</c:f> になります。ここでも COM の見え方は誤解を招きます。Range.Formula は =#REF!A1+1 と表示し、セル部分が生き延びたかのように見せるのに、Excel が保存する XML にはセル部分はまるごとありません。COM の表示を写すと、Excel 自身が決して書かない式テキストが出来上がります。他のツールが読むファイルにとっては悪い賭けです。TXLSXSheets.DeleteByName は格納形を書きます。#REF! への折り畳みの論理は、行や列が消えた参照にも同じく働きます。シート内のケースは行の挿入と削除が式参照をどう調整するかの解説が扱います
3 次元参照の振る舞いは違います。Excel は 3D スパンを壊すのではなく縮めます。シートが Data、Mid、Keep の順に並んでいるとき、Data の削除は SUM(Data:Keep!A1) を SUM(Mid:Keep!A1) に変えます。消えた端が 1 シート分内側へ寄せられるのです。シート順は削除前のタブ順で測ります。スパンが 2 シートだけなら、結果は単一シート形の SUM(Keep!A1) です。Mid の削除は式テキストをまったく変えません。スパンはまだ生きている 2 シートを名指し、間に挟まるものが 1 つ減るだけです。両端が削除されたシートを名指すときに限り、参照は #REF! になります。死んだ参照は式の返す値を変えるので、HotXLS は削除の最中に書き換えたすべての式セルのキャッシュ結果を stale と印を付けます。値を読む前に Recalculate を呼びましょう。名前変更はキャッシュ値に触れません。数値は変わりようがないので
uses
System.SysUtils, lxHandleX;
procedure ShowFormulas(Book: TXLSXWorkbook; const Caption: string);
var
Report: TXLSXWorksheet;
Row: Integer;
begin
Writeln(Caption);
Report := Book.Sheets.SheetByName('Report');
for Row := 1 to 4 do
Writeln(' B', Row, ': ', Report.Cells[Row, 2].Formula);
Writeln(' DataBlock: ', Book.DefinedNames.FindByName('DataBlock').Formula);
end;
var
Book: TXLSXWorkbook;
Report: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Book.Sheets.Add('Data');
Book.Sheets.Add('Mid');
Book.Sheets.Add('Keep');
Report := Book.Sheets.Add('Report');
Book.DefinedNames.Add('DataBlock', 'Data!$A$1:$B$2');
Report.Cells[1, 2].Formula := 'SUM(Data:Keep!A1)';
Report.Cells[2, 2].Formula := 'SUM(Data!A1:B2)+1';
Report.Cells[3, 2].Formula := 'Keep!A1*2';
Report.Cells[4, 2].Formula := 'INDIRECT("Data!A1")';
Book.Sheets.DeleteByName('Data');
ShowFormulas(Book, 'After deleting Data:');
Book.Sheets.SheetByName('Keep').Name := '2025';
ShowFormulas(Book, 'After renaming Keep to 2025:');
Book.Recalculate; // Data を失ったセルは古いキャッシュ値を抱えている
Book.SaveAs('report.xlsx');
finally
Book.Free;
end;
end.
// 期待出力:
// Data を削除した後:
// B1: SUM(Mid:Keep!A1)
// B2: SUM(#REF!)+1
// B3: Keep!A1*2
// B4: INDIRECT("Data!A1")
// DataBlock: #REF!
// Keep を 2025 に名前変更した後:
// B1: SUM('Mid:2025'!A1)
// B2: SUM(#REF!)+1
// B3: '2025'!A1*2
// B4: INDIRECT("Data!A1")
// DataBlock: #REF!
シートの名前変更はブック全体の参照を書き換える
XLSX ブックで TXLSXWorksheet.Name を代入すると、新しい名前は式の住み得るすべての場所へ届きます。DeleteByName は同じ場所を歩いて #REF! への折り畳みを適用します。リストはセル式を大きく超えます:
- 全シートのセル式。shared formula は従属セルごとに展開形で保持され、array formula は根セルに住むので、特別扱いなしでどちらも覆われます
- 条件付き書式の式。データバー、カラースケール、アイコンセットの formula 型しきい値を含みます
- データ入力規則の式。別シートのリストソースなどです
- テーブルの集計列と集計行の式
- defined name。defined name と cross-sheet 式の上に組まれるモデルの背骨です。名前変更では、改名されたシートからソース範囲を読む pivot cache も一緒に追従します
- チャート範囲:系列名、カテゴリ、値、バブルサイズ、データラベル範囲、誤差範囲の plus と minus、チャートタイトルと軸タイトルの裏にあるセル参照
3D スパンは、どちらかの端が引用を要るとき、全体として引用されます。Keep を 2025 に改名すると 'Mid:2025'!A1 が生まれ、Mid:'2025'!A1 にはなりません。スパンは 1 つの qualifier だからです。改名の対象でない qualifier は、書かれたとおりにそのまま再生されます。どこかの式が、技術的には不要なアポストロフィ付きで 'Sheet1'!B4 と言っているなら、後もそのまま言い続けます。通りがかりに qualifier を正規化しても Excel には無害でしょうが、そうすると 1 シートの改名がブック中の全式に触れることになり、生成レポートの 2 版の diff では、本当の変更が何百もの化粧変更の下に埋もれます
シート参照にしか見えないテキスト
パターン検索には Name! qualifier に見えて、実際はそうでないテキストが 3 種あります。文字列リテラルの中では、"Data!A1" はテキストです。だから INDIRECT("Data!A1") は改名や削除の後も古い名前を指し続けます。Excel と同じです。レポートが INDIRECT 用にシート名を組み立てるなら、名前は改名と一緒に自分のコードが更新するセルに置きましょう。角括弧のブックインデックスを伴う qualifier、[1]Data!A1 のようなものは、たまたま Data というシートを持つ外部ブックに属し、ローカルの Data シートの改名は触れてはいけません。[ を含む引用符付き qualifier も同じ扱いです。さらに #REF! 自体は REF という名の qualifier の形をしています。HotXLS が REF! をシート名として扱う前に前の # を検査するのはこのためです。この検査がなければ、REF という名のシートの改名がブック中のすべての #REF! を書き換えます
生成した式を壊しやすいシート名は、空白や句読点を含む名前ではなく、FY2024、Q1、2025 のようなありふれた名前です。Delphi コードが式テキストを組み立てる場所では、どこでも XlsSheetNameNeedsQuotes で引用しましょう。その先では、HotXLS Delphi spreadsheet component での改名や削除は、すべての式を Excel 自身が保存したはずの形に保ちます