Artículo técnico

REGEXTEST y REGEXEXTRACT en Delphi con PCRE2 estático

HotXLS evalúa las funciones de expresión regular 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 de forma estática un build de PCRE2 10.49 de 16 bits fijado en versión, hace match de texto UTF-16 directamente, hace broadcasting sobre arreglos y cobra 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 el cliente escribió. Patrón más texto es todo lo que hace falta para clavar un núcleo de CPU por minutos con backtracking catastrófico, la forma clásica de ReDoS (a+)+$ contra una larga corrida de caracteres a con una b al final. Un servidor que recalcula workbooks subidos no puede permitírselo, así que el trabajo interesante en HotXLS 2.384.112 no fue llamar a PCRE2. Fue hacer a PCRE2 detenible, contable e idéntico en cada target

¿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 usted ya conoce de Excel. El matching es sensible a 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 cuando prefiera mantener el comportamiento dentro del patrón

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

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) usa sintaxis de reemplazo de PCRE2, así que $1 y ${name} referencian capturas. Un occurrence de 0 reemplaza cada match, un valor positivo elige el enésimo match desde el inicio 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, de la misma forma en 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 carga un número de orden de compra
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

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

    // Intercambie 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 plano, así que \d no necesita escape del 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 future-function _xlfn. que Excel espera, y un workbook reabierto los recalcula de nuevo. 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 mantenga las fórmulas regex en salida XLSX

¿Cómo funciona el broadcasting de arreglos 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 conteo de filas y columnas como forma del resultado. Un operando cuya dimensión no es ni 1 ni ese máximo hace que toda la llamada devuelva #VALUE!, el mismatch de arreglos habitual de Excel. Así que REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") rinde 499 Booleans, y una columna de patrones contra una sola celda de texto funciona igual en la otra dirección

Los modos 1 y 2 de REGEXEXTRACT se niegan a hacer broadcasting. Cada elemento produciría un vector de longitud distinta, y un conjunto ragged 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 necesita todos los matches de muchas celdas, itere las celdas y extraiga de una en una, o reforme el problema con closures LET y LAMBDA en el motor de fórmulas de HotXLS de modo que cada llamada de closure vea un valor de texto

La ubicació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, aterriza en esa única celda del resultado mientras las otras celdas conservan sus valores. Un límite de recursos o una cancelación es distinto: detiene la llamada entera y nunca publica un arreglo parcialmente lleno, porque media mitad de resultado que parece completa es peor que una falla explícita

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;   // limite el trabajo que esta fórmula puede hacer

    // Modo 2: grupos de captura del primer match como un arreglo 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 lugar que usted no controla. Evalúa la fórmula como si viviera en la fila y columna dadas, devuelve el arreglo completo en lugar de un único valor cacheado de celda, y toma TXLSFormulaEvaluationOptions con controles explícitos de pasos, memoria de arreglos y cancelación. La guía del motor de fórmulas y funciones personalizadas de HotXLS cubre el resto de esa API de evaluación

¿Cómo detiene HotXLS un patrón con backtracking catastrófico?

HotXLS detiene los patrones desbocados haciendo que PCRE2 reporte de vuelta 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 de Pascal en cada ítem del patrón durante el matching. Ese callback cobra una unidad de trabajo al estado de evaluación activo, revisa el flag de cancelación y revisa la memoria residente. Cuando cualquiera de esos dice stop, el callback devuelve el código de abort de callout de PCRE2 y el match se desenrolla limpiamente dentro de PCRE2. La fórmula emerge como xlfeResourceLimit o un estado de cancelación, no como un hilo colgado

La memoria se mide de la misma forma. Cada contexto de PCRE2 se crea con un general context por instancia cuyo allocator es una función de Pascal, así que el patrón compilado, los match data y los frames internos de backtracking de PCRE2 se asignan todos a través de HotXLS. Cada bloque carga un pequeño header y se alinea a 16 bytes, y su tamaño se suma a un total corrido que el chequeo de presupuesto compara contra el límite de memoria de arreglos de la fórmula. Los buffers de salida, los vectores de extracción y la cadena de reemplazo se agregan al mismo total, lo que significa que un patrón que hace match de millones de cadenas diminutas golpea el techo tan seguro como uno que retrocede para siempre

El callback de presupuesto también corre bajo un guard que importa más de lo que parece. Una excepción de Delphi lanzada dentro de un callback que llamó PCRE2 nunca debe propagarse hacia arriba, porque entre el callback y su código se sientan frames de stack de C compilados sin ningún conocimiento de las tablas de excepciones de Delphi. Desenrollar a través de ellos es indefinido en el mejor caso y un stack corrupto en el peor. El bridge de HotXLS atrapa cada excepción en el callback, fija la instancia en un estado abortado y devuelve un código de error ordinario a PCRE2. Su código ve una fórmula fallida, nunca una excepción que cruzó un frame ajeno

El costo es real y vale declararlo. Los auto callouts hacen el matching 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 bulk sobre datos confiables, una pasada dedicada de regex en Delphi fuera del workbook será más rápida. Dentro de un workbook que le mandan extraños, acotado y predecible le gana a rápido

¿Por qué un build PCRE2 UTF-16 estático sin JIT?

Las cadenas de Excel son UTF-16, y también los valores WideString de Delphi, lo que hizo a la biblioteca PCRE2 de 16 bits la única elección sensata. HotXLS entrega 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 ocurre ningún round trip por UTF-8, así que nada puede desviarse sobre surrogate pairs. Donde un match vacío fuerza a que el escaneo avance, HotXLS sigue la regla de global-match de PCRE2: reintente un match anclado no vacío en la misma posición, luego avance un Unicode scalar value, que se mueve dos code units más allá de un surrogate pair en lugar de partirlo

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

La historia del linking es donde el build se puso inusual. Delphi enlaza archivos de objeto C con {$L}, y su linker resuelve símbolos entre unidades Pascal y objetos de objeto 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 las direcciones. La solución es un unity build: todo PCRE2 se compila como una unidad de traducción en un solo objeto pcre2_unity por target, así que cada referencia interna se resuelve dentro de un único objeto y el linker de Delphi solo ve las funciones _16 exportadas

El runtime de C fue el segundo problema. PCRE2 llama malloc, memcpy, memcmp y amigos, y otros backends de C enlazados en el mismo ejecutable pueden ya traer 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 después del preprocesamiento, así que esos dos existen también, dentro de la misma unidad. La decoración de símbolos tiene que coincidir con cada formato de objeto: el objeto OMF de Win32 espera los nombres C con underscore inicial, mientras que las declaraciones Pascal para el objeto ELF de Win64, el objeto COFF de Win64 y los builds FPC enlazan los nombres desnudos

Los stack probes fueron el sutil. Una función de C con un frame más grande que una página llama primero a una rutina de sondeo, que toca cada página de 4 KB en orden para que Windows pueda comprometer el stack 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, luego crashea la primera vez que un frame profundo salta más allá de la guard page. HotXLS implementa probes reales en assembly 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 todos los demás targets de FPC reportan el backend como no disponible en lugar de caer silenciosamente a un dialecto regex distinto

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

Las funciones regex corren en builds de Delphi y C++Builder para Windows y en FPC Linux x64 y macOS ARM64. En cualquier otro target la fórmula reporta una función no soportada a través de evaluación contextual en lugar de producir respuestas distintas con otro motor. Esa es la falla correcta: un workbook que calcula distinto en una plataforma distinta 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 Microsoft de REGEXTEST, REGEXEXTRACT y REGEXREPLACE, pero el build de Excel usado para la validación local todavía no expone estas funciones de Microsoft 365. Los casos de borde por tanto siguen los contratos explícitos de PCRE2 y de arreglos descritos arriba en lugar de una afirmación de coincidir con cada build de Excel bit a bit. Si sus workbooks dependen de una esquina oscura, como un comportamiento específico de backreference o una clase de propiedad Unicode, pruebe esa esquina directamente

Preguntas frecuentes

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

No. HotXLS enlaza un objeto PCRE2 UTF-16 estático dentro de su 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 matching pasa por un callout que cobra 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 matches, y esos vectores ragged no pueden formar un resultado rectangular. HotXLS reporta la llamada como no soportada en lugar de rellenar o truncar, así que extraiga una celda a la vez

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 componente de hojas de cálculo HotXLS para Delphi