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..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
@@ -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,78 @@ 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 the prefix argument is an empty collection ({@code {}}), 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));
+ }
+
+ /**
+ * 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 the suffix argument is an empty collection ({@code {}}), 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));
+ }
+
+ /**
+ * 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 0ee81532b9..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
@@ -24,7 +24,8 @@
/**
* Tests for FHIRPath string functions as defined in supported.md: - join([separator: String]) :
- * String
+ * String - startsWith(prefix: String) : Boolean - endsWith(suffix: String) : Boolean -
+ * contains(substring: String) : Boolean
*/
public class StringFunctionsDslTest extends FhirPathDslTestBase {
@@ -78,4 +79,108 @@ 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")
+ .testError(
+ "singleString.startsWith(stringArray)",
+ "startsWith() errors when the argument 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")
+ .testError(
+ "singleString.endsWith(stringArray)",
+ "endsWith() errors when the argument 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")
+ .testError(
+ "singleString.contains(stringArray)",
+ "contains() errors when the argument is not singular")
+ .build();
+ }
}
diff --git a/lib/python/tests/test_view.py b/lib/python/tests/test_view.py
index 008fd9707e..e7014a88da 100644
--- a/lib/python/tests/test_view.py
+++ b/lib/python/tests/test_view.py
@@ -131,3 +131,36 @@ def test_foreach_coding_with_sibling_text_topandas(ndjson_test_data_dir, pathlin
"Anemia (disorder)",
),
]
+
+
+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(
+ 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(StringFunctionsRow)
+ assert result.limit(2).collect() == [
+ StringFunctionsRow(True, True, True),
+ StringFunctionsRow(False, False, False),
+ ]
diff --git a/site/docs/fhirpath/index.md b/site/docs/fhirpath/index.md
index 087d3f6081..d0b3794426 100644
--- a/site/docs/fhirpath/index.md
+++ b/site/docs/fhirpath/index.md
@@ -157,9 +157,12 @@ 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 |
+| `endsWith(suffix)` | Test whether the string ends with the given suffix |
+| `contains(substring)` | Test whether the string contains the given substring |
#### Type functions