LibreOffice Calc가 저장한 통합 문서를 열면 hidden="false"로 쓰인 열이 숨겨진 채로 돌아올 수 있습니다. 파일에는 아무 문제가 없는데도. 그 속성의 타입인 xsd:boolean 아래에서는 true, false, 1, 0이 모두 합법이고 주위의 빈칸도 상관없습니다. HotXLS for Delphi는 XLSX 스키마가 불리언을 선언하는 곳마다 그 표기들을 전부 읽습니다
Excel이 결코 쓰지 않는 xsd:boolean의 절반
W3C XML Schema Part 2 §3.2.2는 boolean을 네 개의 합법 리터럴 true, false, 1, 0으로 정의하고 whiteSpace 패싯을 collapse로 고정합니다. 즉 customHeight=" 1 "은 customHeight="1"만큼 유효합니다. 같은 절은 true와 false를 정규 형태로 이름짓습니다. SpreadsheetML은 그것을 좁히는 데 아무것도 하지 않습니다. sml.xsd는 customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix와 그 이웃 수십 개를 평범한 xsd:boolean으로 선언하며, 대부분은 명시적 기본값 false이고 showGridLines 같은 몇몇은 true가 기본값입니다. Excel은 우연히도 정규가 아닌 숫자만 씁니다. LibreOffice와 몇몇 다른 producer들은 단어를 쓰므로, Calc의 패키지는 Excel이 customHeight="1"을 썼을 자리에 <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/>을 나릅니다
그 비대칭이 OOXML reader에서 한 글자 비교가 수년을 버티는 이유입니다. Excel 출력으로 테스트 코퍼스를 만들면 앞으로 볼 모든 불리언은 0이나 1입니다. 비교는 모든 테스트를 통과합니다. 그러다 Calc 파일이 사용자에게 닿습니다. reader 테스트를 굴리는 어떤 XLSX 코퍼스에도 LibreOffice 저장 패키지를 최소 몇 개는 넣으세요. 불리언 표기는 두 producer가 다른 여러 자리 중 하나이며, ODS로 Excel과 LibreOffice 사이에서 통합 문서를 옮길 때 수식과 규칙에서 드러나는 바로 그 간극입니다. 데이터 타입의 한 가지 세부가 파서 설계에 중요합니다. 리터럴은 대소문자를 구별하므로 TRUE는 애초에 유효한 xsd:boolean이 아니며, 당신의 reader를 얼마나 관대하게 만들지 정할 때 돌아오는 지점입니다
한 글자 비교가 틀리는 것
속성을 숫자 하나와 비교하는 reader는 어느 한 방향으로 실패합니다. 어느 방향인지는 비교가 조용히 인코딩하는 기본값이 정합니다. 표는 손으로 쓴 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 |
가운데 줄이 가장 눈에 띄는 피해를 냅니다. 보이는 열의 폭을 기록하면서 producer가 <col> 요소에 hidden="false"을 쓰는 것은 완전히 평범한 일이고, 0이 아닌 텍스트는 설정된 것으로 취급하는 reader는 로드 시 그 열을 숨겨 버립니다. HotXLS는 이 속성들을 전부 하나의 xsd:boolean 변환으로 보내 주위 빈칸을 잘라내고 숫자와 단어를 받아들이며, 그 밖의 것에 대해서는 속성 고유의 기본값을 돌려줍니다. 마지막 분기는 명시할 가치가 있는 설계 선택을 반영합니다. yes 같은 값은 malformed 파일입니다. 그러나 하나의 나쁜 표시 플래그 때문에 통합 문서를 열지 않는 것은 producer의 잘못을 사용자에게 벌주는 일이므로, reader는 속성별 고정 폴백을 지키고 나아갑니다
HotXLS로 LibreOffice식 워크시트 읽기
워크시트 수준 플래그들은 TXLSXWorksheet의 평범한 속성으로 드러납니다. CustomHeight, ThickTop, ThickBottom은 <sheetFormatPr>에서 오고, ColHidden[], RowHidden[], RowThickTop[], RowThickBottom[]은 1 기반 열·행 번호를 받습니다. 불리언 셀은 <v> 요소가 어떤 표기를 썼든 TXLSXCell.Value에 Boolean variant로 도착합니다. 아래 루틴은 첫 시트의 그 모든 것을 인쇄하고, 그 뒤의 목록은 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를 지닙니다
행 힌트가 스키마 자신이 어색해지는 자리입니다. 하나의 생각에 두 표기 — CT_SheetFormatPr는 자기 속성을 thickTop과 thickBottom으로 이름짓고 CT_Row는 thickTop과 thickBot을 쓰므로, 행에서 thickBottom을 찾는 reader는 아무것도 찾지 못합니다. HotXLS는 각 형태를 자기 요소에 되돌려 쓰고, 힌트들은 패키지의 나머지와 함께 저장을 살아남습니다. 무손실 XLSX 왕복에서 손대지 않은 부분들이 그러하듯이
val이 없는 요소를 차트는 어떻게 읽어야 할까?
val 속성이 없는 ChartML 스위치는 켜진 것입니다. DrawingML Charts는 그런 스위치들을 dml-chart.xsd의 CT_Boolean으로 선언하며 그 val은 true가 기본값인 선택적 xsd:boolean입니다. 그래서 <c:plotVisOnly/>와 <c:showHorzBorder/>는 val="1"을 쓴 것과 같은 뜻입니다. 그 기본값은 대부분의 SpreadsheetML 속성과 반대이며, 차트 XML을 텍스트로 읽는 흔한 지름길을 깹니다. 이름으로 요소를 찾고, 그다음 val="을 앞으로 검색하는 식입니다. 요소가 val을 생략하면 그 검색은 시작 태그의 끝을 지나 파트에서 나중에 오는 아무 요소의 값을 빌립니다. 아래 형제 둘째 자리의 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 빠른 스캔에는 같은 두 규칙이 적용됩니다. 전체 어휘 공간으로 변환할 것, 그리고 속성 조회가 자기가 속한 요소의 시작 태그를 절대 벗어나지 않게 할 것. 아래 함수들은 둘 다 합니다. 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의 엄격 분기는 데이터 타입을 글자 하나까지 따르며, 당신의 reader와 HotXLS가 조금 갈라지는 자리가 바로 여기입니다. HotXLS 변환은 단어를 대소문자 무관으로 비교하므로 TRUE는 true로 읽힙니다. 어떤 producer가 대문자로 썼다는 이유로 플래그를 끄는 것보다 스키마가 금지한 표기를 받아들이는 편이 낫습니다. TRUE를 써서 false를 뜻하는 producer는 없으니까요. 반면 검증기는 엄격 분기를 지키고 그 값을 보고해야 합니다. 텍스트 스캐너는 ChartML 스위치에는 안전하고 일반 XML에는 틀린 지름길 둘도 택합니다. >는 속성값 안에서 합법이므로 가장 이른 것에서 멈추면 그것을 품은 값의 태그를 잘라 버릴 수 있고, XML은 = 주위의 공백을 허용하는데 AttrName + '=' 검색은 그것을 보지 못합니다. ChartML 스위치 값은 >가 없는 짧은 토큰이고 등호 주위 빈칸 건너뛰기는 두 줄 확장입니다만, 알려진 스위치 몇 개를 넘서는 무엇이든 위해선 요소의 속성을 하나씩 건네주는 XML 리더가 두 문제를 모두 없애 주며, HotXLS가 워크시트 파트에 쓰는 것이 바로 그것입니다
Calc 통합 문서가 Excel 쌍둥이와 다른 숨겨진 열이나 행 높이로 로드되면 둘 다 압축을 풀고 플래그 표기를 보세요. hidden="false" 대 hidden="0"은 보통 그 간극을 1분 안에 설명합니다. 패딩이 있든 없든 두 표기를 하나의 워크시트, 셀, 차트 속성 집합으로 맞추는 일이 HotXLS Delphi 스프레드시트 컴포넌트가 매 로드에서 하는 일입니다