Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
* <p>If prefix is the empty string ({@code ''}), the result is {@code true}.
*
* <p>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 <a
* href="https://build.fhir.org/ig/HL7/FHIRPath/en/#startswithprefix--string--boolean">FHIRPath
* Specification - startsWith</a>
*/
@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.
*
* <p>If suffix is the empty string ({@code ''}), the result is {@code true}.
*
* <p>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 <a
* href="https://build.fhir.org/ig/HL7/FHIRPath/en/#endswithsuffix--string--boolean">FHIRPath
* Specification - endsWith</a>
*/
@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.
*
* <p>If substring is the empty string ({@code ''}), the result is {@code true}.
*
* <p>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 <a
* href="https://build.fhir.org/ig/HL7/FHIRPath/en/#containssubstring--string--boolean">FHIRPath
* Specification - contains</a>
*/
@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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -78,4 +79,108 @@ public Stream<DynamicTest> testJoin() {
.testError("person.join(',')", "join() errors on non-collection types")
.build();
}

@FhirPathTest
public Stream<DynamicTest> 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")
Comment thread
chgl marked this conversation as resolved.
.testError(
"singleString.startsWith(stringArray)",
"startsWith() errors when the argument is not singular")
.build();
}

@FhirPathTest
public Stream<DynamicTest> 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<DynamicTest> 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();
}
}
33 changes: 33 additions & 0 deletions lib/python/tests/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
9 changes: 6 additions & 3 deletions site/docs/fhirpath/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading