Otwórz skoroszyt zapisany przez LibreOffice Calc, a kolumna zapisana jako hidden="false" może wrócić ukryta, choć w pliku nie ma nic złego. Pod xsd:boolean — typem stojącym za tym atrybutem — true, false, 1 i 0 są wszystkie legalne, z otaczającymi białymi znakami albo bez; HotXLS dla Delphi czyta każdą z tych pisowni wszędzie tam, gdzie schemat XLSX deklaruje boolean
Ta połowa xsd:boolean, której Excel nigdy nie zapisuje
W3C XML Schema Part 2 §3.2.2 definiuje boolean z czterema legalnymi literałami — true, false, 1 i 0 — i przybija jego fasetę whiteSpace na collapse, czyli customHeight=" 1 " jest równie poprawne jak customHeight="1". Ta sama sekcja nazywa true i false formami kanonicznymi. SpreadsheetML nic tego nie zawęża: sml.xsd deklaruje customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix i dziesiątki ich sąsiadów jako gołe xsd:boolean, większość z jawnym domyślnym false, a kilka — jak showGridLines — z domyślnym true. Excel z przypadku zapisuje wyłącznie cyfry niekanoniczne. LibreOffice i niektórzy inni producenci zapisują słowa, więc pakiet z Calca niesie <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> tam, gdzie Excel napisałby customHeight="1"
Ta asymetria jest powodem, dla którego porównania po jednej cyfrze przeżywają latami w czytnikach OOXML. Zbuduj korpus testowy z wyjścia Excela, a każdy boolean, jakiego kiedykolwiek zobaczysz, to 0 albo 1. Porównanie przechodzi każdy test. Potem plik Calca trafia do użytkownika. Włóż przynajmniej kilka pakietów zapisanych przez LibreOffice do każdego korpusu XLSX, który napędza testy czytnika — jego pisownia booleanów to jedno z kilku miejsc, gdzie dwaj producenci się różnią, ta sama przepaść, co wychodzi w formułach i regułach, gdy przenosisz skoroszyty między Excel a LibreOffice przez ODS. Jeden szczegół tego typu danych ma znaczenie dla projektu parsera: literały rozróżniają wielkość liter, więc TRUE w ogóle nie jest poprawnym xsd:boolean — punkt, który wraca, gdy decydujesz, jak pobłażliwy ma być twój własny czytnik
Na czym myli się porównanie po jednym znaku
Czytnik porównujący atrybut z pojedynczą cyfrą myli się w jedną albo drugą stronę. W którą — zależy od domyślnej wartości, którą porównanie po cichu koduje. Tabela wypisuje kształty, które wyrastają w ręcznie pisanym kodzie OOXML, co każdy robi z pisowniami słownymi i kilka atrybutów, gdzie szkody wychodzą na jaw. Pomija showDropDown, którego problemem jest nazwa, a nie pisownia: domyślna schematu to false, ale showDropDown="1" na walidacji listy chowa strzałkę w komórce, więc TXLSXDataValidation.ShowDropDown wystawia znaczenie widziane przez użytkownika (True pokazuje strzałkę) i przechowuje odwróconą flagę OOXML
| Naiwny test | Implikowany domyślny | Źle czyta | Gdzie wychodzi |
|---|---|---|---|
Value = '1' | false | true staje się false | sheetFormatPr customHeight, <v> komórki t="b" |
Value <> '0' | true, gdy obecny | false staje się true | kolumna hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false jest ignorowane, domyślna zostaje włączona | sheetView showGridLines, przełączniki uprawnień sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true jest ucinane do t | ramki tabeli danych ChartML, smooth serii |
Środkowy wiersz produkuje najbardziej widoczne szkody, bo hidden="false" na elemencie <col> to zupełnie normalna rzecz, jaką producent zapisuje, gdy odnotowuje szerokość widocznej kolumny, a czytnik traktujący każdy niezerowy napis jako ustawiony chowa tę kolumnę przy wczytaniu. HotXLS prowadzi każdy z tych atrybutów przez jedną konwersję xsd:boolean, która ścina otaczające białe znaki, przyjmuje cyfry i słowa, a dla czegokolwiek innego zwraca własną domyślną atrybutu. Ta ostatnia gałąź odzwierciedla wybór projektu wart wypowiedzenia: wartość typu yes to uszkodzony plik — ale odmowa otwarcia skoroszytu przez jedną zepsutą flagę widoczności karałaby użytkownika za błąd producenta, więc czytnik trzyma stały fallback per atrybut i jedzie dalej
Czytanie arkusza w stylu LibreOffice z HotXLS
Flagi poziomu arkusza wychodzą jako zwykłe własności na TXLSXWorksheet: CustomHeight, ThickTop i ThickBottom pochodzą z <sheetFormatPr>, podczas gdy ColHidden[], RowHidden[], RowThickTop[] i RowThickBottom[] biorą numery kolumn i wierszy liczone od 1. Komórka boolean przychodzi jako wariant Boolean w TXLSXCell.Value, niezależnie od pisowni użytej przez element <v>. Procedura poniżej wypisuje wszystkie dla pierwszego arkusza, a lista za nią daje oczekiwane wyjście dla znaczników w stylu Calca
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]; // indeksator Sheets jest liczony od 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"/>:CustomHeightwypisuje TRUE<col min="3" max="3" width="20" hidden="false"/>: kolumna 3 nie zostaje wypisana<row r="5" thickBot="true">: wiersz 5 zgłaszathickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 trzyma TRUE
Podpowiedź wierszowa to miejsce, gdzie sam schemat robi się niezdarne. Jedna idea, dwie pisownie — CT_SheetFormatPr nazywa swoje atrybuty thickTop i thickBottom, podczas gdy CT_Row używa thickTop i thickBot, więc czytnik szukający thickBottom na wierszu nie znajduje niczego. HotXLS zapisuje każdą formę z powrotem na jej własnym elemencie, a podpowiedzi przeżywają zapis razem z resztą pakietu — podobnie jak nietknięte części w bezstratnym round trippie XLSX
Jak wykres ma czytać element bez val?
Przełącznik ChartML bez atrybutu val jest włączony. DrawingML Charts deklaruje te przełączniki przez CT_Boolean w dml-chart.xsd, którego val jest opcjonalnym xsd:boolean domyślnym do true, więc <c:plotVisOnly/> i <c:showHorzBorder/> znaczą to samo co zapisanie val="1". Ta domyślna jest odwrotnością większości atrybutów SpreadsheetML i łamie pospolity skrót czytania XML wykresu jako tekstu: znajdź element po nazwie, potem szukaj w przód następnego val=". Gdy element pomija val, to szukanie przebiega za koniec znacznika otwierającego i pożycza wartość dowolnego elementu, który przyjdzie później w części — może val="0" dwa rodzeństwa dalej. Gdy nie pasuje nic, PosEx zwraca 0, a kod krojący przy P + 5 kończy z czytaniem od znaku 5 części, czyli od l w <?xml. Żadna z tych awarii niczego nie podnosi; własność wykresu po prostu rozmija się z plikiem
Cudzysłowy to druga połowa problemu. XML 1.0 pozwala ograniczać wartość atrybutu cudzysłowem podwójnym albo pojedynczym (produkcja AttValue w §2.3), więc <c:majorTickMark val='out'/> jest równie legalny jak forma w podwójnych, a szukanie val=" pomija go w całości. HotXLS zachowuje oryginalny ChartML, żeby umieć edytować wykresy Excela bez gubienia znaczników, których nie modeluje, a przełączniki czytane z tego tekstu są wyszukiwane wyłącznie wewnątrz własnego znacznika otwierającego elementu, z przyjmowaniem obu stylów cudzysłowów i brakującym val czytanym jako domyślna CT_Boolean. Publiczne własności wykresu odzwierciedlają to czytanie
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 jest liczone od 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;
Oczekiwane dla części wykresu zawierającej <c:plotVisOnly/>, dalej element z val="0", tabelę danych z <c:showHorzBorder/><c:showVertBorder val="0"/> i oś wartości z <c:majorTickMark val='out'/>: PlotVisibleOnly i ShowDataTableHorzBorder oba TRUE, a znacznik wypisuje out
Ograniczony czytnik xsd:boolean do własnego kodu OOXML
Gdy sam post-processujesz pakiet — powiedzmy własną część dodawaną przez aplikację albo szybki przegląd XML wykresu w kroku budowania — obowiązują te same dwie reguły: konwertuj z pełną przestrzenią leksykalną i nigdy nie pozwalaj wyszukiwaniu atrybutu opuścić znacznika otwierającego elementu, do którego należy. Funkcje poniżej robią oba. ChartSwitch oczekuje, że TagStart wskazuje < elementu, i stosuje domyślną CT_Boolean, gdy val nie ma
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: wypełnienie jest legalne
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: nie 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;
// nazwa musi zaczynać token, a nie kończyć inną nazwę atrybutu
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: brak val znaczy true
Result := ReadXsdBoolean(V, True);
end;
Ścisła gałąź w TryParseXsdBoolean idzie za typem danych co do litery i tu twój czytnik z HotXLS trochę się rozchodzą: konwersja HotXLS porównuje słowa bez względu na wielkość liter, więc TRUE czyta się jako true. Wolimy przyjąć pisownię, której schemat zabrania, niż wyłączyć flagę, bo jakiś producent ją skapitalizował — żaden producent nie zapisuje TRUE w znaczeniu false; walidator przeciwnie, powinien trzymać ścisłą gałąź i zgłosić wartość. Skaner tekstowy bierze też dwa skróty bezpieczne dla przełączników ChartML i złe dla XML w ogólności. > jest legalny wewnątrz wartości atrybutu, więc zatrzymanie na najwcześniejszym może uciąć znacznik, którego wartości go zawierają, a XML pozwala biel wokół =, czego szukanie AttrName + '=' nie widzi. Wartości przełączników ChartML to krótkie tokeny bez > w środku, a pomijanie bieli wokół znaku równości to rozszerzenie na dwa wiersze, ale dla czegokolwiek ponad garstkę znanych przełączników czytnik XML podający atrybuty po jednym elemencie naraz usuwa oba problemy — i to właśnie używa HotXLS dla części arkuszy
Gdy skoroszyt Calca wczytuje się z innymi ukrytymi kolumnami albo wysokościami wierszy niż jego bliźniak z Excela, rozpakuj oba i zobacz, jak flagi są zapisane; hidden="false" kontra hidden="0" zwykle tłumaczy przepaść w ciągu minuty. Mapowanie obu pisowni, wypchanych i gołych, na jeden zestaw własności arkusza, komórek i wykresu to robota, którą HotXLS, komponent arkuszy dla Delphi wykonuje przy każdym wczytaniu