You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by kr...@apache.org on 2022/01/07 21:57:50 UTC
[tinkerpop] 01/01: Add Text.Regex text predicate
This is an automated email from the ASF dual-hosted git repository.
krlawrence pushed a commit to branch TINKERPOP-2652
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 70e8245821a132b977f8f19c415942f2a9fa4d14
Author: Kelvin Lawrence <gf...@yahoo.com>
AuthorDate: Thu Nov 18 13:47:40 2021 -0600
Add Text.Regex text predicate
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/the-traversal.asciidoc | 9 ++++-
.../language/grammar/GremlinBaseVisitor.java | 10 +++++
.../grammar/TraversalPredicateVisitor.java | 10 +++++
.../tinkerpop/gremlin/process/traversal/Text.java | 44 ++++++++++++++++++++++
.../tinkerpop/gremlin/process/traversal/TextP.java | 18 +++++++++
.../grammar/TraversalPredicateVisitorTest.java | 3 ++
.../tinkerpop/gremlin/process/traversal/PTest.java | 14 +++++++
.../src/Gremlin.Net/Process/Traversal/TextP.cs | 9 +++++
.../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs | 2 +
.../gremlin-javascript/lib/process/traversal.js | 9 +++++
.../gremlin-javascript/test/cucumber/gremlin.js | 2 +
gremlin-language/src/main/antlr4/Gremlin.g4 | 10 +++++
.../python/gremlin_python/process/traversal.py | 17 +++++++++
gremlin-python/src/main/python/radish/gremlin.py | 2 +
gremlin-test/features/filter/Has.feature | 30 ++++++++++++++-
16 files changed, 187 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index d919c0d..9aee9d8 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -23,6 +23,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
[[release-3-6-0]]
=== TinkerPop 3.6.0 (Release Date: NOT OFFICIALLY RELEASED YET)
+* Added `TextP.regex` and `TextP.notRegex`.
* Changed TinkerGraph to allow identifiers to be heterogeneous when filtering.
* Prevented values of `T` to `property()` from being `null`.
* Added `fail()` step.
diff --git a/docs/src/reference/the-traversal.asciidoc b/docs/src/reference/the-traversal.asciidoc
index 98f2e8d..7dd001b 100644
--- a/docs/src/reference/the-traversal.asciidoc
+++ b/docs/src/reference/the-traversal.asciidoc
@@ -4077,7 +4077,8 @@ A `P` is a predicate of the form `Function<Object,Boolean>`. That is, given some
the release of TinkerPop 3.4.0, Gremlin also supports simple text predicates, which only work on `String` values. The `TextP`
text predicates extend the `P` predicates, but are specialized in that they are of the form `Function<String,Boolean>`.
The provided predicates are outlined in the table below and are used in various steps such as <<has-step,`has()`>>-step,
-<<where-step,`where()`>>-step, <<is-step,`is()`>>-step, etc.
+<<where-step,`where()`>>-step, <<is-step,`is()`>>-step, etc. Two new additional `TextP` predicate members were added in the
+TinkerPop 3.6.0 release that allow working with regular expressions. These are `TextP.regex` and `TextP.notRegex`
[width="100%",cols="3,15",options="header"]
|=========================================================
@@ -4099,8 +4100,12 @@ The provided predicates are outlined in the table below and are used in various
| `TextP.notStartingWith(string)` | Does the incoming `String` not start with the provided `String`?
| `TextP.notEndingWith(string)` | Does the incoming `String` not end with the provided `String`?
| `TextP.notContaining(string)` | Does the incoming `String` not contain the provided `String`?
+| `TextP.regex(string)` | Does the incoming `String` match the regular expression in the provided `String`?
+| `TextP.notRegex(string)` | Does the incoming `String` fail to match the regular expression in the provided `String`?
|=========================================================
-
+Note that the TinkerPop reference implementation uses the Java `Pattern` and `Matcher` classes for it regular expression
+engine. Other implementations may decide to use a different regular expression engine. It's a good idea to check
+the documentation for the implementation you are using to verify the allowed regular expression syntax.
[gremlin-groovy]
----
eq(2)
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java
index fd48b79..b25418b 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/GremlinBaseVisitor.java
@@ -1050,6 +1050,16 @@ public class GremlinBaseVisitor<T> extends AbstractParseTreeVisitor<T> implement
public T visitTraversalPredicate_notEndingWith(final GremlinParser.TraversalPredicate_notEndingWithContext ctx) {
notImplemented(ctx); return null;
}
+
+ @Override
+ public T visitTraversalPredicate_regex(final GremlinParser.TraversalPredicate_regexContext ctx) {
+ notImplemented(ctx); return null;
+ }
+
+ @Override
+ public T visitTraversalPredicate_notRegex(final GremlinParser.TraversalPredicate_notRegexContext ctx) {
+ notImplemented(ctx); return null;
+ }
/**
* {@inheritDoc}
*/
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitor.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitor.java
index 608c20d..d46fc49 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitor.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitor.java
@@ -264,4 +264,14 @@ public class TraversalPredicateVisitor extends GremlinBaseVisitor<P> {
public P visitTraversalPredicate_notStartingWith(final GremlinParser.TraversalPredicate_notStartingWithContext ctx) {
return TextP.notStartingWith(GenericLiteralVisitor.getStringLiteral(ctx.stringLiteral()));
}
+
+ @Override
+ public P visitTraversalPredicate_regex(final GremlinParser.TraversalPredicate_regexContext ctx) {
+ return TextP.regex(GenericLiteralVisitor.getStringLiteral(ctx.stringLiteral()));
+ }
+
+ @Override
+ public P visitTraversalPredicate_notRegex(final GremlinParser.TraversalPredicate_notRegexContext ctx) {
+ return TextP.notRegex(GenericLiteralVisitor.getStringLiteral(ctx.stringLiteral()));
+ }
}
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Text.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Text.java
index 9c25825..91fcd7d 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Text.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/Text.java
@@ -19,6 +19,8 @@
package org.apache.tinkerpop.gremlin.process.traversal;
import java.util.function.BiPredicate;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
/**
* {@link Text} is a {@link java.util.function.BiPredicate} that determines whether the first string starts with, starts
@@ -30,6 +32,48 @@ import java.util.function.BiPredicate;
public enum Text implements BiPredicate<String, String> {
/**
+ * Evaluates if the first string has a regex match with the second (pattern).
+ *
+ * @since 3.6.0
+ */
+ regex {
+ @Override
+ public boolean test(final String value, final String regex) {
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(value);
+ return matcher.find();
+ }
+
+ /**
+ * The negative of {@code regex} is {@link #notRegex}.
+ */
+ @Override
+ public Text negate() {
+ return notRegex;
+ }
+ },
+ /**
+ * Evaluates if the first string does not have a regex match with the second (pattern).
+ *
+ * @since 3.6.0
+ */
+ notRegex {
+ @Override
+ public boolean test(final String value, final String regex) {
+ Pattern pattern = Pattern.compile(regex);
+ Matcher matcher = pattern.matcher(value);
+ return !matcher.find();
+ }
+
+ /**
+ * The negative of {@code notRegex} is {@link #regex}.
+ */
+ @Override
+ public Text negate() {
+ return regex;
+ }
+ },
+ /**
* Evaluates if the first string starts with the second.
*
* @since 3.4.0
diff --git a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
index 2c28853..5b72521 100644
--- a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
+++ b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
@@ -106,4 +106,22 @@ public class TextP extends P<String> {
public static TextP notContaining(final String value) {
return new TextP(Text.notContaining, value);
}
+
+ /**
+ * Determines if String has a match with the given REGEX pattern.
+ *
+ * @since 3.6.0
+ */
+ public static TextP regex(final String value) {
+ return new TextP(Text.regex, value);
+ }
+
+ /**
+ * Determines if String has no match with the given REGEX pattern.
+ *
+ * @since 3.6.0
+ */
+ public static TextP notRegex(final String value) {
+ return new TextP(Text.notRegex, value);
+ }
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitorTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitorTest.java
index 7254388..4bb7f30 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitorTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/language/grammar/TraversalPredicateVisitorTest.java
@@ -90,6 +90,9 @@ public class TraversalPredicateVisitorTest {
{"TextP.endingWith('hakuna')", TextP.endingWith("hakuna")},
{"TextP.notEndingWith('hakuna')", TextP.notEndingWith("hakuna")},
{"TextP.notStartingWith('hakuna')", TextP.notStartingWith("hakuna")},
+ {"TextP.regex('^h')", TextP.regex("^h")},
+ {"TextP.notRegex('^h')", TextP.notRegex("^h")},
+ {"TextP.regex('^h').negate()", TextP.regex("^h").negate()},
});
}
diff --git a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
index e58e8be..d29e666 100644
--- a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
+++ b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
@@ -158,6 +158,20 @@ public class PTest {
{TextP.containing("o").and(P.gte("j")).and(TextP.endingWith("ko")), "josh", false},
{TextP.containing("o").and(P.gte("j").and(TextP.endingWith("ko"))), "marko", true},
{TextP.containing("o").and(P.gte("j").and(TextP.endingWith("ko"))), "josh", false},
+ {TextP.regex("^D"), "Dallas Fort Worth", true},
+ {TextP.regex("^d"), "Dallas Fort Worth", false},
+ {TextP.regex("^Da"), "Dallas Forth Worth", true},
+ {TextP.regex("^da"), "Dallas Forth Worth", false},
+ {TextP.regex("^x"), "Dallas Fort Worth", false},
+ {TextP.regex("Dal[l|x]as"), "Dallas Fort Worth", true},
+ {TextP.regex("Dal[f|x]as"), "Dallas Fort Worth", false},
+ {TextP.regex("[a-zA-Z]+ Fort"), "Dallas Fort Worth", true},
+ {TextP.regex("[1-9]{3}"), "123-ABC-456", true},
+ {TextP.regex("[1-9]{3}-[A-Z]{3}-[1-9]{3}"), "123-ABC-456", true},
+ {TextP.regex("[1-9]{3}-[a-z]{3}-[1-9]{3}"), "123-ABC-456", false},
+ {TextP.regex("(?i)[1-9]{3}-[a-z]{3}-[1-9]{3}"), "123-ABC-456", true},
+ {TextP.regex("(?i)abc"), "123-ABC-456", true},
+ {TextP.regex("(?i)[a-b]{3}-[1-9]{3}-[a-z]{3}"), "123-ABC-456", false},
}));
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/TextP.cs b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/TextP.cs
index a77f206..a106fb0 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/TextP.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Process/Traversal/TextP.cs
@@ -75,6 +75,15 @@ namespace Gremlin.Net.Process.Traversal
return new TextP("startingWith", value);
}
+ public static TextP Regex(string value)
+ {
+ return new TextP("regex", value);
+ }
+
+ public static TextP NotRegex(string value)
+ {
+ return new TextP("notRegex", value);
+ }
private static T[] ToGenericArray<T>(ICollection<T> collection)
{
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index ea6bf4d..504f786 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -215,6 +215,8 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
{"g_V_hasXname_not_containingXarkXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name",TextP.NotContaining("ark"))}},
{"g_V_hasXname_not_startingWithXmarXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name",TextP.NotStartingWith("mar"))}},
{"g_V_hasXname_not_endingWithXasXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name",TextP.NotEndingWith("as"))}},
+ {"g_V_hasXname_regexXrMarXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name",TextP.Regex("^mar"))}},
+ {"g_V_hasXname_notRegexXrMarXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("name",TextP.NotRegex("^mar"))}},
{"g_V_hasXp_neqXvXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("p",P.Neq("v"))}},
{"g_V_hasXage_gtX18X_andXltX30XXorXgtx35XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("age",P.Gt(18).And(P.Lt(30)).Or(P.Gt(35)))}},
{"g_V_hasXage_gtX18X_andXltX30XXorXltx35XXX", new List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Has("age",P.Gt(18).And(P.Lt(30)).And(P.Lt(35)))}},
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js
index f72c3f9..a7fb9ac 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/lib/process/traversal.js
@@ -427,6 +427,15 @@ class TextP {
return createTextP('startingWith', args);
}
+ /** @param {...Object} args */
+ static regex(...args) {
+ return createTextP('regex', args);
+ }
+
+ /** @param {...Object} args */
+ static notRegex(...args) {
+ return createTextP('notRegex', args);
+ }
}
function createTextP(operator, args) {
diff --git a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index 52bb2c7..3c73a4e 100644
--- a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -203,6 +203,8 @@ const gremlins = {
g_V_hasXname_not_containingXarkXX: [function({g}) { return g.V().has("name",TextP.notContaining("ark")) }],
g_V_hasXname_not_startingWithXmarXX: [function({g}) { return g.V().has("name",TextP.notStartingWith("mar")) }],
g_V_hasXname_not_endingWithXasXX: [function({g}) { return g.V().has("name",TextP.notEndingWith("as")) }],
+ g_V_hasXname_regexXrMarXX: [function({g}) { return g.V().has("name",TextP.regex("^mar")) }],
+ g_V_hasXname_notRegexXrMarXX: [function({g}) { return g.V().has("name",TextP.notRegex("^mar")) }],
g_V_hasXp_neqXvXX: [function({g}) { return g.V().has("p",P.neq("v")) }],
g_V_hasXage_gtX18X_andXltX30XXorXgtx35XXX: [function({g}) { return g.V().has("age",P.gt(18).and(P.lt(30)).or(P.gt(35))) }],
g_V_hasXage_gtX18X_andXltX30XXorXltx35XXX: [function({g}) { return g.V().has("age",P.gt(18).and(P.lt(30)).and(P.lt(35))) }],
diff --git a/gremlin-language/src/main/antlr4/Gremlin.g4 b/gremlin-language/src/main/antlr4/Gremlin.g4
index e285634..941cdd1 100644
--- a/gremlin-language/src/main/antlr4/Gremlin.g4
+++ b/gremlin-language/src/main/antlr4/Gremlin.g4
@@ -905,6 +905,8 @@ traversalPredicate
| traversalPredicate_notEndingWith
| traversalPredicate_containing
| traversalPredicate_notContaining
+ | traversalPredicate_regex
+ | traversalPredicate_notRegex
| traversalPredicate DOT 'and' LPAREN traversalPredicate RPAREN
| traversalPredicate DOT 'or' LPAREN traversalPredicate RPAREN
| traversalPredicate DOT 'negate' LPAREN RPAREN
@@ -1019,6 +1021,14 @@ traversalPredicate_notEndingWith
: ('TextP.notEndingWith' | 'notEndingWith') LPAREN stringLiteral RPAREN
;
+traversalPredicate_regex
+ : ('TextP.regex' | 'regex') LPAREN stringLiteral RPAREN
+ ;
+
+traversalPredicate_notRegex
+ : ('TextP.notRegex' | 'notRegex') LPAREN stringLiteral RPAREN
+ ;
+
traversalTerminalMethod_explain
: 'explain' LPAREN RPAREN
;
diff --git a/gremlin-python/src/main/python/gremlin_python/process/traversal.py b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
index f7bd31d..67ea377 100644
--- a/gremlin-python/src/main/python/gremlin_python/process/traversal.py
+++ b/gremlin-python/src/main/python/gremlin_python/process/traversal.py
@@ -384,6 +384,14 @@ class TextP(P):
def startingWith(*args):
return TextP("startingWith", *args)
+ @staticmethod
+ def regex(*args):
+ return TextP("regex", *args)
+
+ @staticmethod
+ def notRegex(*args):
+ return TextP("notRegex", *args)
+
def __eq__(self, other):
return isinstance(other, self.__class__) and self.operator == other.operator and self.value == other.value and self.other == other.other
@@ -414,6 +422,11 @@ def notStartingWith(*args):
def startingWith(*args):
return TextP.startingWith(*args)
+def regex(*args):
+ return TextP.regex(*args)
+
+def notRegex(*args):
+ return TextP.notRegex(*args)
statics.add_static('containing', containing)
@@ -427,6 +440,10 @@ statics.add_static('notStartingWith', notStartingWith)
statics.add_static('startingWith', startingWith)
+statics.add_static('regex', regex)
+
+statics.add_static('notRegex', notRegex)
+
diff --git a/gremlin-python/src/main/python/radish/gremlin.py b/gremlin-python/src/main/python/radish/gremlin.py
index 850ee7e..0dcd3e4 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -188,6 +188,8 @@ world.gremlins = {
'g_V_hasXname_not_containingXarkXX': [(lambda g:g.V().has('name',TextP.notContaining('ark')))],
'g_V_hasXname_not_startingWithXmarXX': [(lambda g:g.V().has('name',TextP.notStartingWith('mar')))],
'g_V_hasXname_not_endingWithXasXX': [(lambda g:g.V().has('name',TextP.notEndingWith('as')))],
+ 'g_V_hasXname_regexXrMarXX': [(lambda g:g.V().has('name',TextP.regex('^mar')))],
+ 'g_V_hasXname_notRegexXrMarXX': [(lambda g:g.V().has('name',TextP.notRegex('^mar')))],
'g_V_hasXp_neqXvXX': [(lambda g:g.V().has('p',P.neq('v')))],
'g_V_hasXage_gtX18X_andXltX30XXorXgtx35XXX': [(lambda g:g.V().has('age',P.gt(18).and_(P.lt(30)).or_(P.gt(35))))],
'g_V_hasXage_gtX18X_andXltX30XXorXltx35XXX': [(lambda g:g.V().has('age',P.gt(18).and_(P.lt(30)).and_(P.lt(35))))],
diff --git a/gremlin-test/features/filter/Has.feature b/gremlin-test/features/filter/Has.feature
index bbb9ed4..8739556 100644
--- a/gremlin-test/features/filter/Has.feature
+++ b/gremlin-test/features/filter/Has.feature
@@ -514,6 +514,33 @@ Feature: Step - has()
| v[ripple] |
| v[peter] |
+ Scenario: g_V_hasXname_regexXrMarXX
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("name", TextP.regex("^mar"))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[marko] |
+
+ Scenario: g_V_hasXname_notRegexXrMarXX
+ Given the modern graph
+ And the traversal of
+ """
+ g.V().has("name", TextP.notRegex("^mar"))
+ """
+ When iterated to list
+ Then the result should be unordered
+ | result |
+ | v[josh] |
+ | v[vadas] |
+ | v[lop] |
+ | v[ripple] |
+ | v[peter] |
+
+
Scenario: g_V_hasXp_neqXvXX
Given the modern graph
And the traversal of
@@ -675,4 +702,5 @@ Feature: Step - has()
When iterated to list
Then the result should be unordered
| result |
- | josh |
\ No newline at end of file
+ | josh |
+