Abra um workbook salvo pelo LibreOffice Calc e uma coluna gravada como hidden="false" pode voltar oculta, embora nada no arquivo esteja errado. Sob xsd:boolean, o tipo por trás desse atributo, true, false, 1 e 0 são todos legais, com ou sem brancos ao redor; o HotXLS para Delphi lê cada uma dessas grafias onde quer que um schema XLSX declare um boolean
A metade do xsd:boolean que o Excel nunca grava
A W3C XML Schema Part 2 §3.2.2 define boolean com quatro literais legais, true, false, 1 e 0, e fixa a faceta whiteSpace dele em collapse, o que significa que customHeight=" 1 " é tão válido quanto customHeight="1". A mesma seção nomeia true e false como as formas canônicas. O SpreadsheetML não faz nada para estreitar isso: o sml.xsd declara customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix e dezenas de vizinhos deles como xsd:boolean puro, a maioria com default explícito de false e alguns, como showGridLines, com default true. O Excel por acaso grava só os dígitos não canônicos. LibreOffice e alguns outros produtores gravam as palavras, então um pacote do Calc carrega <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> onde o Excel teria gravado customHeight="1"
Essa assimetria é por que comparações de um dígito sobrevivem por anos em readers OOXML. Monte um corpus de teste a partir da saída do Excel e todo boolean que você jamais verá é 0 ou 1. A comparação passa em todo teste. Então um arquivo do Calc chega a um usuário. Ponha pelo menos alguns pacotes salvos pelo LibreOffice em qualquer corpus XLSX que mova testes de reader — a grafia de booleans dele é um dos vários lugares em que os dois produtores divergem, a mesma lacuna que aparece em fórmulas e regras quando você move workbooks entre Excel e LibreOffice via ODS. Mais um detalhe do datatype importa para o design do parser: os literais são case sensitive, então TRUE não é um xsd:boolean válido de forma alguma, um ponto que volta quando você decide quão tolerante o seu próprio reader deve ser
O que uma comparação de um caractere erra
Um reader que compara um atributo contra um único dígito falha em uma direção ou na outra. Qual direção depende do default que a comparação embute às escuras. A tabela lista as formas que aparecem em código OOXML escrito à mão, o que cada uma faz com as grafias de palavras e alguns atributos onde o dano aparece. Ela deixa de fora o showDropDown, cujo problema é o nome dele e não a grafia: o default do schema é false, mas showDropDown="1" numa validação de lista esconde a seta na célula, então o TXLSXDataValidation.ShowDropDown expõe o significado voltado ao usuário (True mostra a seta) e armazena a flag OOXML invertida
| Teste ingênuo | Default implícito | Lê errado | Onde aparece |
|---|---|---|---|
Value = '1' | false | true vira false | sheetFormatPr customHeight, o <v> de uma célula t="b" |
Value <> '0' | true quando presente | false vira true | coluna hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false é ignorado, o default segue ligado | sheetView showGridLines, switches de permissão do sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true é cortado para t | bordas de data table no ChartML, smooth de série |
A linha do meio produz o dano mais visível, porque hidden="false" num elemento <col> é coisa perfeitamente normal de um produtor gravar quando ele registra uma largura para uma coluna visível, e um reader que trata qualquer texto não zero como definido oculta essa coluna no load. O HotXLS encaminha cada um desses atributos por uma única conversão xsd:boolean que corta brancos ao redor, aceita os dígitos e as palavras, e devolve o default do próprio atributo para qualquer outra coisa. Esse último ramo reflete uma escolha de design que vale enunciar: um valor como yes é arquivo malformado — mas recusar abrir um workbook por causa de uma flag de visibilidade ruim puniria o usuário pelo erro do produtor, então o reader mantém um fallback fixo por atributo e segue em frente
Lendo uma planilha estilo LibreOffice com o HotXLS
As flags de nível de planilha aparecem como propriedades comuns no TXLSXWorksheet: CustomHeight, ThickTop e ThickBottom vêm do <sheetFormatPr>, enquanto ColHidden[], RowHidden[], RowThickTop[] e RowThickBottom[] recebem números de coluna e linha de base um. Uma célula boolean chega como variante Boolean no TXLSXCell.Value, seja qual grafia o elemento <v> tenha usado. A rotina abaixo imprime todas para a primeira planilha, e a lista depois dela dá a saída esperada para markup estilo 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]; // o indexador Sheets é 1-based
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"/>:CustomHeightimprime TRUE<col min="3" max="3" width="20" hidden="false"/>: a coluna 3 não é listada<row r="5" thickBot="true">: a linha 5 reportathickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 guarda TRUE
A dica de linha é onde o próprio schema fica esquisito. Uma ideia, duas grafias — o CT_SheetFormatPr nomeia os atributos dele thickTop e thickBottom, enquanto o CT_Row usa thickTop e thickBot, então um reader que procura thickBottom numa linha não encontra nada. O HotXLS grava cada forma de volta no próprio elemento dela, e as dicas sobrevivem a um save junto com o resto do pacote, muito como as partes intocadas fazem em uma ida e volta XLSX sem perdas
Como um gráfico deve ler um elemento sem val?
Um switch ChartML sem atributo val está ligado. O DrawingML Charts declara esses switches com CT_Boolean no dml-chart.xsd, cujo val é um xsd:boolean opcional com default true, então <c:plotVisOnly/> e <c:showHorzBorder/> significam o mesmo que gravar val="1". Esse default é o inverso da maioria dos atributos SpreadsheetML, e ele quebra o atalho comum de ler XML de gráfico como texto: achar o elemento pelo nome, depois procurar adiante pelo próximo val=". Quando o elemento omite o val, essa busca corre além do fim da start tag e toma emprestado o valor de qualquer elemento que venha depois na part, talvez um val="0" dois irmãos adiante. Quando nada bate, o PosEx retorna 0, e código que corta em P + 5 acaba lendo a partir do caractere 5 da part, que é o l de <?xml. Nenhuma das falhas levanta exceção; a propriedade do gráfico só diverge do arquivo
Aspas são a outra metade do problema. O XML 1.0 permite que um valor de atributo seja delimitado por aspas duplas ou simples (a produção AttValue em §2.3), então <c:majorTickMark val='out'/> é tão legítimo quanto a forma com aspas duplas, e uma busca por val=" o ignora por completo. O HotXLS preserva o ChartML original para que possa editar gráficos do Excel sem perder markup que ele não modela, e os switches lidos desse texto são procurados dentro da própria start tag do elemento apenas, com qualquer estilo de aspas aceito e um val ausente lido como o default do CT_Boolean. As propriedades públicas de gráfico refletem essa leitura
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-based
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;
Esperado para uma part de gráfico contendo <c:plotVisOnly/> seguida mais adiante por um elemento com val="0", um data table com <c:showHorzBorder/><c:showVertBorder val="0"/>, e um eixo de valores com <c:majorTickMark val='out'/>: PlotVisibleOnly e ShowDataTableHorzBorder são ambos TRUE, e o tick mark imprime out
Um reader xsd:boolean com limites para o seu próprio código OOXML
Quando você faz pós-processamento de um pacote por conta própria, digamos uma part customizada que a sua aplicação acrescenta ou um scan rápido do XML de gráfico numa etapa de build, as mesmas duas regras valem: converter com o espaço lexical completo, e nunca deixar uma busca por atributo sair da start tag do elemento a que ela pertence. As funções abaixo fazem as duas coisas. O ChartSwitch espera que o TagStart aponte para o < do elemento e aplica o default do CT_Boolean quando o val está ausente
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: padding é legal
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: não é 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;
// o nome precisa começar um token, não terminar outro nome de atributo
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 ausente significa true
Result := ReadXsdBoolean(V, True);
end;
O ramo estrito no TryParseXsdBoolean segue o datatype à risca, e é aí que o seu reader e o HotXLS divergem um pouco: a conversão do HotXLS compara as palavras sem considerar a caixa, então TRUE lê como true. Nós preferimos aceitar uma grafia que o schema proíbe a desligar uma flag porque algum produtor a escreveu em maiúsculas, já que nenhum produtor grava TRUE para significar false; um validator, ao contrário, deve manter o ramo estrito e reportar o valor. O scanner de texto também toma dois atalhos que são seguros para switches ChartML e errados para XML em geral. Um > é legal dentro de um valor de atributo, então parar no primeiro pode truncar uma tag cujos valores o contenham, e o XML permite whitespace em volta do =, que a busca por AttrName + '=' não vê. Valores de switch ChartML são tokens curtos sem > dentro, e pular brancos em volta do sinal de igual é uma extensão de duas linhas, mas para qualquer coisa além de um punhado de switches conhecidos um reader XML que entrega os atributos de um elemento por vez elimina os dois problemas, e é o que o HotXLS usa para parts de planilha
Quando um workbook do Calc carrega com colunas ocultas ou alturas de linha diferentes das do seu gêmeo do Excel, descompacte os dois e veja como as flags estão grafadas; hidden="false" contra hidden="0" normalmente explica a diferença em um minuto. Mapear as duas grafias, com padding ou sem, para um único conjunto de propriedades de planilha, célula e gráfico é o trabalho que o componente de planilhas Delphi HotXLS faz em todo load