Articolo tecnico

REGEXTEST e REGEXEXTRACT in Delphi via PCRE2 statico

HotXLS valuta le funzioni di espressione regolare di Microsoft 365 REGEXTEST, REGEXEXTRACT e REGEXREPLACE dentro il suo motore di formule Delphi, senza installazione di Excel e senza DLL regex di sistema. Il calcolatore di HotXLS collega staticamente una build PCRE2 10.49 a 16 bit pinnata, fa matching diretto su testo UTF-16, fa broadcasting sugli array e addebita ogni passo di backtracking sul budget di risorse della formula

Quell'ultima parte è il motivo per cui esiste questo articolo. Un workbook che arriva da un cliente è input non fidato, e un'espressione regolare dentro una formula è codice che il cliente ha scritto. Pattern più testo bastano per inchiodare un core della CPU per minuti con backtracking catastrofico, la classica forma ReDoS (a+)+$ contro una lunga sequenza di caratteri a con una b finale. Un server che ricalcola workbook caricati non se lo può permettere, così il lavoro interessante in HotXLS 2.384.112 non era chiamare PCRE2. Era rendere PCRE2 stoppabile, contabile e identico su ogni target

Cosa restituiscono REGEXTEST, REGEXEXTRACT e REGEXREPLACE in HotXLS?

Il motore di formule di HotXLS segue le firme pubblicate da Microsoft, e la semantica degli argomenti è quella che già conoscete da Excel. Il matching è case-sensitive per predefinito. L'argomento finale case_sensitivity prende 0 per case-sensitive e 1 per case-insensitive, e qualunque altro valore restituisce #VALUE!. Le opzioni PCRE2 inline come (?i) o (*UCP) funzionano anche quando preferite tenere il comportamento dentro il pattern

REGEXTEST(text, pattern, [case_sensitivity]) restituisce un Booleano, e un match fallito è semplicemente FALSE, mai un errore. REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity]) ha tre modalità: 0 restituisce il primo match completo, 1 restituisce ogni match completo come array verticale, e 2 restituisce i capture group del primo match come array orizzontale. Nessun match restituisce #N/A, la modalità 2 su un pattern senza capture group restituisce #VALUE!, e un gruppo opzionale che non ha partecipato dà una stringa vuota invece di un errore

REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity]) usa la sintassi di sostituzione PCRE2, quindi $1 e ${name} referenziano le capture. Un occurrence di 0 sostituisce ogni match, un valore positivo prende l'n-esimo match dall'inizio e un valore negativo conta dalla fine. Quando l'occurrence selezionato non esiste, HotXLS restituisce il testo originale invariato. I selettori numerici vengono troncati verso zero, allo stesso modo in cui Excel tratta gli argomenti interi frazionari

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 la cella contiene un numero di ordine d'acquisto
    Sheet.Cells[2, 2].Formula := 'REGEXTEST(A2, "PO-\d{4}-\d{5}")';

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

    // Scambia anno e sequenza con capture numerate: "PO-00417-2026 ..."
    Sheet.Cells[2, 4].Formula :=
      'REGEXREPLACE(A2, "PO-(\d{4})-(\d{5})", "PO-$2-$1")';

    // Test case-insensitive con l'argomento finale
    Sheet.Cells[2, 5].Formula := 'REGEXTEST(A2, "graz", 1)';

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

Il testo della formula in Delphi è un letterale stringa semplice, quindi \d non ha bisogno di escaping dal lato Pascal. Contano solo le virgolette doppie Excel dentro la formula, e seguono le normali regole Excel. Al salvataggio, lo scrittore XLSX memorizza questi nomi con il prefisso future-function _xlfn. che Excel si aspetta, e un workbook riaperto li ricalcola di nuovo. Il motore classico BIFF8 può valutare le funzioni in una chiamata contestuale, ma non può scriverle come token di formula .xls nativi, quindi tenete le formule regex nell'output XLSX

Come funziona il broadcasting di array con le formule regex?

REGEXTEST, REGEXREPLACE e la modalità 0 di REGEXEXTRACT fanno broadcasting attraverso operandi compatibili, che è ciò che li rende utili su colonne vere. Ogni argomento può essere uno scalare, una riga, una colonna o un rettangolo, e HotXLS prende il conteggio maggiore di righe e colonne come forma del risultato. Un operando la cui dimensione non è né 1 né quel massimo fa restituire #VALUE! all'intera chiamata, il solito disallineamento di array di Excel. Così REGEXTEST(A2:A500, "^[A-Z]{2}\d{9}$") dà 499 Booleani, e una colonna di pattern contro una singola cella di testo funziona allo stesso modo nell'altra direzione

Le modalità 1 e 2 di REGEXEXTRACT si rifiutano di fare broadcasting. Ogni elemento produrrebbe un vettore di lunghezza diversa, e un insieme frastagliato di vettori non può occupare un risultato rettangolare di formula. Invece di troncare o riempire in silenzio, HotXLS segnala la chiamata come non supportata. Se vi servono tutti i match di molte celle, iterate le celle ed estraete una alla volta, o rimodellate il problema con closure LET e LAMBDA nel motore di formule HotXLS così ogni chiamata di closure vede un solo valore di testo

Il posizionamento degli errori dentro un risultato in broadcast è deliberato. Un errore Excel per un elemento, per esempio #N/A da un'estrazione che non ha trovato nulla nella riga 37, atterra in quella sola cella del risultato mentre le altre celle conservano i loro valori. Un limite di risorse o un annullamento sono diversi: fermano l'intera chiamata e non pubblicano mai un array parzialmente riempito, perché mezzor risultato che sembra completo è peggio di un fallimento esplicito

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;   // tappa il lavoro che questa formula può fare

    // Modalità 2: capture group del primo match come 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 è il punto d'ingresso giusto quando il pattern viene da un posto che non controllate. Valuta la formula come se vivesse alla riga e colonna date, restituisce l'array completo invece del valore di una singola cella in cache, e prende TXLSFormulaEvaluationOptions con controlli espliciti di passi, memoria array e annullamento. La guida al motore di formule HotXLS e alle funzioni personalizzate copre il resto di quella API di valutazione

Come ferma HotXLS un pattern con backtracking catastrofico?

HotXLS ferma i pattern impazziti facendo sì che PCRE2 riporti al contesto della formula a ogni passo. Il backend compila ogni pattern con PCRE2_AUTO_CALLOUT, che fa sì che PCRE2 chiami una callback Pascal a ogni item del pattern durante il matching. Quella callback addebita un'unità di lavoro allo stato di valutazione attivo, controlla il flag di annullamento e controlla la memoria residente. Quando qualcuna di queste dice stop, la callback restituisce il codice di aborto callout di PCRE2 e il match si srotola pulito dentro PCRE2. La formula emerge come xlfeResourceLimit o uno stato di annullamento, non come un thread appeso

La memoria è misurata allo stesso modo. Ogni contesto PCRE2 viene creato con un contesto generale per istanza il cui allocatore è una funzione Pascal, così il pattern compilato, i dati del match e i frame interni di backtracking di PCRE2 sono tutti allocati attraverso HotXLS. Ogni blocco porta un piccolo header ed è allineato a 16 byte, e la sua dimensione è aggiunta a un totale progressivo che il controllo di budget confronta col limite di memoria array della formula. I buffer di output, i vettori di estrazione e la stringa di sostituzione sono aggiunti allo stesso totale, il che significa che un pattern che fa match di milioni di stringhe minuscole becca il tetto con la stessa certezza di uno che fa backtracking per sempre

La callback del budget gira anche sotto una guardia che conta più di quanto sembri. Un'eccezione Delphi sollevata dentro una callback chiamata da PCRE2 non deve mai propagarsi in su, perché fra la callback e il vostro codice stanno frame di stack C compilati senza alcuna conoscenza delle tabelle di eccezioni Delphi. Srotolarsi attraverso di loro è indefinito nel migliore dei casi e uno stack corrotto nel peggiore. Il ponte HotXLS cattura ogni eccezione nella callback, aggancia l'istanza in uno stato abortito e restituisce un normale codice di errore a PCRE2. Il vostro codice vede una formula fallita, mai un'eccezione che ha attraversato un frame estraneo

Il costo è reale e merita di essere detto. Gli auto callout rendono il matching più lento di un interprete PCRE2 nudo, e HotXLS compila il pattern una volta per elemento valutato, così un broadcasting su 50.000 celle paga 50.000 compilazioni. Per lavoro di testo in blocco su dati fidati, un passaggio regex Delphi dedicato fuori dal workbook sarà più veloce. Dentro un workbook che sconosciuti vi spedono, limitato e prevedibile batte il veloce

Perché una build PCRE2 UTF-16 statica senza JIT?

Le stringhe Excel sono UTF-16, e così sono i valori WideString Delphi, il che ha reso la libreria PCRE2 a 16 bit l'unica scelta sensata. HotXLS passa il testo della formula a pcre2_match_16 con PCRE2_UTF impostato, e ogni offset che torna è un indice di unità di codice UTF-16. Non succede nessun giro di andata e ritorno UTF-8, quindi niente può slittare sulle coppie di surrogati. Dove un match vuoto forza la scansione ad avanzare, HotXLS segue la regola di global-match di PCRE2: riprova un match ancorato non vuoto alla stessa posizione, poi avanza di un valore scalare Unicode, che si muove di due unità di codice oltre una coppia di surrogati invece di spezzarla

Il JIT è spento apposta. Il compilatore JIT ha bisogno di memoria eseguibile e del suo stack JIT, il che porta dentro codice specifico di piattaforma e allocazioni fuori dall'allocatore per formula. L'interprete tiene ogni byte sotto il budget HotXLS, tiene ogni match sotto la callout e si comporta allo stesso modo su tutti e cinque i target. Ogni istanza TXLSRegex, la classe interna dietro queste formule, possiede i propri contesti, così i worker di calcolo paralleli non condividono alcuno stato PCRE2 mutabile

Il capitolo linking è dove la build è diventata insolita. Delphi collega file oggetto C con {$L}, e il suo linker risolve i simboli fra unit Pascal e oggetti C abbastanza bene, ma non risolve riferimenti circolari arbitrari fra file oggetto C separati. PCRE2 è due dozzine di unità di traduzione che si chiamano in ogni direzione. La soluzione è una unity build: tutto PCRE2 è compilato come una sola unità di traduzione in un oggetto pcre2_unity per target, così ogni riferimento interno si risolve dentro un singolo oggetto e il linker Delphi vede solo le funzioni _16 esportate

Il runtime C era il secondo problema. PCRE2 chiama malloc, memcpy, memcmp e simili, e altri backend C collegati nello stesso eseguibile possono già portare le loro copie di quei nomi. HotXLS preprocessa PCRE2 così quelle chiamate vanno a nomi privati come hotxls_regex_memcpy e le implementa in Pascal. Clang può comunque sintetizzare chiamate di memset e memchr semplici dopo il preprocessing, quindi anche quei due esistono, dentro la stessa unit. La decorazione dei simboli deve corrispondere a ogni formato di oggetto: l'oggetto Win32 OMF si aspetta i nomi C con underscore iniziale, mentre le dichiarazioni Pascal per l'oggetto Win64 ELF, l'oggetto Win64 COFF e le build FPC legano i nomi nudi

Le stack probe erano il caso sottile. Una funzione C con un frame più grande di una pagina chiama prima una routine di probe, che tocca ogni pagina da 4 KB in ordine così che Windows possa commettere lo stack attraverso la sua guard page. Uno stub che semplicemente ritorna si collega bene e funziona in ogni unit test con un pattern piccolo, poi crasha la prima volta che un frame profondo salta oltre la guard page. HotXLS implementa probe vere in assembly per ogni formato di oggetto Windows: __chkstk_noalloc per l'oggetto Win32, __chkstk per l'oggetto in formato ELF Win64 e ___chkstk_ms per l'oggetto Win64 COFF. Su Linux x64 e macOS ARM64, FPC collega invece la libreria C di sistema, e ogni altro target FPC riporta il backend come non disponibile invece di ricadere in silenzio su un dialetto regex diverso

Dove sono i confini?

Le funzioni regex girano sulle build Windows di Delphi e C++Builder e su FPC Linux x64 e macOS ARM64. Su qualunque altro target la formula riporta una funzione non supportata attraverso la valutazione contestuale invece di produrre risposte diverse con un altro motore. È il fallimento giusto: un workbook che calcola diversamente su una piattaforma diversa è un bug che nessuno trova finché non arriva un audit

Anche l'equivalenza con Excel ha un limite dichiarato. Le firme e gli esempi documentati seguono le pagine REGEXTEST, REGEXEXTRACT e REGEXREPLACE di Microsoft, ma la build di Excel usata per la validazione locale non espone ancora queste funzioni Microsoft 365. I casi limite seguono quindi i contratti espliciti PCRE2 e array descritti sopra invece di una pretesa di eguagliare ogni build di Excel bit per bit. Se i vostri workbook dipendono da un angolo oscuro, come un comportamento specifico di backreference o una classe di proprietà Unicode, testate quell'angolo direttamente

FAQ

HotXLS ha bisogno di Excel o di una DLL regex per calcolare REGEXTEST?

No. HotXLS collega un oggetto PCRE2 UTF-16 statico nel vostro eseguibile e valuta REGEXTEST, REGEXEXTRACT e REGEXREPLACE nel suo motore di formule Delphi, senza Excel, COM o DLL esterne

Un pattern malevolo può appendere il mio server?

Non oltre il budget configurato. Ogni passo di matching passa per una callout che addebita al contesto della formula, quindi un pattern ReDoS finisce con xlfeResourceLimit o uno stato di annullamento e nessun risultato parziale

Perché la modalità 1 di REGEXEXTRACT fallisce su un argomento di intervallo?

Ogni cella potrebbe restituire un numero diverso di match, e quei vettori frastagliati non possono formare un risultato rettangolare. HotXLS segnala la chiamata come non supportata invece di riempire o troncare, quindi estraete una cella alla volta

HotXLS spedisce le funzioni regex accanto a LAMBDA, GROUPBY, PIVOTBY e al resto del suo motore di calcolo per Delphi, C++Builder e Lazarus. Dettagli, edizioni e download di prova sono sulla pagina del componente di fogli di calcolo HotXLS Delphi