-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMaxLogic.StrUtils.pas
More file actions
1755 lines (1561 loc) · 57.1 KB
/
Copy pathMaxLogic.StrUtils.pas
File metadata and controls
1755 lines (1561 loc) · 57.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
unit maxLogic.StrUtils;
{$IFDEF MSWINDOWS}
{$WARN SYMBOL_PLATFORM OFF}
{$ENDIF}
interface
uses
System.Character, System.Classes, System.SysUtils, System.Types, System.generics.collections, System.generics.defaults,
System.StrUtils, System.RegularExpressions;
const
CR = sLineBreak;
// like system.masks.matchesMasks but platform independant and with the option to be case sensitive/insensitive
function StringMatches(const aValue, aPattern: string;
aCaseSensitive: boolean = True): boolean;
function PutBefore(const AString: string; AChar: char; TotalLength: integer): string; overload;
function PutBefore(num: integer; AChar: char; TotalLength: integer): string; overload;
/// <summary>
/// Extracts a substring located between the first occurrence of aStartMarker
/// and the subsequent first occurrence of aEndMarker, starting the search from aStartOffset.
/// Optionally handles case sensitivity and checks for invalid characters within the potential result.
/// </summary>
/// <param name="aText">The text to search within.</param>
/// <param name="aStartMarker">The starting delimiter string.</param>
/// <param name="aEndMarker">The ending delimiter string.</param>
/// <param name="aStartoffset">The 1-based index within aText to begin searching for aStartMarker.</param>
/// <param name="aValue">Output parameter: Receives the extracted substring (between the markers) if found and valid.</param>
/// <param name="aStartMarkerFoundAtIndex">Output parameter: Receives the 1-based index where aStartMarker was found.</param>
/// <param name="aCasesensitive">If True, the search for markers is case-sensitive. If False, markers and text are compared case-insensitively (using ToLower).</param>
/// <param name="aInvalidChars">An optional array of characters. If any of these characters are found *between* a potential start and end marker pair in the original `aText`, that match is considered invalid, and the search continues.</param>
/// <param name="aInvalidCharsAreSorted">Optimization hint. If True, indicates that the aInvalidChars array is sorted, allowing for a faster binary search for invalid characters. If False, a linear search is performed.</param>
/// <returns>True if a valid substring was found and extracted, False otherwise (markers not found, end marker before start marker, or an invalid character was detected between markers).</returns>
/// <remarks>
/// This is the main public overload. It prepares the text and markers for case-sensitive or case-insensitive searching
/// based on `aCasesensitive` and then calls the internal overload to perform the actual extraction logic.
/// The search for invalid characters (`aInvalidChars`) is *always* performed case-sensitively on the original `aText`.
/// </remarks>
function ExtractString(
const aText, aStartMarker, aEndMarker: string;
aStartoffset: integer;
out aValue: string;
out aStartMarkerFoundAtIndex: integer;
aCaseSensitive: boolean = True;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False): boolean; overload;
/// <summary>
/// Internal implementation for ExtractString. Performs the core logic of finding markers and checking for invalid characters.
/// Assumes that text/markers have already been prepared for case sensitivity by the caller.
/// </summary>
/// <param name="aOrgCasedText">The original text with its original casing. Used for the final Copy operation and the invalid character check.</param>
/// <param name="aTextForCaseSensitiveSearch">The text to perform the marker search in. This might be the original text or a lowercase version, depending on the desired case sensitivity.</param>
/// <param name="aStartMarker">The starting delimiter prepared for case-sensitive search.</param>
/// <param name="aEndMarker">The ending delimiter prepared for case-sensitive search.</param>
/// <param name="aStartoffset">The 1-based index within aTextForCaseSensitiveSearch to continue searching for aStartMarker.</param>
/// <param name="aValue">Output parameter: Receives the extracted substring (from aOrgCasedText) if found and valid.</param>
/// <param name="aStartMarkerFoundAtIndex">Output parameter: Receives the 1-based index where aStartMarker was found in aTextForCaseSensitiveSearch (corresponds to the same position in aOrgCasedText).</param>
/// <param name="aInvalidChars">An optional array of characters. If any character from this array is found within the substring *between* the found markers (checked against `aOrgCasedText`), the match is invalid.</param>
/// <param name="aInvalidCharsAreSorted">Optimization hint for the invalid character search.</param>
/// <returns>True if a valid substring was found, False otherwise.</returns>
/// <remarks>
/// This function iterates using a `while true` loop:
/// 1. Finds the next `aStartMarker` using `PosEx` starting from `aStartoffset`. Exits if not found.
/// 2. Calculates the position `i1` immediately after the found `aStartMarker`.
/// 3. Finds the next `aEndMarker` using `PosEx` starting from `i1`. Exits if not found.
/// 4. Uses `CharPosEx` to check if any `aInvalidChars` exist in `aOrgCasedText` between index `i1` and `i2 - 1`.
/// 5. If an invalid character is found:
/// - Updates `aStartoffset` to `aStartMarkerFoundAtIndex + 1` to ensure the next search attempt starts *after* the beginning of the current invalid match.
/// - Executes `Continue` to restart the loop from step 1.
/// 6. If no invalid character is found:
/// - Copies the valid substring from `aOrgCasedText` between `i1` and `i2 - 1` into `aValue`.
/// - Sets Result to True and exits the function.
/// The loop continues until a valid match is found and returned, or until no more potential start/end marker pairs can be found.
/// </remarks>
function ExtractString(
const aOrgCasedText, aTextForCaseSensitiveSearch,
aStartMarker, aEndMarker: string;
aStartoffset: integer;
out aValue: string;
out aStartMarkerFoundAtIndex: integer;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False): boolean; overload;
function CharPosEx(
const aText: string;
const aChars: TArray<char>;
aCharsArrayIsSorted: boolean;
aStartoffset, aEndOffset: integer;
out aFoundAtOffset, aIndexOfCharFound: integer): boolean;
type
TReplacePlaceholderAction = (
raReplace,
raReplaceAndResumeAtSamePosition, // good for nested occurences,, but a bit slower as it requires in place editing of the source text
raSkip, // does not replace the found placeholder
raStop, // as raSkip but also stops the search for the next item
raReplaceAndStop // replaces this last occurence, but stops afterwards
);
TReplacePlaceholderOnFoundProc = reference to procedure(
// the text between startMarker and endMarker
const aValue: string;
aStartMarkerFoundAtIndex: integer;
// the String to replace the placeholder (StartMarker+value+endmarker)
// default is the whole placeholder that s the value with the start and end markers, so if you do not touch that, it will act as action=raSkip
var aReplaceValue: string;
// default raReplace
var aAction: TReplacePlaceholderAction);
/// <summary>
/// TFastCaseAwareComparer keeps allocations at zero for ASCII-heavy workloads by folding characters
/// through a lookup table and feeding them into a highly parallelized XXHash-style logic with overflow checks disabled.
/// Non-ASCII inputs fall back to TCharHelper.ToUpper calls on the fly, avoiding string allocations.
/// Latest DUnitX perf runs (Debug/Win32, 200k mixed keys) show ~2.8× vs TIStringComparer.Ordinal for case-insensitive hashes
/// and ~1.7× vs TStringComparer.Ordinal for ordinal mode, so prefer this comparer when we own both the key creation and dictionary lifetime.
/// If a caller can’t tolerate case-folding differences or needs locale-aware comparisons, stick with the RTL comparer.
/// Semantics stay strictly ordinal: no locale-driven expansions such as 'ß' → 'SS', ligature breaking, etc.; these inputs remain distinct.
/// Consumers that require locale-aware folding must continue using TIStringComparer/CompareText instead of this fast comparer.
/// </summary>
TFastCaseAwareComparer = class(TInterfacedObject, IEqualityComparer<string>)
private
FCaseSensitive: Boolean;
class var FUpperAscii: array[0..255] of Char;
class var FOrdinalComparer: IEqualityComparer<string>;
class var FOrdinalIgnoreCaseComparer: IEqualityComparer<string>;
class constructor Create;
class destructor Destroy;
class function FoldCharValue(aChar: Char): Word; static; inline;
class function FoldPair(const aPair: Cardinal): Cardinal; static; inline;
class function Rotl32(aValue: Cardinal; aBits: Integer): Cardinal; static; inline;
class function ReadCardinalUnaligned(const aPtr: Pointer): Cardinal; static; inline;
class function HashBytes(const aData: PByte; aLength: NativeInt): Cardinal; static;
class function HashOrdinal(const aValue: string): Integer; static;
class function HashCaseInsensitive(const aValue: string): Integer; static;
class function GetOrdinalComparer: IEqualityComparer<string>; static;
class function GetOrdinalIgnoreCaseComparer: IEqualityComparer<string>; static;
public
constructor Create(aCaseSensitive: Boolean);
class function Ordinal: IEqualityComparer<string>; static;
class function OrdinalIgnoreCase: IEqualityComparer<string>; static;
function Equals(const aLeft, aRight: string): Boolean; reintroduce;
function GetHashCode(const aValue: string): Integer; reintroduce;
end;
/// <summary>
/// Replaces occurrences of placeholders (text between aStartMarker and aEndMarker) within a given text.
/// Calls a provided procedure for each found placeholder to determine the replacement value and action.
/// </summary>
/// <param name="aText">The input text containing potential placeholders.</param>
/// <param name="aStartMarker">The starting delimiter of the placeholder.</param>
/// <param name="aEndMarker">The ending delimiter of the placeholder.</param>
/// <param name="aOnFoundProc">A callback procedure invoked for each valid placeholder found. It receives the placeholder's content (`aValue`), its start index (`aStartMarkerFoundAtIndex`), and allows modification of the replacement string (`aReplaceValue`) and the action (`aAction`) to take.</param>
/// <param name="aStartoffset">The 1-based index in aText to begin searching for placeholders.</param>
/// <param name="aCasesensitive">If True, the search for markers is case-sensitive. If False, markers and text are compared case-insensitively.</param>
/// <param name="aInvalidChars">An optional array of characters that invalidate a potential match if found between the start and end markers (checked case-sensitively on the original text).</param>
/// <param name="aInvalidCharsAreSorted">Optimization hint for the invalid character search within ExtractString.</param>
/// <returns>A new string with placeholders replaced according to the logic defined in aOnFoundProc.</returns>
/// <remarks>
/// The function iterates through the text using the `ExtractString` helper function to find valid placeholders (respecting `aInvalidChars`).
/// For each valid placeholder found:
/// 1. The `aOnFoundProc` callback is invoked.
/// 2. The callback determines the `aReplaceValue` (initially defaulted to the full placeholder including markers) and `aAction`.
/// 3. Based on `aAction`:
/// - `raReplace`: The text before the placeholder and the `aReplaceValue` are appended to the result. The search continues after the replaced section.
/// - `raSkip`: The original placeholder text (including markers) is treated as the replacement value. The search continues after the skipped section.
//// - `raStop`: The loop terminates immediately. No additional text is appended within this iteration. After the loop finishes, the function appends the remainder of the original text starting from the current search offset (`aStartoffset`), which includes the placeholder and everything after it.
/// - `raReplaceAndStop`: The text before the placeholder and `aReplaceValue` are appended, then the loop terminates. After the loop finishes, the remainder of the original text starting from the updated search offset (past the replaced placeholder) is appended.
/// - `raReplaceAndResumeAtSamePosition`: This handles nested or overlapping replacements. The text before the placeholder is appended. The original text buffer (`lOrgCasedText`) and its search version (`lTextForCaseSensitiveSearch`) are *modified in-place* by substituting the placeholder with `aReplaceValue`. The search offset (`aStartoffset`) is reset to 1, and the loop continues, effectively rescanning the modified text from the beginning. This can be less performant due to string manipulation but allows for complex replacement scenarios.
/// The function uses `TStringBuilder` internally for efficient construction of the final result string.
/// After the loop finishes (either by reaching the end of the text or via a `raStop`/`raReplaceAndStop` action), any remaining portion of the text after the last processed position (`aStartoffset`) is appended to the result.
/// </remarks>
function ReplacePlaceholder(
const aText, aStartMarker, aEndMarker: string;
// will be caled for each occurence
const aOnFoundProc: TReplacePlaceholderOnFoundProc;
aStartoffset: integer = 1;
// will search for the startMarker and Marker case sensitive or not
aCaseSensitive: boolean = True;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False
): string;
// will replace occurences of a system enviroment variable name withint the text with its value
// like '%appdata%\tmp\%username%'
function ExpandEnvVars(const aText: string; const aStartToken: string = '%'; const aEndToken: string = '%'): string;
function CombineUrl(const aPart1, aPart2: string; aSeparator: string = '/'): string; overload;
function CombineUrl(const aParts: array of string; aSeparator: string = '/'): string; overload;
// uses internally masks.MatchesMask
// checks if the aText mathes any of the givem filter strings
// returns always true if the filter array is empty
function MatchesFilter(const aText: string; const aFilter: TStringDynArray): boolean;
// Windows Explorer uses StrCmpLogicalW to compare file names. The RTL/VCL does not declare this function so you need to do it yourself.
// on non windows platform we are falling back on CompareStr
function StrCmpLogical(const Left, right: string): integer; inline;
{$IFDEF MSWINDOWS}
function StrCmpLogicalW(psz1, psz2: PWideChar): integer; stdcall; External 'shlwapi.dll' delayed;
{$ENDIF}
type
{
TFilterEx - A record for advanced text filtering based on custom filter syntax.
This filtering algorithm is inspired by the Everything search tool's advanced search syntax.
(https://www.voidtools.com/support/everything/searching/#advanced_search)
It allows for complex filtering rules, including combinations of required terms, optional terms,
and wildcard searches.
Filter Syntax:
- "a b" : The text must contain both 'a' and 'b'.
- "a b|c" : The text must contain 'a' and either 'b' or 'c'.
- "a*" : The text must start with 'a'.
- "a* *b|*c" : The text must start with 'a' and end with either 'b' or 'c'.
Usage Example:
- Create a filter: var Filter := TFilterEx.Create('a b|c');
- Check if a text matches: if Filter.Matches('example text') then ...
}
TFilterEx = record
private type
TKind = (kMask, kContains, kStarts, kEnds);
TFilterItem = record
IsNegated: boolean;
OrElements: TArray<string>;
OrElementKinds: TArray<TKind>;
OrgText: string;
end;
private
fFilter: TArray<TFilterItem>;
fOrgFilterText: string;
// Prepares the internal filter structure based on the provided filter text.
procedure Prepare(const aText: string);
// we need to take care because of the special ~and quotes
function SplitBySpace(const aText: string): TArray<string>;
procedure Preprocess(var p: string; out k: TKind); inline;
public
// Creates a TFilterEx instance from the given filter text.
class function Create(const aFilterText: string): TFilterEx; static;
/// <summary>
/// Returns true if the given text matches the filter or if the filter is empty
/// </summary>
function Matches(const aText: string): boolean;
end;
procedure Split(const line: string; delimiter: char; STRINGS: TStringList); overload;
procedure Split(const line: string; delimiter: char; out STRINGS: TArray<string>); overload;
function Split(delimiter: char; const line: string): TArray<string>; overload;
Function splitstring(Const aText: String; aDelim: char; aPerformTrimOnParts: Boolean = False): TArray<String>;
function SplitInHalfBy(const aText: string; aDelim: char; out aParts: TArray<string>): boolean; overload;
function SplitInHalfBy(const aText: string; aDelim: char; out alLeft, alRight: string): boolean; overload;
function fStr(const d: double; vs: integer = 2; ns: integer = 2): string;
function GuidToHex(const aGuid: TGuid): string;
function Join(const aSeparator: string; const aValues: TArray<integer>): string; overload;
function Join(const aSeparator: string; aValues: TStringList): string; overload;
// this method ensures the num of bytes does not exceed aMaxByteLength
// it supports unicode surrogate pairs
function Utf8TruncateByCodePoint(const AInput: string; aMaxBytesLength: integer): TBytes;
// StrTo Float With Comma Correction
function StrToFloatWCC(const Text: string): double;
function TryStrToFLoatWCC(const Text: string; out Value: double): boolean;
function StrToFLoatWccDef(const Text: string; const default: double): double;
procedure PrepareTextForStrToFloatWcc(var s: string);
function BytesToRawStr(const b: TBytes): rawByteString;
function RawStrToBytes(const s: rawByteString): TBytes;
type
TStringComparerHelper = class helper for TStringComparer
class function OrdinalIgnoreCase: TCustomComparer<string>; static;
end;
// Anonymous-method evaluator type
TMatchEvaluatorProc = reference to function(const match: TMatch): string;
TRegExHelper = record helper for TRegEx
private
type
// Bridges anonymous method -> 'of object' method pointer
TProcAdapter = class
private
fProc: TMatchEvaluatorProc;
public
constructor Create(const aProc: TMatchEvaluatorProc);
function Invoke(const match: TMatch): string;
end;
public
// INSTANCE overloads accepting anonymous methods (what your call needs)
function Replace(const input: string; Evaluator: TMatchEvaluatorProc): string; overload;
function Replace(const input: string; Evaluator: TMatchEvaluatorProc; Count: integer): string; overload;
// Optional: keep your class overloads too
class function Replace(const input, pattern: string; Evaluator: TMatchEvaluatorProc): string; overload; static;
class function Replace(const input, pattern: string; Evaluator: TMatchEvaluatorProc; Options: TRegExOptions): string; overload; static;
end;
function PrettyElapsed(const aMs: Int64): string;
function i2s(const i: integer): string; inline;
// converts a double (its binary delphi representation) into a hex string
// aMinLength: will add or remove leading '0' to match this len
function floatToHex(const f: double; aMinLength: integer = SizeOf(double) * 2): string;
// pretty print a size in bytes
Function PrettyPrintSize(Const ByteCount: int64): String;
Function MegaBytesToStr(Const MegaByteCount: extended): String;
{ This function will replace tokenized keywords (=words enclosed by the token char, like :myToken: by its parameters. (*
like this:
ParseStatement ('SELECT :id:, :field2:, from :t WHERE :f1 = :v1', ['id, 'my_id_field',
'field2', 'my_field2_value,
't ', 'myTableName',
'f1 ', 'myf1Value',
'v1', '''myValue2 please note the quotes''']))
// this should be easier to read then a long statement with many string concatenations and value retriving code...
The params are case insensitive
}
Function ParseStatement(Const Statement: String; Const params: TArray<String>;
Const Token: char = ':'): String;
implementation
uses
System.Masks, AutoFree, System.Math;
function OccurrencesOfChar(const s: string; const c: char): integer;
var
pc: PChar;
lEndPtr: PChar;
begin
Result := 0;
if s <> '' then
begin
pc := PChar(s);
lEndPtr := pc + Length(s);
while pc < lEndPtr do
begin
if pc^ = c then
Inc(Result);
Inc(pc);
end;
end;
end;
// --- Helper function for guaranteed case-sensitive matching using RegEx ---
// This is needed as a fallback on Windows where MatchesMask is insensitive.
function MatchesMaskCaseSensitive_RegEx(const aText, aPattern: string): boolean;
var
lRegexPattern: string;
begin
// 1. Escape special RegEx characters in the original pattern FIRST.
// This ensures characters like '.', '+', '\', '(', ')' etc., are treated literally.
// It also escapes our wildcards '*' and '?' into '\*' and '\?'.
lRegexPattern := TRegEx.Escape(aPattern);
// 2. Replace the ESCAPED wildcard characters with their RegEx equivalents.
// IMPORTANT: Replace '\*' first to avoid issues if '\?' was replaced first
// and the pattern contained something like '\?*'.
lRegexPattern := lRegexPattern.Replace('\*', '.*'); // Replace escaped '*' with '.*'
lRegexPattern := lRegexPattern.Replace('\?', '.'); // Replace escaped '?' with '.'
// 3. Anchor the pattern to match the whole string from start (^) to end ($)
lRegexPattern := '^' + lRegexPattern + '$';
// 4. Perform the case-sensitive match (TRegEx default behavior)
Result := TRegEx.IsMatch(aText, lRegexPattern);
end;
// --------------------------------------------------------------------------
// Performs wildcard matching ('*' for zero or more chars, '?' for one char).
// Handles case sensitivity correctly across platforms.
function StringMatches(const aValue, aPattern: string;
aCaseSensitive: boolean = True): boolean;
begin
if not aCaseSensitive then
begin
// --- Case-Insensitive Matching ---
// Goal: Make the comparison ignore case on ALL platforms.
{$IFDEF MSWINDOWS}
// On Windows, MatchesMask is already case-insensitive. Use it directly.
Result := System.Masks.MatchesMask(aValue, aPattern);
{$ELSE}
// On other platforms (Linux, macOS, etc.), MatchesMask is case-sensitive.
// Force case-insensitivity by comparing lower-case (or upper-case) versions.
Result := System.Masks.MatchesMask(aValue.ToLower, aPattern.ToLower);
{$ENDIF}
end else begin
// --- Case-Sensitive Matching ---
// Goal: Make the comparison respect case on ALL platforms.
{$IFDEF MSWINDOWS}
// On Windows, MatchesMask is case-insensitive, so we *must* use a fallback.
// Use our RegEx helper function for guaranteed case-sensitivity.
Result := MatchesMaskCaseSensitive_RegEx(aValue, aPattern);
{$ELSE}
// On other platforms, MatchesMask is already case-sensitive. Use it directly.
Result := System.Masks.MatchesMask(aValue, aPattern);
{$ENDIF}
end;
end;
function PutBefore(const AString: string; AChar: char; TotalLength: integer): string;
var
X, aMaxBytesLength: integer;
begin
aMaxBytesLength := length(AString);
if aMaxBytesLength > TotalLength then
Result := AString
else
begin
SetLength(Result, TotalLength);
for X := 1 to TotalLength - aMaxBytesLength do
Result[X] := AChar;
for X := TotalLength - aMaxBytesLength + 1 to TotalLength do
Result[X] := AString[X - (TotalLength - aMaxBytesLength)];
end;
end;
function PutBefore(num: integer; AChar: char; TotalLength: integer): string; overload;
begin
Result := PutBefore(IntToStr(num), AChar, TotalLength);
end;
function ExtractString(
const aText, aStartMarker, aEndMarker: string;
aStartoffset: integer;
out aValue: string; out aStartMarkerFoundAtIndex: integer;
aCaseSensitive: boolean = True;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False): boolean;
begin
if aCaseSensitive then
Result := ExtractString(
aText, aText, aStartMarker, aEndMarker,
aStartoffset, aValue, aStartMarkerFoundAtIndex, aInvalidChars, aInvalidCharsAreSorted)
else
Result := ExtractString(
aText, aText.ToLower, aStartMarker.ToLower, aEndMarker.ToLower,
aStartoffset, aValue, aStartMarkerFoundAtIndex, aInvalidChars, aInvalidCharsAreSorted);
end;
function ExtractString(
const aOrgCasedText, aTextForCaseSensitiveSearch,
aStartMarker, aEndMarker: string;
aStartoffset: integer;
out aValue: string;
out aStartMarkerFoundAtIndex: integer;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False): boolean;
var
i1, i2: integer;
lInvalidCharOffset, lIndexOfInvalidChar: integer;
begin
while True do
begin
aStartMarkerFoundAtIndex := PosEx(aStartMarker, aTextForCaseSensitiveSearch, aStartoffset);
if aStartMarkerFoundAtIndex = 0 then
exit(False);
i1 := aStartMarkerFoundAtIndex + length(aStartMarker);
i2 := PosEx(aEndMarker, aTextForCaseSensitiveSearch, i1);
if i2 = 0 then
exit(False);
if CharPosEx(aOrgCasedText,
aInvalidChars,
aInvalidCharsAreSorted,
i1, i2 - 1,
lInvalidCharOffset, lIndexOfInvalidChar) then
begin
aStartoffset := i1;
Continue;
end;
aValue := copy(aOrgCasedText, i1, i2 - i1);
exit(True);
end;
end;
function ReplacePlaceholder(
const aText, aStartMarker, aEndMarker: string;
const aOnFoundProc: TReplacePlaceholderOnFoundProc;
aStartoffset: integer = 1;
aCaseSensitive: boolean = True;
{ aInvalidChars: used for the following scenario If we search for #*#, but only on the same line,
we can pass aInvalidChars = [#10], in that case if the #10 will come before the end marker, it will
indicates that this is indeed not a end marker at all
Note: the search for invalidChars is always case sensitive}
const aInvalidChars: TArray<char> = [];
aInvalidCharsAreSorted: boolean = False
): string;
var
lValue, lReplacementValue: string;
lStartMarkerFoundAtIndex: integer;
lAction: TReplacePlaceholderAction;
lFound: boolean;
lOrgCasedText, lTextForCaseSensitiveSearch, lStartMarker, lEndMarker: string;
lMarkersLen, lPlaceHolderLen: integer;
sb: TStringBuilder; // well, I know, there was much hate for the TStringBuilder in delphi. But at least since delphi 12 this is faster then ordinary delphi string + string,expecially for large strings
begin
Result := '';
gc(sb, TStringBuilder.Create);
sb.Capacity := length(aText) * 2;
if aStartoffset < 1 then
aStartoffset := 1;
// init our output with the text before the start offset, so we do not loose it
if aStartoffset > 1 then
sb.append(copy(aText, 1, aStartoffset - 1));
lOrgCasedText := aText;
lMarkersLen := length(aStartMarker) + length(aEndMarker);
if aCaseSensitive then
begin
lTextForCaseSensitiveSearch := aText;
lStartMarker := aStartMarker;
lEndMarker := aEndMarker;
end
else
begin
lTextForCaseSensitiveSearch := aText.ToLower;
lStartMarker := aStartMarker.ToLower;
lEndMarker := aEndMarker.ToLower;
end;
repeat
lFound := ExtractString(
lOrgCasedText, lTextForCaseSensitiveSearch, lStartMarker, lEndMarker,
aStartoffset,
lValue,
lStartMarkerFoundAtIndex,
aInvalidChars, aInvalidCharsAreSorted);
if not lFound then
break;
lAction := raReplace;
lPlaceHolderLen := length(lValue) + lMarkersLen;
lReplacementValue := copy(lOrgCasedText, lStartMarkerFoundAtIndex, lPlaceHolderLen);
aOnFoundProc(lValue, lStartMarkerFoundAtIndex, lReplacementValue, lAction);
case lAction of
raReplace:
begin
lAction := raReplace;
end;
raReplaceAndStop:
begin
lAction := raReplaceAndStop;
end;
raStop:
break;
raSkip:
begin
lReplacementValue := copy(lOrgCasedText, lStartMarkerFoundAtIndex, lPlaceHolderLen);
lAction := raReplace;
end;
raReplaceAndResumeAtSamePosition:
begin
// that one is a bit tricky
// 1. flush the part before the marker start pos to our output
sb.append(
copy(lOrgCasedText,
aStartoffset, (lStartMarkerFoundAtIndex - aStartoffset)));
// 2. trim both text buffers and prepend with the pepalacement value
lOrgCasedText := lReplacementValue +
copy(lOrgCasedText, lStartMarkerFoundAtIndex + lPlaceHolderLen, length(lOrgCasedText));
if not aCaseSensitive then
lReplacementValue := lReplacementValue.ToLower;
lTextForCaseSensitiveSearch := lReplacementValue +
copy(lTextForCaseSensitiveSearch, lStartMarkerFoundAtIndex + lPlaceHolderLen, length(lTextForCaseSensitiveSearch));
// 3. Reset the start offset to 1 as we want to search from the start again
aStartoffset := 1;
Continue; // we want to skip the code that follows
end;
end;
// copy the part from the start position until the position of the marker, then add the replacement text to the output
sb.append(
copy(lOrgCasedText,
aStartoffset, (lStartMarkerFoundAtIndex - aStartoffset)));
sb.append(lReplacementValue);
aStartoffset := lStartMarkerFoundAtIndex + lPlaceHolderLen;
until (not lFound) or (lAction in [raStop, raReplaceAndStop]);
// append the rest of the Text
if aStartoffset <= length(lOrgCasedText) then
sb.append(copy(lOrgCasedText, aStartoffset, length(lOrgCasedText)));
Result := sb.ToString;
end;
function CombineUrl(const aPart1, aPart2: string; aSeparator: string = '/'): string;
begin
Result := CombineUrl([aPart1, aPart2], aSeparator);
end;
function CombineUrl(const aParts: array of string; aSeparator: string = '/'): string;
var
X: integer;
begin
case length(aParts) of
0:
Result := '';
1:
Result := aParts[0];
else
begin
Result := aParts[0];
for X := 1 to length(aParts) - 1 do
begin
if not endsText(aSeparator, Result) then
Result := Result + aSeparator;
if StartsText(aSeparator, aParts[X]) then
Result := Result + copy(aParts[X], length(aSeparator) + 1, length(aParts[X]))
else
Result := Result + aParts[X];
end;
end;
end;
end;
function MatchesFilter(const aText: string;
const aFilter: TStringDynArray): boolean;
var
lFilter: string;
begin
if length(aFilter) = 0 then
exit(True);
Result := False;
for lFilter in aFilter do
begin
Result := System.Masks.MatchesMask(aText, lFilter);
if Result then
break;
end;
end;
function ExpandEnvVars(const aText: string; const aStartToken: string = '%'; const aEndToken: string = '%'): string;
begin
Result := ReplacePlaceholder(
aText, aStartToken, aEndToken,
procedure(
// the text between startMarker and endMarker
const aValue: string;
aStartMarkerFoundAtIndex: integer;
// the String to replace the placeholder (StartMarker+value+endmarker)
// default is the whole placeholder that s the value with the start and end markers, so if you do not touch that, it will act as action=raSkip
var aReplaceValue: string;
// default raReplace
var aAction: TReplacePlaceholderAction)
var
v: string;
begin
v := System.SysUtils.GetEnvironmentVariable(aValue);
if v = '' then
aAction := raSkip
else
aReplaceValue := v;
end,
1, False);
end;
function StrCmpLogical(const Left, right: string): integer;
begin
{$IFDEF MSWINDOWS}
Result := StrCmpLogicalW(PWideChar(Left), PWideChar(right));
{$ELSE}
Result := CompareStr(Left, right);
{$ENDIF}
end;
{ TFilterEx }
class function TFilterEx.Create(const aFilterText: string): TFilterEx;
begin
Result := default(TFilterEx);
Result.Prepare(aFilterText);
end;
procedure TFilterEx.Prepare(const aText: string);
var
ar: TArray<string>;
fi: TFilterItem;
l: TStringList;
p: string;
k: TKind;
begin
gc(l, TStringList.Create);
l.StrictDelimiter := True;
l.delimiter := '|';
l.QuoteChar := '"';
fOrgFilterText := aText;
ar := SplitBySpace(aText.Trim.ToLower);
SetLength(fFilter, length(ar));
for var X := 0 to High(ar) do
begin
fi := default(TFilterItem);
p := Trim(ar[X]);
if p = '' then
Continue;
fi.OrgText := p;
if StartsText('!', p) then
begin
delete(p, 1, 1);
fi.IsNegated := True;
if p = '' then
Continue;
end;
l.delimitedText := p;
SetLength(fi.OrElements, l.Count);
SetLength(fi.OrElementKinds, l.Count);
for var y := 0 to l.Count - 1 do
begin
p := l[y];
Preprocess(p, k);
fi.OrElements[y] := p;
fi.OrElementKinds[y] := k;
end;
fFilter[X] := fi;
end;
end;
procedure TFilterEx.Preprocess(var p: string; out k: TKind);
var
i1, i2: integer;
begin
if pos('?', p) > 0 then
k := kMask
else
begin
i1 := pos('*', p);
if i1 < 1 then
k := kContains
else if i1 = length(p) then
begin
delete(p, length(p), 1);
k := kStarts;
end
else if i1 = 1 then
begin
i2 := 2;
i2 := PosEx('*', p, i2);
if i2 < 1 then
begin
delete(p, 1, 1);
k := kEnds;
end
else if i2 = length(p) then
begin
p := copy(p, 2, length(p) - 2);
k := kContains;
end
else
k := kMask;
end
else
k := kMask;
end;
end;
function TFilterEx.SplitBySpace(const aText: string): TArray<string>;
var
s, p: string;
i1, i2: integer;
l: TList<string>;
lIsInQuote: boolean;
c: char;
begin
s := aText + ' ';
gc(l, TList<string>.Create);
lIsInQuote := False;
i1 := 1;
i2 := 1; // why not 2? because the first char may contain a '"' char....
while i2 <= length(s) do
begin
c := s[i2];
if lIsInQuote then
begin
if c = '"' then
lIsInQuote := False;
end
else if c = '"' then
lIsInQuote := True
else if c = ' ' then
begin
if i1 <> i2 then
begin
p := copy(s, i1, (i2 - i1));
l.Add(p);
end;
i1 := i2 + 1;
end;
Inc(i2);
end;
Result := l.ToArray;
end;
function TFilterEx.Matches(const aText: string): boolean;
var
lMatchesANy: boolean;
fi: TFilterItem;
s: string;
lText: string;
begin
Result := True;
lText := aText.ToLower;
for fi in fFilter do
begin
lMatchesANy := False;
for var X := 0 to length(fi.OrElements) - 1 do
begin
s := fi.OrElements[X];
case fi.OrElementKinds[X] of
kMask:
lMatchesANy := System.Masks.MatchesMask(lText, s);
kContains:
lMatchesANy := lText.Contains(s);
kStarts:
lMatchesANy := startsStr(s, lText);
kEnds:
lMatchesANy := EndsStr(s, lText);
end;
if lMatchesANy then
break;
end;
if fi.IsNegated then
begin
if lMatchesANy then
exit(False);
end
else if not lMatchesANy then
exit(False);
end;
end;
{ other }
function SplitInHalfBy(const aText: string; aDelim: char; out aParts: TArray<string>): boolean;
var
i: integer;
begin
Result := False;
i := pos(aDelim, aText);
if i < 1 then
aParts := [aText]
else
begin
Result := True;
aParts := [
copy(aText, 1, i - 1),
copy(aText, i + 1, length(aText))
];
end;
end;
function SplitInHalfBy(const aText: string; aDelim: char; out alLeft, alRight: string): boolean; overload;
var
i: integer;
begin
Result := False;
i := pos(aDelim, aText);
if i >= 1 then
begin
Result := True;
alLeft := copy(aText, 1, i - 1);
alRight := copy(aText, i + 1, length(aText));
end;
end;
function Split(delimiter: char; const line: string): TArray<string>;
begin
Split(line, delimiter, Result);
end;
procedure Split(const line: string; delimiter: char; out STRINGS: TArray<string>);
var
l: TStringList;
begin
gc(l, TStringList.Create);
l.StrictDelimiter := True;
l.delimiter := delimiter;
l.delimitedText := line;
STRINGS := l.ToStringArray;
end;
Function splitstring(Const aText: String; aDelim: char; aPerformTrimOnParts: Boolean = False): TArray<String>;
begin
Result:= Split(aDelim, aText);
if aPerformTrimOnParts then
for var x := 0 to length(Result) -1 do
Result[x]:= Result[x].Trim;
end;
procedure Split(const line: string; delimiter: char; STRINGS: TStringList);
var
l: TStringList;
begin
gc(l, TStringList.Create);
l.StrictDelimiter := True;
l.delimiter := delimiter;
l.delimitedText := line;
STRINGS.Clear;
STRINGS.AddStrings(l);
end;
function fStr(const d: double; vs: integer = 2; ns: integer = 2): string;
var
s: string;
begin
s := '0.' + PutBefore('0', '0', ns);
Result := FormatFloat(s, d);
end;
function GuidToHex(const aGuid: TGuid): string;
begin
SetLength(Result, SizeOf(TGuid) * 2);
BinToHex(@aGuid, PChar(Result), SizeOf(TGuid));
end;
function Join(const aSeparator: string; aValues: TStringList): string; overload;
begin
Result:= String.Join(aSeparator, aValues.ToStringArray);
end;
function Join(const aSeparator: string; const aValues: TArray<integer>): string;
var
lValues: TArray<string>;
begin
SetLength(lValues, length(aValues));
for var X := 0 to length(aValues) - 1 do
lValues[X] := aValues[X].ToString;
Result := string.Join(aSeparator, lValues);
end;
function Utf8SequenceLength(const aLead: Byte): Integer; inline;
begin
if (aLead and $80) = 0 then
Exit(1);
if (aLead and $E0) = $C0 then
Exit(2);
if (aLead and $F0) = $E0 then
Exit(3);
if (aLead and $F8) = $F0 then
Exit(4);
Result := 1;
end;
function IsUtf8ContinuationByte(const aByte: Byte): Boolean; inline;
begin
Result := (aByte and $C0) = $80;
end;
function Utf8TruncateByCodePoint(const AInput: string; aMaxBytesLength: integer): TBytes;
var
src: TBytes;