Get which capture group matched a result using Delphi's TRegex - regex

I've written a regex whose job is to return all matches to its three alternative capture groups. My goal is to learn which capture group produced each match. PCRE seems able to produce that information. But I haven't yet been able coerce the TRegEx class in Delphi XE8 to yield meaningful capture group info for matches. I can't claim to be at the head of regex class, and TRegEx is new to me, so who knows what errors I'm making.
The regex (regex101.com workpad) is:
(?'word'\b[a-zA-Z]{3,}\b)|(?'id'\b\d{1,3}\b)|(?'course'\b[BL]\d{3}\b)
This test text:
externship L763 clinic 207 B706 b512
gives five matches in test environments. But a simple test program that walks the TGroupCollection of each TMatch in the TMatchCollection shows bizarre results about groups: all matches have more than one group (2, 3 or 4) with each group's Success true, and often the matched text is duplicated in several groups or is empty. So this data structure (below) isn't what I'd expect:
Using TRegEx
Regex: (?'word'\b[a-zA-Z]{3,}\b)|(?'id'\b\d{1,3}\b)|(?'course'\b[BL]\d{3}\b)
Text: externship L763 clinic 207 B706 b512
5 matches
'externship' with 2 groups:
length 10 at 1 value 'externship' (Sucess? True)
length 10 at 1 value 'externship' (Sucess? True)
'L763' with 4 groups:
length 4 at 12 value 'L763' (Sucess? True)
length 0 at 1 value '' (Sucess? True)
length 0 at 1 value '' (Sucess? True)
length 4 at 12 value 'L763' (Sucess? True)
'clinic' with 2 groups:
length 6 at 17 value 'clinic' (Sucess? True)
length 6 at 17 value 'clinic' (Sucess? True)
'207' with 3 groups:
length 3 at 24 value '207' (Sucess? True)
length 0 at 1 value '' (Sucess? True)
length 3 at 24 value '207' (Sucess? True)
'B706' with 4 groups:
length 4 at 28 value 'B706' (Sucess? True)
length 0 at 1 value '' (Sucess? True)
length 0 at 1 value '' (Sucess? True)
length 4 at 28 value 'B706' (Sucess? True)
My simple test runner is this:
program regex_tester;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,
System.RegularExpressions,
System.RegularExpressionsCore;
var
Matched : Boolean;
J : integer;
Group : TGroup;
Match : TMatch;
Matches : TMatchCollection;
RegexText,
TestText : String;
RX : TRegEx;
RXPerl : TPerlRegEx;
begin
try
RegexText:='(?''word''\b[a-zA-Z]{3,}\b)|(?''id''\b\d{1,3}\b)|(?''course''\b[BL]\d{3}\b)';
TestText:='externship L763 clinic 207 B706 b512';
RX:=TRegex.Create(RegexText);
Matches:=RX.Matches(TestText);
Writeln(Format(#10#13#10#13'Using TRegEx'#10#13'Regex: %s'#10#13'Text: %s'#10#13,[RegexText, TestText]));
Writeln(Format('%d matches', [Matches.Count]));
for Match in Matches do
begin
Writeln(Format(' ''%s'' with %d groups:', [Match.Value,Match.Groups.Count]));
for Group in Match.Groups do
Writeln(Format(#9'length %d at %d value ''%s'' (Sucess? %s)', [Group.Length,Group.Index,Group.Value,BoolToStr(Group.Success, True)]));
end;
RXPerl:=TPerlRegEx.Create;
RXPerl.Subject:=TestText;
RXPerl.RegEx:=RegexText;
Writeln(Format(#10#13#10#13'Using TPerlRegEx'#10#13'Regex: %s'#10#13'Text: %s'#10#13,[RXPerl.Regex, RXPerl.Subject]));
Matched:=RXPerl.Match;
if Matched then
repeat
begin
Writeln(Format(' ''%s'' with %d groups:', [RXPerl.MatchedText,RXPerl.GroupCount]));
for J:=1 to RXPerl.GroupCount do
Writeln(Format(#9'length %d at %d, value ''%s''',[RXPerl.GroupLengths[J],RXPerl.GroupOffsets[J],RXPerl.Groups[J]]));
Matched:=RXPerl.MatchAgain;
end;
until Matched=false;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
I'd certainly appreciate a nudge in the right direction. If TRegEx is broken, I can of course use an alternative -- or I can let go of the perceived elegance of the solution, instead using three simpler tests to find the bits of info I need.
Added Information and Interpretation
As #andrei-galatyn notes, TRegEx uses TPerlRegEx for its work. So I added a section to my testing program (output below) where I experiment with that, too. It isn't as convenient to use as TRegEx, but its result is what it should be -- and without the problems of TRegEx's broken TGroup data structures. Whichever class I use, the last group's index (less 1 for TRegEx) is the capturing group I want.
Along the way I was reminded that Pascal arrays are often based on 1 rather than 0.
Using TPerlRegEx
Regex: (?'word'\b[a-zA-Z]{3,}\b)|(?'id'\b\d{1,3}\b)|(?'course'\b[BL]\d{3}\b)
Text: externship L763 clinic 207 B706 b512
'externship' with 1 groups:
length 10 at 1, value 'externship'
'L763' with 3 groups:
length 0 at 1, value ''
length 0 at 1, value ''
length 4 at 12, value 'L763'
'clinic' with 1 groups:
length 6 at 17, value 'clinic'
'207' with 2 groups:
length 0 at 1, value ''
length 3 at 24, value '207'
'B706' with 3 groups:
length 0 at 1, value ''
length 0 at 1, value ''
length 4 at 28, value 'B706'

Internally Delphi uses class TPerlRegEx and it has such description for GroupCount property:
Number of matched groups stored in the Groups array. This number is the number of the highest-numbered capturing group in your regular expression that actually participated in the last match. It may be less than the number of capturing groups in your regular expression.
E.g. when the regex "(a)|(b)" matches "a", GroupCount will be 1. When the same regex matches "b", GroupCount will be 2.
TRegEx class always adds one more group (for whole expression i guess).
In your case it should be enough to check every match like this:
case Match.Groups.Count-1 of
1: ; // "word" found
2: ; // "id" found
3: ; // "course" found
end;
It doesn't answer why Groups are filled with strange data, indeed it seems to be enough to answer your question. :)

I realize this is 4 years too late, but for others who may search for this, here's what I found (in Delphi RIO at least, I have not tested earlier versions, however the source URL below says as of XE), however:
From https://www.regular-expressions.info/delphi.html (important bit is bold italic)
The TMatch record provides several properties with details about the
match. Success indicates if a match was found. If this is False, all
other properties and methods are invalid. Value returns the matched
string. Index and Length indicate the position in the input string and
the length of the match. Groups returns a TGroupCollection record that
stores a TGroup record in its default Item[] property for each
capturing group. You can use a numeric index to Item[] for numbered
capturing groups, and a string index for named capturing groups.
So, if you NAME your matching group like this for example:
(?'MatchName'[^\/\?]*)
Which would match a string up until a / or ? character, you can then reference that capture group by name like this:
Matches := TRegEx.Matches(StrToMatch, RegexString, [ roExplicitCapture ] );
for Match in Matches do begin
// ... maybe some logic here to determine if this is the match you want ...
try
whatGotMatched := Match.Groups['MatchName'].Value;
except
whatGotMatched := '';
end;
end;
How exactly you get to your match groups in the Matches is dependent on how you structure your RegEx etc, how many matches you create, etc. I surround the Match.Groups['MatchName'].Value in a try-except block because if the match wasn't found, you will generate an index out of bounds error when accessing .Value since it will still say the match would have been at character X, for 0 characters. You could also check Match.Groups['MatchName'].Length for 0 before trying to access .Value to probably better results...but if you reference it by name, you don't have to decipher specifically what groups matched what, etc ... just ask for the named match you wanted, and if it matched, you will get it.
Edit: they reference to .Length to help verify before throwing an error about index out of bounds. However, if your search is constructed where a named search is not matched, and therefore not returned at all, you will get an error thrown for an invalid match name, so you may still may need to capture for that.

Related

Can I use regular expressions, the Like operator, and/or Instr() find the index of a pattern within a larger string?

I have a large list (a table with one field) of non-standardized strings, imported from a poorly managed legacy database. I need to extract the single-digit number (surrounded by spaces) that occurs exactly once in each of those strings (though the strings have other multi-digit numbers sometimes too). For example, from the following string:
"Quality Assurance File System And Records Retention Johnson, R.M. 004 4 2999 ss/ds/free ReviewMo = Aug Effective 1/31/2012 FileOpen-?"
I would want to pull the number 4 (or 4's position in the string, i.e. 71)
I can use
WHERE rsLegacyList.F1 LIKE "* # *"
inside a select statement to find if each string has a lone digit, and thereby filter my list. But it doesn't tell me where the digit is so I can extract the digit itself (with mid() function) and start sorting the list. The goal is to create a second field with that digit by itself as a method of sorting the larger strings in the first field.
Is there a way to use Instr() along with regular expressions to find where a regular expression occurs within a larger string? Something like
intMarkerLocation = instr(rsLegacyList.F1, Like "* # *")
but that actually works?
I appreciate any suggestions, or workarounds that avoid the problem entirely.
#Lee Mac, I made a function RegExFindStringIndex as shown here:
Public Function RegExFindStringIndex(strToSearch As String, strPatternToMatch As String) As Integer
Dim regex As RegExp
Dim Matching As Match
Set regex = New RegExp
With regex
.MultiLine = False
.Global = True
.IgnoreCase = False
.Pattern = strPatternToMatch
Matching = .Execute(strToSearch)
RegExFindStringIndex = Matching.FirstIndex
End With
Set regex = Nothing
Set Matching = Nothing
End Function
But it gives me an error Invalid use of property at line Matching = .Execute(strToSearch)
Using Regular Expressions
If you were to use Regular Expressions, you would need to define a VBA function to instantiate a RegExp object, set the pattern property to something like \s\d\s (whitespace-digit-whitespace) and then invoke the Execute method to obtain a match (or matches), each of which will provide an index of the pattern within the string. If you want to pursue this route, here are some existing examples for Excel, but the RegExp manipulation will be identical in MS Access.
Here is an example function demonstrating how to use the first result returned by the Execute method:
Public Function RegexInStr(strStr As String, strPat As String) As Integer
With New RegExp
.Multiline = False
.Global = True
.IgnoreCase = False
.Pattern = strPat
With .Execute(strStr)
If .Count > 0 Then RegexInStr = .Item(0).FirstIndex + 1
End With
End With
End Function
Note that the above uses early binding and so you will need to add a reference to the Microsoft VBScript Regular Expressions 5.5 library to your project.
Example Immediate Window evaluation:
?InStr("abc 1 123", " 1 ")
4
?RegexInStr("abc 1 123", "\s\w\s")
4
Using InStr
An alternative using the in-built instr function within a query might be the following inelegant (and probably very slow) query:
select
switch
(
instr(rsLegacyList.F1," 0 ")>0,instr(rsLegacyList.F1," 0 ")+1,
instr(rsLegacyList.F1," 1 ")>0,instr(rsLegacyList.F1," 1 ")+1,
instr(rsLegacyList.F1," 2 ")>0,instr(rsLegacyList.F1," 2 ")+1,
instr(rsLegacyList.F1," 3 ")>0,instr(rsLegacyList.F1," 3 ")+1,
instr(rsLegacyList.F1," 4 ")>0,instr(rsLegacyList.F1," 4 ")+1,
instr(rsLegacyList.F1," 5 ")>0,instr(rsLegacyList.F1," 5 ")+1,
instr(rsLegacyList.F1," 6 ")>0,instr(rsLegacyList.F1," 6 ")+1,
instr(rsLegacyList.F1," 7 ")>0,instr(rsLegacyList.F1," 7 ")+1,
instr(rsLegacyList.F1," 8 ")>0,instr(rsLegacyList.F1," 8 ")+1,
instr(rsLegacyList.F1," 9 ")>0,instr(rsLegacyList.F1," 9 ")+1,
true, null
) as intMarkerLocation
from
rsLegacyList
where
rsLegacyList.F1 like "* # *"
How about:
select
instr(rsLegacyList.F1, " # ") + 1 as position
from rsLegacyList.F1
where rsLegacyList.F1 LIKE "* # *"

RegEx for matching group in multiline texts

I have this multi-line text, I want to extract the numerical value before the 'Next' text (in this case 13). The numerical values will change, but the location will stay the same, it indicates total # of pages on website. I am having trouble writing the correct regex to return this value:
Previous
1
2
3
...
13
Next
Showing 1 - 100 of 1227 Results[EXTRACT]
pattern =re.compile(r'(\d{1,2})\r\nNext', re.M)
result = pattern.match(text)
The expected return value is 13.
import re
t = """Previous
1
2
3
...
13
Next
Showing 1 - 100 of 1227 Results[EXTRACT]"""
re.search(r"\d+(?=\s+Next)", t).group(0)
Returns: '13'
The regular expression does a lookahead assertion to see if there is any amount (>1) of digits followed by any amount (>1) of whitespace characters followed by the word Next.

Regular Expression for parsing a sports score

I'm trying to validate that a form field contains a valid score for a volleyball match. Here's what I have, and I think it works, but I'm not an expert on regular expressions, by any means:
r'^ *([0-9]{1,2} *- *[0-9]{1,2})((( *[,;] *)|([,;] *)|( *[,;])|[,;]| +)[0-9]{1,2} *- *[0-9]{1,2})* *$'
I'm using python/django, not that it really matters for the regex match. I'm also trying to learn regular expressions, so a more optimal regex would be useful/helpful.
Here are rules for the score:
1. There can be one or more valid set (set=game) results included
2. Each result must be of the form dd-dd, where 0 <= dd <= 99
3. Each additional result must be separated by any of [ ,;]
4. Allow any number of sets >=1 to be included
5. Spaces should be allowed anywhere except in the middle of a number
So, the following are all valid:
25-10 or 25 -0 or 25- 9 or 23 - 25 (could be one or more spaces)
25-10,25-15 or 25-10 ; 25-15 or 25-10 25-15 (again, spaces allowed)
25-1 2 -25, 25- 3 ;4 - 25 15-10
Also, I need each result as a separate unit for parsing. So in the last example above, I need to be able to separately work on:
25-1
2 -25
25- 3
4 - 25
15-10
It'd be great if I could strip the spaces from within each result. I can't just strip all spaces, because a space is a valid separator between result sets.
I think this is solution for your problem.
str.replace(r"(\d{1,2})\s*-\s*(\d{1,2})", "$1-$2")
How it works:
(\d{1,2}) capture group of 1 or 2 numbers.
\s* find 0 or more whitespace.
- find -.
$1 replace content with content of capture group 1
$2 replace content with content of capture group 2
you can also look at this.

Separating column using separate (tidyr) via dplyr on a first encountered digit

I'm trying to separate a rather messy column into two columns containing period and description. My data resembles the extract below:
set.seed(1)
dta <- data.frame(indicator=c("someindicator2001", "someindicator2011",
"some text 20022008", "another indicator 2003"),
values = runif(n = 4))
Desired results
Desired results should look like that:
indicator period values
1 someindicator 2001 0.2655087
2 someindicator 2011 0.3721239
3 some text 20022008 0.5728534
4 another indicator 2003 0.9082078
Characteristics
Indicator descriptions are in one column
Numeric values (counting from first digit with the first digit are in the second column)
Code
require(dplyr); require(tidyr); require(magrittr)
dta %<>%
separate(col = indicator, into = c("indicator", "period"),
sep = "^[^\\d]*(2+)", remove = TRUE)
Naturally this does not work:
> head(dta, 2)
indicator period values
1 001 0.2655087
2 011 0.3721239
Other attempts
I have also tried the default separation method sep = "[^[:alnum:]]" but it breaks down the column into too many columns as it appears to be matching all of the available digits.
The sep = "2*" also doesn't work as there are too many 2s at times (example: 20032006).
What I'm trying to do boils down to:
Identifying the first digit in the string
Separating on that charter. As a matter of fact, I would be happy to preserve that particular character as well.
I think this might do it.
library(tidyr)
separate(dta, indicator, c("indicator", "period"), "(?<=[a-z]) ?(?=[0-9])")
# indicator period values
# 1 someindicator 2001 0.2655087
# 2 someindicator 2011 0.3721239
# 3 some text 20022008 0.5728534
# 4 another indicator 2003 0.9082078
The following is an explanation of the regular expression, brought to you by regex101.
(?<=[a-z]) is a positive lookbehind - it asserts that [a-z] (match a single character present in the range between a and z (case sensitive)) can be matched
? matches the space character in front of it literally, between zero and one time, as many times as possible, giving back as needed
(?=[0-9]) is a positive lookahead - it asserts that [0-9] (match a single character present in the range between 0 and 9) can be matched
You could also use unglue::unnest() :
dta <- data.frame(indicator=c("someindicator2001", "someindicator2011",
"some text 20022008", "another indicator 2003"),
values = runif(n = 4))
# remotes::install_github("moodymudskipper/unglue")
library(unglue)
unglue_unnest(dta, indicator, "{indicator}{=\\s*}{period=\\d*}")
#> values indicator period
#> 1 0.43234262 someindicator 2001
#> 2 0.65890900 someindicator 2011
#> 3 0.93576805 some text 20022008
#> 4 0.01934736 another indicator 2003
Created on 2019-09-14 by the reprex package (v0.3.0)

Regex Optional Groups Captures with Values

I'm having difficulties extracting irregular data using Regex. I attempted to use Lookheads however when the value doesn't exist the entire match returns false. The data set is consistent all the way until I reach the characters starting with RXX. The RXX are unique identifiers (groups) and the numeric values in between each set of Rxx's is what I would like to capture and assigned them to group names.
The Rxx values are random from R01 to R15 and 1 to all 15 could exist in the string.
The string values could vary from
12*000000000**S304JB01811*8*0*8*4*4*34R0332R152~~~
12*000000000**S304JB01811*9*0*4*3*4*224R023R032R10234R1325~~~
I'm able to extract the values and assign a group name until I reach the Rxx
My attempt are extracting the values are as follow
S304JB0...(?<Total1>[\d]+).(?<Total2>[\d]+).(?<Total3>[\d]+).(?<Total4>[\d]+).(?<Total5>[\d]+).(?<Total6>[\d]+).(?<Total7>[\d]+)
Which gives me what I want below
Total1 `1`
Total2 `8`
Total3 `0`
Total4 `8`
Total5 `4`
Total6 `4`
Total7 `34`
Capturing the R03 value and assigning it to Row is achieved below but if the value R03 doesn't exist in the string then the entire match returns false
(?<Row3>(R03)[\d]+)
Looking how I can make these regex statements optional allowing me to return the following
Total1 `1`
Total2 `8`
Total3 `0`
Total4 `8`
Total5 `4`
Total6 `4`
Total7 `34`
Row1 `32`
Row15 `2`
S304JB0...(?<Total1>[\d]+).(?<Total2>[\d]+).(?<Total3>[\d]+).(?<Total4>[\d]+).(?<Total5>[\d]+).(?<Total6>[\d]+).(?<Total7>[\d]+)(?<Row3>(R03)[\d]+)(?<Row4>(R04)[\d]+) ------> (?<Row15>(R15)[\d]+)
Thanks for your help
-Edited
Thanks for the quick reply Jorge
The input data will be
12*000000000**S304JB01811*8*0*8*4*4*34R0332R152~~~
The output will be 9 captured groups results
Group | Result
Total1 = 1
Total2 = 8
Total3 = 0
Total4 = 8
Total5 = 4
Total6 = 4
Total7 = 34
Row1 = 32
Row15 = 2
My example is shared below with input and
https://regex101.com/r/wG3aM3/68
Hopefully this helped to clarify things
D.
I'm certain this would be easier parsing char by char and storing each value.
As for the regex question, basically what you want to do is create all the groups, just like you've already tried, but you also want to make them optional, because not all groups might be there.
You can make the group optional with a construct like:
(?:R01(?<Row1>\d+))?
So you should add one of each to get the values in different capture groups. Notice I used the construct (?:non-capturing) which is exactly the same as a group, but it doesn't create a backreference. You can read about it here.
Edit: One more thing. You're using a . to allow any delimiter. However, performance-wise it would be better to use something like \D (anything except digits). In case of failure, it saves the regex engine quite a few backtracking steps.
This would be the whole expression, assuming the Rxx groups are always ordered.
S304JB0...(?<Total1>\d+)\D(?<Total2>\d+)\D(?<Total3>\d+)\D(?<Total4>\d+)\D(?<Total5>\d+)\D(?<Total6>\d+)\D(?<Total7>\d+)(?:R01(?<Row1>\d+))?(?:R02(?<Row2>\d+))?(?:R03(?<Row3>\d+))?(?:R04(?<Row4>\d+))?(?:R05(?<Row5>\d+))?(?:R06(?<Row6>\d+))?(?:R07(?<Row7>\d+))?(?:R08(?<Row8>\d+))?(?:R09(?<Row9>\d+))?(?:R10(?<Row10>\d+))?(?:R11(?<Row11>\d+))?(?:R12(?<Row12>\d+))?(?:R13(?<Row13>\d+))?(?:R14(?<Row14>\d+))?(?:R15(?<Row15>\d+))?
DEMO