From fa5efe6e2234706b633134dc4a66100a18e6c0ad Mon Sep 17 00:00:00 2001 From: chgl Date: Wed, 24 Jun 2026 13:05:02 +0200 Subject: [PATCH 1/7] feat: implemented startsWith function --- .../function/provider/StringFunctions.java | 27 ++++++++++++++ .../fhirpath/dsl/StringFunctionsDslTest.java | 35 ++++++++++++++++++- site/docs/fhirpath/index.md | 7 ++-- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java index e007ff709c..8e3fea87ab 100644 --- a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java +++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java @@ -21,11 +21,14 @@ import au.csiro.pathling.fhirpath.annotations.SqlOnFhirConformance; import au.csiro.pathling.fhirpath.annotations.SqlOnFhirConformance.Profile; +import au.csiro.pathling.fhirpath.collection.BooleanCollection; import au.csiro.pathling.fhirpath.collection.StringCollection; +import au.csiro.pathling.fhirpath.column.ColumnRepresentation; import au.csiro.pathling.fhirpath.column.DefaultRepresentation; import au.csiro.pathling.fhirpath.function.FhirPathFunction; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.apache.spark.sql.Column; /** * Contains functions for manipulating strings. @@ -67,4 +70,28 @@ public static StringCollection join( ? separator.asSingular().getColumn() : DefaultRepresentation.literal(JOIN_DEFAULT_SEPARATOR))); } + + /** + * The startsWith function returns true when the input string starts with the given prefix. + * + *

If prefix is the empty string ({@code ''}), the result is {@code true}. + * + *

If the input collection or prefix is empty, the result is empty. + * + * @param input The input string + * @param prefix The prefix to check for + * @return A {@link BooleanCollection} containing the result + * @see FHIRPath + * Specification - startsWith + */ + @FhirPathFunction + @SqlOnFhirConformance(Profile.EXPERIMENTAL) + @Nonnull + public static BooleanCollection startsWith( + @Nonnull final StringCollection input, @Nonnull final StringCollection prefix) { + return BooleanCollection.build( + ColumnRepresentation.binaryOperator( + input.asSingular().getColumn(), prefix.asSingular().getColumn(), Column::startsWith)); + } } diff --git a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java index 0ee81532b9..3793ba79cf 100644 --- a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java +++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java @@ -24,7 +24,7 @@ /** * Tests for FHIRPath string functions as defined in supported.md: - join([separator: String]) : - * String + * String - startsWith(prefix: String) : Boolean */ public class StringFunctionsDslTest extends FhirPathDslTestBase { @@ -78,4 +78,37 @@ public Stream testJoin() { .testError("person.join(',')", "join() errors on non-collection types") .build(); } + + @FhirPathTest + public Stream testStartsWith() { + return builder() + .withSubject( + sb -> + sb + // Empty values + .stringEmpty("emptyString") + // Single values + .string("singleString", "Hello, world!") + // Arrays of strings + .stringArray("stringArray", "one", "two", "three")) + .group("startsWith() function with single values") + .testTrue( + "singleString.startsWith('Hello')", "startsWith() returns true when prefix matches") + .testFalse( + "singleString.startsWith('world')", + "startsWith() returns false when prefix does not match") + .testTrue("singleString.startsWith('')", "startsWith() returns true for an empty prefix") + .testTrue( + "singleString.startsWith('Hello, world!')", + "startsWith() returns true when prefix equals the whole string") + .group("startsWith() function with empty values") + .testEmpty("emptyString.startsWith('Hello')", "startsWith() on empty input returns empty") + .testEmpty( + "singleString.startsWith({})", "startsWith() with empty prefix argument returns empty") + .group("startsWith() function error cases") + .testError( + "stringArray.startsWith('o')", + "startsWith() errors when input collection is not singular") + .build(); + } } diff --git a/site/docs/fhirpath/index.md b/site/docs/fhirpath/index.md index 087d3f6081..9aace42597 100644 --- a/site/docs/fhirpath/index.md +++ b/site/docs/fhirpath/index.md @@ -157,9 +157,10 @@ current `name` element, matching `name.select(use | given)`. #### String functions -| Function | Description | -| ------------------ | ------------------------------------ | -| `join(separator?)` | Join strings with optional separator | +| Function | Description | +| -------------------- | ---------------------------------------------------- | +| `join(separator?)` | Join strings with optional separator | +| `startsWith(prefix)` | Test whether the string starts with the given prefix | #### Type functions From 6d6dde93e0774f9099136a52c9e7b2ca4eb99c9c Mon Sep 17 00:00:00 2001 From: chgl Date: Wed, 24 Jun 2026 13:08:00 +0200 Subject: [PATCH 2/7] feat: implemented endsWith --- .../function/provider/StringFunctions.java | 24 ++++++++++++++ .../fhirpath/dsl/StringFunctionsDslTest.java | 32 ++++++++++++++++++- site/docs/fhirpath/index.md | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java index 8e3fea87ab..87c313f655 100644 --- a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java +++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java @@ -94,4 +94,28 @@ public static BooleanCollection startsWith( ColumnRepresentation.binaryOperator( input.asSingular().getColumn(), prefix.asSingular().getColumn(), Column::startsWith)); } + + /** + * The endsWith function returns true when the input string ends with the given suffix. + * + *

If suffix is the empty string ({@code ''}), the result is {@code true}. + * + *

If the input collection or suffix is empty, the result is empty. + * + * @param input The input string + * @param suffix The suffix to check for + * @return A {@link BooleanCollection} containing the result + * @see FHIRPath + * Specification - endsWith + */ + @FhirPathFunction + @SqlOnFhirConformance(Profile.EXPERIMENTAL) + @Nonnull + public static BooleanCollection endsWith( + @Nonnull final StringCollection input, @Nonnull final StringCollection suffix) { + return BooleanCollection.build( + ColumnRepresentation.binaryOperator( + input.asSingular().getColumn(), suffix.asSingular().getColumn(), Column::endsWith)); + } } diff --git a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java index 3793ba79cf..5f57b4d39c 100644 --- a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java +++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java @@ -24,7 +24,7 @@ /** * Tests for FHIRPath string functions as defined in supported.md: - join([separator: String]) : - * String - startsWith(prefix: String) : Boolean + * String - startsWith(prefix: String) : Boolean - endsWith(suffix: String) : Boolean */ public class StringFunctionsDslTest extends FhirPathDslTestBase { @@ -111,4 +111,34 @@ public Stream testStartsWith() { "startsWith() errors when input collection is not singular") .build(); } + + @FhirPathTest + public Stream testEndsWith() { + return builder() + .withSubject( + sb -> + sb + // Empty values + .stringEmpty("emptyString") + // Single values + .string("singleString", "Hello, world!") + // Arrays of strings + .stringArray("stringArray", "one", "two", "three")) + .group("endsWith() function with single values") + .testTrue("singleString.endsWith('world!')", "endsWith() returns true when suffix matches") + .testFalse( + "singleString.endsWith('Hello')", "endsWith() returns false when suffix does not match") + .testTrue("singleString.endsWith('')", "endsWith() returns true for an empty suffix") + .testTrue( + "singleString.endsWith('Hello, world!')", + "endsWith() returns true when suffix equals the whole string") + .group("endsWith() function with empty values") + .testEmpty("emptyString.endsWith('world!')", "endsWith() on empty input returns empty") + .testEmpty( + "singleString.endsWith({})", "endsWith() with empty suffix argument returns empty") + .group("endsWith() function error cases") + .testError( + "stringArray.endsWith('o')", "endsWith() errors when input collection is not singular") + .build(); + } } diff --git a/site/docs/fhirpath/index.md b/site/docs/fhirpath/index.md index 9aace42597..878fa0495d 100644 --- a/site/docs/fhirpath/index.md +++ b/site/docs/fhirpath/index.md @@ -161,6 +161,7 @@ current `name` element, matching `name.select(use | given)`. | -------------------- | ---------------------------------------------------- | | `join(separator?)` | Join strings with optional separator | | `startsWith(prefix)` | Test whether the string starts with the given prefix | +| `endsWith(suffix)` | Test whether the string ends with the given suffix | #### Type functions From be4366e498d4e129217e975dbb3d19baf59c61d9 Mon Sep 17 00:00:00 2001 From: chgl Date: Wed, 24 Jun 2026 14:40:55 +0200 Subject: [PATCH 3/7] feat: implemented contains --- .../function/provider/StringFunctions.java | 24 +++++++++++++ .../fhirpath/dsl/StringFunctionsDslTest.java | 35 ++++++++++++++++++- site/docs/fhirpath/index.md | 11 +++--- 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java index 87c313f655..c7cf12dcc9 100644 --- a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java +++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java @@ -118,4 +118,28 @@ public static BooleanCollection endsWith( ColumnRepresentation.binaryOperator( input.asSingular().getColumn(), suffix.asSingular().getColumn(), Column::endsWith)); } + + /** + * The contains function returns true when the input string contains the given substring. + * + *

If substring is the empty string ({@code ''}), the result is {@code true}. + * + *

If the input collection or substring is empty, the result is empty. + * + * @param input The input string + * @param substring The substring to check for + * @return A {@link BooleanCollection} containing the result + * @see FHIRPath + * Specification - contains + */ + @FhirPathFunction + @SqlOnFhirConformance(Profile.EXPERIMENTAL) + @Nonnull + public static BooleanCollection contains( + @Nonnull final StringCollection input, @Nonnull final StringCollection substring) { + return BooleanCollection.build( + ColumnRepresentation.binaryOperator( + input.asSingular().getColumn(), substring.asSingular().getColumn(), Column::contains)); + } } diff --git a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java index 5f57b4d39c..3a8cbbddf1 100644 --- a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java +++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java @@ -24,7 +24,8 @@ /** * Tests for FHIRPath string functions as defined in supported.md: - join([separator: String]) : - * String - startsWith(prefix: String) : Boolean - endsWith(suffix: String) : Boolean + * String - startsWith(prefix: String) : Boolean - endsWith(suffix: String) : Boolean - + * contains(substring: String) : Boolean */ public class StringFunctionsDslTest extends FhirPathDslTestBase { @@ -141,4 +142,36 @@ public Stream testEndsWith() { "stringArray.endsWith('o')", "endsWith() errors when input collection is not singular") .build(); } + + @FhirPathTest + public Stream testContains() { + return builder() + .withSubject( + sb -> + sb + // Empty values + .stringEmpty("emptyString") + // Single values + .string("singleString", "Hello, world!") + // Arrays of strings + .stringArray("stringArray", "one", "two", "three")) + .group("contains() function with single values") + .testTrue( + "singleString.contains('lo, wo')", "contains() returns true when substring matches") + .testFalse( + "singleString.contains('goodbye')", + "contains() returns false when substring does not match") + .testTrue("singleString.contains('')", "contains() returns true for an empty substring") + .testTrue( + "singleString.contains('Hello, world!')", + "contains() returns true when substring equals the whole string") + .group("contains() function with empty values") + .testEmpty("emptyString.contains('lo')", "contains() on empty input returns empty") + .testEmpty( + "singleString.contains({})", "contains() with empty substring argument returns empty") + .group("contains() function error cases") + .testError( + "stringArray.contains('o')", "contains() errors when input collection is not singular") + .build(); + } } diff --git a/site/docs/fhirpath/index.md b/site/docs/fhirpath/index.md index 878fa0495d..d0b3794426 100644 --- a/site/docs/fhirpath/index.md +++ b/site/docs/fhirpath/index.md @@ -157,11 +157,12 @@ current `name` element, matching `name.select(use | given)`. #### String functions -| Function | Description | -| -------------------- | ---------------------------------------------------- | -| `join(separator?)` | Join strings with optional separator | -| `startsWith(prefix)` | Test whether the string starts with the given prefix | -| `endsWith(suffix)` | Test whether the string ends with the given suffix | +| Function | Description | +| --------------------- | ---------------------------------------------------- | +| `join(separator?)` | Join strings with optional separator | +| `startsWith(prefix)` | Test whether the string starts with the given prefix | +| `endsWith(suffix)` | Test whether the string ends with the given suffix | +| `contains(substring)` | Test whether the string contains the given substring | #### Type functions From 491d9749aa222b8c8ebb0ca0684a014e4c9f9302 Mon Sep 17 00:00:00 2001 From: chgl Date: Wed, 24 Jun 2026 15:29:14 +0200 Subject: [PATCH 4/7] test: python view test --- lib/python/tests/test_view.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py index 008fd9707e..3a71406bf8 100644 --- a/lib/python/tests/test_view.py +++ b/lib/python/tests/test_view.py @@ -131,3 +131,23 @@ def test_foreach_coding_with_sibling_text_topandas(ndjson_test_data_dir, pathlin "Anemia (disorder)", ), ] + +def test_view_with_string_functions(ndjson_test_data_dir, pathling_ctx): + data_source = pathling_ctx.read.ndjson(ndjson_test_data_dir) + result = data_source.view( + resource="Patient", + select=[ + { + "column": [ + {"path": "name.given.first().startsWith('Sey')", "name": "starts_with_sey"}, + {"path": "name.given.first().endsWith('r882')", "name": "ends_with_r882"}, + {"path": "name.family.first().contains('k43')", "name": "contains_k43"}, + ] + } + ], + ) + assert result.columns == list(ResultRow) + assert result.limit(2).collect() == [ + ResultRow("true", "true", "true"), + ResultRow("false", "false", "false"), + ] From e50ed5873699c13b7169eadd53a2d31126d49f52 Mon Sep 17 00:00:00 2001 From: chgl <5307555+chgl@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:44:01 +0200 Subject: [PATCH 5/7] test: fixed python test --- lib/python/tests/test_view.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py index 3a71406bf8..7f41b224b1 100644 --- a/lib/python/tests/test_view.py +++ b/lib/python/tests/test_view.py @@ -132,6 +132,8 @@ def test_foreach_coding_with_sibling_text_topandas(ndjson_test_data_dir, pathlin ), ] +StringFunctionsRow = Row("starts_with_sey", "ends_with_r882", "contains_k43") + def test_view_with_string_functions(ndjson_test_data_dir, pathling_ctx): data_source = pathling_ctx.read.ndjson(ndjson_test_data_dir) result = data_source.view( @@ -146,8 +148,8 @@ def test_view_with_string_functions(ndjson_test_data_dir, pathling_ctx): } ], ) - assert result.columns == list(ResultRow) + assert result.columns == list(StringFunctionsRow) assert result.limit(2).collect() == [ - ResultRow("true", "true", "true"), - ResultRow("false", "false", "false"), + StringFunctionsRow(True, True, True), + StringFunctionsRow(False, False, False), ] From 649b2f58f3f57d1ec89c25cc132006e80c5ea45e Mon Sep 17 00:00:00 2001 From: chgl <5307555+chgl@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:19:59 +0200 Subject: [PATCH 6/7] chore: formatted python file --- lib/python/tests/test_view.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py index 7f41b224b1..e7014a88da 100644 --- a/lib/python/tests/test_view.py +++ b/lib/python/tests/test_view.py @@ -132,8 +132,10 @@ def test_foreach_coding_with_sibling_text_topandas(ndjson_test_data_dir, pathlin ), ] + StringFunctionsRow = Row("starts_with_sey", "ends_with_r882", "contains_k43") + def test_view_with_string_functions(ndjson_test_data_dir, pathling_ctx): data_source = pathling_ctx.read.ndjson(ndjson_test_data_dir) result = data_source.view( @@ -141,9 +143,18 @@ def test_view_with_string_functions(ndjson_test_data_dir, pathling_ctx): select=[ { "column": [ - {"path": "name.given.first().startsWith('Sey')", "name": "starts_with_sey"}, - {"path": "name.given.first().endsWith('r882')", "name": "ends_with_r882"}, - {"path": "name.family.first().contains('k43')", "name": "contains_k43"}, + { + "path": "name.given.first().startsWith('Sey')", + "name": "starts_with_sey", + }, + { + "path": "name.given.first().endsWith('r882')", + "name": "ends_with_r882", + }, + { + "path": "name.family.first().contains('k43')", + "name": "contains_k43", + }, ] } ], From efb1c2534edfb34e390e92a263b250235d9ad01b Mon Sep 17 00:00:00 2001 From: chgl <5307555+chgl@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:15:39 +0200 Subject: [PATCH 7/7] test: updated error test cases with collection argument --- .../fhirpath/function/provider/StringFunctions.java | 6 ++++-- .../pathling/fhirpath/dsl/StringFunctionsDslTest.java | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java index c7cf12dcc9..d29c71108b 100644 --- a/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java +++ b/fhirpath/src/main/java/au/csiro/pathling/fhirpath/function/provider/StringFunctions.java @@ -76,7 +76,8 @@ public static StringCollection join( * *

If prefix is the empty string ({@code ''}), the result is {@code true}. * - *

If the input collection or prefix is empty, the result is empty. + *

If the input collection or the prefix argument is an empty collection ({@code {}}), the + * result is empty. * * @param input The input string * @param prefix The prefix to check for @@ -100,7 +101,8 @@ public static BooleanCollection startsWith( * *

If suffix is the empty string ({@code ''}), the result is {@code true}. * - *

If the input collection or suffix is empty, the result is empty. + *

If the input collection or the suffix argument is an empty collection ({@code {}}), the + * result is empty. * * @param input The input string * @param suffix The suffix to check for diff --git a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java index 3a8cbbddf1..cbba4fc218 100644 --- a/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java +++ b/fhirpath/src/test/java/au/csiro/pathling/fhirpath/dsl/StringFunctionsDslTest.java @@ -110,6 +110,9 @@ public Stream testStartsWith() { .testError( "stringArray.startsWith('o')", "startsWith() errors when input collection is not singular") + .testError( + "singleString.startsWith(stringArray)", + "startsWith() errors when the argument is not singular") .build(); } @@ -140,6 +143,9 @@ public Stream testEndsWith() { .group("endsWith() function error cases") .testError( "stringArray.endsWith('o')", "endsWith() errors when input collection is not singular") + .testError( + "singleString.endsWith(stringArray)", + "endsWith() errors when the argument is not singular") .build(); } @@ -172,6 +178,9 @@ public Stream testContains() { .group("contains() function error cases") .testError( "stringArray.contains('o')", "contains() errors when input collection is not singular") + .testError( + "singleString.contains(stringArray)", + "contains() errors when the argument is not singular") .build(); } }