Abra un libro guardado por LibreOffice Calc y una columna escrita como hidden="false" puede volver oculta, aunque nada esté mal en el archivo. Bajo xsd:boolean, el tipo detrás de ese atributo, true, false, 1 y 0 son todos legales, con o sin espacios alrededor; HotXLS para Delphi lee cada una de esas grafías dondequiera que un schema XLSX declare un booleano
La mitad de xsd:boolean que Excel nunca escribe
W3C XML Schema Parte 2 §3.2.2 define boolean con cuatro literales legales, true, false, 1 y 0, y fija su faceta whiteSpace en collapse, lo que significa que customHeight=" 1 " es tan válido como customHeight="1". La misma sección nombra a true y false como las formas canónicas. SpreadsheetML no hace nada por estrechar eso: sml.xsd declara customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix y decenas de sus vecinos como xsd:boolean pelado, la mayoría con un valor por defecto explícito de false y unos pocos, como showGridLines, con defecto true. A Excel le pasa escribir solo los dígitos no canónicos. LibreOffice y algunos otros productores escriben las palabras, así que un paquete de Calc lleva <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> donde Excel habría escrito customHeight="1"
Esa asimetría es la razón por la que comparaciones de un dígito sobreviven años en lectores de OOXML. Arme un corpus de pruebas con salida de Excel y cada booleano que vaya a ver será 0 o 1: la comparación pasa todas las pruebas. Entonces un archivo de Calc llega a un usuario. Meta al menos unos cuantos paquetes guardados por LibreOffice en cualquier corpus XLSX que impulse las pruebas de su lector — su grafía de los booleanos es uno de varios puntos donde los dos productores difieren, la misma brecha que aparece en fórmulas y reglas cuando usted mueve libros entre Excel y LibreOffice vía ODS. Un detalle más del datatype importa para el diseño del parser: los literales distinguen mayúsculas, así que TRUE no es un xsd:boolean válido en absoluto, un punto que vuelve cuando usted decide qué tan tolerante debe ser su propio lector
Lo que una comparación de un carácter lee mal
Un lector que compara un atributo contra un solo dígito falla en una dirección o en la otra, y la dirección depende del valor por defecto que la comparación codifica a escondidas. La tabla lista las formas que aparecen en código OOXML escrito a mano, qué le hacen a las grafías de palabras y algunos atributos donde el daño se nota. Deja fuera showDropDown, cuyo problema es su nombre y no su grafía: el defecto del schema es false, pero showDropDown="1" en una validación de lista oculta la flecha en celda, así que TXLSXDataValidation.ShowDropDown expone el significado de cara al usuario (True muestra la flecha) y guarda la bandera OOXML invertida
| Prueba ingenua | Defecto implicado | Lectura errónea | Dónde se nota |
|---|---|---|---|
Value = '1' | false | true se vuelve false | sheetFormatPr customHeight, el <v> de una celda t="b" |
Value <> '0' | true cuando está presente | false se vuelve true | columna hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false se ignora, el defecto queda activo | sheetView showGridLines, interruptores de permisos de sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true queda cortado a t | bordes de data table de ChartML, smooth de series |
La fila del medio produce el daño más visible, porque hidden="false" en un elemento <col> es algo perfectamente normal para un productor que registra el ancho de una columna visible, y un lector que trata cualquier texto distinto de cero como activado oculta esa columna al cargar. HotXLS pasa cada uno de estos atributos por una sola conversión xsd:boolean que recorta los espacios alrededor, acepta los dígitos y las palabras, y devuelve el defecto propio del atributo para cualquier otra cosa. Esa última rama refleja una decisión de diseño que vale la pena enunciar: un valor como yes es un archivo malformado — pero negarse a abrir un libro por una bandera de visibilidad mala castigaría al usuario por el error del productor, así que el lector guarda un fallback fijo por atributo y sigue
Leer una hoja estilo LibreOffice con HotXLS
Las banderas a nivel de hoja aparecen como propiedades ordinarias en TXLSXWorksheet: CustomHeight, ThickTop y ThickBottom vienen de <sheetFormatPr>, mientras que ColHidden[], RowHidden[], RowThickTop[] y RowThickBottom[] toman números de columna y fila base 1. Una celda booleana llega como variant Boolean en TXLSXCell.Value, sea cual sea la grafía que usó el elemento <v>. La rutina de abajo imprime todas para la primera hoja, y la lista posterior da la salida 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]; // el indexador de Sheets es base 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"/>:CustomHeightimprime TRUE<col min="3" max="3" width="20" hidden="false"/>: la columna 3 no aparece<row r="5" thickBot="true">: la fila 5 reportathickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 contiene TRUE
La pista de fila es donde el schema en sí se pone incómodo: una idea, dos grafías — CT_SheetFormatPr nombra sus atributos thickTop y thickBottom, mientras que CT_Row usa thickTop y thickBot, así que un lector que busca thickBottom en una fila no encuentra nada. HotXLS escribe cada forma de vuelta en su propio elemento, y las pistas sobreviven un guardado junto con el resto del paquete, igual que las partes intactas en un viaje de ida y vuelta XLSX sin pérdidas
¿Cómo debe leer un gráfico un elemento sin val?
Un interruptor de ChartML sin atributo val está activado. DrawingML Charts declara esos interruptores con CT_Boolean en dml-chart.xsd, cuyo val es un xsd:boolean opcional con defecto true, así que <c:plotVisOnly/> y <c:showHorzBorder/> significan lo mismo que escribir val="1". Ese defecto es el inverso de la mayoría de los atributos de SpreadsheetML, y rompe el atajo común de leer el XML de gráficos como texto: encontrar el elemento por nombre y luego buscar hacia adelante el siguiente val=". Cuando el elemento omite el val, esa búsqueda corre más allá del final del tag de apertura y pide prestado el valor del elemento que venga después en la parte, quizás un val="0" dos hermanos más abajo. Cuando no coincide nada, PosEx devuelve 0, y un código que corta en P + 5 acaba leyendo desde el carácter 5 de la parte, que es la l de <?xml. Ninguno de los dos fallos lanza nada; la propiedad del gráfico simplemente discrepa del archivo
Las comillas son la otra mitad del problema. XML 1.0 permite delimitar el valor de un atributo con comillas dobles o simples (la producción AttValue de la §2.3), así que <c:majorTickMark val='out'/> es tan legítimo como la forma con comillas dobles, y una búsqueda de val=" se lo salta por completo. HotXLS conserva el ChartML original para poder editar gráficos de Excel sin perder markup que no modela, y los interruptores leídos de ese texto se buscan únicamente dentro del tag de apertura del propio elemento, aceptando cualquier estilo de comillas y leyendo un val ausente como el defecto de CT_Boolean. Las propiedades públicas del gráfico reflejan esa lectura
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 es base 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;
Esperado para una parte de gráfico que contiene <c:plotVisOnly/> seguido más adelante por un elemento con val="0", una data table con <c:showHorzBorder/><c:showVertBorder val="0"/>, y un eje de valores con <c:majorTickMark val='out'/>: PlotVisibleOnly y ShowDataTableHorzBorder son ambos TRUE, y la marca de tick imprime out
Un lector xsd:boolean acotado para su propio código OOXML
Cuando usted postprocesa un paquete por su cuenta — digamos una parte a medida que su aplicación añade o un escaneo rápido del XML de gráficos en un paso de build — aplican las mismas dos reglas: convertir con el espacio léxico completo y nunca dejar que una búsqueda de atributo salga del tag de apertura del elemento al que pertenece. Las funciones de abajo hacen ambas cosas. ChartSwitch espera que TagStart apunte al < del elemento y aplica el defecto de CT_Boolean cuando falta el val
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: el relleno es 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: no es 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;
// el nombre debe abrir un token, no cerrar el nombre de otro 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: un val ausente significa true
Result := ReadXsdBoolean(V, True);
end;
La rama estricta de TryParseXsdBoolean sigue el datatype al pie de la letra, y ahí su lector y HotXLS se separan un poco: la conversión de HotXLS compara las palabras sin distinguir mayúsculas, así que TRUE lee como true. Preferimos aceptar una grafía que el schema prohíbe antes que apagar una bandera porque algún productor la escribió en mayúsculas, ya que ningún productor escribe TRUE para decir false; un validador, en cambio, debería quedarse con la rama estricta y reportar el valor. El escáner de texto también toma dos atajos que son seguros para interruptores de ChartML y equivocados para XML en general: un > es legal dentro del valor de un atributo, así que parar en el primero puede truncar un tag cuyos valores lo contengan, y XML permite espacios alrededor del =, que la búsqueda de AttrName + '=' no ve. Los valores de los interruptores ChartML son tokens cortos sin >, y saltarse los espacios alrededor del signo igual es una extensión de dos líneas, pero para algo más allá de un puñado de interruptores conocidos, un lector XML que le entrega los atributos de un elemento a la vez elimina ambos problemas, y es lo que HotXLS usa para las partes de hojas de cálculo
Cuando un libro de Calc carga con columnas ocultas o alturas de fila distintas a las de su gemelo de Excel, descomprima ambos y mire cómo están escritas las banderas; hidden="false" contra hidden="0" normalmente explica la brecha en un minuto. Mapear ambas grafías, con relleno o desnudas, sobre un único set de propiedades de hoja, celda y gráfico es el trabajo que el componente de hojas de cálculo HotXLS para Delphi hace en cada carga