From c405400496dce580a0eb0c4abdcd3ffbb5cabb8f Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Wed, 29 Apr 2026 08:54:11 +0000 Subject: [PATCH] #68: Support for coalesce function Co-Authored-By: Claude --- fhirpathpy/engine/__init__.py | 8 +++++ fhirpathpy/engine/invocations/__init__.py | 1 + fhirpathpy/engine/invocations/combining.py | 9 ++++++ fhirpathpy/engine/invocations/navigation.py | 2 +- fhirpathpy/engine/nodes.py | 4 +++ pyproject.toml | 1 + tests/cases/5.2.8_coalesce.yaml | 33 +++++++++++++++++++++ 7 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/cases/5.2.8_coalesce.yaml diff --git a/fhirpathpy/engine/__init__.py b/fhirpathpy/engine/__init__.py index b50ba58..ce7c424 100644 --- a/fhirpathpy/engine/__init__.py +++ b/fhirpathpy/engine/__init__.py @@ -64,6 +64,14 @@ def doInvoke(ctx, fn_name, data, raw_params): if "nullable_input" in invocation and util.is_nullable(data): return [] + if "variadic" in invocation: + tp = invocation["variadic"] + raw_params_list = raw_params if isinstance(raw_params, list) else [] + thisValue = ctx["$this"] if "$this" in ctx else ctx["dataRoot"] + params = [make_param(ctx, thisValue, tp, pr) for pr in raw_params_list] + res = invocation["fn"](ctx, util.arraify(data), *params) + return util.arraify(res) + if "arity" not in invocation: if raw_params is None or util.is_empty(raw_params): res = invocation["fn"](ctx, util.arraify(data)) diff --git a/fhirpathpy/engine/invocations/__init__.py b/fhirpathpy/engine/invocations/__init__.py index c0966f7..2111b02 100644 --- a/fhirpathpy/engine/invocations/__init__.py +++ b/fhirpathpy/engine/invocations/__init__.py @@ -50,6 +50,7 @@ "skip": {"fn": filtering.skip_fn, "arity": {1: ["Integer"]}}, "intersect": {"fn": subsetting.intersect_fn, "arity": {1: ["AnyAtRoot"]}}, "combine": {"fn": combining.combine_fn, "arity": {1: ["AnyAtRoot"]}}, + "coalesce": {"fn": combining.coalesce_fn, "variadic": "Expr"}, "iif": {"fn": misc.iif_macro, "arity": {2: ["Expr", "Expr"], 3: ["Expr", "Expr", "Expr"]}}, "trace": {"fn": misc.trace_fn, "arity": {0: [], 1: ["String"]}}, "toInteger": {"fn": misc.to_integer}, diff --git a/fhirpathpy/engine/invocations/combining.py b/fhirpathpy/engine/invocations/combining.py index 364147f..5f2df62 100644 --- a/fhirpathpy/engine/invocations/combining.py +++ b/fhirpathpy/engine/invocations/combining.py @@ -1,3 +1,4 @@ +from fhirpathpy.engine import util from fhirpathpy.engine.invocations import existence """ @@ -15,3 +16,11 @@ def combine_fn(ctx, coll1, coll2): def exclude_fn(ctx, coll1, coll2): return [element for element in coll1 if element not in coll2] + + +def coalesce_fn(ctx, data, *exprs): + for expr in exprs: + result = expr(data) + if not util.is_empty(result): + return result + return [] diff --git a/fhirpathpy/engine/invocations/navigation.py b/fhirpathpy/engine/invocations/navigation.py index e6bbf95..f539f3d 100644 --- a/fhirpathpy/engine/invocations/navigation.py +++ b/fhirpathpy/engine/invocations/navigation.py @@ -58,7 +58,7 @@ def func(acc, res): actualTypes = model["choiceTypePaths"].get(altPropName, []) if len(actualTypes) > 0: # If it is, we can use it - fullPath = f"{res.propName}.{prop[:-len(childPath)]}" + fullPath = f"{res.propName}.{prop[: -len(childPath)]}" if isinstance(value, list): mapped = [ diff --git a/fhirpathpy/engine/nodes.py b/fhirpathpy/engine/nodes.py index f2b6de8..05c06c2 100644 --- a/fhirpathpy/engine/nodes.py +++ b/fhirpathpy/engine/nodes.py @@ -646,6 +646,8 @@ def __str__(self): return time_str return self.asStr + __hash__ = None + def __eq__(self, other): if isinstance(other, str): return self.getTimeMatchStr() @@ -734,6 +736,8 @@ def __str__(self): return iso_str return self.asStr + __hash__ = None + def __eq__(self, other): if isinstance(other, str): return self.getDateTimeMatchStr() diff --git a/pyproject.toml b/pyproject.toml index 715b147..be26f8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ minversion = "6.0" addopts = "-ra -q --color=yes --cov=fhirpathpy --cov-report=xml" testpaths = ["tests"] +filterwarnings = ["ignore::pytest.PytestRemovedIn9Warning"] log_cli = true log_cli_level = "INFO" python_functions = "*_test" diff --git a/tests/cases/5.2.8_coalesce.yaml b/tests/cases/5.2.8_coalesce.yaml new file mode 100644 index 0000000..e7c9b1a --- /dev/null +++ b/tests/cases/5.2.8_coalesce.yaml @@ -0,0 +1,33 @@ +tests: + - desc: '5.2.8 coalesce' + + - desc: '** returns first non-empty value from identifier' + expression: "coalesce(Patient.identifier.where(system='required-system').value.first(), 'unknown')" + result: + - required-value + + - desc: '** returns fallback when first is empty' + expression: "coalesce(Patient.identifier.where(system='other-system').value.first(), 'unknown')" + result: + - unknown + + - desc: '** returns first non-empty collection among multiple args' + expression: "coalesce({}, (1 | 2), (3 | 4))" + result: + - 1 + - 2 + + - desc: '** returns single value when first arg is empty' + expression: "coalesce({}, 1)" + result: + - 1 + + - desc: '** returns empty when only arg is empty' + expression: "coalesce({})" + result: [] + +subject: + resourceType: Patient + identifier: + - system: required-system + value: required-value