Skip to content

Commit d4cde3a

Browse files
[MOD] XQuery, fn:matching-segments: named capture groups. qtspecs#2660
1 parent fb31a01 commit d4cde3a

9 files changed

Lines changed: 200 additions & 89 deletions

File tree

basex-core/src/main/java/org/basex/query/func/Records.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public enum Records {
6464
MATCHING_SEGMENT(FN_URI, "matching-segment",
6565
field("substring", Types.STRING_O),
6666
field("position", Types.INTEGER_O),
67-
field("groups", MapType.get(BasicType.INTEGER, MATCHING_GROUP.get().seqType()).seqType())),
67+
field("groups", MapType.get(ChoiceItemType.get(BasicType.INTEGER, BasicType.STRING),
68+
MATCHING_GROUP.get().seqType()).seqType())),
6869
/** Record definition. */
6970
MEMBER(ARRAY_URI, "member",
7071
field("value", Types.ITEM_ZM)),

basex-core/src/main/java/org/basex/query/func/fn/FnMatchingSegments.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import org.basex.query.*;
66
import org.basex.query.func.*;
7+
import org.basex.query.util.regex.*;
78
import org.basex.query.value.*;
89
import org.basex.query.value.item.*;
910
import org.basex.query.value.map.*;
@@ -21,15 +22,21 @@ public Value value(final QueryContext qc) throws QueryException {
2122
final byte[] pattern = toToken(arg(1), qc);
2223
final byte[] flags = toZeroToken(arg(2), qc);
2324

24-
final Matcher matcher = pattern(pattern, flags, qc).matcher(value);
25+
final RegExpr regExpr = regExpr(pattern, flags, qc);
26+
final String[] names = regExpr.getGroupNames();
27+
final Matcher matcher = regExpr.pattern.matcher(value);
2528
final ValueBuilder vb = new ValueBuilder(qc);
2629
while(matcher.find()) {
2730
final MapBuilder groups = new MapBuilder();
2831
final int gc = matcher.groupCount();
2932
for(int g = 1; g <= gc; g++) {
3033
final int s = matcher.start(g);
31-
if(s >= 0) groups.put(Itr.get(g), new XQRecordMap(Records.MATCHING_GROUP.get(),
32-
Str.get(matcher.group(g)), Itr.get(s + 1)));
34+
if(s >= 0) {
35+
final XQRecordMap group = new XQRecordMap(Records.MATCHING_GROUP.get(),
36+
Str.get(matcher.group(g)), Itr.get(s + 1));
37+
final String name = g <= names.length ? names[g - 1] : null;
38+
groups.put(name != null ? Str.get(name) : Itr.get(g), group);
39+
}
3340
}
3441
vb.add(new XQRecordMap(Records.MATCHING_SEGMENT.get(),
3542
Str.get(matcher.group()), Itr.get(matcher.start() + 1), groups.map()));

basex-core/src/main/java/org/basex/query/util/regex/Group.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public final class Group extends RegExp {
1111
private final RegExp encl;
1212
/** Capture flag. */
1313
private final boolean capture;
14+
/** Group name (can be {@code null}). */
15+
private final String name;
1416
/** Back-reference flag. */
1517
private boolean hasBackRef;
1618
/** Atom path of this group: sequence numbers of ancestor branches and atoms. */
@@ -23,8 +25,21 @@ public final class Group extends RegExp {
2325
* @param atomPath atom path of this group
2426
*/
2527
public Group(final RegExp encl, final boolean capture, final Integer[] atomPath) {
28+
this(encl, capture, null, atomPath);
29+
}
30+
31+
/**
32+
* Constructor.
33+
* @param encl enclosed expression
34+
* @param capture capture flag
35+
* @param name group name (can be {@code null})
36+
* @param atomPath atom path of this group
37+
*/
38+
public Group(final RegExp encl, final boolean capture, final String name,
39+
final Integer[] atomPath) {
2640
this.encl = encl;
2741
this.capture = capture;
42+
this.name = name;
2843
hasBackRef = false;
2944
this.atomPath = atomPath;
3045
}
@@ -62,7 +77,7 @@ public Integer[] getAtomPath() {
6277

6378
@Override
6479
void toRegEx(final StringBuilder sb) {
65-
sb.append(capture ? "(" : "(?:");
80+
sb.append(capture ? name != null ? "(?<" + name + '>' : "(" : "(?:");
6681
encl.toRegEx(sb);
6782
sb.append(')');
6883
}

basex-core/src/main/java/org/basex/query/util/regex/RegExpr.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ public boolean[] getAssertionFlags() {
4242
return groupInfo.assertionFlags;
4343
}
4444

45+
/**
46+
* Returns the names of capturing groups.
47+
* @return names: element i contains the name of capturing group i+1, or {@code null}.
48+
*/
49+
public String[] getGroupNames() {
50+
if(groupInfo == null) groupInfo = GroupScanner.groupInfo(pattern.pattern());
51+
return groupInfo.groupNames;
52+
}
53+
4554
/**
4655
* Information about capturing groups.
4756
*/
@@ -50,15 +59,19 @@ public static class GroupInfo {
5059
public final int[] parentGroups;
5160
/** Assertion status: element i tells whether capturing group i+1 occurs in an assertion. */
5261
public final boolean[] assertionFlags;
62+
/** Group names: element i contains the name of capturing group i+1, or {@code null}. */
63+
public final String[] groupNames;
5364

5465
/**
5566
* Constructor.
5667
* @param parentGroup parent group IDs
5768
* @param assertionFlags inside of assertion indicators
69+
* @param groupNames group names
5870
*/
59-
GroupInfo(final int[] parentGroup, final boolean[] assertionFlags) {
71+
GroupInfo(final int[] parentGroup, final boolean[] assertionFlags, final String[] groupNames) {
6072
parentGroups = parentGroup;
6173
this.assertionFlags = assertionFlags;
74+
this.groupNames = groupNames;
6275
}
6376
}
6477

@@ -74,6 +87,8 @@ public static final class GroupScanner {
7487
private int pos;
7588
/** Length of most recent code point. */
7689
private int chrCount;
90+
/** Name of the most recently scanned named capturing group ({@code null} otherwise). */
91+
private String name;
7792

7893
/**
7994
* Constructor.
@@ -97,13 +112,14 @@ public static GroupInfo groupInfo(final String pattern) {
97112
open.push(0);
98113
int[] parentGroups = { };
99114
boolean[] inAssertion = { };
115+
String[] groupNames = { };
100116
boolean quoted = false;
101117
int classLevel = 0;
102118
int assrtMark = 0;
103119
for(;;) {
104120
switch(gnd.nxtToken()) {
105121
case EOP -> {
106-
return new GroupInfo(parentGroups, inAssertion);
122+
return new GroupInfo(parentGroups, inAssertion, groupNames);
107123
}
108124
case LBRACKET -> {
109125
if(!quoted) ++classLevel;
@@ -123,6 +139,8 @@ public static GroupInfo groupInfo(final String pattern) {
123139
parentGroups[parentGroups.length - 1] = open.peek();
124140
inAssertion = Arrays.copyOf(inAssertion, inAssertion.length + 1);
125141
inAssertion[inAssertion.length - 1] = assrtMark != 0;
142+
groupNames = Arrays.copyOf(groupNames, groupNames.length + 1);
143+
groupNames[groupNames.length - 1] = gnd.name;
126144
open.push(parentGroups.length);
127145
}
128146
}
@@ -151,6 +169,7 @@ public static GroupInfo groupInfo(final String pattern) {
151169
* @return next token
152170
*/
153171
private Token nxtToken() {
172+
name = null;
154173
return switch(nxtCp()) {
155174
case -1 -> Token.EOP;
156175
case ']' -> Token.RBRACKET;
@@ -165,13 +184,15 @@ private Token nxtToken() {
165184
case '(' -> switch(nxtCp()) {
166185
case '?' -> switch(nxtCp()) {
167186
case '=', '!' -> Token.ASSRT_LPAREN;
168-
case '<' -> switch(nxtCp()) {
169-
case '=', '!' -> Token.ASSRT_LPAREN;
170-
default -> {
171-
reset();
172-
yield Token.CAPT_LPAREN;
173-
}
174-
};
187+
case '<' -> {
188+
final int c = nxtCp();
189+
if(c == '=' || c == '!') yield Token.ASSRT_LPAREN;
190+
// named capturing group (?<name>...): record the name
191+
final StringBuilder nm = new StringBuilder();
192+
for(int n = c; n != '>' && n != -1; n = nxtCp()) nm.append((char) n);
193+
name = nm.toString();
194+
yield Token.CAPT_LPAREN;
195+
}
175196
default -> {
176197
reset();
177198
yield Token.LPAREN;

basex-core/src/main/java/org/basex/query/util/regex/parse/RegExLexer.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,23 @@ private int normal() {
117117
case '=': return POS_LOOKAHEAD;
118118
case '!': return NEG_LOOKAHEAD;
119119
case '<':
120-
switch(next(skipCmt)) {
120+
final int la = next(skipCmt);
121+
switch(la) {
121122
case '=': return POS_LOOKBEHIND;
122123
case '!': return NEG_LOOKBEHIND;
124+
default:
125+
// named capturing group: (?<name>...)
126+
if(la >= 'a' && la <= 'z' || la >= 'A' && la <= 'Z') {
127+
final TokenBuilder name = new TokenBuilder().add(la);
128+
for(int nc = next(skipCmt);; nc = next(skipCmt)) {
129+
if(nc == '>') break;
130+
if(nc >= 'a' && nc <= 'z' || nc >= 'A' && nc <= 'Z' ||
131+
nc >= '0' && nc <= '9') name.add(nc);
132+
else throw error("(?<" + name + ">", nc);
133+
}
134+
payload = name.toString();
135+
return NAMED_PAR_OPEN;
136+
}
123137
}
124138
}
125139
break;

0 commit comments

Comments
 (0)