Skip to content

Commit b207233

Browse files
Support quantified \D and \W in regex patterns (#15355)
`regexp_extract` (and `rlike` / `regexp_replace`) fell back to the CPU whenever the pattern contained a quantified `\D` or `\W` — e.g. `regexp_extract(col, '(\\D+)', 1)` — because the pattern failed to transpile with "Preceding token cannot be quantified". The set of predefined character classes allowed as a repetition base in `CudfRegexTranspiler` was missing `\D` and `\W`, even though it contained the lowercase `\d` and `\w` (and other negation classes like `\H`). Since `\D` and `\W` otherwise transpile into valid patterns, they are fine as a repetition base. Quantified forms (`\D+`, `\W*`, `\D{2,3}`, `(\D+)`, …) now transpile and run on the GPU. Results are unchanged (identical to the CPU); only the execution location changes (GPU instead of CPU fallback). Added a unit test for quantified `\D` and `\W` that transpiles and compares cuDF vs Java for both find and replace. Also added an integration test that exercises `rlike`, `regexp_extract`, and `regexp_replace` for quantified `\D`/`\W`, plus previously-missing coverage for quantified `\d`/`\w`. Signed-off-by: Igor Peshansky <ipeshansky@nvidia.com>
1 parent 8e6bcbf commit b207233

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

integration_tests/src/main/python/regexp_test.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,30 @@ def test_regexp_replace_word():
921921
),
922922
conf=_regexp_conf)
923923

924+
def test_regexp_quantified_digit_word():
925+
# https://github.com/NVIDIA/cudf-spark/issues/15306
926+
gen = mk_str_gen('[a-d]{1,3}[0-9]{1,3}[ _!]{0,2}[a-d]{0,3}') \
927+
.with_special_case('䤫畍킱곂⬡❽ࢅ獰᳌蛫青') \
928+
.with_special_case('a\n2\r\n3')
929+
assert_gpu_and_cpu_are_equal_collect(
930+
lambda spark: unary_op_df(spark, gen).selectExpr(
931+
'rlike(a, "\\\\d+")',
932+
'rlike(a, "\\\\w+")',
933+
'rlike(a, "\\\\D+")',
934+
'rlike(a, "\\\\W+")',
935+
'rlike(a, "[a-d]+\\\\D+[0-9]+")',
936+
'rlike(a, "\\\\D{2,3}")',
937+
'rlike(a, "\\\\W{2,3}")',
938+
'regexp_extract(a, "(\\\\d+)", 1)',
939+
'regexp_extract(a, "(\\\\D+)", 1)',
940+
'regexp_extract(a, "([a-d]+)(\\\\D+)([a-d]+)", 2)',
941+
'regexp_extract(a, "(\\\\W+)", 1)',
942+
'regexp_replace(a, "(\\\\w+)", "#")',
943+
'regexp_replace(a, "(\\\\D+)", "#")',
944+
'regexp_replace(a, "(\\\\W+)", "@")',
945+
),
946+
conf=_regexp_conf)
947+
924948
def test_predefined_character_classes():
925949
gen = mk_str_gen('[a-zA-Z]{0,2}[\r\n!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~]{0,2}[0-9]{0,2}')
926950
assert_gpu_and_cpu_are_equal_collect(

sql-plugin/src/main/scala/com/nvidia/spark/rapids/RegexParser.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -811,7 +811,7 @@ class CudfRegexTranspiler(mode: RegexMode) {
811811
private def getUnsupportedRepetitionBaseOption(e: RegexAST): Option[RegexAST] = {
812812
e match {
813813
case RegexEscaped(ch) => ch match {
814-
case 'd' | 'w' | 's' | 'S' | 'h' | 'H' | 'v' | 'V' => None
814+
case 'd' | 'D' | 'w' | 'W' | 's' | 'S' | 'h' | 'H' | 'v' | 'V' => None
815815
case _ => Some(e)
816816
}
817817
case RegexChar(a) if "$^".contains(a) =>

tests/src/test/scala/com/nvidia/spark/rapids/RegularExpressionTranspilerSuite.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,19 @@ class RegularExpressionTranspilerSuite extends AnyFunSuite {
472472
assertCpuGpuMatchesRegexpReplace(patterns, inputs)
473473
}
474474

475+
test("quantified \\D and \\W") {
476+
// see https://github.com/NVIDIA/cudf-spark/issues/15306
477+
val inputs = Seq("", "abc", "123", "a1b2", "___", "a_b 1", "!!!", "ab12cd",
478+
"a\rb", "1\r2", "1\n2", "1\r\n2", " \t\r\n")
479+
val findPatterns = Seq(raw"\D+", raw"\W+", raw"\D*", raw"\W*", raw"\D{2,3}", raw"\W{2,3}",
480+
raw"(\D+)", raw"(\W+)")
481+
// \D* and \W* omitted from replace: empty-match semantics differ (see issue 4884)
482+
val replacePatterns =
483+
Seq(raw"\D+", raw"\W+", raw"\D{2,3}", raw"\W{2,3}", raw"(\D+)", raw"(\W+)")
484+
assertCpuGpuMatchesRegexpFind(findPatterns, inputs)
485+
assertCpuGpuMatchesRegexpReplace(replacePatterns, inputs)
486+
}
487+
475488
test("dot matches CR on GPU but not on CPU") {
476489
// see https://github.com/rapidsai/cudf/issues/9619
477490
val pattern = "1."

0 commit comments

Comments
 (0)