Articolo tecnico

Excel scrive 1, LibreOffice scrive true: i boolean di XLSX

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

Parsing degli attributi xsd:boolean con HotXLS in Delphi: Excel scrive customHeight="1" mentre LibreOffice Calc scrive customHeight="true", entrambi dentro lo spazio lessicale a quattro letterali di true, false, 1 e 0 con collapse degli spazi, letti con una sola conversione HotXLS
Excel emette solo le cifre e LibreOffice Calc emette le parole, eppure entrambe le forme stanno dentro un datatype a quattro letterali che un reader deve accettare per intero

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 ingenuoDefault implicitoLegge maleDove si vede
Value = '1'falsetrue diventa falsesheetFormatPr customHeight, la <v> di una cella t="b"
Value <> '0'true se presentefalse diventa truecolonna hidden, definedName hidden, alignment wrapText
Value = '0'truefalse viene ignorato, il default resta accesosheetView showGridLines, interruttori di permesso sheetProtection
Copy(Raw, P + 5, 1) = '1'falsetrue viene troncato a tbordi 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

Il reader degli attributi boolean XLSX di HotXLS confrontato con i test ingenui Value = '1', Value <> '0', Value = '0' e Copy(Raw, P + 5, 1) in Delphi, mostrando quale forma con parole legge male ciascuno per customHeight, colonna hidden, showGridLines e ChartML smooth
Ogni test a un carattere passa su 1 e 0 e fallisce su esattamente una forma con parole, ed è per questo che un corpus di test solo Excel non lo becca mai

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"/>: CustomHeight stampa TRUE
  • <col min="3" max="3" width="20" hidden="false"/>: la colonna 3 non compare
  • <row r="5" thickBot="true">: la riga 5 riferisce thickBot=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

Lettura degli interruttori ChartML CT_Boolean con HotXLS in Delphi: una ricerca in avanti di val=" prende in prestito un val="0" successivo, non vede val='out' e affetta <?xml quando PosEx restituisce 0, mentre HotXLS legge solo lo start tag per PlotVisibleOnly e ShowDataTableHorzBorder
Limitare la ricerca di val allo start tag dell'elemento stesso sistemata tre letture sbagliate silenziose di un colpo: un valore preso in prestito, un valore con virgolette singole saltato e un taglio partito dalla dichiarazione XML
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