Aprite una cartella salvata da LibreOffice Calc e una colonna scritta come hidden="false" può tornare nascosta, anche se nel file non c'è nulla di sbagliato. Secondo xsd:boolean, il tipo dietro quell'attributo, true, false, 1 e 0 sono tutti legali, con o senza spazi attorno; HotXLS per Delphi legge ognuna di quelle forme ovunque uno schema XLSX dichiari un boolean
La metà di xsd:boolean che Excel non scrive mai
W3C XML Schema Parte 2 §3.2.2 definisce boolean con quattro letterali legali, true, false, 1 e 0, e fissa la sua facet whiteSpace su collapse, il che significa che customHeight=" 1 " è valido quanto customHeight="1". La stessa sezione nomina true e false come forme canoniche. SpreadsheetML non fa nulla per restringere il campo: sml.xsd dichiara customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix e decine dei loro vicini come semplici xsd:boolean, la maggior parte con default esplicito false e qualcuno, come showGridLines, che vale true per default. Excel per abitudine scrive solo le cifre non canoniche. LibreOffice e altri producer scrivono le parole, quindi un pacchetto da Calc porta <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> dove Excel avrebbe scritto customHeight="1"
Quell'asimmetria è il motivo per cui i confronti a una cifra sopravvivono per anni nei reader OOXML. Costruite un corpus di test con output di Excel e ogni boolean che vedrete mai è 0 o 1: il confronto passa ogni test. Poi un file di Calc arriva a un utente. Mettete almeno qualche pacchetto salvato da LibreOffice in qualsiasi corpus XLSX che guida i test dei reader — la sua forma di scrivere i boolean è uno dei vari punti in cui i due producer differiscono, lo stesso scarto che compare in formule e regole quando spostate cartelle fra Excel e LibreOffice tramite ODS. Un altro dettaglio del datatype conta per la progettazione del parser: i letterali distinguono maiuscole e minuscole, quindi TRUE non è affatto un xsd:boolean valido, un punto che torna quando decidete quanto permissivo deve essere il vostro reader
Cosa sbaglia un confronto a un carattere
Un reader che confronta un attributo con una sola cifra fallisce in una direzione o nell'altra, e la direzione dipende dal default che il confronto codifica in silenzio. La tabella elenca le forme che compaiono nel codice OOXML scritto a mano, cosa fanno alle forme con parole e qualche attributo in cui il danno si vede. Lascia fuori showDropDown, il cui problema è il nome più che la forma: il default dello schema è false, eppure showDropDown="1" su una validazione a elenco nasconde la freccia nella cella, quindi TXLSXDataValidation.ShowDropDown espone il significato rivolto all'utente (True mostra la freccia) e conserva la flag OOXML invertita
| Test ingenuo | Default implicito | Legge male | Dove si vede |
|---|---|---|---|
Value = '1' | false | true diventa false | sheetFormatPr customHeight, la <v> di una cella t="b" |
Value <> '0' | true se presente | false diventa true | colonna hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false viene ignorato, il default resta acceso | sheetView showGridLines, interruttori di permesso sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true viene troncato a t | bordi della data table ChartML, smooth delle serie |
La riga di mezzo produce il danno più visibile, perché hidden="false" su un elemento <col> è una cosa perfettamente normale da scrivere per un producer che registra una larghezza per una colonna visibile, e un reader che tratta qualunque testo non nullo come impostato nasconde quella colonna al caricamento. HotXLS instrada ognuno di questi attributi attraverso una sola conversione xsd:boolean che toglie gli spazi attorno, accetta le cifre e le parole, e restituisce il default dell'attributo per tutto il resto. Quell'ultimo ramo riflette una scelta di progetto che vale la pena dichiarare: un valore tipo yes è un file malformato — ma rifiutarsi di aprire una cartella per una flag di visibilità sbagliata punirebbe l'utente per l'errore del producer, quindi il reader mantiene un fallback fisso per attributo e va avanti
Leggere un foglio in stile LibreOffice con HotXLS
Le flag a livello di foglio affiorano come proprietà ordinarie su TXLSXWorksheet: CustomHeight, ThickTop e ThickBottom vengono da <sheetFormatPr>, mentre ColHidden[], RowHidden[], RowThickTop[] e RowThickBottom[] prendono numeri di colonna e di riga a base uno. Una cella boolean arriva come variante Boolean in TXLSXCell.Value, qualunque forma abbia usato l'elemento <v>. La routine qui sotto le stampa tutte per il primo foglio, e l'elenco dopo dà l'output atteso per markup in stile 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]; // l'indicizzatore di Sheets è a base uno
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"/>:CustomHeightstampa TRUE<col min="3" max="3" width="20" hidden="false"/>: la colonna 3 non compare<row r="5" thickBot="true">: la riga 5 riferiscethickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 contiene TRUE
L'indizio sulle righe è dove lo schema in persona si fa contorto. Un'idea, due forme — CT_SheetFormatPr nomina i propri attributi thickTop e thickBottom, mentre CT_Row usa thickTop e thickBot, quindi un reader che cerca thickBottom su una riga non trova nulla. HotXLS riscrive ciascuna forma sul proprio elemento, e gli indizi sopravvivono a un save insieme al resto del pacchetto, molto come fanno le parti intatte in un round trip XLSX senza perdite
Come deve leggere un grafico un elemento senza val?
Un interruttore ChartML senza attributo val è acceso. DrawingML Charts dichiara quegli interruttori con CT_Boolean in dml-chart.xsd, il cui val è un xsd:boolean opzionale che vale true per default, quindi <c:plotVisOnly/> e <c:showHorzBorder/> significano lo stesso che scrivere val="1". Quel default è il contrario della maggior parte degli attributi SpreadsheetML, e rompe la scorciatoia comune di leggere l'XML dei grafici come testo: trovare l'elemento per nome, poi cercare in avanti il prossimo val=". Quando l'elemento omette val, quella ricerca corre oltre la fine dello start tag e prende in prestito il valore di qualunque elemento venga più avanti nella parte, magari un val="0" due fratelli più giù. Quando non corrisponde nulla, PosEx restituisce 0, e codice che affetta a P + 5 finisce per leggere dal carattere 5 della parte, cioè la l di <?xml. Nessuno dei due guasti solleva nulla; la proprietà del grafico semplicemente non è d'accordo col file
Le virgolette sono l'altra metà del problema. XML 1.0 permette a un valore di attributo di essere delimitato da virgolette doppie o singole (la produzione AttValue in §2.3), quindi <c:majorTickMark val='out'/> è legittimo quanto la forma con doppie, e una ricerca di val=" la salta del tutto. HotXLS conserva il ChartML originale così che possa modificare i grafici Excel senza perdere il markup che non modella, e gli interruttori letti da quel testo vengono cercati solo dentro lo start tag dell'elemento stesso, con entrambi gli stili di virgolette accettati e un val assente letto come default CT_Boolean. Le proprietà pubbliche dei grafici riflettono questa lettura
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 è a base zero
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;
Atteso per una parte di grafico contenente <c:plotVisOnly/> seguito più avanti da un elemento con val="0", una data table con <c:showHorzBorder/><c:showVertBorder val="0"/>, e un asse dei valori con <c:majorTickMark val='out'/>: PlotVisibleOnly e ShowDataTableHorzBorder sono entrambi TRUE, e il tick mark stampa out
Un reader xsd:boolean con confini per il vostro codice OOXML
Quando fate voi il post-processing di un pacchetto, diciamo una parte personalizzata che la vostra applicazione aggiunge o una scorsa rapida dell'XML dei grafici in una fase di build, valgono le stesse due regole: convertite con lo spazio lessicale completo, e non lasciate mai che una ricerca di attributo esca dallo start tag dell'elemento a cui appartiene. Le funzioni qui sotto fanno entrambe le cose. ChartSwitch si aspetta che TagStart punti alla < dell'elemento e applica il default CT_Boolean quando val manca
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: il padding è legale
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: non è 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;
// il nome deve iniziare un token, non chiudere il nome di un altro attributo
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: un val assente vale true
Result := ReadXsdBoolean(V, True);
end;
Il ramo severo di TryParseXsdBoolean segue il datatype alla lettera, ed è lì che il vostro reader e HotXLS si dividono un poco: la conversione di HotXLS confronta le parole senza riguardo per il maiuscolo/minuscolo, quindi TRUE si legge come true. Preferiamo accettare una forma che lo schema vieta piuttosto che spegnere una flag perché qualche producer l'ha scritta in maiuscolo, visto che nessun producer scrive TRUE per dire false; un validator, al contrario, dovrebbe tenere il ramo severo e riferire il valore. Lo scanner di testo prende anche due scorciatoie che sono sicure per gli interruttori ChartML e sbagliate per l'XML in generale: una > è legale dentro un valore di attributo, quindi fermarsi alla prima può troncare un tag i cui valori la contengono, e l'XML ammette spazi attorno al =, che la ricerca AttrName + '=' non vede. I valori degli interruttori ChartML sono token corti senza > dentro, e saltare gli spazi attorno al segno di uguale è un'estensione di due righe, ma per tutto ciò che va oltre una manciata di interruttori noti un reader XML che vi consegna gli attributi di un elemento alla volta elimina entrambi i problemi, ed è ciò che HotXLS usa per le parti dei fogli
Quando una cartella Calc si carica con colonne nascoste o altezze di riga diverse dalla sua gemella Excel, scompattate entrambe e guardate come sono scritte le flag; hidden="false" contro hidden="0" di solito spiega lo scarto in un minuto. Mappare entrambe le forme, con padding o nude, su un solo insieme di proprietà di foglio, cella e grafico è il lavoro che il componente fogli di calcolo HotXLS per Delphi fa a ogni caricamento