O HotXLS avalia as funções de expressão regular do Microsoft 365 REGEXTEST, REGEXEXTRACT e REGEXREPLACE dentro do seu próprio motor de fórmulas Delphi, sem instalação do Excel e sem DLL de regex do sistema. A calculadora do HotXLS liga estaticamente um build fixado do PCRE2 10.49 de 16 bits, casa texto UTF-16 diretamente, faz broadcast sobre arrays e cobra cada passo de backtracking do orçamento de recursos da fórmula
Essa última parte é o motivo de este artigo existir. Uma pasta de trabalho que chega de um cliente é entrada não confiável, e uma expressão regular dentro de uma fórmula é código que o cliente escreveu. Padrão mais texto é tudo o que é preciso para prender um núcleo de CPU por minutos com backtracking catastrófico, a clássica forma ReDoS (a+)+$ contra uma longa sequência de caracteres a com um b no final. Um servidor que recalcula pastas de trabalho enviadas não pode se dar a esse luxo, então o trabalho interessante no HotXLS 2.384.112 não foi chamar o PCRE2. Foi tornar o PCRE2 parável, responsável por contas e idêntico em todos os alvos
O que REGEXTEST, REGEXEXTRACT e REGEXREPLACE retornam no HotXLS?
O motor de fórmulas do HotXLS segue as assinaturas publicadas pela Microsoft, e a semântica dos argumentos é a que você já conhece do Excel. O casamento é case-sensitive por padrão. O argumento final case_sensitivity recebe 0 para case-sensitive e 1 para case-insensitive, e qualquer outro valor retorna #VALUE!. Opções inline do PCRE2 como (?i) ou (*UCP) também funcionam quando você prefere manter o comportamento dentro do padrão
REGEXTEST(text, pattern, [case_sensitivity]) retorna um Boolean, e um casamento que falha é simplesmente FALSE, nunca um erro. O REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) tem três modos: 0 retorna o primeiro casamento completo, 1 retorna todo casamento completo como um array vertical, e 2 retorna os capture groups do primeiro casamento como um array horizontal. Nenhum casamento retorna #N/A, o modo 2 em um padrão sem capture groups retorna #VALUE!, e um grupo opcional que não participou rende uma string vazia em vez de um erro
O REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) usa a sintaxe de substituição do PCRE2, então $1 e ${name} referenciam capturas. Um occurrence de 0 substitui todo casamento, um valor positivo escolhe o enésimo casamento desde o início e um valor negativo conta do fim. Quando a ocorrência selecionada não existe, o HotXLS retorna o texto original inalterado. Seletores numéricos são truncados em direção a zero, do mesmo jeito que o Excel trata argumentos inteiros fracionários
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 quando a célula guarda um número de pedido de compra
Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';
// Primeiro casamento completo: "PO-2026-00417"
Sheet.Cells[2, 3].Formula := 'REGEXEXTRACT(A2, "PO-\d{4}-\d{5}")';
// Troque ano e sequência com capturas numeradas: "PO-00417-2026 ..."
Sheet.Cells[2, 4].Formula :=
'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';
// Teste case-insensitive com o argumento final
Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';
Book.Recalculate;
Book.SaveAs('orders.xlsx');
finally
Book.Free;
end;
end;
O texto de fórmula em Delphi é um literal de string comum, então \d não precisa de escape no lado Pascal. Só as aspas duplas do Excel dentro da fórmula importam, e elas seguem as regras normais do Excel. Ao salvar, o escritor XLSX guarda esses nomes com o prefixo de future-function _xlfn. que o Excel espera, e uma pasta de trabalho reaberta os recalcula de novo. O motor clássico BIFF8 consegue avaliar as funções em uma chamada contextual, mas não consegue escrevê-las como tokens de fórmula nativos .xls, então mantenha fórmulas de regex na saída XLSX
Como funciona o broadcast de arrays com fórmulas de regex?
REGEXTEST, REGEXREPLACE e o modo 0 do REGEXEXTRACT fazem broadcast sobre operandos compatíveis, que é o que os torna úteis em colunas reais. Cada argumento pode ser um escalar, uma linha, uma coluna ou um retângulo, e o HotXLS toma a maior contagem de linhas e colunas como a forma do resultado. Um operando cuja dimensão não é 1 nem esse máximo faz a chamada inteira retornar #VALUE!, o descasamento de array usual do Excel. Então REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") rende 499 Booleans, e uma coluna de padrões contra uma única célula de texto funciona do mesmo jeito na outra direção
Os modos 1 e 2 do REGEXEXTRACT se recusam a fazer broadcast. Cada elemento produziria um vetor de comprimento diferente, e um conjunto irregular de vetores não pode ocupar um resultado retangular de fórmula. Em vez de truncar ou preencher em silêncio, o HotXLS reporta a chamada como não suportada. Se você precisa de todos os casamentos de muitas células, itere as células e extraia uma de cada vez, ou remodele o problema com closures LET e LAMBDA no motor de fórmulas do HotXLS para que cada chamada de closure veja um valor de texto
O posicionamento de erros dentro de um resultado de broadcast é deliberado. Um erro do Excel para um elemento, digamos #N/A de uma extração que não achou nada na linha 37, cai naquela única célula do resultado enquanto as outras células mantêm os seus valores. Um limite de recurso ou um cancelamento é diferente: ele para a chamada inteira e nunca publica um array parcialmente preenchido, porque meio resultado que parece completo é pior do que uma falha 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 o trabalho que esta fórmula pode fazer
// Modo 2: capture groups do primeiro casamento como um 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;
O TXLSXWorksheet.EvaluateFormulaAt é o ponto de entrada certo quando o padrão vem de algum lugar que você não controla. Ele avalia a fórmula como se ela vivesse na linha e coluna dadas, retorna o array completo em vez de um único valor de célula em cache, e recebe TXLSFormulaEvaluationOptions com controles explícitos de passos, memória de array e cancelamento. O guia do motor de fórmulas e de funções customizadas do HotXLS cobre o resto dessa API de avaliação
Como o HotXLS para um padrão de backtracking catastrófico?
O HotXLS para padrões desgarrados fazendo o PCRE2 reportar de volta ao contexto da fórmula a cada passo. O backend compila todo padrão com PCRE2_AUTO_CALLOUT, o que faz o PCRE2 chamar um callback Pascal em cada item do padrão durante o casamento. Esse callback cobra uma unidade de trabalho do estado de avaliação ativo, verifica a flag de cancelamento e verifica a memória residente. Quando qualquer um deles diz pare, o callback retorna o código de abort de callout do PCRE2 e o casamento se desenrola limpo dentro do PCRE2. A fórmula aparece como xlfeResourceLimit ou um status de cancelamento, não como uma thread pendurada
A memória é medida do mesmo jeito. Todo contexto PCRE2 é criado com um general context por instância cujo alocador é uma função Pascal, então o padrão compilado, os match data e os frames internos de backtracking do PCRE2 são todos alocados através do HotXLS. Cada bloco carrega um pequeno header e é alinhado a 16 bytes, e o seu tamanho é somado a um total acumulado que a verificação de orçamento compara com o limite de memória de array da fórmula. Buffers de saída, vetores de extração e a string de substituição entram no mesmo total, o que significa que um padrão que casa milhões de strings minúsculas bate no teto tão certamente quanto um que faz backtracking para sempre
O callback de orçamento também roda sob uma guarda que importa mais do que parece. Uma exceção Delphi levantada dentro de um callback que o PCRE2 chamou nunca deve propagar para cima, porque entre o callback e o seu código ficam frames de pilha C compilados sem qualquer conhecimento das tabelas de exceção do Delphi. Desenrolar através deles é indefinido no melhor caso e uma pilha corrompida no pior. A ponte do HotXLS captura toda exceção no callback, trava a instância em um estado abortado e retorna um código de erro comum ao PCRE2. O seu código vê uma fórmula que falhou, nunca uma exceção que atravessou um frame estrangeiro
O custo é real e vale ser dito. Auto callouts deixam o casamento mais lento do que um interpretador PCRE2 nu, e o HotXLS compila o padrão uma vez por elemento avaliado, então um broadcast sobre 50.000 células paga 50.000 compilações. Para trabalho de texto em massa sobre dados confiáveis, uma passada dedicada de regex em Delphi fora da pasta de trabalho será mais rápida. Dentro de uma pasta de trabalho que estranhos te enviam, limitado e previsível ganha de rápido
Por que um build PCRE2 UTF-16 estático sem JIT?
Strings do Excel são UTF-16, e valores WideString do Delphi também, o que fez da biblioteca PCRE2 de 16 bits a única escolha sensata. O HotXLS entrega o texto da fórmula ao pcre2_match_16 com PCRE2_UTF definido, e todo offset que volta é um índice de code unit UTF-16. Nenhuma viagem de ida e volta por UTF-8 acontece, então nada pode desviar em surrogate pairs. Onde um casamento vazio força a varredura a avançar, o HotXLS segue a regra de global match do PCRE2: tentar de novo um casamento ancorado não vazio na mesma posição, e depois avançar um scalar value Unicode, o que move duas code units além de um surrogate pair em vez de parti-lo
O JIT está desligado de propósito. O compilador JIT precisa de memória executável e da sua própria pilha JIT, o que puxa código específico de plataforma e alocações fora do alocador por fórmula. O interpretador mantém cada byte sob o orçamento do HotXLS, mantém cada casamento sob o callout e se comporta igual nos cinco alvos. Cada instância de TXLSRegex, a classe interna por trás dessas fórmulas, possui os seus próprios contextos, então workers de cálculo paralelos não compartilham estado PCRE2 mutável
A história de linkedição é onde o build ficou incomum. O Delphi liga arquivos objeto C com {$L}, e o linker dele resolve símbolos entre units Pascal e objetos C bem o suficiente, mas não resolve referências circulares arbitrárias entre arquivos objeto C separados. O PCRE2 são duas dúzias de translation units que se chamam em todas as direções. A correção é um unity build: todo o PCRE2 é compilado como uma translation unit em um único objeto pcre2_unity por alvo, então toda referência interna resolve dentro de um único objeto e o linker do Delphi só vê as funções _16 exportadas
O runtime C foi o segundo problema. O PCRE2 chama malloc, memcpy, memcmp e companhia, e outros backends C ligados no mesmo executável podem já trazer as suas próprias cópias desses nomes. O HotXLS pré-processa o PCRE2 para que essas chamadas vão para nomes privados como hotxls_regex_memcpy e os implementa em Pascal. O Clang ainda pode sintetizar chamadas simples de memset e memchr depois do pré-processamento, então esses dois também existem, dentro da mesma unit. A decoração de símbolos tem que casar com cada formato de objeto: o objeto Win32 OMF espera os nomes C com underscore inicial, enquanto as declarações Pascal para o objeto Win64 ELF, o objeto Win64 COFF e os builds FPC ligam os nomes nus
Os stack probes foram o sutil. Uma função C com um frame maior que uma página chama uma rotina de probe primeiro, que toca cada página de 4 KB em ordem para que o Windows possa commitar a pilha através da sua guard page. Um stub que simplesmente retorna liga bem e funciona em todo teste de unit com um padrão pequeno, e depois crasha na primeira vez em que um frame fundo pula a guard page. O HotXLS implementa probes reais em assembly para cada formato de objeto Windows: __chkstk_noalloc para o objeto Win32, __chkstk para o objeto Win64 em formato ELF e ___chkstk_ms para o objeto Win64 COFF. No Linux x64 e no macOS ARM64, o FPC liga a biblioteca C do sistema em vez disso, e todo outro alvo FPC reporta o backend como indisponível em vez de cair em silêncio em um dialeto de regex diferente
Onde ficam os limites?
As funções de regex rodam nos builds Windows do Delphi e do C++Builder e no FPC Linux x64 e macOS ARM64. Em qualquer outro alvo, a fórmula reporta uma função não suportada através da avaliação contextual em vez de produzir respostas diferentes com outro motor. Essa é a falha certa: uma pasta de trabalho que calcula diferente em uma plataforma diferente é um bug que ninguém acha até uma auditoria
A equivalência com o Excel tem um limite declarado também. As assinaturas e os exemplos documentados seguem as páginas REGEXTEST, REGEXEXTRACT e REGEXREPLACE da Microsoft, mas o build do Excel usado para validação local ainda não expõe essas funções do Microsoft 365. Os edge cases portanto seguem os contratos explícitos de PCRE2 e de arrays descritos acima em vez de uma alegação de casar com todo build do Excel bit a bit. Se as suas pastas de trabalho dependem de um canto obscuro, como um comportamento específico de backreference ou uma classe de propriedade Unicode, teste esse canto diretamente
FAQ
O HotXLS precisa do Excel ou de uma DLL de regex para calcular REGEXTEST?
Não. O HotXLS liga um objeto PCRE2 UTF-16 estático no seu executável e avalia REGEXTEST, REGEXEXTRACT e REGEXREPLACE no seu próprio motor de fórmulas Delphi, sem Excel, COM ou DLL externa
Um padrão malicioso pode pendurar o meu servidor?
Não além do orçamento configurado. Cada passo de casamento passa por um callout que cobra o contexto da fórmula, então um padrão ReDoS termina com xlfeResourceLimit ou um status de cancelamento e sem resultado parcial
Por que o modo 1 do REGEXEXTRACT falha com um argumento de intervalo?
Cada célula poderia retornar um número diferente de casamentos, e esses vetores irregulares não podem formar um resultado retangular. O HotXLS reporta a chamada como não suportada em vez de preencher ou truncar, então extraia uma célula de cada vez
O HotXLS entrega as funções de regex junto com LAMBDA, GROUPBY, PIVOTBY e o resto do seu motor de cálculo para Delphi, C++Builder e Lazarus. Detalhes, edições e o download de teste estão na página do componente de planilha HotXLS para Delphi