Artículo técnico

Excel escribe 1, LibreOffice escribe true: booleanos XLSX

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

HotXLS parseo de atributos xsd:boolean en Delphi: Excel escribe customHeight="1" mientras LibreOffice Calc escribe customHeight="true", ambos dentro del espacio léxico de cuatro literales true, false, 1 y 0 con colapso de espacios, leídos por una sola conversión de HotXLS
Excel solo emite los dígitos y LibreOffice Calc emite las palabras, pero ambas grafías viven dentro de un datatype de cuatro literales que un lector debe aceptar por completo

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 ingenuaDefecto implicadoLectura erróneaDónde se nota
Value = '1'falsetrue se vuelve falsesheetFormatPr customHeight, el <v> de una celda t="b"
Value <> '0'true cuando está presentefalse se vuelve truecolumna hidden, definedName hidden, alignment wrapText
Value = '0'truefalse se ignora, el defecto queda activosheetView showGridLines, interruptores de permisos de sheetProtection
Copy(Raw, P + 5, 1) = '1'falsetrue queda cortado a tbordes 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

Lector de atributos booleanos XLSX de HotXLS comparado con las pruebas ingenuas Value = '1', Value <> '0', Value = '0' y Copy(Raw, P + 5, 1) en Delphi, mostrando qué grafías de palabras lee mal cada una para customHeight, column hidden, showGridLines y ChartML smooth
Toda prueba de un carácter pasa con 1 y 0 y falla con exactamente una grafía de palabra, razón por la que un corpus de pruebas solo de Excel jamás la detecta

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"/>: CustomHeight imprime TRUE
  • <col min="3" max="3" width="20" hidden="false"/>: la columna 3 no aparece
  • <row r="5" thickBot="true">: la fila 5 reporta thickBot=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

HotXLS lectura de interruptores CT_Boolean de ChartML en Delphi: una búsqueda hacia adelante de val=" toma prestado un val="0" posterior, se pierde val='out' y corta <?xml cuando PosEx devuelve 0, mientras que HotXLS lee solo el tag de apertura para PlotVisibleOnly y ShowDataTableHorzBorder
Acotar la búsqueda del val al tag de apertura del propio elemento arregla de una vez tres lecturas erróneas silenciosas de gráficos: un valor prestado, un valor con comillas simples saltado y un corte desde la declaración 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 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