Artículo técnico

REGEXTEST y REGEXEXTRACT en Delphi con PCRE2 estático

HotXLS evalúa las funciones de expresiones regulares de Microsoft 365 REGEXTEST, REGEXEXTRACT y REGEXREPLACE dentro de su propio motor de fórmulas Delphi, sin instalación de Excel y sin DLL de regex del sistema. La calculadora de HotXLS enlaza estáticamente una build fija de PCRE2 10.49 de 16 bits, casa texto UTF-16 directamente, hace broadcasting sobre arrays y carga cada paso de backtracking contra el presupuesto de recursos de la fórmula

Esa última parte es la razón de que exista este artículo. Un workbook que llega de un cliente es entrada no confiable, y una expresión regular dentro de una fórmula es código que escribió el cliente. Patrón más texto es todo lo que hace falta para clavar un núcleo de CPU durante minutos con backtracking catastrófico, la forma ReDoS clásica de (a+)+$ contra una larga ronda de caracteres a con una b al final. Un servidor que recalcula workbooks subidos no se lo puede permitir, así que el trabajo interesante en HotXLS 2.384.112 no fue llamar a PCRE2. Fue hacer a PCRE2 parable, contable e idéntica en cada plataforma

¿Qué devuelven REGEXTEST, REGEXEXTRACT y REGEXREPLACE en HotXLS?

El motor de fórmulas de HotXLS sigue las firmas publicadas por Microsoft, y la semántica de los argumentos es la que ya conoces de Excel. El casado distingue mayúsculas por defecto. El argumento final case_sensitivity toma 0 para sensible a mayúsculas y 1 para insensible, y cualquier otro valor devuelve #VALUE!. Las opciones inline de PCRE2 como (?i) o (*UCP) también funcionan si prefieres mantener el comportamiento dentro del patrón

REGEXTEST(text, pattern, [case_sensitivity]) devuelve un Boolean, y un casado fallido es simplemente FALSE, nunca un error. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) tiene tres modos: 0 devuelve el primer casado completo, 1 devuelve cada casado completo como array vertical, y 2 devuelve los grupos de captura del primer casado como array horizontal. Sin casado devuelve #N/A, el modo 2 sobre un patrón sin grupos de captura devuelve #VALUE!, y un grupo opcional que no participó produce una cadena vacía en lugar de un error

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) usa la sintaxis de reemplazo de PCRE2, así que $1 y ${name} referencian capturas. Un occurrence de 0 reemplaza cada casado, un valor positivo elige el nth casado desde el principio y un valor negativo cuenta desde el final. Cuando el occurrence seleccionado no existe, HotXLS devuelve el texto original sin cambios. Los selectores numéricos se truncan hacia cero, igual que Excel trata los argumentos enteros fraccionarios

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Orders');
    Sheet.Cells[2, 1].Value := 'PO-2026-00417 shipped to Graz';

    // TRUE cuando la celda guarda un número de orden de compra
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

    // Primer casado completo: "PO-2026-00417"
    Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';

    // Intercambia año y secuencia con capturas numeradas: "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Test insensible a mayúsculas con el argumento final
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

    Book.Recalculate;
    Book.SaveAs('orders.xlsx');
  finally
    Book.Free;
  end;
end;

El texto de fórmula en Delphi es un string literal corriente, así que \d no necesita escape en el lado Pascal. Solo importan las comillas dobles de Excel dentro de la fórmula, y siguen las reglas normales de Excel. Al guardar, el escritor XLSX almacena estos nombres con el prefijo de función futura _xlfn. que Excel espera, y un workbook reabierto los recalcula otra vez. El motor clásico BIFF8 puede evaluar las funciones en una llamada contextual, pero no puede escribirlas como tokens de fórmula nativos de .xls, así que deja las fórmulas regex en salida XLSX

¿Cómo funciona el broadcasting de arrays con fórmulas regex?

REGEXTEST, REGEXREPLACE y el modo 0 de REGEXEXTRACT hacen broadcasting sobre operandos compatibles, que es lo que los hace útiles en columnas reales. Cada argumento puede ser un escalar, una fila, una columna o un rectángulo, y HotXLS toma el mayor recuento de filas y columnas como forma del resultado. Un operando cuya dimensión no es 1 ni ese máximo hace que toda la llamada devuelva #VALUE!, el desfase de arrays de siempre en Excel. Así, REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") produce 499 Booleans, y una columna de patrones contra una sola celda de texto funciona igual al revés

Los modos 1 y 2 de REGEXEXTRACT se niegan a hacer broadcasting. Cada elemento produciría un vector de longitud distinta, y un conjunto desigual de vectores no puede ocupar un resultado rectangular de fórmula. En lugar de truncar o rellenar en silencio, HotXLS reporta la llamada como no soportada. Si necesitas todos los casados de muchas celdas, itera las celdas y extrae de una en una, o reestructura el problema con cierres LET y LAMBDA en el motor de fórmulas de HotXLS de modo que cada llamada de cierre vea un valor de texto

La colocación de errores dentro de un resultado de broadcasting es deliberada. Un error de Excel para un elemento, digamos #N/A de una extracción que no encontró nada en la fila 37, cae en esa única celda del resultado mientras las demás celdas conservan sus valores. Un límite de recursos o una cancelación es distinto: detiene toda la llamada y nunca publica un array parcialmente lleno, porque medio resultado que parece completo es peor que un fallo explícito

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Opts: TXLSFormulaEvaluationOptions;
  Eval: TXLSFormulaEvaluationResult;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Log');
    Sheet.Cells[1, 1].Value := 'GET /api/v2/orders 200 18ms';

    Opts := XLSDefaultFormulaEvaluationOptions;
    Opts.MaxEvaluationSteps := 200000;   // limita el trabajo que esta fórmula puede hacer

    // Modo 2: grupos de captura del primer casado como array 1 x 3
    Eval := Sheet.EvaluateFormulaAt(1, 2,
      'REGEXEXTRACT(A1, "(GET|POST) (\S+) (\d{3})", 2)', xlfrsA1, Opts);

    case Eval.Status of
      xlfeOk:
        if VarIsArray(Eval.Value) then
          for I := VarArrayLowBound(Eval.Value, 2) to
                   VarArrayHighBound(Eval.Value, 2) do
            Log(VarToStr(Eval.Value[0, I]));
      xlfeResourceLimit:
        Log('Pattern exceeded its budget: ' + Eval.Issue.Message);
    else
      Log('Evaluation failed: ' + Eval.Issue.Message);
    end;
  finally
    Book.Free;
  end;
end;

TXLSXWorksheet.EvaluateFormulaAt es el punto de entrada correcto cuando el patrón viene de un sitio que no controlas. Evalúa la fórmula como si viviera en la fila y columna dadas, devuelve el array completo en lugar de un único valor de celda cacheado, y toma TXLSFormulaEvaluationOptions con controles explícitos de pasos, memoria de arrays y cancelación. La guía del motor de fórmulas de HotXLS y funciones personalizadas cubre el resto de esa API de evaluación

¿Cómo frena HotXLS un patrón de backtracking catastrófico?

HotXLS detiene los patrones desbocados haciendo que PCRE2 reporte al contexto de la fórmula en cada paso. El backend compila cada patrón con PCRE2_AUTO_CALLOUT, lo que hace que PCRE2 llame a un callback Pascal en cada elemento del patrón durante el casado. Ese callback carga una unidad de trabajo al estado de evaluación activo, comprueba el flag de cancelación y comprueba la memoria residente. Cuando alguno de esos dice para, el callback devuelve el código de aborto de callout de PCRE2 y el casado se desenrolla limpio dentro de PCRE2. La fórmula aflora como xlfeResourceLimit o un estado de cancelación, no como un hilo colgado

La memoria se mide igual. Cada contexto de PCRE2 se crea con un contexto general por instancia cuyo alocador es una función Pascal, así que el patrón compilado, los datos del casado y los frames internos de backtracking de PCRE2 se alojan todos a través de HotXLS. Cada bloque lleva una pequeña cabecera y se alinea a 16 bytes, y su tamaño se suma a un total corriente que la comprobación de presupuesto compara contra el límite de memoria de arrays de la fórmula. Los buffers de salida, los vectores de extracción y la cadena de reemplazo se suman al mismo total, lo que significa que un patrón que casa millones de cadenas diminutas choca contra el techo tan seguramente como uno que hace backtracking para siempre

El callback de presupuesto corre además bajo un guardia que importa más de lo que parece. Una excepción de Delphi lanzada dentro de un callback al que llamó PCRE2 jamás debe propagarse hacia arriba, porque entre el callback y tu código hay frames de pila C compilados sin conocimiento alguno de las tablas de excepciones de Delphi. Desenrollar a través de ellos es indefinido en el mejor caso y una pila corrompida en el peor. El puente de HotXLS captura cada excepción en el callback, fija la instancia en estado abortado y devuelve un código de error ordinario a PCRE2. Tu código ve una fórmula fallida, nunca una excepción que cruzó un frame ajeno

El coste es real y merece declararse. Los auto callouts hacen el casado más lento que un intérprete PCRE2 desnudo, y HotXLS compila el patrón una vez por elemento evaluado, así que un broadcasting sobre 50.000 celdas paga 50.000 compilaciones. Para trabajo de texto en volumen sobre datos confiables, una pasada dedicada de regex en Delphi fuera del workbook será más rápida. Dentro de un workbook que te mandan desconocidos, lo acotado y predecible le gana a lo rápido

¿Por qué una build estática de PCRE2 UTF-16 sin JIT?

Las cadenas de Excel son UTF-16, y también lo son los valores WideString de Delphi, lo que hizo de la biblioteca PCRE2 de 16 bits la única elección sensata. HotXLS le pasa el texto de fórmula a pcre2_match_16 con PCRE2_UTF activado, y cada offset que vuelve es un índice de code unit UTF-16. No hay round trip UTF-8, así que nada puede desviarse en pares surrogate. Donde un casado vacío obliga al escaneo a avanzar, HotXLS sigue la regla de casado global de PCRE2: reintentar un casado anclado no vacío en la misma posición y luego avanzar un scalar value Unicode, que salta dos code units más allá de un par surrogate en lugar de partirlo

El JIT está apagado a propósito. El compilador JIT necesita memoria ejecutable y su propia pila JIT, lo que arrastra código específico de plataforma y alocaciones fuera del alocador por fórmula. El intérprete mantiene cada byte bajo el presupuesto de HotXLS, mantiene cada casado bajo el callout y se comporta igual en los cinco objetivos. Cada instancia TXLSRegex, la clase interna detrás de estas fórmulas, es dueña de sus propios contextos, así que los workers de cálculo paralelo no comparten estado mutable de PCRE2

La historia del enlazado es donde la build se puso rara. Delphi enlaza archivos de objeto C con {$L}, y su linker resuelve símbolos entre unidades Pascal y objetos C lo bastante bien, pero no resuelve referencias circulares arbitrarias entre archivos de objeto C separados. PCRE2 son dos docenas de unidades de traducción que se llaman en todas direcciones. La solución es un unity build: todo PCRE2 se compila como una unidad de traducción en un objeto pcre2_unity por plataforma, así que cada referencia interna se resuelve dentro de un único objeto y el linker de Delphi solo ve las funciones exportadas _16

El runtime de C fue el segundo problema. PCRE2 llama a malloc, memcpy, memcmp y compañía, y otros backends C enlazados en el mismo ejecutable pueden traer ya sus propias copias de esos nombres. HotXLS preprocesa PCRE2 para que esas llamadas vayan a nombres privados como hotxls_regex_memcpy y los implementa en Pascal. Clang aún puede sintetizar llamadas memset y memchr planas tras el preprocesado, así que esos dos existen también, dentro de la misma unidad. La decoración de símbolos tiene que casar con cada formato de objeto: el objeto OMF de Win32 espera los nombres C con guion bajo inicial, mientras que las declaraciones Pascal para el objeto ELF de Win64, el objeto COFF de Win64 y las builds FPC ligan los nombres desnudos

Los probes de pila fueron lo sutil. Una función C con un frame mayor que una página llama primero a una rutina de probe, que toca cada página de 4 KB en orden para que Windows pueda commitir la pila a través de su guard page. Un stub que simplemente retorna enlaza bien y funciona en cada unit test con un patrón pequeño, y luego revienta la primera vez que un frame profundo salta la guard page. HotXLS implementa probes reales en ensamblador para cada formato de objeto de Windows: __chkstk_noalloc para el objeto Win32, __chkstk para el objeto en formato ELF de Win64 y ___chkstk_ms para el objeto COFF de Win64. En Linux x64 y macOS ARM64, FPC enlaza la biblioteca C del sistema, y todo otro objetivo FPC reporta el backend como no disponible en lugar de caer en silencio a un dialecto regex distinto

¿Dónde están los límites?

Las funciones regex corren en builds Windows de Delphi y C++Builder y en FPC Linux x64 y macOS ARM64. En cualquier otro objetivo la fórmula reporta una función no soportada mediante evaluación contextual en lugar de producir respuestas distintas con otro motor. Ese es el fallo correcto: un workbook que calcula distinto en otra plataforma es un bug que nadie encuentra hasta una auditoría

La equivalencia con Excel también tiene un límite declarado. Las firmas y los ejemplos documentados siguen las páginas de REGEXTEST, REGEXEXTRACT y REGEXREPLACE de Microsoft, pero la build de Excel usada para la validación local aún no expone estas funciones de Microsoft 365. Los casos de borde siguen por tanto los contratos explícitos de PCRE2 y de arrays descritos arriba en lugar de una afirmación de igualar cada build de Excel bit a bit. Si tus workbooks dependen de un rincón oscuro, como un comportamiento concreto de backreference o una clase de propiedad Unicode, prueba ese rincón directamente

FAQ

¿Necesita HotXLS Excel o una DLL de regex para calcular REGEXTEST?

No. HotXLS enlaza un objeto PCRE2 UTF-16 estático en tu ejecutable y evalúa REGEXTEST, REGEXEXTRACT y REGEXREPLACE en su propio motor de fórmulas Delphi, sin Excel, sin COM y sin DLL externa

¿Puede un patrón malicioso colgar mi servidor?

No más allá del presupuesto configurado. Cada paso de casado pasa por un callout que carga al contexto de la fórmula, así que un patrón ReDoS termina con xlfeResourceLimit o un estado de cancelación y sin resultado parcial

¿Por qué el modo 1 de REGEXEXTRACT falla con un argumento de rango?

Cada celda podría devolver un número distinto de casados, y esos vectores desiguales no pueden formar un resultado rectangular. HotXLS reporta la llamada como no soportada en lugar de rellenar o truncar, así que extrae celda a celda

HotXLS trae las funciones regex junto a LAMBDA, GROUPBY, PIVOTBY y el resto de su motor de cálculo para Delphi, C++Builder y Lazarus. Detalles, ediciones y la descarga de prueba están en la página del HotXLS Delphi spreadsheet component