예산 통합 문서에 FY2024라는 시트가 있고 요약 셀이 =FY2024!B7이라 말하는데 Excel이 수식을 읽지 못합니다. FY2024는 셀 주소이기도 하므로 이름에 어포스트로피가 필요합니다. 'FY2024'!B7처럼. HotXLS는 Excel이 저장할 때 적용하는 같은 규칙으로 그것들을 쓰며, 시트의 이름이 바뀌거나 지워지면 XLSX 통합 문서의 모든 수식을 다시 씁니다
어느 시트 이름이 수식에서 따옴표를 필요로 할까?
수식 파서가 그것을 시트 이름이 아닌 다른 것으로 읽을 수 있을 때마다 시트 이름은 어포스트로피가 필요합니다. 숫자, 불리언, 셀 참조입니다. ECMA-376 Part 1 §18.17의 수식 문법은 따옴표 친 이름과 안 친 이름을 모두 다루지만, 실전 참조는 Excel이 sheet1.xml의 <f> 요소에 쓰는 텍스트입니다. 다른 모든 reader가 그 텍스트에 맞춰 길들여졌기 때문입니다. 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부터 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도 모두 주소이며 셋 모두 따옴표가 필요합니다. 경계 사례들은 반대로 갑니다. A0에는 0행이 없고, XFE1은 가장자리 한 열 너머이며, XFD1048577은 한 행 너머이고, ABCD1은 글자 넷이라 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을 인용합니다. 단순한 이유로 수용할 만합니다. 어떤 시트 이름의 인용된 표기도 유효한 수식 문법이므로 수식을 파싱하는 모든 reader가 받아들이는 반면, Excel과 이름 하나까지 맞춰 보려던 목록은 누구도 진술할 수 없는 규칙 위에 서게 되기 때문입니다. Chart1, Region1, Cost2024 같은 평범한 이름은 두 검사 모두 밖에 있어 맨으로 남습니다. R1C1 텍스트도 함께 다룬다면 HotXLS의 R1C1 수식 표기법에 대한 노트가 그 참조들을 애초에 어떻게 파싱하는지 기술합니다
ASCII 밖의 글자를 지닌 이름도 과잉 인용됩니다. Excel은 여기서 더 관대합니다. Excel은 강세 문자와 CJK 글자를 파서에게 이름 문자로 셈하므로 Données!A1과 数据!A1을 어포스트로피 없이 씁니다. HotXLS는 Excel이 그렇게 만들므로 파일을 열 때 그런 맨 한정자를 읽지만, 그런 이름은 'Données'!A1처럼 인용해 씁니다. 인용된 형태는 어디서나 유효하고, writer의 검사는 모든 소비자가 동의해야 하는 유니코드 글자 표가 아니라 짧고 예측 가능한 목록으로 남습니다
Delphi에서 시트 한정자 빚기
lxStandard.XlsSheetNameNeedsQuotes는 그 규칙의 공개 형태이며 XLSX writer와 고전 엔진이 시트 참조를 뱉을 때 둘 다 쓰는 함수입니다. 지역별, 연도별로 시트 하나씩 가리키는 요약 시트처럼 코드가 시트 이름에서 수식 텍스트를 조립한다면 Name + '!'을 이어 붙이는 대신 각 이름을 이 함수로 돌리세요. 함수는 예 아니오만 답합니다. 안쪽 어포스트로피를 겹치는 것은 호출자의 몫이며 아래 헬퍼가 그렇게 하고, XLSX 셀 수식은 선행 = 없이 할당됩니다
program SheetQualifiers;
{$APPTYPE CONSOLE}
uses
System.SysUtils, lxStandard, lxHandleX;
// 손으로 빚은 수식 텍스트를 위한 한정자: 이름에 필요하면 어포스트로피를
// 붙이고 이름 안의 어포스트로피는 겹쳐 씁니다
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 세터가 둘 다 이를 집행합니다. 금지 문자는 밑줄로 바뀌고, 충돌에는 (2) 접미사가 붙으며, 예약되었거나 빈 이름은 Sheet로 폴백합니다. 따라서 할당한 값과 읽어 돌려받은 값은 달라질 수 있고, 요청한 이름에서 빚은 한정자는 존재하지 않는 시트를 가리킵니다. 매 할당 뒤에 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!로 바꿉니다. SUM(Data!A1:B2)+1은 SUM(#REF!)+1로, COUNT(Data!C:C)는 COUNT(#REF!)로, Data!$A$1:$B$2를 가리키던 정의 이름은 맨 #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)이 됩니다. 사라진 끝이 탭 순서를 삭제 전 기준으로 한 시트 안쪽으로 물러나는 것입니다. 범위에 시트가 둘뿐이면 결과는 단일 시트 형태 SUM(Keep!A1)입니다. Mid를 지우는 것은 수식 텍스트를 하나도 바꾸지 않습니다. 범위가 여전히 산 시트 둘을 이름지며 그저 사이를 하나 덜 덮을 뿐이기 때문입니다. 양 끝이 모두 지워진 시트를 이름지을 때만 참조가 #REF!가 됩니다. 죽은 참조는 수식이 돌려주는 것을 바꾸므로, HotXLS는 삭제 중에 다시 쓴 모든 수식 셀의 캐시 결과를 낡은 것으로 표시합니다. 값을 읽기 전에 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! 무너짐을 적용합니다. 목록은 셀 수식을 훨씬 넘어섭니다:
- 모든 시트의 셀 수식. 공유 수식은 추종 셀마다 전개된 채로 보관되고 배열 수식은 루트 셀에 삽니다. 그래서 특별한 처리 없이도 둘 다 덮입니다
- 조건부 서식 수식. 데이터 막대, 색조, 아이콘 집합의 수식 타입 임계값 포함
- 데이터 유효성 검사 수식. 다른 시트의 목록 원천 같은 것
- 테이블 계산 열과 합계 행 수식
- 정의 이름. 정의 이름과 교차 시트 수식 위에 세운 모델의 등뼈입니다. 이름 바꾸기에서는 원본 범위를 바뀐 시트에서 읽는 피벗 캐시도 따라옵니다
- 차트 범위: 시리즈 이름, 범주, 값, 거품 크기와 데이터 레이블 범위, 오차 막대 플러스·마이너스 범위, 차트와 축 제목 뒤의 셀 참조
3D 범위는 어느 끝이라도 따옴표가 필요하면 통째로 인용됩니다. Keep을 2025로 바꾸면 'Mid:2025'!A1이 나오지 Mid:'2025'!A1은 결코 나오지 않습니다. 범위는 하나의 한정자이기 때문입니다. 이름 바꾸기가 대상이 아닌 모든 한정자는 쓰인 그대로 재생됩니다. 다른 곳의 수식이 기술적으로 불필요한 어포스트로피를 지닌 'Sheet1'!B4라 말한다면 그 뒤에도 그렇게 말합니다. 지나가는 길에 그 한정자들을 정규화하는 것은 Excel에 해가 없겠지만, 시트 하나의 이름 바꾸기가 통합 문서의 모든 수식을 건드리게 되고 생성된 보고서 두 버전 사이의 diff가 진짜 변경을 수백 겹의 외견 변경 아래 묻어 버립니다
시트 참조처럼만 보이는 텍스트
패턴 검색에는 Name! 한정자처럼 보이지만 그렇지 않은 텍스트가 셋 있습니다. 문자열 리터럴 안에서 "Data!A1"은 텍스트이므로 INDIRECT("Data!A1")은 이름 바꾸기나 삭제 후에도 옛 이름을 가리키며, Excel에서와 같습니다. 보고서가 INDIRECT용 시트 이름을 빚는다면 그것들은 이름 바꾸기와 함께 코드가 갱신하는 셀에 두세요. [1]Data!A1처럼 괄호 친 책 인덱스가 앞선 한정자는 우연히 Data라는 시트를 지닌 외부 통합 문서에 속하며, 로컬 Data 시트의 이름 바꾸기가 그것을 건드려서는 안 됩니다. [를 포함한 인용된 한정자도 같게 취급됩니다. 그리고 #REF! 자신은 REF라 이름지어진 한정자의 형태를 지니며, HotXLS가 REF!를 시트 이름으로 취급하기 전에 앞선 #을 검사하는 이유가 이것입니다. 그 검사가 없으면 REF라 불리는 시트의 이름 바꾸기가 통합 문서의 모든 #REF!를 다시 써 버립니다
생성된 수식을 깨뜨릴 가능성이 가장 높은 시트 이름은 공백이나 문장부호를 지닌 이름이 아니라 FY2024, Q1, 2025 같은 평범한 이름입니다. Delphi 코드가 수식 텍스트를 빚는 곳마다 XlsSheetNameNeedsQuotes로 인용하세요. 그 순간부터 HotXLS Delphi 스프레드시트 컴포넌트의 이름 바꾸기와 삭제는 모든 수식을 Excel 자신이 저장했을 형태로 둡니다