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 <[email protected]> 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 | +
