Skip to content

Commit

Permalink
Add support for FHIRPath's upper(), lower(), and replace() functions.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 311050011
  • Loading branch information
aaronnash authored and nickgeorge committed May 14, 2020
1 parent 92e9a18 commit ac09aa9
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 3 deletions.
1 change: 1 addition & 0 deletions cc/google/fhir/fhir_path/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ cc_library(
"@com_google_absl//absl/time",
"@com_google_absl//absl/types:optional",
"@com_google_protobuf//:protobuf",
"@icu//:common",
"@org_tensorflow//tensorflow/core:lib",
],
)
Expand Down
149 changes: 146 additions & 3 deletions cc/google/fhir/fhir_path/fhir_path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "google/fhir/status/statusor.h"
#include "google/fhir/util.h"
#include "proto/r4/core/datatypes.pb.h"
#include "icu4c/source/common/unicode/unistr.h"

namespace google {
namespace fhir {
Expand Down Expand Up @@ -794,6 +795,70 @@ class EndsWithFunction : public StringTestFunction {
}
};

class StringTransformationFunction : public ZeroParameterFunctionNode {
public:
StringTransformationFunction(
const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: ZeroParameterFunctionNode(child, params) {}

Status Evaluate(WorkSpace* work_space,
std::vector<WorkspaceMessage>* results) const override {
std::vector<WorkspaceMessage> child_results;
FHIR_RETURN_IF_ERROR(child_->Evaluate(work_space, &child_results));

if (child_results.size() > 1) {
return InvalidArgumentError("Function must be invoked on a string.");
}

if (child_results.empty()) {
return absl::OkStatus();
}

FHIR_ASSIGN_OR_RETURN(
std::string item,
MessagesToString(work_space->GetPrimitiveHandler(), child_results));

Message* result =
work_space->GetPrimitiveHandler()->NewString(Transform(item));
work_space->DeleteWhenFinished(result);
results->push_back(WorkspaceMessage(result));
return absl::OkStatus();
}

virtual std::string Transform(const std::string& input) const = 0;

const Descriptor* ReturnType() const override { return String::descriptor(); }
};

class LowerFunction : public StringTransformationFunction {
public:
LowerFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: StringTransformationFunction(child, params) {}

std::string Transform(const std::string& input) const override {
std::string uppercase_string;
icu::UnicodeString::fromUTF8(input).toLower().toUTF8String(
uppercase_string);
return uppercase_string;
}
};

class UpperFunction : public StringTransformationFunction {
public:
UpperFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: StringTransformationFunction(child, params) {}

std::string Transform(const std::string& input) const override {
std::string uppercase_string;
icu::UnicodeString::fromUTF8(input).toUpper().toUTF8String(
uppercase_string);
return uppercase_string;
}
};

// Implements the FHIRPath .contains() function.
class ContainsFunction : public StringTestFunction {
public:
Expand Down Expand Up @@ -852,6 +917,84 @@ class MatchesFunction : public SingleValueFunctionNode {
}
};

class ReplaceFunction : public FunctionNode {
public:
ReplaceFunction(const std::shared_ptr<ExpressionNode>& child,
const std::vector<std::shared_ptr<ExpressionNode>>& params)
: FunctionNode(child, params) {}

Status Evaluate(WorkSpace* work_space,
std::vector<WorkspaceMessage>* results) const override {
std::vector<WorkspaceMessage> pattern_param;
FHIR_RETURN_IF_ERROR(params_[0]->Evaluate(work_space, &pattern_param));

std::vector<WorkspaceMessage> replacement_param;
FHIR_RETURN_IF_ERROR(params_[1]->Evaluate(work_space, &replacement_param));

return EvaluateWithParam(work_space, pattern_param, replacement_param,
results);
}

Status EvaluateWithParam(
WorkSpace* work_space, const std::vector<WorkspaceMessage>& pattern_param,
const std::vector<WorkspaceMessage>& replacement_param,
std::vector<WorkspaceMessage>* results) const {
std::vector<WorkspaceMessage> child_results;
FHIR_RETURN_IF_ERROR(child_->Evaluate(work_space, &child_results));

// If the input collection, pattern, or substitution are empty, the result
// is empty ({ }).
// http:https://hl7.org/fhirpath/N1/#replacepattern-string-substitution-string-string
if (child_results.empty() || pattern_param.empty() ||
replacement_param.empty()) {
return absl::OkStatus();
}

FHIR_ASSIGN_OR_RETURN(
std::string item,
MessagesToString(work_space->GetPrimitiveHandler(), child_results));
FHIR_ASSIGN_OR_RETURN(
std::string pattern,
MessagesToString(work_space->GetPrimitiveHandler(), pattern_param));
FHIR_ASSIGN_OR_RETURN(
std::string replacement,
MessagesToString(work_space->GetPrimitiveHandler(), replacement_param));

if (!item.empty() && pattern.empty()) {
// "If pattern is the empty string (''), every character in the input
// string is surrounded by the substitution, e.g. 'abc'.replace('','x')
// becomes 'xaxbxcx'."
std::string replaced(replacement);
replaced.reserve(replacement.length() * (item.length() + 1) +
item.length());
icu::UnicodeString original = icu::UnicodeString::fromUTF8(item);
for (int i = 0; i < original.length(); i++) {
original.tempSubStringBetween(i, i + 1).toUTF8String(replaced);
replaced.append(replacement);
}
item = std::move(replaced);
} else {
item = absl::StrReplaceAll(item, {{pattern, replacement}});
}

Message* result = work_space->GetPrimitiveHandler()->NewString(item);
work_space->DeleteWhenFinished(result);
results->push_back(WorkspaceMessage(result));
return absl::OkStatus();
}

const Descriptor* ReturnType() const override { return String::descriptor(); }

static Status ValidateParams(
const std::vector<std::shared_ptr<ExpressionNode>>& params) {
return params.size() == 2
? absl::OkStatus()
: InvalidArgumentError(absl::StrCat(
"replace() requires exactly two parameters. Got ",
params.size(), "."));
}
};

class ReplaceMatchesFunction : public FunctionNode {
public:
explicit ReplaceMatchesFunction(
Expand Down Expand Up @@ -3481,9 +3624,9 @@ class FhirPathCompilerVisitor : public FhirPathBaseVisitor {
{"toTime", UnimplementedFunction},
{"indexOf", FunctionNode::Create<IndexOfFunction>},
{"substring", UnimplementedFunction},
{"upper", UnimplementedFunction},
{"lower", UnimplementedFunction},
{"replace", UnimplementedFunction},
{"upper", FunctionNode::Create<UpperFunction>},
{"lower", FunctionNode::Create<LowerFunction>},
{"replace", FunctionNode::Create<ReplaceFunction>},
{"endsWith", FunctionNode::Create<EndsWithFunction>},
{"toChars", UnimplementedFunction},
{"today", UnimplementedFunction},
Expand Down
36 changes: 36 additions & 0 deletions cc/google/fhir/fhir_path/fhir_path_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,24 @@ FHIR_VERSION_TEST(FhirPathTest, TestFunctionIndexOf, {
EXPECT_THAT(Evaluate("''.indexOf({})"), EvalsToEmpty());
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionUpper, {
EXPECT_THAT(Evaluate("{}.upper()"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.upper()"), EvalsToStringThatMatches(StrEq("")));
EXPECT_THAT(Evaluate("'aBa'.upper()"),
EvalsToStringThatMatches(StrEq("ABA")));
EXPECT_THAT(Evaluate("'ABA'.upper()"),
EvalsToStringThatMatches(StrEq("ABA")));
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionLower, {
EXPECT_THAT(Evaluate("{}.lower()"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.lower()"), EvalsToStringThatMatches(StrEq("")));
EXPECT_THAT(Evaluate("'aBa'.lower()"),
EvalsToStringThatMatches(StrEq("aba")));
EXPECT_THAT(Evaluate("'aba'.lower()"),
EvalsToStringThatMatches(StrEq("aba")));
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionMatches, {
EXPECT_THAT(Evaluate("{}.matches('')"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.matches('')"), EvalsToTrue());
Expand All @@ -673,6 +691,24 @@ FHIR_VERSION_TEST(FhirPathTest, TestFunctionReplaceMatches, {
EvalsToStringThatMatches(StrEq("b")));
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionReplace, {
EXPECT_THAT(Evaluate("{}.replace('', '')"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.replace({}, '')"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.replace('', {})"), EvalsToEmpty());
EXPECT_THAT(Evaluate("''.replace('', 'x')"),
EvalsToStringThatMatches(StrEq("")));
EXPECT_THAT(Evaluate("'abcdefg'.replace('x', '123')"),
EvalsToStringThatMatches(StrEq("abcdefg")));
EXPECT_THAT(Evaluate("'abcdefg'.replace('cde', '123')"),
EvalsToStringThatMatches(StrEq("ab123fg")));
EXPECT_THAT(Evaluate("'abcdefg'.replace('cde', '')"),
EvalsToStringThatMatches(StrEq("abfg")));
EXPECT_THAT(Evaluate("'abc'.replace('', 'x')"),
EvalsToStringThatMatches(StrEq("xaxbxcx")));
EXPECT_THAT(Evaluate("'£'.replace('', 'x')"),
EvalsToStringThatMatches(StrEq("x£x")));
})

FHIR_VERSION_TEST(FhirPathTest, TestFunctionReplaceMatchesWrongArgCount, {
StatusOr<EvaluationResult> result = Evaluate("''.replaceMatches()");
EXPECT_THAT(result.status().code(), Eq(absl::StatusCode::kInvalidArgument))
Expand Down

0 comments on commit ac09aa9

Please sign in to comment.