Artículo técnico

Validación de listas en HotXLS: límite de 255 y x14

Excel se niega a abrir un workbook XLSX cuya validación de datos de lista contiene una lista inline de más de 255 caracteres, y eso se mantiene incluso para un archivo que el propio Excel guardó. HotXLS, el componente Excel nativo para Delphi y C++Builder, carga esas listas y modela las reglas de lista entre hojas como extensiones x14, pero el arreglo durable es el mismo en todas partes: conserve las listas largas en un rango de worksheet o en un nombre definido

El reporte de bug suele llegar de un cliente, no de sus pruebas. Una plantilla le crece una columna Region o Product cuyo desplegable arrancó con ocho entradas y ahora tiene sesenta. Todo sigue funcionando en la copia XLSB que alguien conserva en una unidad compartida. Luego un trabajo de exportación escribe la misma regla en un XLSX, y Excel responde con un diálogo de reparación o una negativa seca. Nada en su código cambió salvo la longitud de una cadena, que es exactamente por qué vale la pena entender esta frontera antes de que ella lo encuentre

¿Dónde aplica en realidad el límite de 255 caracteres de la lista?

El límite aplica a la forma inline de una validación de lista en XLSX, la cadena entre comillas separada por comas guardada en formula1, y Excel la hace cumplir al abrir el archivo, no solo cuando usted teclea la lista en el diálogo de validación de datos. Probar contra Excel directamente dio un resultado limpio: una lista de 253 caracteres abrió, mientras que las de 260 y 421 caracteres fueron rechazadas. El caso de 421 caracteres es el instructivo, porque vino de un workbook que el propio Excel había creado. Excel escribe un archivo que Excel mismo no va a releer

Excel rechaza las listas inline XLSX de más de 255 caracteres mientras los tokens PtgStr de XLSB llevan listas largas, mostrado como matriz de resultados por contenedor en HotXLS Delphi
el muro es una restricción de aplicación por contenedor: Excel rechaza la forma inline XLSX que él mismo puede escribir, mientras los tokens XLSB y las listas respaldadas por rangos llevan los mismos elementos

El esquema no lo salva aquí. ECMA-376 Parte 1, §18.3.1.32 define el elemento dataValidation y sus fórmulas sin enunciar un techo de 255 caracteres, así que un archivo puede ser válido según el esquema y aun así ser rechazado. Esa es una restricción de la aplicación, y la única referencia confiable para ella es el comportamiento del propio Excel

HotXLS no le hace cumplir ese techo. TXLSXWorksheet.AddListValidation envuelve los elementos en comillas y los guarda tal cual; ni trunca ni rechaza una lista larga, así que una lista de 400 caracteres entra al XLSX guardado exactamente como se pidió y Excel luego rechaza el resultado. Es una elección deliberada, porque recortar una lista en silencio corrompería datos de una forma que nadie nota, pero deja la verificación de longitud de su lado. Un helper pequeño la mantiene honesta:

const
  // Excel abrió una lista de 253 caracteres y rechazó las de 260 y 421.
  // Guarde un margen en vez de apuntar al borde exacto.
  MaxInlineListChars = 250;

function InlineListLength(const AItems: array of WideString): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := 0 to High(AItems) do
    Inc(Result, Length(AItems[I]));
  if Length(AItems) > 1 then
    Inc(Result, High(AItems));  // comas separadoras
end;

Trate las comillas dobles incrustadas como un costo de dos caracteres, porque AddListValidation las duplica para mantener válida la cadena de fórmula

Por qué XLSB puede llevar listas que XLSX no

XLSB guarda una lista inline como un único token de cadena PtgStr cuya longitud es un campo de 16 bits, así que el formato binario no tiene muro de 255 caracteres. Una muestra nativa con 60 elementos y 419 caracteres vive en un token y sobrevive a ida y vuelta en Excel sin quejas. La misma lista en XLSX es el caso de 421 caracteres de arriba

Un loader que aplicara el límite de 255 caracteres del diálogo a XLSB rechazaría ese archivo. La verificación de tokens de validación XLSB de HotXLS acota un token de cadena en 32,767 caracteres en cambio, el mismo techo que una cadena de celda de Excel. Dos salvedades valen enunciarlas sin rodeos. El loader también acota una fórmula de validación completa en 16,384 bytes, así que el techo práctico para una lista inline es menor de lo que la verificación de token sola sugiere. Y las listas separadas por caracteres NUL dentro de un PtgStr quedan fuera del perfil soportado, así que se reportan como no soportadas en vez de adivinarse

La trampa está en la conversión. Una lista larga perfectamente sana en XLSB se vuelve un archivo rechazado en el momento en que cualquier cosa, HotXLS incluido, guarde ese workbook como XLSX. Si un pipeline convierte archivos XLSB de clientes, corra la verificación de longitud sobre cada regla de lista antes del guardado XLSX, no después de que un cliente lo reporte

¿Qué pasa con las listas que apuntan a otra hoja?

Excel guarda una lista cuya fuente está en un worksheet distinto en la extensión x14:dataValidations, no en el elemento ordinario dataValidations. La regla vive dentro del extLst del worksheet bajo el URI de extensión {CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}, definido en [MS-XLSX], y su referencia de fuente es un elemento xm:f como Lists!$A$2:$A$61. XLSB conserva la misma regla en sus propios registros de extensión

HotXLS parsea esas reglas de extensión XLSX en la colección tipada DataValidations y marca cada una con TXLSXDataValidation.IsExtension. Una lista entre hojas aparece por tanto junto a las reglas ordinarias con su rango, tipo y fórmula legibles, y sobrevive a ediciones, guardados y reaperturas. Al guardar, el writer mantiene las reglas de extensión fuera del elemento ordinario y reconstruye la extensión x14 en su posición de esquema al final del worksheet. Ambas fórmulas se conservan, así que las reglas de número entero, decimal, longitud de texto, fecha y hora que usan between o notBetween conservan ambos límites

Anatomía de la extensión x14 para las validaciones de lista entre hojas en Delphi con HotXLS: las reglas viven en el extLst del worksheet con una fuente xm:f, parseadas a colecciones tipadas
las reglas de lista entre hojas viven en la extensión x14, y HotXLS las mantiene tipadas, marcadas con IsExtension y reconstruidas al final del worksheet al guardar

El perfil tipado de extensión cubre la regla entera. Además de type, operator, allowBlank, las dos banderas show, el rango y ambas fórmulas, una regla de extensión conserva su título y texto de mensaje de entrada, su título y texto de alerta de error, el estilo de alerta, el modo IME y la opción de ocultar el desplegable a través de una carga y re-guardado, con las mismas reglas de escape que una validación ordinaria

uses
  SysUtils, lxHandleX, lxDiagnostics;

procedure AuditListRules(const FileName: WideString);
var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Dv: TXLSXDataValidation;
  S, I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open(FileName) <> 1 then
      raise Exception.Create('Cannot open ' + FileName);
    for S := 1 to Book.Sheets.Count do        // Sheets[] es base 1
    begin
      Sheet := Book.Sheets[S];
      for I := 0 to Sheet.DataValidations.Count - 1 do
      begin
        Dv := Sheet.DataValidations[I];
        if Dv.ValidationType <> xlsxDvList then
          Continue;
        if Dv.IsExtension then
          Writeln(Sheet.Name, ' ', Dv.Range, ': x14 list from ', Dv.Formula1)
        else if (Dv.Formula1 <> '') and (Dv.Formula1[1] = '"') and
                (Length(Dv.Formula1) - 2 > MaxInlineListChars) then
          Writeln(Sheet.Name, ' ', Dv.Range, ': inline list of ',
            Length(Dv.Formula1) - 2, ' chars, refused by Excel as XLSX');
      end;
    end;
  finally
    Book.Free;
  end;
end;

Para leer los elementos detrás de un desplegable, seleccionar entradas y verificar la entrada tecleada contra estas reglas, vea leer elementos de desplegables y validar entrada con HotXLS; este artículo se queda con los formatos de almacenamiento

Cómo tratan Excel y HotXLS los tipos de validación desconocidos

Excel abre un dataValidation cuyo atributo type contiene un token no reconocido como una regla de tipo none, conserva su fórmula, y al guardar lo reescribe como un elemento dataValidation desnudo sin atributo type, descartando el atributo desconocido. Tales tokens vienen de XML editado a mano, generadores de terceros y funciones futuras de Office, y la respuesta de Excel es tolerancia, no rechazo

HotXLS sigue la misma tolerancia. En vez de descartar la regla, lo que la perdería al re-guardar sin rastro, el lector la conserva como xlsxDvNone con sus fórmulas y rango, y registra un diagnóstico de advertencia por regla normalizada con código 1307, expuesto como la constante xlsDiagnosticValidationTypeNormalized en lxDiagnostics. Al guardar, una regla none se escribe como el elemento desnudo, la misma forma que produce Excel. Una regla none no valida nada, así que el diagnóstico es su señal de que una regla que el autor creía activa ya no restringe la entrada

for I := 0 to Book.Diagnostics.Count - 1 do
  if Book.Diagnostics[I].Code = xlsDiagnosticValidationTypeNormalized then
    Writeln('Untyped rule kept on sheet ', Book.Diagnostics[I].SheetName,
      ': ', Book.Diagnostics[I].Message);

¿Por qué Excel puede rechazar un workbook tras un viaje de ida y vuelta?

Excel rechaza un workbook re-guardado cuando un fragmento de extensión re-reproducido usa un prefijo de espacio de nombres que el archivo nuevo nunca declara, porque el resultado no es XML bien formado. Los fragmentos de extensión suelen llevar atributos con prefijo como xr:uid, cuya declaración xmlns:xr vive en la raíz del worksheet original, no dentro del fragmento. Copie el fragmento textualmente a una raíz de worksheet nueva que carece de esa declaración y las reglas de espacios de nombres XML quedan rotas antes de que Excel mire una sola celda

HotXLS maneja esto en el paso de captura. Mientras lee, escanea cada fragmento re-reproducido buscando prefijos de elementos y atributos que el fragmento no declara por sí mismo, resuelve cada prefijo a su URI a través del lector, y escribe esas declaraciones en la raíz del worksheet al momento de guardar. La ruta de guardado en streaming construye la raíz por la misma rutina, así que ambos modos de guardado producen salida bien formada, y un archivo re-guardado reabre en Excel con cada regla de validación presente, incluida una lista x14 entre hojas re-reproducida. El panorama más amplio de cómo HotXLS transporta contenido extLst que no modela está cubierto en viajes de ida y vuelta XLSX sin pérdida para themes, extLst y calcChain, y las reglas generales detrás del diálogo de reparación de Excel están en por qué Excel muestra un diálogo de reparación

Las listas largas pertenecen a un rango o a un nombre definido

El diseño robusto para cualquier lista que pueda crecer es guardar los elementos en celdas del worksheet y apuntar la validación hacia ellas mediante un nombre definido. Una fuente de rango no tiene límite de 255 caracteres en ningún formato, los elementos se pueden editar sin tocar la regla, y un nombre definido mantiene la regla en el elemento ordinario dataValidation incluso cuando las celdas están en una hoja aparte, que todo consumidor del archivo puede leer

Decisión de la fuente de una validación de lista en Delphi con HotXLS: las listas cortas quedan inline, las largas se mudan a un rango referenciado por un nombre definido
corra la verificación de longitud antes de crear la regla, y el = inicial es lo que hace que un nombre definido sea una referencia en vez de una lista de texto de un elemento
// Lists es una hoja dedicada, p. ej. Book.Sheets.Add('Lists')
function AddListRule(Book: TXLSXWorkbook; Target, Lists: TXLSXWorksheet;
  ACol: Integer; const ARange, AName: WideString;
  const AItems: array of WideString): Integer;
var
  I: Integer;
  ColRef: WideString;
begin
  if InlineListLength(AItems) <= MaxInlineListChars then
  begin
    Result := Target.AddListValidation(ARange, AItems);
    Exit;
  end;
  for I := 0 to High(AItems) do
    Lists.WriteAutoValue(I + 1, ACol, AItems[I]);   // fila y columna base 1
  ColRef := Chr(Ord('A') + ACol - 1);               // columnas A..Z
  Book.DefinedNames.Add(AName, Lists.Name + '!$' + ColRef + '$1:$' +
    ColRef + '$' + IntToStr(Length(AItems)));
  // El '=' inicial marca una referencia; sin él el nombre se cita como texto
  Result := Target.AddListValidation(ARange, '=' + AName);
end;

Ese signo igual inicial importa. AddListValidation trata un argumento que empieza con = como una referencia y le quita el signo, deja un argumento que contiene ! sin comillas como rango, y cita todo lo demás como lista inline. Pase 'StatusList' sin el signo y obtiene un desplegable de un elemento que literalmente dice StatusList. Dos puntos menores: WriteAutoValue convierte texto como lo hace la entrada de celda de Excel, así que un elemento como TRUE se vuelve un Boolean, y un nombre de hoja con espacios necesita comillas simples en la referencia

Para una regla que deba apuntar directo a otra hoja sin nombre, ponga IsExtension := True en la regla del índice que devolvió AddListValidation, y HotXLS la escribe como la extensión x14, la misma forma que el propio Excel usa. La ruta del nombre definido es más simple y sigue funcionando para herramientas que nunca parsean extensiones, razón por la que es la recomendación por defecto aquí

Todo el comportamiento descrito aquí se distribuye en el componente Excel HotXLS para Delphi para Delphi, C++Builder y Lazarus, donde la página del producto lista los tipos de validación soportados, los formatos de archivo y la referencia completa de diagnósticos