Ejecute una conversión por lotes sobre diez mil hojas de cálculo durante la noche, y para la mañana tres de ellas regresan False. Eso es toda la autopsia que un resultado booleano de guardado le da: un conteo de fallos, sin nada sobre qué archivo, qué hoja, o cuál de una docena de causas posibles fue responsable. HotXLS, el componente nativo de losLab para Delphi y C++Builder para archivos Excel, reemplaza ese único bit con diagnósticos estructurados. La interfaz IXLSWorkbookProgress expone una lista Diagnostics y un evento OnDiagnostic que reportan un código numérico estable, un nivel de severidad, la operación que falló, y la hoja donde ocurrió, para cada llamada a Open, SaveAs y Recalculate
¿Por qué un resultado booleano de guardado falla a escala?
Un solo archivo fallido no es el problema que crea un resultado booleano; mil de ellos sí lo son. Cuando SaveAs devuelve algo distinto de éxito para tres archivos de diez mil, la siguiente pregunta siempre es la misma: ¿estos tres se pueden reintentar, o necesitan un humano? Un error de permisos en un recurso compartido de red no es el mismo incidente que una fórmula que el motor de cálculo no puede evaluar, y ninguno de los dos es igual a una hoja de cálculo que silenciosamente excedió un límite de formato. Con solo un resultado de aprobado/reprobado con el que trabajar, cada uno de esos se convierte en un ticket de soporte idéntico, y alguien tiene que abrir cada archivo a mano, en Excel, y mirarlo fijamente hasta que la causa se vuelva obvia. Ese triaje manual es el verdadero costo de una API booleana, y escala linealmente con el tamaño del lote, que es exactamente la propiedad que no se quiere del manejo de errores
Dentro de IXLSWorkbookProgress: qué lleva un TXLSDiagnostic
IXLSWorkbookProgress es la interfaz que usa HotXLS para reportar tanto cómo va una operación como qué salió mal dentro de ella, y las dos mitades comparten un contrato por una razón: ambas son cosas que una llamada larga a Open, SaveAs, o Recalculate necesita comunicar sin lanzar una excepción a mitad de la operación. La mitad de progreso es OnProgress y OnProgressEx, que se disparan con una fase, un estado, y un par actual/total. La mitad de diagnósticos es de la que trata este artículo: una propiedad Diagnostics que devuelve una lista TXLSDiagnostics, un atajo LastDiagnostic para la entrada más reciente, y un evento OnDiagnostic que se dispara en el momento en que se crea cada registro TXLSDiagnostic. Cada registro lleva un Code numérico, un TXLSDiagnosticSeverity, la TXLSDiagnosticOperation que lo produjo, un Message legible por humanos, un SheetIndex y SheetName, y un NativeCode que conserva cualquier valor de retorno de nivel más bajo que haya disparado la entrada
var
Book: TXLSXWorkbook;
Diag: TXLSDiagnostic;
I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.SaveAs('quarterly-report.xlsx') <> 1 then
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
Writeln(Format('[%d] severity=%d sheet="%s": %s',
[Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
end;
finally
Book.Free;
end;
end;
Leer Diagnostics así ya supera a un resultado booleano por sí solo, porque Code y SheetName convierten un misterio en un hecho específico y filtrable. El registro TXLSDiagnostic llega más lejos que lo que imprime este ejemplo: RecordId y StreamOffset existen para análisis forense a nivel de byte dentro de un flujo BIFF, y PartName contiene la entrada zip de OOXML, como xl/worksheets/sheet3.xml, de donde vino un problema. Vale la pena saber antes de construir herramientas alrededor de ellos: en la versión actual, ninguno de los puntos de llamada de diagnóstico integrados llena RecordId o StreamOffset, así que ambos permanecen en su valor predeterminado de constructor de -1, que significa "no aplicable" en lugar de "cero". Trate su ausencia como normal, no como un error en su manejador
Dos motores, una forma, una diferencia silenciosa
HotXLS incluye dos motores detrás de este mismo modelo de reporte, una fachada BIFF8 para archivos .xls heredados y una fachada OOXML para .xlsx, y no exponen IXLSWorkbookProgress de manera idéntica. TXLSWorkbook, el motor .xls, implementa formalmente IXLSWorkbookProgress, así que se puede pasar a cualquier lugar donde se espere ese tipo de interfaz. TXLSXWorkbook, el motor .xlsx, expone los mismos miembros Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress, y OnProgressEx con nombres y tipos idénticos, pero como una clase simple en lugar de una implementación formal de esa interfaz, así que no satisfará un parámetro IXLSWorkbookProgress por sí sola. En la práctica esto rara vez importa, porque la mayoría del código trabaja contra una clase de libro concreta a la vez, pero sí significa que no se puede escribir un único helper tipado como IXLSWorkbookProgress y entregarle el objeto de libro de cualquiera de los dos motores indistintamente. La única diferencia de campo que se sigue directamente de la división de formato es PartName: solo el motor XLSX lo llena, porque solo OOXML tiene partes zip que nombrar
¿Qué hace que un código de diagnóstico sea algo sobre lo que se pueda bifurcar con seguridad?
El campo Code es la única parte de un diagnóstico que vale la pena codificar de forma fija en una comparación; Message no lo es, porque el texto prosa es exactamente el tipo de cosa que se reformula, se retraduce, o se amplía con más detalle en una versión posterior sin que nadie lo trate como un cambio incompatible. Los códigos de diagnóstico integrados de HotXLS ya se leen como si hubieran sido diseñados con esa distinción en mente: los códigos relacionados con guardado van del 1000 al 1005, los códigos relacionados con apertura están en 1100 y 1101, los códigos relacionados con cálculo en 1200 y 1201, y un código de formato no soportado en 1300, con huecos dejados dentro de cada banda en lugar de que los códigos corran consecutivamente a través de todas ellas. Ese espaciado es lo que permite a un proveedor agregar un nuevo modo de fallo en tiempo de guardado en, digamos, 1006 sin renumerar los códigos de los que ya depende su sentencia switch, y vale la pena verificarlo en cualquier API de diagnósticos antes de comprometerse a comparar por un código en producción, no solo en esta. Mantenga una rama predeterminada en su propia lógica de despacho sin importar cuán estable parezca la numeración, porque nuevos modos de fallo son exactamente lo que un analizador o escritor en evolución sigue descubriendo. NativeCode y ExceptionClass se sitúan una capa debajo de Code para cuando se necesita escalar: NativeCode conserva el valor de retorno subyacente, un HRESULT de una llamada de Structured Storage entre ellos, y ExceptionClass registra el tipo de excepción Delphi cuando hubo una involucrada, que suele ser suficiente para abrir una solicitud de soporte precisa sin adjuntar una traza de pila completa
La severidad y la operación deciden qué hace su código a continuación
La severidad y la operación son lo que convierten un diagnóstico de una línea de log en una decisión de enrutamiento. TXLSDiagnosticSeverity abarca Info, Warning, Error, y Fatal, y TXLSDiagnosticOperation etiqueta cada entrada con la llamada que la produjo: Open, Save, Calculate, o Export. Los dos ejes son independientes por diseño: xlsDiagnosticUnhandledException es un código fijo que se dispara con Operation establecido en cualquiera que sea la llamada que realmente lo lanzó, así que Code responde qué salió mal mientras que Operation responde por separado dónde, en lugar de necesitar un código distinto para una excepción durante la apertura frente a una durante el guardado. Esa composabilidad es también lo que hace mecánico el enrutamiento: registrar una advertencia y continuar, un guardado cancelado mediante la bandera Aborted es un ejemplo típico; contar un error y mantener el lote en ejecución, una hoja de cálculo que falló al serializarse es un ejemplo típico; detener el lote ante una severidad fatal, porque ese nivel significa que una excepción no manejada ya desenrolló la llamada y continuar arriesga trabajar desde un estado a medio actualizar. Una advertencia honesta: Info existe en la enumeración como el valor predeterminado con el que empieza un TXLSDiagnostic recién creado, pero cada punto de llamada de diagnóstico integrado en la versión actual de HotXLS solo llega a lanzar Warning, Error, o Fatal; Info está reservado para uso futuro, no algo que el motor emita hoy
// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
Diag := Book.Diagnostics[I];
case Diag.Severity of
xlsDiagnosticWarning:
Writeln(Format('WARN [%d] %s', [Diag.Code, Diag.Message]));
xlsDiagnosticError:
begin
Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
[Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
Inc(FailedSheetCount);
end;
xlsDiagnosticFatal:
raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
end;
end;
Conectar OnDiagnostic a un pipeline por lotes
Sondear Diagnostics después de cada llamada funciona para un solo archivo; deja de funcionar en cuanto se vuelve a ese lote nocturno de diez mil, porque Diagnostics se limpia al inicio de cada llamada a Open, SaveAs, y Recalculate. Léalo después del tercer archivo en un bucle y verá solo los diagnósticos del tercer archivo; lo que reportaron los dos primeros archivos ya desapareció. OnDiagnostic resuelve esto convirtiendo la colección en un flujo: suscríbase una vez antes de que empiece el bucle, y el mismo manejador se dispara para cada archivo, en orden, con el nombre del archivo todavía en alcance mediante un campo de instancia
type
TBatchConverter = class
private
FCurrentFile: string;
FFailedFiles: TStringList;
procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
end;
procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
if Diagnostic.Severity >= xlsDiagnosticError then
FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
[FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;
// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
FCurrentFile := FileNames[I];
if Book.Open(FCurrentFile) = 1 then
Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;
Lo que realmente cuesta el callback
OnDiagnostic es barato por una razón estructural: solo se dispara cuando algo ya salió mal, y estar mal es raro comparado con el número de celdas, filas, u hojas de cálculo que contiene un libro. Contraste eso con OnProgress y OnProgressEx, que reportan progreso rutinario y tuvieron que diseñarse en torno a la frecuencia de llamada desde el principio. HotXLS dispara el progreso a nivel de hoja una vez por hoja durante Open y SaveAs, no una vez por celda o fila, que es lo que mantiene bajo el costo por llamada incluso en libros con millones de celdas; Recalculate va más allá y limita su propio evento de progreso a aproximadamente cada cuatro por ciento del grafo de dependencias, así que un recálculo completo le da un latido en lugar de inundar su hilo de interfaz con eventos. Los diagnósticos no necesitaron nada de esa limitación, porque el conteo de eventos está acotado por el número de problemas reales, no por el tamaño del archivo
El único lugar donde el rendimiento todavía depende de usted es dentro del propio manejador. OnDiagnostic se dispara sincrónicamente, en el hilo que ejecuta Open, SaveAs, o Recalculate, así que un manejador que bloquea, una escritura síncrona a un servicio de registro remoto por ejemplo, se convierte en parte del tiempo de reloj de esa llamada. Para un solo archivo eso es invisible. Multiplicado a través de un lote de diez mil archivos es la diferencia entre un trabajo que termina durante la noche y uno que todavía está corriendo a la hora del almuerzo, así que almacene en búfer lo que el manejador necesita hacer y vuélquelo de forma asíncrona en lugar de hacer la parte lenta en línea
Los diagnósticos estructurados son más valiosos exactamente donde un resultado booleano es más débil, en flujos de trabajo que tocan muchos archivos en lugar de uno. Un pipeline de auditoría y conversión de libros es el ejemplo más claro: en lugar de registrar un simple aprobado/reprobado por archivo, adjunte la lista Diagnostics de cada archivo a su registro de auditoría, y el informe le dice no solo qué falló sino por qué, que es la mayor parte de lo que intenta lograr nuestro artículo sobre la construcción de un banco de trabajo de auditoría y conversión de libros en primer lugar. El mismo emparejamiento de progreso y diagnósticos también pertenece a cualquier flujo de trabajo que ya necesite reporte de progreso por derecho propio, que es exactamente el territorio cubierto en nuestra guía sobre el rendimiento de libros grandes en HotXLS, donde una llamada larga a Open o SaveAs es lo bastante común como para que OnProgress ya esté conectado y OnDiagnostic sea una adición natural, casi gratuita, junto a él
Nada de esto requiere Excel instalado en ninguna parte del pipeline, y nada de esto requiere capturar una excepción genérica y adivinar qué significaba. IXLSWorkbookProgress y sus miembros Diagnostics, LastDiagnostic, y OnDiagnostic son parte del componente HotXLS estándar para Delphi y C++Builder, junto con la referencia completa de códigos de diagnóstico y el resto de la superficie de Open, SaveAs, y Recalculate que ha recorrido este artículo