type
TSarSearchOption = (soIgnoreCase, soWholeWord, soUnderscoreIsWord, soReplaceAll);
TSarSearchOptions = set of TSarSearchOption;
// Simple ASCII word character test (ANSI). Treats letters and digits as word chars.
// If soUnderscoreIsWord is in Options then '_' counts as a word character.
function SarIsWordCharAnsi(ch: Char; Options: TSarSearchOptions): Boolean;
begin
Result := (ch in ['0'..'9', 'A'..'Z', 'a'..'z']) or (soUnderscoreIsWord in Options) and (ch = '_');
end;
// Find next whole-word match (1-based index) or 0 if not found.
// StartPos is 1-based. Options controls case-sensitivity and whole-word behavior.
function FindWholeWordANSI(const Source, OldWord: string; StartPos: Integer; Options: TSarSearchOptions): Integer;
var
Hay, Needle: string;
LHay, LNeedle, i, relPos: Integer;
beforeOK, afterOK: Boolean;
begin
Result := 0;
if (OldWord = '') or (Source = '') then Exit;
if StartPos < 1 then StartPos := 1;
// prepare case folded hay/needle if ignoring case
if soIgnoreCase in Options then
begin
Hay := AnsiUpperCase(Source);
Needle := AnsiUpperCase(OldWord);
end
else
begin
Hay := Source;
Needle := OldWord;
end;
LHay := Length(Hay);
LNeedle := Length(Needle);
if LNeedle = 0 then Exit;
if StartPos > LHay - LNeedle + 1 then Exit;
// search loop using Pos on substring of Hay (keeps things ANSI-safe)
relPos := StartPos;
while relPos <= LHay - LNeedle + 1 do
begin
i := Pos(Copy(Needle, 1, LNeedle), Copy(Hay, relPos, LHay - relPos + 1));
if i = 0 then Exit; // no more matches
i := i + relPos - 1; // absolute position in Hay/Source
// if whole-word checking requested, validate boundaries
if soWholeWord in Options then
begin
if i = 1 then
beforeOK := True
else
beforeOK := not SarIsWordCharAnsi(Source[i - 1], Options);
if i + LNeedle - 1 >= LHay then
afterOK := True
else
afterOK := not SarIsWordCharAnsi(Source[i + LNeedle], Options);
end
else
begin
beforeOK := True;
afterOK := True;
end;
if beforeOK and afterOK then
begin
Result := i;
Exit;
end
else
relPos := i + 1; // continue searching after this candidate
end;
end;
// Replace whole-word occurrences. Returns number of replacements performed.
// StartPos is 1-based; if soReplaceAll is in Options all matches after StartPos are replaced,
// otherwise only the first matching occurrence is replaced.
function sarReplaceWholeWordANSI(var Source: string; const OldWord, NewWord: string; StartPos: Integer; Options: TSarSearchOptions): Integer;
var
posFound, LOld, LNew, nextStart: Integer;
hayUpper, needleUpper: string;
begin
Result := 0;
if (OldWord = '') or (Source = '') then Exit;
if StartPos < 1 then StartPos := 1;
// We'll use FindWholeWordANSI to locate matches; FindWholeWordANSI handles case folding.
LOld := Length(OldWord);
LNew := Length(NewWord);
posFound := FindWholeWordANSI(Source, OldWord, StartPos, Options);
while posFound > 0 do
begin
// Replace exactly at posFound (ANSI byte/char indexing)
Delete(Source, posFound, LOld);
Insert(NewWord, Source, posFound);
Inc(Result);
// If not replacing all, return after first replacement
if not (soReplaceAll in Options) then Exit;
// Move search start just after the inserted replacement to avoid re-matching inside it
nextStart := posFound + LNew;
// Continue searching from nextStart
posFound := FindWholeWordANSI(Source, OldWord, nextStart, Options);
end;
end;