From 9f0bffb69b17e416d1ec2c46442c652ec6aa3f9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:22:44 +0000 Subject: [PATCH 01/17] Bump Microsoft.NET.Test.Sdk and Newtonsoft.Json Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) and [Newtonsoft.Json](https://github.com/JamesNK/Newtonsoft.Json). These dependencies needed to be updated together. Updates `Microsoft.NET.Test.Sdk` from 17.12.0 to 17.13.0 - [Release notes](https://github.com/microsoft/vstest/releases) - [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md) - [Commits](https://github.com/microsoft/vstest/compare/v17.12.0...v17.13.0) Updates `Newtonsoft.Json` from 13.0.3 to 13.0.1 - [Release notes](https://github.com/JamesNK/Newtonsoft.Json/releases) - [Commits](https://github.com/JamesNK/Newtonsoft.Json/compare/13.0.3...13.0.1) --- updated-dependencies: - dependency-name: Microsoft.NET.Test.Sdk dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: Newtonsoft.Json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- src/firely-net-sdk-tests.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firely-net-sdk-tests.props b/src/firely-net-sdk-tests.props index d4f7ec1be5..d2fd3da841 100644 --- a/src/firely-net-sdk-tests.props +++ b/src/firely-net-sdk-tests.props @@ -21,7 +21,7 @@ - + From 7976adca56233197c2354b1436e4ecf51c1c58ef Mon Sep 17 00:00:00 2001 From: mmsmits Date: Wed, 12 Feb 2025 13:43:29 +0100 Subject: [PATCH 02/17] allow code "xml", "json", and "ttl" as valid mimetypes --- .../Terminology/MimeTypeTerminologyService.cs | 12 +++++++++++- .../MimeTypeTerminologyServiceTests.cs | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Hl7.Fhir.Base/Specification/Terminology/MimeTypeTerminologyService.cs b/src/Hl7.Fhir.Base/Specification/Terminology/MimeTypeTerminologyService.cs index 5fb6dcd1af..18f8d3bd42 100644 --- a/src/Hl7.Fhir.Base/Specification/Terminology/MimeTypeTerminologyService.cs +++ b/src/Hl7.Fhir.Base/Specification/Terminology/MimeTypeTerminologyService.cs @@ -12,14 +12,24 @@ public sealed class MimeTypeTerminologyService : CustomValueSetTerminologyServic private const string MIMETYPE_SYSTEM = "urn:ietf:bcp:13"; public const string MIMETYPE_VALUESET_R4_AND_UP = "http://hl7.org/fhir/ValueSet/mimetypes"; public const string MIMETYPE_VALUESET_STU3 = "http://www.rfc-editor.org/bcp/bcp13.txt"; + private const string XML_CODE = "xml"; + private const string JSON_CODE = "json"; + private const string TTL_CODE = "ttl"; public MimeTypeTerminologyService() : base("MIME type", MIMETYPE_SYSTEM, [MIMETYPE_VALUESET_STU3, MIMETYPE_VALUESET_R4_AND_UP]) { } - + //mime-type format: type "/" [tree "."] subtype ["+" suffix]* [";" parameter]; + //FHIR also allows for the following codes: xml, json, ttl override protected bool ValidateCodeType(string code) { + //This is a temporary fix until we support additional bindings. + if (code == XML_CODE || code == JSON_CODE || code == TTL_CODE) + { + return true; + } + var entries = code.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); return entries.Length == 2; } diff --git a/src/Hl7.Fhir.Support.Tests/Specification/MimeTypeTerminologyServiceTests.cs b/src/Hl7.Fhir.Support.Tests/Specification/MimeTypeTerminologyServiceTests.cs index 58b373dd1c..b03de01fd1 100644 --- a/src/Hl7.Fhir.Support.Tests/Specification/MimeTypeTerminologyServiceTests.cs +++ b/src/Hl7.Fhir.Support.Tests/Specification/MimeTypeTerminologyServiceTests.cs @@ -39,6 +39,15 @@ public async Task MimeTypeValidationTest() result.Parameter.Should().Contain(p => p.Name == "result") .Subject.Value.Should().BeEquivalentTo(new FhirBoolean(true)); + parameters = new ValidateCodeParameters() + .WithValueSet(MIMETYPEVS) + .WithCode(code: "json") + .Build(); + + result = await _service.ValueSetValidateCode(parameters); + result.Parameter.Should().Contain(p => p.Name == "result") + .Subject.Value.Should().BeEquivalentTo(new FhirBoolean(true)); + parameters = new ValidateCodeParameters() .WithValueSet(ADMINGENDERVS) .WithCode(code: "application/json", context: "context") @@ -61,6 +70,11 @@ public async Task MimeTypeValidationTest() validateCode = async () => await _service.ValueSetValidateCode(parameters); await validateCode.Should().ThrowAsync().WithMessage("Unknown system 'http://hl7.org/fhir/administrative-gender'"); + + + + validateCode = async () => await _service.ValueSetValidateCode(parameters); + await validateCode.Should().ThrowAsync().WithMessage("Unknown system 'http://hl7.org/fhir/administrative-gender'"); } [DataRow(MIMETYPE_VERSIONED_VS)] From 3605c91f063b3d68616c1afb1b6a498067ba8c8b Mon Sep 17 00:00:00 2001 From: mmsmits Date: Wed, 12 Feb 2025 16:29:08 +0100 Subject: [PATCH 03/17] update release notes --- release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 8037ffe543..394976f841 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,3 +1,3 @@ ## Intro: -A hotfix in the `Canonical` class to also allow fragment only canonicals. +A hotfix in the MimeTypeTerminologyService to allow for the FHIR format types "xml", "json" and "ttl". \ No newline at end of file From b3c879a67b265dc5749372222f9ba0af19d38126 Mon Sep 17 00:00:00 2001 From: mmsmits Date: Wed, 12 Feb 2025 17:15:26 +0100 Subject: [PATCH 04/17] start development phase 5.11.5 --- src/firely-net-sdk.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/firely-net-sdk.props b/src/firely-net-sdk.props index 7c95ce250f..84f8535dfa 100644 --- a/src/firely-net-sdk.props +++ b/src/firely-net-sdk.props @@ -6,7 +6,7 @@ - 5.11.4 + 5.11.5 Firely (info@fire.ly) and contributors Firely (https://fire.ly) @@ -44,7 +44,7 @@ Debug;Release;FullDebug true true - 5.11.3 + 5.11.4 From 28776c0432dbe84443c2aa253babdcaac40fc433 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:23:33 +0000 Subject: [PATCH 05/17] Bump MSTest.TestAdapter from 3.6.2 to 3.8.0 Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.6.2 to 3.8.0. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/compare/v3.6.2...v3.8.0) --- updated-dependencies: - dependency-name: MSTest.TestAdapter dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/firely-net-sdk-tests.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firely-net-sdk-tests.props b/src/firely-net-sdk-tests.props index d2fd3da841..2a4bf48755 100644 --- a/src/firely-net-sdk-tests.props +++ b/src/firely-net-sdk-tests.props @@ -23,7 +23,7 @@ - + all From 3eb117ca25715790210b07bc06e75f1665d634cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:28:54 +0000 Subject: [PATCH 06/17] Bump MSTest.TestFramework from 3.6.2 to 3.8.0 Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.6.2 to 3.8.0. - [Release notes](https://github.com/microsoft/testfx/releases) - [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md) - [Commits](https://github.com/microsoft/testfx/compare/v3.6.2...v3.8.0) --- updated-dependencies: - dependency-name: MSTest.TestFramework dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- src/firely-net-sdk-tests.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firely-net-sdk-tests.props b/src/firely-net-sdk-tests.props index d2fd3da841..eb12215d8e 100644 --- a/src/firely-net-sdk-tests.props +++ b/src/firely-net-sdk-tests.props @@ -22,7 +22,7 @@ - + From e9c98551ab64d8b2e63367d8f4df3cbe397955e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:34:58 +0000 Subject: [PATCH 07/17] Bump System.Threading.Tasks.Dataflow from 9.0.1 to 9.0.2 Bumps [System.Threading.Tasks.Dataflow](https://github.com/dotnet/runtime) from 9.0.1 to 9.0.2. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v9.0.1...v9.0.2) --- updated-dependencies: - dependency-name: System.Threading.Tasks.Dataflow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Hl7.Fhir.Specification.R4.Tests.csproj | 2 +- .../Hl7.Fhir.Specification.R4B.Tests.csproj | 2 +- .../Hl7.Fhir.Specification.R5.Tests.csproj | 2 +- src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj | 2 +- src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj | 2 +- src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj index 8a537c40df..db43403dbd 100644 --- a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj index 0d47257cfb..6bf05d7611 100644 --- a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj index c0e7962c73..c4b9f2c8ff 100644 --- a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj index 6c4356e201..1e3a723472 100644 --- a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj +++ b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj index 57f201b207..85a7fae0d8 100644 --- a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj +++ b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj @@ -21,7 +21,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj index a8716f1321..fbd2b862ae 100644 --- a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj +++ b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj @@ -9,7 +9,7 @@ - + From 8213cbafdb8e72667b955956a9c3f775d7ab6b89 Mon Sep 17 00:00:00 2001 From: Alexander Zautke Date: Tue, 18 Feb 2025 10:06:38 +0100 Subject: [PATCH 08/17] Fix typo in LocalTerminologyService --- .../Specification/Terminology/LocalTerminologyService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hl7.Fhir.Shims.Base/Specification/Terminology/LocalTerminologyService.cs b/src/Hl7.Fhir.Shims.Base/Specification/Terminology/LocalTerminologyService.cs index 1ec670d688..f0c8d0e266 100644 --- a/src/Hl7.Fhir.Shims.Base/Specification/Terminology/LocalTerminologyService.cs +++ b/src/Hl7.Fhir.Shims.Base/Specification/Terminology/LocalTerminologyService.cs @@ -137,7 +137,7 @@ private async Task getExpandedValueSet(ValueSet vs, string operation) { // Unprocessable entity throw new FhirOperationException( - $"Operation {operation} failed: creating the required expansion failed mith message \"{e.Message}\".", + $"Operation {operation} failed: creating the required expansion failed with message \"{e.Message}\".", (HttpStatusCode)422); } From b31cbf0a068ca16f2f48ddab0ced14be07637902 Mon Sep 17 00:00:00 2001 From: Marco Visser Date: Fri, 21 Feb 2025 13:20:49 +0100 Subject: [PATCH 09/17] Use v1 of Firely Azure templates Use version `v1` (base version) of the Firely Azure templates (FirelyTeam/azure-pipeline-templates). --- build/azure-pipelines.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 6b0ce5bc94..fcf4917e82 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -32,7 +32,9 @@ resources: - repository: templates type: github name: FirelyTeam/azure-pipeline-templates - endpoint: FirelyTeam + endpoint: FirelyTeam + ref: refs/tags/v1 + stages: - stage: build From 3d1fb33c9b87df5d03a475104790b41347c0149d Mon Sep 17 00:00:00 2001 From: Ewout Kramer Date: Tue, 25 Feb 2025 18:25:16 +0100 Subject: [PATCH 10/17] Made sure focus/params with just an empty primitive also propagate null. --- .../FhirPath/EvaluationContext.cs | 2 +- .../FhirPath/Expressions/Invokee.cs | 365 ++++---- .../FhirPath/Expressions/SymbolTable.cs | 204 ++--- .../Expressions/SymbolTableExtensions.cs | 75 ++ .../FhirPath/Expressions/SymbolTableInit.cs | 782 +++++++++--------- .../FhirPath/Functions/StringOperators.cs | 336 ++++---- .../PocoTests/FhirPathParallelTest.cs | 9 +- .../Functions/FunctionsTests.cs | 43 +- .../Functions/StringFunctionsTests.cs | 320 ++++--- 9 files changed, 1091 insertions(+), 1045 deletions(-) create mode 100644 src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs diff --git a/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs index 9b71ec64f2..63fb38c261 100644 --- a/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs +++ b/src/Hl7.Fhir.Base/FhirPath/EvaluationContext.cs @@ -60,7 +60,7 @@ public EvaluationContext(ITypedElement? resource, ITypedElement? rootResource, I /// /// A delegate that handles the output for the trace() function. /// - public Action>? Tracer { get; set; } + public Action>? Tracer { get; set; } } public static class EvaluationContextExtensions diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs index a89c57d534..d492d0db12 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -1,239 +1,238 @@ -/* +/* * Copyright (c) 2015, Firely (info@fire.ly) and contributors * See the file CONTRIBUTORS for details. - * + * * This file is licensed under the BSD 3-Clause license * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + using Hl7.Fhir.ElementModel; using Hl7.FhirPath.Functions; using System; using System.Collections.Generic; using System.Linq; +using FocusCollection = System.Collections.Generic.IEnumerable; +// ReSharper disable InconsistentNaming + +namespace Hl7.FhirPath.Expressions; + +internal delegate FocusCollection Invokee(Closure context, IEnumerable arguments); -namespace Hl7.FhirPath.Expressions +internal static class InvokeeFactory { - internal delegate IEnumerable Invokee(Closure context, IEnumerable arguments); + public static readonly IEnumerable EmptyArgs = []; - internal static class InvokeeFactory - { - public static readonly IEnumerable EmptyArgs = Enumerable.Empty(); + public static FocusCollection GetThis(Closure context, IEnumerable _) => context.GetThis(); + public static FocusCollection GetTotal(Closure context, IEnumerable _) => context.GetTotal(); - public static IEnumerable GetThis(Closure context, IEnumerable _) - { - return context.GetThis(); - } + public static FocusCollection GetContext(Closure context, IEnumerable _) => + context.GetOriginalContext(); - public static IEnumerable GetTotal(Closure context, IEnumerable _) - { - return context.GetTotal(); - } + public static FocusCollection GetResource(Closure context, IEnumerable _) => + context.GetResource(); - public static IEnumerable GetContext(Closure context, IEnumerable _) - { - return context.GetOriginalContext(); - } + public static FocusCollection GetRootResource(Closure context, IEnumerable arguments) => + context.GetRootResource(); - public static IEnumerable GetResource(Closure context, IEnumerable _) - { - return context.GetResource(); - } - public static IEnumerable GetRootResource(Closure context, IEnumerable arguments) - { - return context.GetRootResource(); - } + public static FocusCollection GetThat(Closure context, IEnumerable _) => + context.GetThat(); - public static IEnumerable GetThat(Closure context, IEnumerable _) - { - return context.GetThat(); - } + public static FocusCollection GetIndex(Closure context, IEnumerable args) => + context.GetIndex(); + + private static readonly Predicate PROPAGATE_WHEN_EMPTY = focus => !focus.Any(); + private static readonly Predicate PROPAGATE_NEVER = _ => false; + + private static readonly Predicate PROPAGATE_EMPTY_PRIMITIVE = focus => + { + var first = focus.FirstOrDefault(); + if (first is null) return true; + + // If this is not a primitive, then it is not empty. + if (first.InstanceType is null || !char.IsLower(first.InstanceType[0])) return false; - public static IEnumerable GetIndex(Closure context, IEnumerable args) + return first.Value is null; + }; + + private static Predicate getPropagator(bool doNullProp, Type argType) => + doNullProp switch { + true when isPrimitiveDotNetType(argType) => PROPAGATE_EMPTY_PRIMITIVE, + true => PROPAGATE_WHEN_EMPTY, + _ => PROPAGATE_NEVER + }; + + private static bool isPrimitiveDotNetType(Type t) => t.IsPrimitive || t == typeof(string) || t == typeof(Decimal); - return context.GetIndex(); - } + public static Invokee Wrap(Func func) + { + return (_, _) => Typecasts.CastTo(func()); + } - public static Invokee Wrap(Func func) + public static Invokee Wrap(Func func, bool propNull) + { + return (ctx, args) => { - return (ctx, args) => + if (typeof(A) != typeof(EvaluationContext)) { - return Typecasts.CastTo>(func()); - }; - } + var focus = args.First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + return Typecasts.CastTo(func(Typecasts.CastTo(focus))); + } + + A lastPar = (A)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(lastPar)); + }; + } - public static Invokee Wrap(Func func, bool propNull) + /// + /// Wraps a function that is only supposed to propagate null in the focus, not in the other arguments. + /// + internal static Invokee WrapWithPropNullForFocus(Func func) + { + return (ctx, args) => { - return (ctx, args) => - { - if (typeof(A) != typeof(EvaluationContext)) - { - var focus = args.First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !focus.Any()) return ElementNode.EmptyList; - - return Typecasts.CastTo>(func(Typecasts.CastTo(focus))); - } - else - { - A lastPar = (A)(object)ctx.EvaluationContext; - return Typecasts.CastTo>(func(lastPar)); - } - }; - } + // propagate only null for focus + var focus = args.First()(ctx, EmptyArgs); + if (getPropagator(true,typeof(A))(focus)) return ElementNode.EmptyList; + + return Wrap(func, false)(ctx, args); + }; + } - internal static Invokee WrapWithPropNullForFocus(Func func) + public static Invokee Wrap(Func func, bool propNull) + { + return (ctx, args) => { - return (ctx, args) => + var focus = args.First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + + if (typeof(B) != typeof(EvaluationContext)) { - // propagate only null for focus - var focus = args.First()(ctx, InvokeeFactory.EmptyArgs); - if (!focus.Any()) return ElementNode.EmptyList; + var argA = args.Skip(1).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; - return Wrap(func, false)(ctx, args); - }; - } + return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA))); + } + else + { + B lastPar = (B)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(focus), lastPar)); + } + }; + } - public static Invokee Wrap(Func func, bool propNull) + public static Invokee Wrap(Func func, bool propNull) + { + return (ctx, args) => { - return (ctx, args) => + var focus = args.First()(ctx, EmptyArgs); + if (getPropagator(propNull,typeof(A))(focus)) return ElementNode.EmptyList; + + var argA = args.Skip(1).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; + + if (typeof(C) != typeof(EvaluationContext)) { - var focus = args.First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !focus.Any()) return ElementNode.EmptyList; - - if (typeof(B) != typeof(EvaluationContext)) - { - var argA = args.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argA.Any()) return ElementNode.EmptyList; - - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA))); - } - else - { - B lastPar = (B)(object)ctx.EvaluationContext; - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), lastPar)); - } - }; - } + var argB = args.Skip(2).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; - public static Invokee Wrap(Func func, bool propNull) - { - return (ctx, args) => + return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), + Typecasts.CastTo(argB))); + } + else { - var focus = args.First()((Closure)ctx, InvokeeFactory.EmptyArgs); - if (propNull && !focus.Any()) return ElementNode.EmptyList; - - var argA = args.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argA.Any()) return ElementNode.EmptyList; - - if (typeof(C) != typeof(EvaluationContext)) - { - var argB = args.Skip(2).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argB.Any()) return ElementNode.EmptyList; - - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), - Typecasts.CastTo(argB))); - } - else - { - C lastPar = (C)(object)ctx.EvaluationContext; - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), - Typecasts.CastTo(argA), lastPar)); - } - }; - } + C lastPar = (C)(object)ctx.EvaluationContext; + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + Typecasts.CastTo(argA), lastPar)); + } + }; + } - public static Invokee Wrap(Func func, bool propNull) + public static Invokee Wrap(Func func, bool propNull) + { + return (ctx, args) => { - return (ctx, args) => + var focus = args.First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + + var argA = args.Skip(1).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; + var argB = args.Skip(2).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; + + if (typeof(D) != typeof(EvaluationContext)) { - var focus = args.First()((Closure)ctx, InvokeeFactory.EmptyArgs); - if (propNull && !focus.Any()) return ElementNode.EmptyList; - - var argA = args.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argA.Any()) return ElementNode.EmptyList; - var argB = args.Skip(2).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argB.Any()) return ElementNode.EmptyList; - - if (typeof(D) != typeof(EvaluationContext)) - { - var argC = args.Skip(3).First()(ctx, InvokeeFactory.EmptyArgs); - if (propNull && !argC.Any()) return ElementNode.EmptyList; - - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), - Typecasts.CastTo(argA), Typecasts.CastTo(argB), Typecasts.CastTo(argC))); - } - else - { - D lastPar = (D)(object)ctx.EvaluationContext; - - return Typecasts.CastTo>(func(Typecasts.CastTo(focus), - Typecasts.CastTo(argA), Typecasts.CastTo(argB), lastPar)); - - } - }; - } + var argC = args.Skip(3).First()(ctx, EmptyArgs); + if (getPropagator(propNull, typeof(D))(argC)) return ElementNode.EmptyList; - public static Invokee WrapLogic(Func, Func, bool?> func) - { - return (ctx, args) => + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + Typecasts.CastTo(argA), Typecasts.CastTo(argB), Typecasts.CastTo(argC))); + } + else { - // Ignore focus - // NOT GOOD, arguments need to be evaluated in the context of the focus to give "$that" meaning. - var left = args.Skip(1).First(); - var right = args.Skip(2).First(); - - // Return function that actually executes the Invokee at the last moment - return Typecasts.CastTo>(func(() => left(ctx, InvokeeFactory.EmptyArgs).BooleanEval(), () => right(ctx, InvokeeFactory.EmptyArgs).BooleanEval())); - }; - } + D lastPar = (D)(object)ctx.EvaluationContext; - public static Invokee Return(ITypedElement value) - { - return (_, __) => (new[] { (ITypedElement)value }); - } + return Typecasts.CastTo(func(Typecasts.CastTo(focus), + Typecasts.CastTo(argA), Typecasts.CastTo(argB), lastPar)); + + } + }; + } - public static Invokee Return(IEnumerable value) + public static Invokee WrapLogic(Func, Func, bool?> func) + { + return (ctx, args) => { - return (_, __) => value; - } + // Ignore focus + // NOT GOOD, arguments need to be evaluated in the context of the focus to give "$that" meaning. + var left = args.Skip(1).First(); + var right = args.Skip(2).First(); + + // Return function that actually executes the Invokee at the last moment + return Typecasts.CastTo( + func(() => left(ctx, EmptyArgs).BooleanEval(), () => right(ctx, EmptyArgs).BooleanEval())); + }; + } + + public static Invokee Return(ITypedElement value) => (_, _) => [value]; - public static Invokee Invoke(string functionName, IEnumerable arguments, Invokee invokee) + public static Invokee Return(FocusCollection value) => (_, _) => value; + + public static Invokee Invoke(string functionName, IEnumerable arguments, Invokee invokee) + { + return (ctx, _) => { - return (ctx, _) => - { - try - { - var wrappedArguments = arguments.Skip(1).Select(wrapWithNextContext); - return invokee(ctx, [arguments.First(),.. wrappedArguments]); - } - catch (Exception e) - { - throw new InvalidOperationException( - $"Invocation of {formatFunctionName(functionName)} failed: {e.Message}"); - } - }; - - Invokee wrapWithNextContext(Invokee unwrappedArgument) + try { - return (ctx, args) => - { - return unwrappedArgument(ctx.Nest(ctx.GetThis()), args); - }; + var wrappedArguments = arguments.Skip(1).Select(wrapWithNextContext); + return invokee(ctx, [arguments.First(),.. wrappedArguments]); } - - string formatFunctionName(string name) + catch (Exception e) { - if (name.StartsWith(BinaryExpression.BIN_PREFIX)) - return $"operator '{name.Substring(BinaryExpression.BIN_PREFIX_LEN)}'"; - else if (name.StartsWith(UnaryExpression.URY_PREFIX)) - return $"operator '{name.Substring(UnaryExpression.URY_PREFIX_LEN)}'"; - else - return $"function '{name}'"; + throw new InvalidOperationException( + $"Invocation of {formatFunctionName(functionName)} failed: {e.Message}"); } + }; + + static Invokee wrapWithNextContext(Invokee unwrappedArgument) + { + return (ctx, args) => unwrappedArgument(ctx.Nest(ctx.GetThis()), args); } + string formatFunctionName(string name) + { + if (name.StartsWith(BinaryExpression.BIN_PREFIX)) + return $"operator '{name.Substring(BinaryExpression.BIN_PREFIX_LEN)}'"; + else if (name.StartsWith(UnaryExpression.URY_PREFIX)) + return $"operator '{name.Substring(UnaryExpression.URY_PREFIX_LEN)}'"; + else + return $"function '{name}'"; + } } + } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTable.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTable.cs index 7b58ae46d7..9be42428dd 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTable.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTable.cs @@ -6,165 +6,109 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ -using Hl7.Fhir.ElementModel; -using System; +#nullable enable + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; -namespace Hl7.FhirPath.Expressions -{ +namespace Hl7.FhirPath.Expressions; - public class SymbolTable +/// +/// Holds the functions and constants available for the FhirPath engine to bind to. +/// +public class SymbolTable +{ + /// + /// An empty symbol table. + /// + public SymbolTable() { - public SymbolTable() - { - - } + // Nothing + } - public SymbolTable(SymbolTable parent) - { - Parent = parent; - } + /// + /// A local symbol table inside a parent scope. + /// + public SymbolTable(SymbolTable parent) + { + Parent = parent; + } - public int Count() - { - var cnt = _entries.Count; - if (Parent != null) cnt += Parent.Count(); + /// + /// The number of entries in the symbol table, including the parent scope (if any). + /// + public int Count() + { + var cnt = _entries.Count; + if (Parent != null) cnt += Parent.Count(); - return cnt; - } + return cnt; + } - internal Invokee First() - { - return _entries.Any() ? _entries.First().Body : (Parent?.First()); - } + internal Invokee? First() => _entries.Any() ? _entries.First().Body : (Parent?.First()); - public SymbolTable Parent { get; private set; } + /// + /// The parent scope for this symbol table. + /// + public SymbolTable? Parent { get; private set; } - [System.Diagnostics.DebuggerDisplay(@"\{{DebuggerDisplayValue()}}")] - private class TableEntry + [System.Diagnostics.DebuggerDisplay(@"\{{DebuggerDisplayValue()}}")] + private class TableEntry(CallSignature signature, Invokee body) + { + public string DebuggerDisplayValue() { - public string DebuggerDisplayValue() - { - if (Signature != null) - { - var sb = new StringBuilder(); - sb.Append(Signature.ReturnType.Name); - sb.Append(' '); - sb.Append(Signature.Name); - sb.Append(" ("); - bool b = false; - foreach (var item in Signature.ArgumentTypes) - { - if (b) - sb.Append(", "); - sb.Append(item.Name); - b = true; - } - sb.Append(')'); - return sb.ToString(); - } - return null; - } - - public CallSignature Signature { get; private set; } - public Invokee Body { get; private set; } + var sb = new StringBuilder(); + sb.Append(Signature.ReturnType.Name); + sb.Append(' '); + sb.Append(Signature.Name); + sb.Append(" ("); + bool b = false; - public TableEntry(CallSignature signature, Invokee body) + foreach (var item in Signature.ArgumentTypes) { - Signature = signature; - Body = body; + if (b) + sb.Append(", "); + sb.Append(item.Name); + b = true; } - } - - private ConcurrentBag _entries = new(); + sb.Append(')'); - internal void Add(CallSignature signature, Invokee body) - { - _entries.Add(new TableEntry(signature, body)); + return sb.ToString(); } - public SymbolTable Filter(string name, int argCount) - { - var result = new SymbolTable - { - _entries = new(_entries.Where(e => e.Signature.Matches(name, argCount))) - }; - - if (Parent != null) - result.Parent = Parent.Filter(name, argCount); - - return result; - } - - internal Invokee DynamicGet(string name, IEnumerable args) - { - var exactMatches = _entries.Where(e => e.Signature.DynamicExactMatches(name, args)); - TableEntry entry = exactMatches.Union(_entries.Where(e => e.Signature.DynamicMatches(name, args))).FirstOrDefault(); - - if (entry == null && Parent != null) return Parent.DynamicGet(name, args); - - return entry?.Body; - } + public CallSignature Signature { get; } = signature; + public Invokee Body { get; } = body; } + private ConcurrentBag _entries = []; - public static class SymbolTableExtensions + internal void Add(CallSignature signature, Invokee body) { - public static void Add(this SymbolTable table, string name, Func func) - { - table.Add(new CallSignature(name, typeof(R)), InvokeeFactory.Wrap(func)); - } - - public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) - { - if (typeof(A) != typeof(EvaluationContext)) - table.Add(new CallSignature(name, typeof(R), typeof(A)), InvokeeFactory.Wrap(func, doNullProp)); - else - table.Add(new CallSignature(name, typeof(R)), InvokeeFactory.Wrap(func, doNullProp)); - } - - public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) - { - if (typeof(B) != typeof(EvaluationContext)) - table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B)), InvokeeFactory.Wrap(func, doNullProp)); - else - table.Add(new CallSignature(name, typeof(R), typeof(A)), InvokeeFactory.Wrap(func, doNullProp)); - } + _entries.Add(new TableEntry(signature, body)); + } - public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) + public SymbolTable Filter(string name, int argCount) + { + var result = new SymbolTable { - if (typeof(C) != typeof(EvaluationContext)) - table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C)), InvokeeFactory.Wrap(func, doNullProp)); - else - table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B)), InvokeeFactory.Wrap(func, doNullProp)); - } + _entries = new ConcurrentBag(_entries.Where(e => e.Signature.Matches(name, argCount))) + }; - public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) - { - if (typeof(D) != typeof(EvaluationContext)) - table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C), typeof(D)), InvokeeFactory.Wrap(func, doNullProp)); - else - table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C)), InvokeeFactory.Wrap(func, doNullProp)); + if (Parent != null) + result.Parent = Parent.Filter(name, argCount); - } + return result; + } - public static void AddLogic(this SymbolTable table, string name, Func, Func, bool?> func) - { - table.Add(new CallSignature(name, typeof(bool?), typeof(object), typeof(Func), typeof(Func)), - InvokeeFactory.WrapLogic(func)); - } + internal Invokee? DynamicGet(string name, IEnumerable args) + { + var exactMatches = _entries.Where(e => e.Signature.DynamicExactMatches(name, args)); + var entry = exactMatches.Union(_entries.Where(e => e.Signature.DynamicMatches(name, args))).FirstOrDefault(); - public static void AddVar(this SymbolTable table, string name, object value) - { - table.AddVar(name, ElementNode.ForPrimitive(value)); - } + if (entry == null && Parent != null) return Parent.DynamicGet(name, args); - public static void AddVar(this SymbolTable table, string name, ITypedElement value) - { - table.Add(new CallSignature(name, typeof(string)), InvokeeFactory.Return(value)); - } + return entry?.Body; } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs new file mode 100644 index 0000000000..205518bb22 --- /dev/null +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2015, Firely (info@fire.ly) and contributors + * See the file CONTRIBUTORS for details. + * + * This file is licensed under the BSD 3-Clause license + * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE + */ + +#nullable enable + +using Hl7.Fhir.ElementModel; +using System; + +namespace Hl7.FhirPath.Expressions; + +public static class SymbolTableExtensions +{ + public static void Add(this SymbolTable table, string name, Func func) + { + table.Add(new CallSignature(name, typeof(R)), InvokeeFactory.Wrap(func)); + } + + public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) + { + table.Add( + typeof(A) != typeof(EvaluationContext) + ? new CallSignature(name, typeof(R), typeof(A)) + : new CallSignature(name, typeof(R)), InvokeeFactory.Wrap(func, doNullProp)); + } + + public static void Add(this SymbolTable table, string name, Func func, bool doNullProp = false) + { + if (typeof(B) != typeof(EvaluationContext)) + table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B)), InvokeeFactory.Wrap(func, doNullProp)); + else + table.Add(new CallSignature(name, typeof(R), typeof(A)), InvokeeFactory.Wrap(func, doNullProp)); + } + + public static void Add(this SymbolTable table, string name, Func func, + bool doNullProp = false) + { + if (typeof(C) != typeof(EvaluationContext)) + table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C)), + InvokeeFactory.Wrap(func, doNullProp)); + else + table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B)), InvokeeFactory.Wrap(func, doNullProp)); + } + + public static void Add(this SymbolTable table, string name, Func func, + bool doNullProp = false) + { + if (typeof(D) != typeof(EvaluationContext)) + table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C), typeof(D)), + InvokeeFactory.Wrap(func, doNullProp)); + else + table.Add(new CallSignature(name, typeof(R), typeof(A), typeof(B), typeof(C)), + InvokeeFactory.Wrap(func, doNullProp)); + } + + public static void AddLogic(this SymbolTable table, string name, Func, Func, bool?> func) + { + table.Add(new CallSignature(name, typeof(bool?), typeof(object), typeof(Func), typeof(Func)), + InvokeeFactory.WrapLogic(func)); + } + + public static void AddVar(this SymbolTable table, string name, object value) + { + table.AddVar(name, ElementNode.ForPrimitive(value)); + } + + public static void AddVar(this SymbolTable table, string name, ITypedElement value) + { + table.Add(new CallSignature(name, typeof(string)), InvokeeFactory.Return(value)); + } +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs index e7544ce47c..4b2ba6f94f 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs @@ -6,6 +6,8 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + using Hl7.Fhir.ElementModel; using Hl7.Fhir.Utility; using Hl7.FhirPath.FhirPath.Functions; @@ -16,423 +18,370 @@ using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; -namespace Hl7.FhirPath.Expressions +namespace Hl7.FhirPath.Expressions; + +public static class SymbolTableInit { - public static class SymbolTableInit + /// + /// Add the function library for the standard FhirPath Normative dialect to the . + /// + public static SymbolTable AddStandardFP(this SymbolTable t) { - /// - /// Add the function library for the standard FhirPath Normative dialect to the . - /// - public static SymbolTable AddStandardFP(this SymbolTable t) - { - // Functions that operate on the focus, without null propagation - t.Add("empty", (IEnumerable f) => !f.Any()); - t.Add("exists", (IEnumerable f) => f.Any()); - - t.Add("count", (IEnumerable f) => f.Count()); - t.Add("trace", (IEnumerable f, string name, EvaluationContext ctx) - => f.Trace(name, ctx)); - - t.Add("allTrue", (IEnumerable f) => f.All(e => e.Value as bool? == true)); - t.Add("anyTrue", (IEnumerable f) => f.Any(e => e.Value as bool? == true)); - t.Add("allFalse", (IEnumerable f) => f.All(e => e.Value as bool? == false)); - t.Add("anyFalse", (IEnumerable f) => f.Any(e => e.Value as bool? == false)); - t.Add("combine", (IEnumerable l, IEnumerable r) => l.Concat(r)); - t.Add("binary.|", (object _, IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); - t.Add("union", (IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); - t.Add("binary.contains", (object _, IEnumerable a, ITypedElement b) => a.Contains(b)); - t.Add("binary.in", (object _, ITypedElement a, IEnumerable b) => b.Contains(a)); - t.Add("distinct", (IEnumerable f) => f.Distinct()); - t.Add("isDistinct", (IEnumerable f) => f.IsDistinct()); - t.Add("subsetOf", (IEnumerable f, IEnumerable a) => f.SubsetOf(a)); - t.Add("supersetOf", (IEnumerable f, IEnumerable a) => a.SubsetOf(f)); - t.Add("intersect", (IEnumerable f, IEnumerable a) => f.Intersect(a)); - t.Add("exclude", (IEnumerable f, IEnumerable a) => f.Exclude(a)); - - t.Add("today", (object _) => P.Date.Today()); - t.Add("now", (object _) => P.DateTime.Now()); - t.Add("timeOfDay", (object _) => P.Time.Now()); - - t.Add("binary.&", (object _, string a, string b) => (a ?? "") + (b ?? "")); - - t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee), typeof(Invokee)), runIif); - t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee)), runIif); - - // Functions that use normal null propagation and work with the focus (buy may ignore it) - t.Add("not", (IEnumerable f) => f.Not(), doNullProp: true); - // t.Add("builtin.children", (IEnumerable f, string a) => f.Navigate(a), doNullProp: true); - t.AddBuiltinChildren(); - - t.Add("children", (IEnumerable f) => f.Children(), doNullProp: true); - t.Add("descendants", (IEnumerable f) => f.Descendants(), doNullProp: true); - - t.Add("binary.=", (object f, IEnumerable a, IEnumerable b) => a.IsEqualTo(b), doNullProp: true); - t.Add("binary.!=", (object f, IEnumerable a, IEnumerable b) => !a.IsEqualTo(b), doNullProp: true); - t.Add("binary.~", (object f, IEnumerable a, IEnumerable b) => a.IsEquivalentTo(b), doNullProp: false); - t.Add("binary.!~", (object f, IEnumerable a, IEnumerable b) => !a.IsEquivalentTo(b), doNullProp: false); - - t.Add("unary.-", (object f, int a) => -a, doNullProp: true); - t.Add("unary.-", (object f, long a) => -a, doNullProp: true); - t.Add("unary.-", (object f, decimal a) => -a, doNullProp: true); - t.Add("unary.-", (object f, P.Quantity a) => new P.Quantity(-a.Value, a.Unit), doNullProp: true); - t.Add("unary.+", (object f, int a) => a, doNullProp: true); - t.Add("unary.+", (object f, long a) => a, doNullProp: true); - t.Add("unary.+", (object f, decimal a) => a, doNullProp: true); - t.Add("unary.+", (object f, P.Quantity a) => a, doNullProp: true); - - t.Add("binary.*", (object f, int a, int b) => a * b, doNullProp: true); - t.Add("binary.*", (object f, long a, long b) => a * b, doNullProp: true); - t.Add("binary.*", (object f, decimal a, decimal b) => a * b, doNullProp: true); - t.Add("binary.*", (object f, P.Quantity a, P.Quantity b) => a * b, doNullProp: true); - - t.Add("binary./", (object f, decimal a, decimal b) => b != 0 ? a / b : (decimal?)null, doNullProp: true); - t.Add("binary./", (object f, P.Quantity a, P.Quantity b) => a / b, doNullProp: true); - - t.Add("binary.+", (object f, int a, int b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, long a, long b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, decimal a, decimal b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, string a, string b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, P.DateTime a, P.Quantity b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, P.Date a, P.Quantity b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, P.Quantity a, P.Quantity b) => a + b, doNullProp: true); - - t.Add("binary.-", (object f, int a, int b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, long a, long b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, decimal a, decimal b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, P.DateTime a, P.Quantity b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, P.Date a, P.Quantity b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, P.Quantity a, P.Quantity b) => a - b, doNullProp: true); - - t.Add("binary.div", (object f, int a, int b) => b != 0 ? a / b : (int?)null, doNullProp: true); - t.Add("binary.div", (object f, long a, long b) => b != 0 ? a / b : (long?)null, doNullProp: true); - t.Add("binary.div", (object f, decimal a, decimal b) => b != 0 ? (long?)Math.Truncate(a / b) : null, doNullProp: true); - - t.Add("binary.mod", (object f, int a, int b) => b != 0 ? a % b : (int?)null, doNullProp: true); - t.Add("binary.mod", (object f, long a, long b) => b != 0 ? a % b : (long?)null, doNullProp: true); - t.Add("binary.mod", (object f, decimal a, decimal b) => b != 0 ? a % b : (decimal?)null, doNullProp: true); - - t.Add("binary.>", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">"), doNullProp: true); - t.Add("binary.<", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<"), doNullProp: true); - t.Add("binary.<=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<="), doNullProp: true); - t.Add("binary.>=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">="), doNullProp: true); - - t.Add("single", (IEnumerable f) => f.Single(), doNullProp: true); - t.Add("skip", (IEnumerable f, long a) => f.Skip((int)a), doNullProp: true); - t.Add("first", (IEnumerable f) => f.First(), doNullProp: true); - t.Add("last", (IEnumerable f) => f.Last(), doNullProp: true); - t.Add("tail", (IEnumerable f) => f.Tail(), doNullProp: true); - t.Add("take", (IEnumerable f, long a) => f.Take((int)a), doNullProp: true); - t.Add("builtin.item", (IEnumerable f, long a) => f.Item((int)a), doNullProp: true); - - t.Add("toBoolean", (P.Any f) => f.ToBoolean(), doNullProp: true); - t.Add("convertsToBoolean", (P.Any f) => f.ConvertsToBoolean(), doNullProp: true); - t.Add("toInteger", (P.Any f) => f.ToInteger(), doNullProp: true); - t.Add("convertsToInteger", (P.Any f) => f.ConvertsToInteger(), doNullProp: true); - t.Add("toLong", (P.Any f) => f.ToLong(), doNullProp: true); - t.Add("convertsToLong", (P.Any f) => f.ConvertsToLong(), doNullProp: true); - t.Add("toDecimal", (P.Any f) => f.ToDecimal(), doNullProp: true); - t.Add("convertsToDecimal", (P.Any f) => f.ConvertsToDecimal(), doNullProp: true); - t.Add("toQuantity", (P.Any f) => f.ToQuantity(), doNullProp: true); - t.Add("convertsToQuantity", (P.Any f) => f.ConvertsToQuantity(), doNullProp: true); - t.Add("toString", (P.Any f) => f.ToStringRepresentation(), doNullProp: true); - t.Add("convertsToString", (P.Any f) => f.ConvertsToString(), doNullProp: true); - t.Add("toDate", (P.Any f) => f.ToDate(), doNullProp: true); - t.Add("convertsToDate", (P.Any f) => f.ConvertsToDate(), doNullProp: true); - t.Add("toDateTime", (P.Any f) => f.ToDateTime(), doNullProp: true); - t.Add("convertsToDateTime", (P.Any f) => f.ConvertsToDateTime(), doNullProp: true); - t.Add("toTime", (P.Any f) => f.ToTime(), doNullProp: true); - t.Add("convertsToTime", (P.Any f) => f.ConvertsToTime(), doNullProp: true); - - t.Add("upper", (string f) => f.ToUpper(), doNullProp: true); - t.Add("lower", (string f) => f.ToLower(), doNullProp: true); - t.Add("toChars", (string f) => f.ToChars(), doNullProp: true); - t.Add("substring", (string f, int a) => f.FpSubstring(a, null), doNullProp: true); - t.Add("trim", (string f) => f.Trim(), doNullProp: true); - t.Add("encode", (string f, string enc) => f.FpEncode(enc), doNullProp: true); - t.Add("decode", (string f, string enc) => f.FpDecode(enc), doNullProp: true); - t.Add("escape", (string f, string enc) => f.FpEscape(enc), doNullProp: true); - t.Add("unescape", (string f, string enc) => f.FpUnescape(enc), doNullProp: true); - - //special case: only focus should be Null propagated: - t.Add(new CallSignature("substring", typeof(string), typeof(string), typeof(int), typeof(int?)), - InvokeeFactory.WrapWithPropNullForFocus((string f, int a, int? b) => f.FpSubstring(a, b))); - t.Add("startsWith", (string f, string fragment) => f.StartsWith(fragment), doNullProp: true); - t.Add("endsWith", (string f, string fragment) => f.EndsWith(fragment), doNullProp: true); - t.Add("matches", (string f, string regex) => Regex.IsMatch(f, regex), doNullProp: true); - t.Add("indexOf", (string f, string fragment) => f.FpIndexOf(fragment), doNullProp: true); - t.Add("contains", (string f, string fragment) => f.Contains(fragment), doNullProp: true); - t.Add("replaceMatches", (string f, string regex, string subst) => Regex.Replace(f, regex, subst), doNullProp: true); - t.Add("replace", (string f, string regex, string subst) => f.FpReplace(regex, subst), doNullProp: true); - t.Add("length", (string f) => f.Length, doNullProp: true); - t.Add("split", (string f, string seperator) => f.FpSplit(seperator), doNullProp: true); - t.Add("join", (IEnumerable f, string separator) => f.FpJoin(separator), doNullProp: true); - t.Add("join", (IEnumerable f) => f.FpJoin(), doNullProp: true); - t.Add("indexOf", (IEnumerable f, ITypedElement elem, int start) => f.IndexOf(elem, start), doNullProp: true); - t.Add("indexOf", (IEnumerable f, ITypedElement elem) => f.IndexOf(elem), doNullProp: true); - t.Add("lastIndexOf", (IEnumerable f, ITypedElement elem, int start) => f.LastIndexOf(elem, start), doNullProp: true); - t.Add("lastIndexOf", (IEnumerable f, ITypedElement elem) => f.LastIndexOf(elem), doNullProp: true); - - // Math functions - t.Add("abs", (decimal f) => Math.Abs(f), doNullProp: true); - t.Add("abs", (P.Quantity f) => new P.Quantity(Math.Abs(f.Value), f.Unit), doNullProp: true); - t.Add("ceiling", (decimal f) => Math.Ceiling(f), doNullProp: true); - t.Add("exp", (decimal f) => Math.Exp((double)f), doNullProp: true); - t.Add("floor", (decimal f) => Math.Floor(f), doNullProp: true); - t.Add("ln", (decimal f) => Math.Log((double)f), doNullProp: true); - t.Add("log", (decimal f, decimal @base) => Math.Log((double)f, (double)@base), doNullProp: true); - t.Add("power", (decimal f, decimal exponent) => f.Power(exponent), doNullProp: true); - t.Add("round", (decimal f, long precision) => Math.Round(f, (int)precision), doNullProp: true); - t.Add("round", (decimal f) => Math.Round(f), doNullProp: true); - t.Add("sqrt", (decimal f) => f.Sqrt(), doNullProp: true); - t.Add("truncate", (decimal f) => Math.Truncate((double)f), doNullProp: true); - - // The next two functions existed pre-normative, so we have kept them. - t.Add("is", (ITypedElement f, string name) => f.Is(name), doNullProp: true); - t.Add("as", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); - - t.Add("ofType", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); - t.Add("binary.is", (object f, ITypedElement left, string name) => left.Is(name), doNullProp: true); - t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); - - // Kept for backwards compatibility, but no longer part of the spec - t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); - - t.Add("extension", (IEnumerable f, string url) => f.Extension(url), doNullProp: true); - - // Logic operators do not use null propagation and may do short-cut eval - t.AddLogic("binary.and", (a, b) => a.And(b)); - t.AddLogic("binary.or", (a, b) => a.Or(b)); - t.AddLogic("binary.xor", (a, b) => a.XOr(b)); - t.AddLogic("binary.implies", (a, b) => a.Implies(b)); - - // Special late-bound functions - t.Add(new CallSignature("where", typeof(IEnumerable), typeof(object), typeof(Invokee)), runWhere); - t.Add(new CallSignature("select", typeof(IEnumerable), typeof(object), typeof(Invokee)), runSelect); - t.Add(new CallSignature("all", typeof(bool), typeof(object), typeof(Invokee)), runAll); - t.Add(new CallSignature("any", typeof(bool), typeof(object), typeof(Invokee)), runAny); - t.Add(new CallSignature("exists", typeof(bool), typeof(object), typeof(Invokee)), runAny); - t.Add(new CallSignature("repeat", typeof(IEnumerable), typeof(object), typeof(Invokee)), runRepeat); - t.Add(new CallSignature("trace", typeof(IEnumerable), typeof(string), typeof(object), typeof(Invokee)), Trace); - t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string)), DefineVariable); - t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string), typeof(Invokee)), DefineVariable); - - t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee)), runAggregate); - t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate); - - t.AddVar("sct", "http://snomed.info/sct"); - t.AddVar("loinc", "http://loinc.org"); - t.AddVar("ucum", "http://unitsofmeasure.org"); - - t.Add("builtin.coreexturl", (object f, string id) => getCoreExtensionUrl(id)); - t.Add("builtin.corevsurl", (object f, string id) => getCoreValueSetUrl(id)); - - return t; - } + // Functions that operate on the focus, without null propagation + t.Add("empty", (IEnumerable f) => !f.Any()); + t.Add("exists", (IEnumerable f) => f.Any()); + + t.Add("count", (IEnumerable f) => f.Count()); + t.Add("trace", (IEnumerable f, string name, EvaluationContext ctx) + => f.Trace(name, ctx)); + + t.Add("allTrue", (IEnumerable f) => f.All(e => e.Value as bool? == true)); + t.Add("anyTrue", (IEnumerable f) => f.Any(e => e.Value as bool? == true)); + t.Add("allFalse", (IEnumerable f) => f.All(e => e.Value as bool? == false)); + t.Add("anyFalse", (IEnumerable f) => f.Any(e => e.Value as bool? == false)); + t.Add("combine", (IEnumerable l, IEnumerable r) => l.Concat(r)); + t.Add("binary.|", (object _, IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); + t.Add("union", (IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); + t.Add("binary.contains", (object _, IEnumerable a, ITypedElement b) => a.Contains(b)); + t.Add("binary.in", (object _, ITypedElement a, IEnumerable b) => b.Contains(a)); + t.Add("distinct", (IEnumerable f) => f.Distinct()); + t.Add("isDistinct", (IEnumerable f) => f.IsDistinct()); + t.Add("subsetOf", (IEnumerable f, IEnumerable a) => f.SubsetOf(a)); + t.Add("supersetOf", (IEnumerable f, IEnumerable a) => a.SubsetOf(f)); + t.Add("intersect", (IEnumerable f, IEnumerable a) => f.Intersect(a)); + t.Add("exclude", (IEnumerable f, IEnumerable a) => f.Exclude(a)); + + t.Add("today", (object _) => P.Date.Today()); + t.Add("now", (object _) => P.DateTime.Now()); + t.Add("timeOfDay", (object _) => P.Time.Now()); + + t.Add("binary.&", (object _, string a, string b) => (a ?? "") + (b ?? "")); + + t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee), typeof(Invokee)), runIif); + t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee)), runIif); + + // Functions that use normal null propagation and work with the focus (buy may ignore it) + t.Add("not", (IEnumerable f) => f.Not(), doNullProp: true); + // t.Add("builtin.children", (IEnumerable f, string a) => f.Navigate(a), doNullProp: true); + t.AddBuiltinChildren(); + + t.Add("children", (IEnumerable f) => f.Children(), doNullProp: true); + t.Add("descendants", (IEnumerable f) => f.Descendants(), doNullProp: true); + + t.Add("binary.=", (object f, IEnumerable a, IEnumerable b) => a.IsEqualTo(b), doNullProp: true); + t.Add("binary.!=", (object f, IEnumerable a, IEnumerable b) => !a.IsEqualTo(b), doNullProp: true); + t.Add("binary.~", (object f, IEnumerable a, IEnumerable b) => a.IsEquivalentTo(b), doNullProp: false); + t.Add("binary.!~", (object f, IEnumerable a, IEnumerable b) => !a.IsEquivalentTo(b), doNullProp: false); + + t.Add("unary.-", (object f, int a) => -a, doNullProp: true); + t.Add("unary.-", (object f, long a) => -a, doNullProp: true); + t.Add("unary.-", (object f, decimal a) => -a, doNullProp: true); + t.Add("unary.-", (object f, P.Quantity a) => new P.Quantity(-a.Value, a.Unit), doNullProp: true); + t.Add("unary.+", (object f, int a) => a, doNullProp: true); + t.Add("unary.+", (object f, long a) => a, doNullProp: true); + t.Add("unary.+", (object f, decimal a) => a, doNullProp: true); + t.Add("unary.+", (object f, P.Quantity a) => a, doNullProp: true); + + t.Add("binary.*", (object f, int a, int b) => a * b, doNullProp: true); + t.Add("binary.*", (object f, long a, long b) => a * b, doNullProp: true); + t.Add("binary.*", (object f, decimal a, decimal b) => a * b, doNullProp: true); + t.Add("binary.*", (object f, P.Quantity a, P.Quantity b) => a * b, doNullProp: true); + + t.Add("binary./", (object f, decimal a, decimal b) => b != 0 ? a / b : (decimal?)null, doNullProp: true); + t.Add("binary./", (object f, P.Quantity a, P.Quantity b) => a / b, doNullProp: true); + + t.Add("binary.+", (object f, int a, int b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, long a, long b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, decimal a, decimal b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, string a, string b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, P.DateTime a, P.Quantity b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, P.Date a, P.Quantity b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, P.Quantity a, P.Quantity b) => a + b, doNullProp: true); + + t.Add("binary.-", (object f, int a, int b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, long a, long b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, decimal a, decimal b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, P.DateTime a, P.Quantity b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, P.Date a, P.Quantity b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, P.Quantity a, P.Quantity b) => a - b, doNullProp: true); + + t.Add("binary.div", (object f, int a, int b) => b != 0 ? a / b : (int?)null, doNullProp: true); + t.Add("binary.div", (object f, long a, long b) => b != 0 ? a / b : (long?)null, doNullProp: true); + t.Add("binary.div", (object f, decimal a, decimal b) => b != 0 ? (long?)Math.Truncate(a / b) : null, doNullProp: true); + + t.Add("binary.mod", (object f, int a, int b) => b != 0 ? a % b : (int?)null, doNullProp: true); + t.Add("binary.mod", (object f, long a, long b) => b != 0 ? a % b : (long?)null, doNullProp: true); + t.Add("binary.mod", (object f, decimal a, decimal b) => b != 0 ? a % b : (decimal?)null, doNullProp: true); + + t.Add("binary.>", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">"), doNullProp: true); + t.Add("binary.<", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<"), doNullProp: true); + t.Add("binary.<=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<="), doNullProp: true); + t.Add("binary.>=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">="), doNullProp: true); + + t.Add("single", (IEnumerable f) => f.Single(), doNullProp: true); + t.Add("skip", (IEnumerable f, long a) => f.Skip((int)a), doNullProp: true); + t.Add("first", (IEnumerable f) => f.First(), doNullProp: true); + t.Add("last", (IEnumerable f) => f.Last(), doNullProp: true); + t.Add("tail", (IEnumerable f) => f.Tail(), doNullProp: true); + t.Add("take", (IEnumerable f, long a) => f.Take((int)a), doNullProp: true); + t.Add("builtin.item", (IEnumerable f, long a) => f.Item((int)a), doNullProp: true); + + t.Add("toBoolean", (P.Any f) => f.ToBoolean(), doNullProp: true); + t.Add("convertsToBoolean", (P.Any f) => f.ConvertsToBoolean(), doNullProp: true); + t.Add("toInteger", (P.Any f) => f.ToInteger(), doNullProp: true); + t.Add("convertsToInteger", (P.Any f) => f.ConvertsToInteger(), doNullProp: true); + t.Add("toLong", (P.Any f) => f.ToLong(), doNullProp: true); + t.Add("convertsToLong", (P.Any f) => f.ConvertsToLong(), doNullProp: true); + t.Add("toDecimal", (P.Any f) => f.ToDecimal(), doNullProp: true); + t.Add("convertsToDecimal", (P.Any f) => f.ConvertsToDecimal(), doNullProp: true); + t.Add("toQuantity", (P.Any f) => f.ToQuantity(), doNullProp: true); + t.Add("convertsToQuantity", (P.Any f) => f.ConvertsToQuantity(), doNullProp: true); + t.Add("toString", (P.Any f) => f.ToStringRepresentation(), doNullProp: true); + t.Add("convertsToString", (P.Any f) => f.ConvertsToString(), doNullProp: true); + t.Add("toDate", (P.Any f) => f.ToDate(), doNullProp: true); + t.Add("convertsToDate", (P.Any f) => f.ConvertsToDate(), doNullProp: true); + t.Add("toDateTime", (P.Any f) => f.ToDateTime(), doNullProp: true); + t.Add("convertsToDateTime", (P.Any f) => f.ConvertsToDateTime(), doNullProp: true); + t.Add("toTime", (P.Any f) => f.ToTime(), doNullProp: true); + t.Add("convertsToTime", (P.Any f) => f.ConvertsToTime(), doNullProp: true); + + t.Add("upper", (string f) => f.ToUpper(), doNullProp: true); + t.Add("lower", (string f) => f.ToLower(), doNullProp: true); + t.Add("toChars", (string f) => f.ToChars(), doNullProp: true); + t.Add("substring", (string f, int a) => f.FpSubstring(a, null), doNullProp: true); + t.Add("trim", (string f) => f.Trim(), doNullProp: true); + t.Add("encode", (string f, string enc) => f.FpEncode(enc), doNullProp: true); + t.Add("decode", (string f, string enc) => f.FpDecode(enc), doNullProp: true); + t.Add("escape", (string f, string enc) => f.FpEscape(enc), doNullProp: true); + t.Add("unescape", (string f, string enc) => f.FpUnescape(enc), doNullProp: true); + + //special case: only focus should be Null propagated: + t.Add(new CallSignature("substring", typeof(string), typeof(string), typeof(int), typeof(int?)), + InvokeeFactory.WrapWithPropNullForFocus((string f, int a, int? b) => f.FpSubstring(a, b))); + t.Add("startsWith", (string f, string fragment) => f.StartsWith(fragment), doNullProp: true); + t.Add("endsWith", (string f, string fragment) => f.EndsWith(fragment), doNullProp: true); + t.Add("matches", (string f, string regex) => Regex.IsMatch(f, regex), doNullProp: true); + t.Add("indexOf", (string f, string fragment) => f.FpIndexOf(fragment), doNullProp: true); + t.Add("contains", (string f, string fragment) => f.Contains(fragment), doNullProp: true); + t.Add("replaceMatches", (string f, string regex, string subst) => Regex.Replace(f, regex, subst), doNullProp: true); + t.Add("replace", (string f, string regex, string subst) => f.FpReplace(regex, subst), doNullProp: true); + t.Add("length", (string f) => f.Length, doNullProp: true); + t.Add("split", (string f, string seperator) => f.FpSplit(seperator), doNullProp: true); + t.Add("join", (IEnumerable f, string separator) => f.FpJoin(separator), doNullProp: true); + t.Add("join", (IEnumerable f) => f.FpJoin(), doNullProp: true); + t.Add("indexOf", (IEnumerable f, ITypedElement elem, int start) => f.IndexOf(elem, start), doNullProp: true); + t.Add("indexOf", (IEnumerable f, ITypedElement elem) => f.IndexOf(elem), doNullProp: true); + t.Add("lastIndexOf", (IEnumerable f, ITypedElement elem, int start) => f.LastIndexOf(elem, start), doNullProp: true); + t.Add("lastIndexOf", (IEnumerable f, ITypedElement elem) => f.LastIndexOf(elem), doNullProp: true); + + // Math functions + t.Add("abs", (decimal f) => Math.Abs(f), doNullProp: true); + t.Add("abs", (P.Quantity f) => new P.Quantity(Math.Abs(f.Value), f.Unit), doNullProp: true); + t.Add("ceiling", (decimal f) => Math.Ceiling(f), doNullProp: true); + t.Add("exp", (decimal f) => Math.Exp((double)f), doNullProp: true); + t.Add("floor", (decimal f) => Math.Floor(f), doNullProp: true); + t.Add("ln", (decimal f) => Math.Log((double)f), doNullProp: true); + t.Add("log", (decimal f, decimal @base) => Math.Log((double)f, (double)@base), doNullProp: true); + t.Add("power", (decimal f, decimal exponent) => f.Power(exponent), doNullProp: true); + + t.Add("round", (decimal f, long precision) => Math.Round(f, (int)precision), doNullProp: true); + t.Add("round", (decimal f) => Math.Round(f), doNullProp: true); + t.Add("sqrt", (decimal f) => f.Sqrt(), doNullProp: true); + t.Add("truncate", (decimal f) => Math.Truncate((double)f), doNullProp: true); + + // The next two functions existed pre-normative, so we have kept them. + t.Add("is", (ITypedElement f, string name) => f.Is(name), doNullProp: true); + t.Add("as", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); + + t.Add("ofType", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); + t.Add("binary.is", (object f, ITypedElement left, string name) => left.Is(name), doNullProp: true); + t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); + + // Kept for backwards compatibility, but no longer part of the spec + t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); + + t.Add("extension", (IEnumerable f, string url) => f.Extension(url), doNullProp: true); + + // Logic operators do not use null propagation and may do short-cut eval + t.AddLogic("binary.and", (a, b) => a.And(b)); + t.AddLogic("binary.or", (a, b) => a.Or(b)); + t.AddLogic("binary.xor", (a, b) => a.XOr(b)); + t.AddLogic("binary.implies", (a, b) => a.Implies(b)); + + // Special late-bound functions + t.Add(new CallSignature("where", typeof(IEnumerable), typeof(object), typeof(Invokee)), runWhere); + t.Add(new CallSignature("select", typeof(IEnumerable), typeof(object), typeof(Invokee)), runSelect); + t.Add(new CallSignature("all", typeof(bool), typeof(object), typeof(Invokee)), runAll); + t.Add(new CallSignature("any", typeof(bool), typeof(object), typeof(Invokee)), runAny); + t.Add(new CallSignature("exists", typeof(bool), typeof(object), typeof(Invokee)), runAny); + t.Add(new CallSignature("repeat", typeof(IEnumerable), typeof(object), typeof(Invokee)), runRepeat); + t.Add(new CallSignature("trace", typeof(IEnumerable), typeof(string), typeof(object), typeof(Invokee)), Trace); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string)), DefineVariable); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string), typeof(Invokee)), DefineVariable); + + t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee)), runAggregate); + t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate); + + t.AddVar("sct", "http://snomed.info/sct"); + t.AddVar("loinc", "http://loinc.org"); + t.AddVar("ucum", "http://unitsofmeasure.org"); + + t.Add("builtin.coreexturl", (object f, string id) => getCoreExtensionUrl(id)); + t.Add("builtin.corevsurl", (object f, string id) => getCoreValueSetUrl(id)); + + return t; + } - /// - /// With the regular Add extension methods, a Wrap is added to each argument to turn it into IEnumerable<ITypedElement>. - /// For 'builtin.children' we know that the focus and the result are already of the correct type, - /// so we created an optimized implementation avoiding the Wrap. - /// - /// - internal static void AddBuiltinChildren(this SymbolTable table) + /// + /// With the regular Add extension methods, a Wrap is added to each argument to turn it into IEnumerable<ITypedElement>. + /// For 'builtin.children' we know that the focus and the result are already of the correct type, + /// so we created an optimized implementation avoiding the Wrap. + /// + /// + internal static void AddBuiltinChildren(this SymbolTable table) + { + table.Add(new CallSignature("builtin.children", + typeof(IEnumerable), + typeof(IEnumerable), + typeof(string)), ( + ctx, invokees) => { - table.Add(new CallSignature("builtin.children", - typeof(IEnumerable), - typeof(IEnumerable), - typeof(string)), ( - ctx, invokees) => - { - var iks = invokees.ToArray(); - var focus = iks[0].Invoke(ctx, InvokeeFactory.EmptyArgs); - var name = (string)iks[1].Invoke(ctx, InvokeeFactory.EmptyArgs).First().Value; - var result= focus.Navigate(name); + var iks = invokees.ToArray(); + var focus = iks[0].Invoke(ctx, InvokeeFactory.EmptyArgs); + var name = (string?)iks[1].Invoke(ctx, InvokeeFactory.EmptyArgs).First().Value; + var result= focus.Navigate(name); - return result; - }); - } + return result; + }); + } + + private static string getCoreExtensionUrl(string id) + { + return "http://hl7.org/fhir/StructureDefinition/" + id; + } - private static string getCoreExtensionUrl(string id) + private static string getCoreValueSetUrl(string id) + { + return "http://hl7.org/fhir/ValueSet/" + id; + } + + private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var incrExpre = arguments.Skip(1).First(); + IEnumerable initialValue = ElementNode.EmptyList; + if (arguments.Count() > 2) { - return "http://hl7.org/fhir/StructureDefinition/" + id; + var initialValueExpr = arguments.Skip(2).First(); + initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs); } - private static string getCoreValueSetUrl(string id) + var totalContext = ctx.Nest(); + totalContext.SetTotal(initialValue); + + foreach (ITypedElement element in focus) { - return "http://hl7.org/fhir/ValueSet/" + id; + var newFocus = ElementNode.CreateList(element); + var newContext = totalContext.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetTotal(totalContext.GetTotal()); + var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); + totalContext.SetTotal(newTotalResult); } - private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var incrExpre = arguments.Skip(1).First(); - IEnumerable initialValue = ElementNode.EmptyList; - if (arguments.Count() > 2) - { - var initialValueExpr = arguments.Skip(2).First(); - initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs); - } + return totalContext.GetTotal(); + } - var totalContext = ctx.Nest(); - totalContext.SetTotal(initialValue); + private static IEnumerable Trace(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; - foreach (ITypedElement element in focus) - { - var newFocus = ElementNode.CreateList(element); - var newContext = totalContext.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetTotal(totalContext.GetTotal()); - var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); - totalContext.SetTotal(newTotalResult); - } + List selectArgs = [arguments.First(), .. arguments.Skip(2)]; + var selectResults = runSelect(ctx, selectArgs); + ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); - return totalContext.GetTotal(); - } + return focus; + } - private static IEnumerable Trace(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - string name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; + private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments) + { + Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); + var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); + var name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; - List selectArgs = [arguments.First(), .. arguments.Skip(2)]; - var selectResults = runSelect(ctx, selectArgs); - ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); + if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); - return focus; - } - - private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments) + if (enumerable.Length == 2) { - Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); - var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); - string name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.Value as string; - - if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); - - if (enumerable.Length == 2) - { - ctx.SetValue(name, focus); - } - else - { - var newContext = ctx.Nest(focus); - newContext.SetThis(focus); - var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); - ctx.SetValue(name, result); - } - - return focus; + ctx.SetValue(name, focus); } - - private static IEnumerable runIif(Closure ctx, IEnumerable arguments) + else { - // iif(criterion: expression, true-result: collection [, otherwise-result: collection]) : collection - // note: short-circuit behavior is expected in this function - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var newContext = ctx.Nest(focus); newContext.SetThis(focus); - - var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs); - var trueResult = arguments.Skip(2).First(); - var otherResult = arguments.Skip(3).FirstOrDefault(); + var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); + ctx.SetValue(name, result); + } - if (expression.Count() > 1) - throw Error.InvalidOperation($"Result of {nameof(expression)} is not of type boolean"); + return focus; + } - return (expression.BooleanEval() ?? false) - ? trueResult(newContext, InvokeeFactory.EmptyArgs) // share focus with this function - : otherResult == null ? ElementNode.EmptyList : otherResult(newContext, InvokeeFactory.EmptyArgs); - } + private static IEnumerable runIif(Closure ctx, IEnumerable arguments) + { + // iif(criterion: expression, true-result: collection [, otherwise-result: collection]) : collection + // note: short-circuit behavior is expected in this function + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - private static IEnumerable runWhere(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); + var newContext = ctx.Nest(focus); + newContext.SetThis(focus); - return CachedEnumerable.Create(runForeach()); + var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs); + var trueResult = arguments.Skip(2).First(); + var otherResult = arguments.Skip(3).FirstOrDefault(); - IEnumerable runForeach() - { - var index = 0; - - foreach (ITypedElement element in focus) - { - var newFocus = ElementNode.CreateList(element); - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(ElementNode.CreateList(index)); - index++; - - if (lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval() == true) - yield return element; - } - } - } + if (expression.Count() > 1) + throw Error.InvalidOperation($"Result of {nameof(expression)} is not of type boolean"); - private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); + return (expression.BooleanEval() ?? false) + ? trueResult(newContext, InvokeeFactory.EmptyArgs) // share focus with this function + : otherResult == null ? ElementNode.EmptyList : otherResult(newContext, InvokeeFactory.EmptyArgs); + } - return CachedEnumerable.Create(runForeach()); + private static IEnumerable runWhere(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); - IEnumerable runForeach() - { - var index = 0; - - foreach (ITypedElement element in focus) - { - var newFocus = ElementNode.CreateList(element); - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(ElementNode.CreateList(index)); - index++; - - var result = lambda(newContext, InvokeeFactory.EmptyArgs); - foreach (var resultElement in result) // implement SelectMany() - yield return resultElement; - } - } - } + return CachedEnumerable.Create(runForeach()); - private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments) + IEnumerable runForeach() { - var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); - var lambda = arguments.Skip(1).First(); - - var fullResult = new List(); + var index = 0; - while (newNodes.Any()) + foreach (ITypedElement element in focus) { - var index = 0; - var current = newNodes; - newNodes = []; - - foreach (ITypedElement element in current) - { - var newFocus = ElementNode.CreateList(element); - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(ElementNode.CreateList(index)); - index++; - - var candidates = lambda(newContext, InvokeeFactory.EmptyArgs); - var uniqeNewNodes = candidates.Except(fullResult, EqualityOperators.TypedElementEqualityComparer); - - newNodes.AddRange(uniqeNewNodes); - } + var newFocus = ElementNode.CreateList(element); + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(ElementNode.CreateList(index)); + index++; - fullResult.AddRange(newNodes); + if (lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval() == true) + yield return element; } - - return fullResult; } + } + + private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + + return CachedEnumerable.Create(runForeach()); - private static IEnumerable runAll(Closure ctx, IEnumerable arguments) + IEnumerable runForeach() { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); var index = 0; foreach (ITypedElement element in focus) @@ -443,21 +392,27 @@ private static IEnumerable runAll(Closure ctx, IEnumerable runRepeat(Closure ctx, IEnumerable arguments) + { + var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); + var lambda = arguments.Skip(1).First(); + + var fullResult = new List(); - private static IEnumerable runAny(Closure ctx, IEnumerable arguments) + while (newNodes.Any()) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); var index = 0; + var current = newNodes; + newNodes = []; - foreach (ITypedElement element in focus) + foreach (ITypedElement element in current) { var newFocus = ElementNode.CreateList(element); var newContext = ctx.Nest(newFocus); @@ -465,11 +420,58 @@ private static IEnumerable runAny(Closure ctx, IEnumerable runAll(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + var index = 0; + + foreach (ITypedElement element in focus) + { + var newFocus = ElementNode.CreateList(element); + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(ElementNode.CreateList(index)); + index++; + + var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); + if (result == null) return ElementNode.EmptyList; + if (result == false) return ElementNode.CreateList(false); } + + return ElementNode.CreateList(true); + } + + private static IEnumerable runAny(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + var index = 0; + + foreach (ITypedElement element in focus) + { + var newFocus = ElementNode.CreateList(element); + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(ElementNode.CreateList(index)); + index++; + + var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); + if (result == true) return ElementNode.CreateList(true); + } + + return ElementNode.CreateList(false); } -} +} \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/Functions/StringOperators.cs b/src/Hl7.Fhir.Base/FhirPath/Functions/StringOperators.cs index 6f21c0898b..9995e8095d 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Functions/StringOperators.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Functions/StringOperators.cs @@ -6,6 +6,8 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ +#nullable enable + using Hl7.Fhir.ElementModel; using System; using System.Collections.Generic; @@ -14,218 +16,216 @@ using System.Text; using System.Text.RegularExpressions; -namespace Hl7.FhirPath.Functions +namespace Hl7.FhirPath.Functions; + +internal static class StringOperators { - internal static class StringOperators + public static string? FpSubstring(this string me, long start, long? length) { - public static string FpSubstring(this string me, long start, long? length) + var l = length switch { - var l = length switch - { - < 0 => 0, - null => me.Length, - _ => length.Value - }; + < 0 => 0, + null => me.Length, + _ => length.Value + }; - if (start < 0 || start >= me.Length) return null; - l = Math.Min(l, me.Length - start); + if (start < 0 || start >= me.Length) return null; + l = Math.Min(l, me.Length - start); - return me.Substring((int)start, (int)l); - } + return me.Substring((int)start, (int)l); + } - public static ITypedElement FpIndexOf(this string me, string fragment) - { - return ElementNode.ForPrimitive(me.IndexOf(fragment, StringComparison.Ordinal)); - } + public static ITypedElement FpIndexOf(this string me, string fragment) + { + return ElementNode.ForPrimitive(me.IndexOf(fragment, StringComparison.Ordinal)); + } - public static IEnumerable ToChars(this string me) => - me.ToCharArray().Select(c => ElementNode.ForPrimitive(c)); + public static IEnumerable ToChars(this string me) => + me.ToCharArray().Select(c => ElementNode.ForPrimitive(c)); - public static string FpReplace(this string me, string find, string replace) + public static string FpReplace(this string me, string find, string replace) + { + if (find == String.Empty) { - if (find == String.Empty) - { - // weird, but as specified: "abc".replace("","x") = "xaxbxcx" - return replace + String.Join(replace, me.ToCharArray()) + replace; - } - else - return me.Replace(find, replace); + // weird, but as specified: "abc".replace("","x") = "xaxbxcx" + return replace + String.Join(replace, me.ToCharArray()) + replace; } + else + return me.Replace(find, replace); + } - public static IEnumerable FpSplit(this string me, string seperator) - { - var results = me.Split(new[] { seperator }, StringSplitOptions.None); - return results.Select(s => ElementNode.ForPrimitive(s)); - } + public static IEnumerable FpSplit(this string me, string seperator) + { + var results = me.Split([seperator], StringSplitOptions.None); + return results.Select(ElementNode.ForPrimitive); + } - public static string FpEncode(this string me, string encoding) + public static string FpEncode(this string me, string encoding) + { + return encoding switch { - return encoding switch - { - "base64" => EncodeBase64(me), - "urlbase64" => EncodeUrlBase64(me), - "hex" => EncodeHex(me), - _ => throw new ArgumentException($"Unknown encoding '{encoding}'.", nameof(encoding)) - }; - } + "base64" => EncodeBase64(me), + "urlbase64" => EncodeUrlBase64(me), + "hex" => EncodeHex(me), + _ => throw new ArgumentException($"Unknown encoding '{encoding}'.", nameof(encoding)) + }; + } - public static string FpDecode(this string me, string encoding) + public static string FpDecode(this string me, string encoding) + { + return encoding switch { - return encoding switch - { - "base64" => DecodeBase64(me), - "urlbase64" => DecodeUrlBase64(me), - "hex" => DecodeHex(me), - _ => throw new ArgumentException($"Unknown encoding '{encoding}'.", nameof(encoding)) - }; - } + "base64" => DecodeBase64(me), + "urlbase64" => DecodeUrlBase64(me), + "hex" => DecodeHex(me), + _ => throw new ArgumentException($"Unknown encoding '{encoding}'.", nameof(encoding)) + }; + } - internal static string EncodeBase64(string data) - { - if (string.IsNullOrEmpty(data)) return data; + internal static string EncodeBase64(string data) + { + if (string.IsNullOrEmpty(data)) return data; - byte[] toEncodeAsBytes = Encoding.UTF8.GetBytes(data); - return Convert.ToBase64String(toEncodeAsBytes); - } + byte[] toEncodeAsBytes = Encoding.UTF8.GetBytes(data); + return Convert.ToBase64String(toEncodeAsBytes); + } - internal static string DecodeBase64(string data) - { - if (string.IsNullOrEmpty(data)) return data; + internal static string DecodeBase64(string data) + { + if (string.IsNullOrEmpty(data)) return data; - byte[] decodedBase64 = Convert.FromBase64String(data); - return Encoding.UTF8.GetString(decodedBase64, 0, decodedBase64.Length); - } + byte[] decodedBase64 = Convert.FromBase64String(data); + return Encoding.UTF8.GetString(decodedBase64, 0, decodedBase64.Length); + } - internal static string EncodeUrlBase64(string data) - { - if (string.IsNullOrEmpty(data)) return data; + internal static string EncodeUrlBase64(string data) + { + if (string.IsNullOrEmpty(data)) return data; - // There's a function in System.Web (UrlTokenEncode), but don't need another dependency for this stuff. - var b64 = EncodeBase64(data); - return b64 - // .Trim('=') // trim padding - in current draft this is not used - .Replace('+', '-').Replace('/', '_'); // use alternative 62nd/63rd - } + // There's a function in System.Web (UrlTokenEncode), but don't need another dependency for this stuff. + var b64 = EncodeBase64(data); + return b64 + // .Trim('=') // trim padding - in current draft this is not used + .Replace('+', '-').Replace('/', '_'); // use alternative 62nd/63rd + } - internal static string DecodeUrlBase64(string data) - { - if (string.IsNullOrEmpty(data)) return data; + internal static string DecodeUrlBase64(string data) + { + if (string.IsNullOrEmpty(data)) return data; - // There's a function in System.Web (UrlTokenDecode), but don't need another dependency for this stuff. - var incoming = data.Replace('_', '/').Replace('-', '+'); - - switch (incoming.Length % 4) - { - case 2: incoming += "=="; break; - case 3: incoming += "="; break; - } + // There's a function in System.Web (UrlTokenDecode), but don't need another dependency for this stuff. + var incoming = data.Replace('_', '/').Replace('-', '+'); - return DecodeBase64(incoming); + switch (incoming.Length % 4) + { + case 2: incoming += "=="; break; + case 3: incoming += "="; break; } - internal static string EncodeHex(string data) - { - if (string.IsNullOrEmpty(data)) return data; + return DecodeBase64(incoming); + } - return string.Concat(Encoding.UTF8.GetBytes(data) - .Select(b => Convert.ToString(b, toBase: 16))); - } + internal static string EncodeHex(string data) + { + if (string.IsNullOrEmpty(data)) return data; - internal static string DecodeHex(string data) - { - if (string.IsNullOrEmpty(data)) return data; - if (data.Length % 2 != 0) throw new ArgumentException("Hex data should contain an even number of characters."); + return string.Concat(Encoding.UTF8.GetBytes(data) + .Select(b => Convert.ToString(b, toBase: 16))); + } - var bytes = hexStringToBytes(data).ToArray(); - return Encoding.UTF8.GetString(bytes, 0, bytes.Length); // stupid netstd11 + internal static string DecodeHex(string data) + { + if (string.IsNullOrEmpty(data)) return data; + if (data.Length % 2 != 0) throw new ArgumentException("Hex data should contain an even number of characters."); + + var bytes = hexStringToBytes(data).ToArray(); + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); // stupid netstd11 - static IEnumerable hexStringToBytes(string data) + static IEnumerable hexStringToBytes(string data) + { + for (int i = 0; i < data.Length; i += 2) { - for (int i = 0; i < data.Length; i += 2) - { - var hexChar = data.Substring(i, 2); - yield return Convert.ToByte(hexChar, fromBase: 16); - } + var hexChar = data.Substring(i, 2); + yield return Convert.ToByte(hexChar, fromBase: 16); } } + } - public static string FpEscape(this string data, string encoding) + public static string FpEscape(this string data, string encoding) + { + return encoding switch { - return encoding switch - { - "json" => EscapeJson(data), - "html" => EscapeHtml(data), - _ => throw new ArgumentException($"Unknown escaping method '{encoding}'.", nameof(encoding)) - }; - } + "json" => EscapeJson(data), + "html" => EscapeHtml(data), + _ => throw new ArgumentException($"Unknown escaping method '{encoding}'.", nameof(encoding)) + }; + } - public static string FpUnescape(this string data, string encoding) + public static string FpUnescape(this string data, string encoding) + { + return encoding switch { - return encoding switch - { - "json" => UnescapeJson(data), - "html" => UnescapeHtml(data), - _ => throw new ArgumentException($"Unknown escaping method '{encoding}'.", nameof(encoding)) - }; - } + "json" => UnescapeJson(data), + "html" => UnescapeHtml(data), + _ => throw new ArgumentException($"Unknown escaping method '{encoding}'.", nameof(encoding)) + }; + } - internal static string EscapeHtml(string data) => WebUtility.HtmlEncode(data); + internal static string EscapeHtml(string data) => WebUtility.HtmlEncode(data); - internal static string UnescapeHtml(string data) => WebUtility.HtmlDecode(data); + internal static string UnescapeHtml(string data) => WebUtility.HtmlDecode(data); - internal static string EscapeJson(string data) + internal static string EscapeJson(string data) + { + // Note: there are framework utility methods to do this, + // but they all require expensive dependencies to be loaded. + // This code is taken from Mono's implementation of one of these, + // we should this to be ok. + var sb = new StringBuilder(); + + int start = 0; + for (int i = 0; i < data.Length; i++) { - // Note: there are framework utility methods to do this, - // but they all require expensive dependencies to be loaded. - // This code is taken from Mono's implementation of one of these, - // we should this to be ok. - var sb = new StringBuilder(); - - int start = 0; - for (int i = 0; i < data.Length; i++) - { - if (needEscape(data, i)) - { - sb.Append(data, start, i - start); - switch (data[i]) - { - case '\b': sb.Append("\\b"); break; - case '\f': sb.Append("\\f"); break; - case '\n': sb.Append("\\n"); break; - case '\r': sb.Append("\\r"); break; - case '\t': sb.Append("\\t"); break; - case '\"': sb.Append("\\\""); break; - case '\\': sb.Append("\\\\"); break; - case '/': sb.Append("\\/"); break; - default: - sb.Append("\\u"); - sb.Append(((int)data[i]).ToString("x04")); - break; - } - start = i + 1; - } - } - - sb.Append(data, start, data.Length - start); - return sb.ToString(); + if (!needEscape(data, i)) continue; - static bool needEscape(string src, int i) + sb.Append(data, start, i - start); + switch (data[i]) { - char c = src[i]; - return c < 32 || c == '"' || c == '\\' - // Broken lead surrogate - || (c >= '\uD800' && c <= '\uDBFF' && - (i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF')) - // Broken tail surrogate - || (c >= '\uDC00' && c <= '\uDFFF' && - (i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF')) - // To produce valid JavaScript - || c == '\u2028' || c == '\u2029'; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + case '\"': sb.Append("\\\""); break; + case '\\': sb.Append(@"\\"); break; + case '/': sb.Append("\\/"); break; + default: + sb.Append("\\u"); + sb.Append(((int)data[i]).ToString("x04")); + break; } + start = i + 1; } - // This probably does a bit more than a JSON string will ever contain, but - // it should also at least do what json requires. Good enough. - internal static string UnescapeJson(string data) => Regex.Unescape(data); + sb.Append(data, start, data.Length - start); + return sb.ToString(); + + static bool needEscape(string src, int i) + { + char c = src[i]; + return c < 32 || c == '"' || c == '\\' + // Broken lead surrogate + || (c is >= '\uD800' and <= '\uDBFF' && + (i == src.Length - 1 || src[i + 1] < '\uDC00' || src[i + 1] > '\uDFFF')) + // Broken tail surrogate + || (c is >= '\uDC00' and <= '\uDFFF' && + (i == 0 || src[i - 1] < '\uD800' || src[i - 1] > '\uDBFF')) + // To produce valid JavaScript + || c == '\u2028' || c == '\u2029'; + } } -} + + // This probably does a bit more than a JSON string will ever contain, but + // it should also at least do what json requires. Good enough. + internal static string UnescapeJson(string data) => Regex.Unescape(data); +} \ No newline at end of file diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathParallelTest.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathParallelTest.cs index c08b10229c..eb625bcc04 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathParallelTest.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathParallelTest.cs @@ -17,17 +17,16 @@ namespace Vonk.FhirPath.R4.Tests public class FhirPathExtensionsTests { private static Dictionary Resources; - private static TestContext Context; + + public TestContext TestContext { get; set; } [ClassInitialize] public static void Initialize(TestContext ctx) { - Context = ctx; var specSource = ZipSource.CreateValidationSource(); Resources = specSource.FindAll().ToDictionary(sd => sd.Url); //By putting all the url's in a dictionary we can be sure there are no duplicates. - } [TestMethod] @@ -46,7 +45,7 @@ public async Tasks.Task TestSelectMethods() /// This may indicate a multithreading problem in the FhirPath evaluation. /// You may need to run the test in Release mode to reveal the error. /// - public static async Tasks.Task MassiveParallelSelectsShouldBeCorrect(string testName, Func> selector) + public async Tasks.Task MassiveParallelSelectsShouldBeCorrect(string testName, Func> selector) { var actual = new ConcurrentBag<(string canonical, ValueSet resource)>(); var buffer = new BufferBlock(); @@ -74,7 +73,7 @@ public static async Tasks.Task MassiveParallelSelectsShouldBeCorrect(string test buffer.Complete(); await processor.Completion; sw.Stop(); - Context.WriteLine($"Extracting urls took {sw.Elapsed.ToString("c")} ms"); + TestContext.WriteLine($"Extracting urls took {sw.Elapsed.ToString("c")} ms"); Assert.AreEqual(actual.Count(), resources.Count(), $"{testName}: All Resources should have a url."); Assert.IsFalse(actual diff --git a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs index b4f63d3a12..9601342771 100644 --- a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs @@ -7,11 +7,13 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace HL7.FhirPath.Tests { [TestClass] + [SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments")] public class FunctionsTests { public static IEnumerable ExistenceFunctionTestcases() => @@ -558,11 +560,11 @@ public void ContextNestingLevelTest() { Coding c = new("http://nu.nl", "nl"); var te = c.ToTypedElement(ModelInspector.Base); - Assert.IsTrue(te.IsBoolean($"system.endsWith(code)", true)); - Assert.IsTrue(te.IsBoolean($"system.endsWith(%context.code)", true)); - Assert.IsTrue(te.IsBoolean($"system.endsWith('nl')", true)); - Assert.IsTrue(te.IsBoolean($"system.endsWith(code.toString())", true)); - Assert.IsTrue(te.IsBoolean($"system.endsWith('banana')", false)); + Assert.IsTrue(te.IsBoolean("system.endsWith(code)", true)); + Assert.IsTrue(te.IsBoolean("system.endsWith(%context.code)", true)); + Assert.IsTrue(te.IsBoolean("system.endsWith('nl')", true)); + Assert.IsTrue(te.IsBoolean("system.endsWith(code.toString())", true)); + Assert.IsTrue(te.IsBoolean("system.endsWith('banana')", false)); } /// @@ -576,5 +578,34 @@ public void TestFhirPathRepeatsStringLiteral() var result = nav.Select("repeat('teststring')"); result.Should().ContainSingle(ite => ((string)ite.Value) == "teststring"); } + + public static IEnumerable EmptyStringValueTestcases() => + new[]{ + "startsWith('a')", + "replace('a','b')", + "toBoolean()", + "toChars()", + }.Select(e => new object[] { e, typeof(FhirString) }); + + public static IEnumerable EmptyDecimalValueTestcases() => + new[]{ + "round()", + "toBoolean()" + }.Select(e => new object[] { e, typeof(FhirDecimal) }); + + public static IEnumerable EmptyValueTestcases() => EmptyStringValueTestcases().Union(EmptyDecimalValueTestcases()); + + [DataTestMethod] + [DynamicData(nameof(EmptyValueTestcases), DynamicDataSourceType.Method)] + public void AssertEmptyValueTestcases(string expression, Type data) + { + // Create a primitive with no value, just an extension + var emptyPrimitive = (PrimitiveType)Activator.CreateInstance(data)!; + emptyPrimitive.SetBoolExtension("http://nu.nl", true); + var dummy = emptyPrimitive.ToTypedElement(ModelInspector.Base); + + // Run the expression. With the correct null prop, this should return empty. + dummy.Select(expression).Should().BeEmpty(); + } } -} +} \ No newline at end of file diff --git a/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs index 3c4dbc38de..a2da473bc9 100644 --- a/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs @@ -1,174 +1,170 @@ using FluentAssertions; -using Hl7.Fhir.ElementModel; using Hl7.FhirPath.Functions; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; using System.Linq; -namespace HL7.FhirPath.Tests.Functions +namespace HL7.FhirPath.Tests.Functions; + +[TestClass] +public class StringFunctionsTests { - [TestClass] - public class StringFunctionsTests + [TestMethod] + public void StringSplit() + { + CollectionAssert.AreEqual(new[] { "A", "B", "C" }, StringOperators.FpSplit("A,B,C", ",").Select(r => r.Value.ToString()).ToArray()); + + // verify the empty string + CollectionAssert.AreEqual(new[] { "A", "", "C" }, StringOperators.FpSplit("A,,C", ",").Select(r => r.Value.ToString()).ToArray()); + + // Verify dups aren't removed + CollectionAssert.AreEqual(new[] { "A", "B", "C", "C" }, StringOperators.FpSplit("A,B,C,C", ",").Select(r => r.Value.ToString()).ToArray()); + + // The test from the spec + Assert.AreEqual(5, StringOperators.FpSplit("Peter,James,Jim,Peter,James", ",").Count()); + } + + private static IEnumerable substringTests() + { + yield return ["Ewout", 0, (long?)3, "Ewo"]; + yield return ["Ewout", 2, (long?)3, "out"]; + yield return ["Ewout", 0, null, "Ewout"]; + yield return ["Ewout", 0, (long?)0, ""]; + yield return ["Ewout", 0, (long?)-1, ""]; + } + + [DataTestMethod] + [DynamicData(nameof(substringTests), DynamicDataSourceType.Method)] + public void SubString(string input, long start, long? length, string expected) + { + input.FpSubstring(start, length).Should().Be(expected); + } + + [TestMethod] + public void EncodeBase64() + { + StringOperators.EncodeBase64(null).Should().BeNull(); + StringOperators.EncodeBase64("").Should().Be(""); + StringOperators.EncodeBase64("Ewout").Should().Be("RXdvdXQ="); + StringOperators.EncodeBase64("Ewout>").Should().Be("RXdvdXQ+"); + StringOperators.EncodeBase64("编码方式").Should().Be("57yW56CB5pa55byP"); + + StringOperators.FpEncode("Ewout", "base64").Should().Be("RXdvdXQ="); + } + + + [TestMethod] + public void DecodeBase64() + { + StringOperators.DecodeBase64(null).Should().BeNull(); + StringOperators.DecodeBase64("").Should().Be(""); + StringOperators.DecodeBase64("RXdvdXQ=").Should().Be("Ewout"); + StringOperators.DecodeBase64("RXdvdXQ+").Should().Be("Ewout>"); + StringOperators.DecodeBase64("57yW56CB5pa55byP").Should().Be("编码方式"); + + StringOperators.FpDecode("RXdvdXQ=", "base64").Should().Be("Ewout"); + } + + + [TestMethod] + public void EncodeBase64Url() + { + StringOperators.EncodeUrlBase64(null).Should().BeNull(); + StringOperators.EncodeUrlBase64("").Should().Be(""); + // StringOperators.EncodeUrlBase64("Ewout").Should().Be("RXdvdXQ"); + StringOperators.EncodeUrlBase64("Ewout").Should().Be("RXdvdXQ="); + StringOperators.EncodeUrlBase64("Ewout>").Should().Be("RXdvdXQ-"); + StringOperators.EncodeUrlBase64("编码方式").Should().Be("57yW56CB5pa55byP"); + + StringOperators.FpEncode("Ewout", "urlbase64").Should().Be("RXdvdXQ="); + //StringOperators.FpEncode("Ewout", "urlbase64").Should().Be("RXdvdXQ"); + } + + [TestMethod] + public void DencodeBase64Url() + { + StringOperators.DecodeUrlBase64(null).Should().BeNull(); + StringOperators.DecodeUrlBase64("").Should().Be(""); + StringOperators.DecodeUrlBase64("RXdvdXQ").Should().Be("Ewout"); + StringOperators.DecodeUrlBase64("RXdvdXQ-").Should().Be("Ewout>"); + StringOperators.DecodeUrlBase64("RXdvdXQ=").Should().Be("Ewout"); + StringOperators.DecodeUrlBase64("57yW56CB5pa55byP").Should().Be("编码方式"); + + StringOperators.FpDecode("RXdvdXQ", "urlbase64").Should().Be("Ewout"); + } + + [TestMethod] + public void EncodeHex() + { + StringOperators.EncodeHex(null).Should().BeNull(); + StringOperators.EncodeHex("").Should().Be(""); + StringOperators.EncodeHex("Ewout").Should().Be("45776f7574"); + StringOperators.EncodeHex("编码方式").Should().Be("e7bc96e7a081e696b9e5bc8f"); + + StringOperators.FpEncode("Ewout", "hex").Should().Be("45776f7574"); + } + + [TestMethod] + public void DecodeHex() + { + StringOperators.DecodeHex(null).Should().BeNull(); + StringOperators.DecodeHex("").Should().Be(""); + StringOperators.DecodeHex("45776f7574").Should().Be("Ewout"); + StringOperators.DecodeHex("e7bc96e7a081e696b9e5bc8f").Should().Be("编码方式"); + + StringOperators.FpDecode("45776f7574", "hex").Should().Be("Ewout"); + } + + [TestMethod] + public void UnknownEncoding() + { + Action act = () => StringOperators.FpEncode("Ewout", "reverse_polish"); + act.Should().Throw().Which.Message.StartsWith("Unknown encoding 'reverse_polish'."); + + act = () => StringOperators.FpDecode("Ewout", "reverse_polish"); + act.Should().Throw().Which.Message.StartsWith("Unknown encoding 'reverse_polish'."); + } + + [TestMethod] + public void EscapeJson() + { + StringOperators.EscapeJson("hi\t\"there\"! \\/").Should().Be("hi\\t\\\"there\\\"! \\\\/"); + + StringOperators.FpEscape("hi\nthere", "json").Should().Be("hi\\nthere"); + } + + [TestMethod] + public void UnescapeJson() + { + StringOperators.UnescapeJson("hi\\t\\\"there\\\"! \\\\/").Should().Be("hi\t\"there\"! \\/"); + + StringOperators.FpUnescape("hi\\nthere", "json").Should().Be("hi\nthere"); + } + + [TestMethod] + public void EscapeHtml() { - [TestMethod] - public void StringSplit() - { - CollectionAssert.AreEqual(new[] { "A", "B", "C" }, StringOperators.FpSplit("A,B,C", ",").Select(r => r.Value.ToString()).ToArray()); - - // verify the empty string - CollectionAssert.AreEqual(new[] { "A", "", "C" }, StringOperators.FpSplit("A,,C", ",").Select(r => r.Value.ToString()).ToArray()); - - // Verify dups aren't removed - CollectionAssert.AreEqual(new[] { "A", "B", "C", "C" }, StringOperators.FpSplit("A,B,C,C", ",").Select(r => r.Value.ToString()).ToArray()); - - // The test from the spec - Assert.AreEqual(5, StringOperators.FpSplit("Peter,James,Jim,Peter,James", ",").Count()); - } - - private static IEnumerable substringTests() - { - yield return ["Ewout", 0, (long?)3, "Ewo"]; - yield return ["Ewout", 2, (long?)3, "out"]; - yield return ["Ewout", 0, null, "Ewout"]; - yield return ["Ewout", 0, (long?)0, ""]; - yield return ["Ewout", 0, (long?)-1, ""]; - } - - [DataTestMethod] - [DynamicData(nameof(substringTests), DynamicDataSourceType.Method)] - public void SubString(string input, long start, long? length, string expected) - { - input.FpSubstring(start, length).Should().Be(expected); - } - - [TestMethod] - public void EncodeBase64() - { - StringOperators.EncodeBase64(null).Should().BeNull(); - StringOperators.EncodeBase64("").Should().Be(""); - StringOperators.EncodeBase64("Ewout").Should().Be("RXdvdXQ="); - StringOperators.EncodeBase64("Ewout>").Should().Be("RXdvdXQ+"); - StringOperators.EncodeBase64("编码方式").Should().Be("57yW56CB5pa55byP"); - - StringOperators.FpEncode("Ewout", "base64").Should().Be("RXdvdXQ="); - } - - - [TestMethod] - public void DecodeBase64() - { - StringOperators.DecodeBase64(null).Should().BeNull(); - StringOperators.DecodeBase64("").Should().Be(""); - StringOperators.DecodeBase64("RXdvdXQ=").Should().Be("Ewout"); - StringOperators.DecodeBase64("RXdvdXQ+").Should().Be("Ewout>"); - StringOperators.DecodeBase64("57yW56CB5pa55byP").Should().Be("编码方式"); - - StringOperators.FpDecode("RXdvdXQ=", "base64").Should().Be("Ewout"); - } - - - [TestMethod] - public void EncodeBase64Url() - { - StringOperators.EncodeUrlBase64(null).Should().BeNull(); - StringOperators.EncodeUrlBase64("").Should().Be(""); - // StringOperators.EncodeUrlBase64("Ewout").Should().Be("RXdvdXQ"); - StringOperators.EncodeUrlBase64("Ewout").Should().Be("RXdvdXQ="); - StringOperators.EncodeUrlBase64("Ewout>").Should().Be("RXdvdXQ-"); - StringOperators.EncodeUrlBase64("编码方式").Should().Be("57yW56CB5pa55byP"); - - StringOperators.FpEncode("Ewout", "urlbase64").Should().Be("RXdvdXQ="); - //StringOperators.FpEncode("Ewout", "urlbase64").Should().Be("RXdvdXQ"); - } - - [TestMethod] - public void DencodeBase64Url() - { - StringOperators.DecodeUrlBase64(null).Should().BeNull(); - StringOperators.DecodeUrlBase64("").Should().Be(""); - StringOperators.DecodeUrlBase64("RXdvdXQ").Should().Be("Ewout"); - StringOperators.DecodeUrlBase64("RXdvdXQ-").Should().Be("Ewout>"); - StringOperators.DecodeUrlBase64("RXdvdXQ=").Should().Be("Ewout"); - StringOperators.DecodeUrlBase64("57yW56CB5pa55byP").Should().Be("编码方式"); - - StringOperators.FpDecode("RXdvdXQ", "urlbase64").Should().Be("Ewout"); - } - - [TestMethod] - public void EncodeHex() - { - StringOperators.EncodeHex(null).Should().BeNull(); - StringOperators.EncodeHex("").Should().Be(""); - StringOperators.EncodeHex("Ewout").Should().Be("45776f7574"); - StringOperators.EncodeHex("编码方式").Should().Be("e7bc96e7a081e696b9e5bc8f"); - - StringOperators.FpEncode("Ewout", "hex").Should().Be("45776f7574"); - } - - [TestMethod] - public void DecodeHex() - { - StringOperators.DecodeHex(null).Should().BeNull(); - StringOperators.DecodeHex("").Should().Be(""); - StringOperators.DecodeHex("45776f7574").Should().Be("Ewout"); - StringOperators.DecodeHex("e7bc96e7a081e696b9e5bc8f").Should().Be("编码方式"); - - StringOperators.FpDecode("45776f7574", "hex").Should().Be("Ewout"); - } - - [TestMethod] - public void UnknownEncoding() - { - Action act = () => StringOperators.FpEncode("Ewout", "reverse_polish"); - act.Should().Throw().Which.Message.StartsWith("Unknown encoding 'reverse_polish'."); - - act = () => StringOperators.FpDecode("Ewout", "reverse_polish"); - act.Should().Throw().Which.Message.StartsWith("Unknown encoding 'reverse_polish'."); - } - - [TestMethod] - public void EscapeJson() - { - StringOperators.EscapeJson("hi\t\"there\"! \\/").Should().Be("hi\\t\\\"there\\\"! \\\\/"); - - StringOperators.FpEscape("hi\nthere", "json").Should().Be("hi\\nthere"); - } - - [TestMethod] - public void UnescapeJson() - { - StringOperators.UnescapeJson("hi\\t\\\"there\\\"! \\\\/").Should().Be("hi\t\"there\"! \\/"); - - StringOperators.FpUnescape("hi\\nthere", "json").Should().Be("hi\nthere"); - } - - [TestMethod] - public void EscapeHtml() - { - StringOperators.EscapeHtml("\"Me < Me & Jou, Jou > Me.\", said he.").Should().Be(""Me < Me & Jou, Jou > Me.", said he."); - - StringOperators.FpEscape("1 < 5", "html").Should().Be("1 < 5"); - } - - [TestMethod] - public void UnescapeHtml() - { - StringOperators.UnescapeHtml(""Me < Me & Jou, Jou > Me.", said he.").Should().Be("\"Me < Me & Jou, Jou > Me.\", said he."); - - StringOperators.FpUnescape("1 < 5", "html").Should().Be("1 < 5"); - } - - [TestMethod] - public void UnknownEscape() - { - Action act = () => StringOperators.FpEscape("Ewout", "reverse_polish"); - act.Should().Throw().Which.Message.StartsWith("Unknown escaping method 'reverse_polish'."); - - act = () => StringOperators.FpUnescape("Ewout", "reverse_polish"); - act.Should().Throw().Which.Message.StartsWith("Unknown escaping method 'reverse_polish'."); - } + StringOperators.EscapeHtml("\"Me < Me & Jou, Jou > Me.\", said he.").Should().Be(""Me < Me & Jou, Jou > Me.", said he."); + StringOperators.FpEscape("1 < 5", "html").Should().Be("1 < 5"); } -} + [TestMethod] + public void UnescapeHtml() + { + StringOperators.UnescapeHtml(""Me < Me & Jou, Jou > Me.", said he.").Should().Be("\"Me < Me & Jou, Jou > Me.\", said he."); + + StringOperators.FpUnescape("1 < 5", "html").Should().Be("1 < 5"); + } + + [TestMethod] + public void UnknownEscape() + { + Action act = () => StringOperators.FpEscape("Ewout", "reverse_polish"); + act.Should().Throw().Which.Message.StartsWith("Unknown escaping method 'reverse_polish'."); + + act = () => StringOperators.FpUnescape("Ewout", "reverse_polish"); + act.Should().Throw().Which.Message.StartsWith("Unknown escaping method 'reverse_polish'."); + } +} \ No newline at end of file From 372656ef20cbd8b4b5b46f7f875774676c194785 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Wed, 26 Feb 2025 17:45:07 +0100 Subject: [PATCH 11/17] wip --- .../FhirPath/Expressions/SymbolTableExtensions.cs | 2 +- src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs index 205518bb22..5672151eea 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs @@ -68,7 +68,7 @@ public static void AddVar(this SymbolTable table, string name, object value) table.AddVar(name, ElementNode.ForPrimitive(value)); } - public static void AddVar(this SymbolTable table, string name, ITypedElement value) + public static void AddVar(this SymbolTable table, string name, PocoNode value) { table.Add(new CallSignature(name, typeof(string)), InvokeeFactory.Return(value)); } diff --git a/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs b/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs index 5133fc9181..8ef960c8db 100644 --- a/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs +++ b/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs @@ -6,12 +6,10 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-net-sdk/master/LICENSE */ -using Hl7.Fhir.ElementModel; -using Hl7.Fhir.Model; + using Hl7.FhirPath.Expressions; using Hl7.FhirPath.Parser; using Hl7.FhirPath.Sprache; -using System; namespace Hl7.FhirPath { From d0fc3f816e49914848a11ef0aa93f3745a33db2c Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Wed, 26 Feb 2025 17:56:18 +0100 Subject: [PATCH 12/17] merge conflicts --- .../FhirPath/Expressions/Invokee.cs | 33 +- .../FhirPath/Expressions/SymbolTableInit.cs | 783 +++++++++--------- .../FhirPath/FhirPathCompiler.cs | 78 +- 3 files changed, 443 insertions(+), 451 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs index d492d0db12..4b93d7414f 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -13,7 +13,7 @@ using System; using System.Collections.Generic; using System.Linq; -using FocusCollection = System.Collections.Generic.IEnumerable; +using FocusCollection = System.Collections.Generic.IEnumerable; // ReSharper disable InconsistentNaming namespace Hl7.FhirPath.Expressions; @@ -49,12 +49,7 @@ public static FocusCollection GetIndex(Closure context, IEnumerable arg private static readonly Predicate PROPAGATE_EMPTY_PRIMITIVE = focus => { var first = focus.FirstOrDefault(); - if (first is null) return true; - - // If this is not a primitive, then it is not empty. - if (first.InstanceType is null || !char.IsLower(first.InstanceType[0])) return false; - - return first.Value is null; + return first is null or PrimitiveNode { Value: null }; }; private static Predicate getPropagator(bool doNullProp, Type argType) => @@ -80,7 +75,7 @@ public static Invokee Wrap(Func func, bool propNull) if (typeof(A) != typeof(EvaluationContext)) { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(A))(focus)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus))); } @@ -98,7 +93,7 @@ internal static Invokee WrapWithPropNullForFocus(Func fu { // propagate only null for focus var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(true,typeof(A))(focus)) return ElementNode.EmptyList; + if (getPropagator(true,typeof(A))(focus)) return []; return Wrap(func, false)(ctx, args); }; @@ -109,12 +104,12 @@ public static Invokee Wrap(Func func, bool propNull) return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(A))(focus)) return []; if (typeof(B) != typeof(EvaluationContext)) { var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(B))(argA)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA))); } @@ -131,15 +126,15 @@ public static Invokee Wrap(Func func, bool propNull) return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull,typeof(A))(focus)) return ElementNode.EmptyList; + if (getPropagator(propNull,typeof(A))(focus)) return []; var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(B))(argA)) return []; if (typeof(C) != typeof(EvaluationContext)) { var argB = args.Skip(2).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(C))(argB)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), Typecasts.CastTo(argB))); @@ -158,17 +153,17 @@ public static Invokee Wrap(Func func, bool propNul return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(A))(focus)) return []; var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(B))(argA)) return []; var argB = args.Skip(2).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(C))(argB)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(C))(argB)) return []; if (typeof(D) != typeof(EvaluationContext)) { var argC = args.Skip(3).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(D))(argC)) return ElementNode.EmptyList; + if (getPropagator(propNull, typeof(D))(argC)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), Typecasts.CastTo(argB), Typecasts.CastTo(argC))); @@ -199,8 +194,6 @@ public static Invokee WrapLogic(Func, Func, bool?> func) }; } - public static Invokee Return(ITypedElement value) => (_, _) => [value]; - public static Invokee Return(FocusCollection value) => (_, _) => value; public static Invokee Invoke(string functionName, IEnumerable arguments, Invokee invokee) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs index b9b2e628cb..3f976d2bdb 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableInit.cs @@ -18,423 +18,369 @@ using System.Text.RegularExpressions; using P = Hl7.Fhir.ElementModel.Types; -namespace Hl7.FhirPath.Expressions +namespace Hl7.FhirPath.Expressions; + +public static class SymbolTableInit { - public static class SymbolTableInit + /// + /// Add the function library for the standard FhirPath Normative dialect to the . + /// + public static SymbolTable AddStandardFP(this SymbolTable t) { - /// - /// Add the function library for the standard FhirPath Normative dialect to the . - /// - public static SymbolTable AddStandardFP(this SymbolTable t) - { - // Functions that operate on the focus, without null propagation - t.Add("empty", (IEnumerable f) => !f.Any()); - t.Add("exists", (IEnumerable f) => f.Any()); - - t.Add("count", (IEnumerable f) => f.Count()); - t.Add("trace", (IEnumerable f, string name, EvaluationContext ctx) - => f.Trace(name, ctx)); - - t.Add("allTrue", (IEnumerable f) => f.All(e => e.GetValue() is true)); - t.Add("anyTrue", (IEnumerable f) => f.Any(e => e.GetValue() is true)); - t.Add("allFalse", (IEnumerable f) => f.All(e => e.GetValue() is false)); - t.Add("anyFalse", (IEnumerable f) => f.Any(e => e.GetValue() is false)); - t.Add("combine", (IEnumerable l, IEnumerable r) => l.Concat(r)); - t.Add("binary.|", (object _, IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); - t.Add("union", (IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); - t.Add("binary.contains", (object _, IEnumerable a, PocoNode b) => a.Contains(b)); - t.Add("binary.in", (object _, PocoNode a, IEnumerable b) => b.Contains(a)); - t.Add("distinct", (IEnumerable f) => f.Distinct()); - t.Add("isDistinct", (IEnumerable f) => f.IsDistinct()); - t.Add("subsetOf", (IEnumerable f, IEnumerable a) => f.SubsetOf(a)); - t.Add("supersetOf", (IEnumerable f, IEnumerable a) => a.SubsetOf(f)); - t.Add("intersect", (IEnumerable f, IEnumerable a) => f.Intersect(a)); - t.Add("exclude", (IEnumerable f, IEnumerable a) => f.Exclude(a)); - - t.Add("today", (object _) => P.Date.Today()); - t.Add("now", (object _) => P.DateTime.Now()); - t.Add("timeOfDay", (object _) => P.Time.Now()); - - t.Add("binary.&", (object _, string a, string b) => (a ?? "") + (b ?? "")); - - t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee), typeof(Invokee)), runIif); - t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee)), runIif); - - // Functions that use normal null propagation and work with the focus (buy may ignore it) - t.Add("not", (IEnumerable f) => f.Not(), doNullProp: true); - // t.Add("builtin.children", (IEnumerable f, string a) => f.Navigate(a), doNullProp: true); - t.AddBuiltinChildren(); - - t.Add("children", (IEnumerable f) => f.SelectMany(node => node.Children().SelectMany(n => n)), doNullProp: true); - t.Add("descendants", (IEnumerable f) => f.Descendants(), doNullProp: true); - - t.Add("binary.=", (object f, IEnumerable a, IEnumerable b) => a.IsEqualTo(b), doNullProp: true); - t.Add("binary.!=", (object f, IEnumerable a, IEnumerable b) => !a.IsEqualTo(b), doNullProp: true); - t.Add("binary.~", (object f, IEnumerable a, IEnumerable b) => a.IsEquivalentTo(b), doNullProp: false); - t.Add("binary.!~", (object f, IEnumerable a, IEnumerable b) => !a.IsEquivalentTo(b), doNullProp: false); - - t.Add("unary.-", (object f, int a) => -a, doNullProp: true); - t.Add("unary.-", (object f, long a) => -a, doNullProp: true); - t.Add("unary.-", (object f, decimal a) => -a, doNullProp: true); - t.Add("unary.-", (object f, P.Quantity a) => new P.Quantity(-a.Value, a.Unit), doNullProp: true); - t.Add("unary.+", (object f, int a) => a, doNullProp: true); - t.Add("unary.+", (object f, long a) => a, doNullProp: true); - t.Add("unary.+", (object f, decimal a) => a, doNullProp: true); - t.Add("unary.+", (object f, P.Quantity a) => a, doNullProp: true); - - t.Add("binary.*", (object f, int a, int b) => a * b, doNullProp: true); - t.Add("binary.*", (object f, long a, long b) => a * b, doNullProp: true); - t.Add("binary.*", (object f, decimal a, decimal b) => a * b, doNullProp: true); - // t.Add("binary.*", (object f, P.Quantity a, P.Quantity b) => a * b, doNullProp: true); - - t.Add("binary./", (object f, decimal a, decimal b) => b != 0 ? a / b : (decimal?)null, doNullProp: true); - // t.Add("binary./", (object f, P.Quantity a, P.Quantity b) => a / b, doNullProp: true); - - t.Add("binary.+", (object f, int a, int b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, long a, long b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, decimal a, decimal b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, string a, string b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, P.DateTime a, P.Quantity b) => a + b, doNullProp: true); - t.Add("binary.+", (object f, P.Date a, P.Quantity b) => a + b, doNullProp: true); - // t.Add("binary.+", (object f, P.Quantity a, P.Quantity b) => a + b, doNullProp: true); - - t.Add("binary.-", (object f, int a, int b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, long a, long b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, decimal a, decimal b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, P.DateTime a, P.Quantity b) => a - b, doNullProp: true); - t.Add("binary.-", (object f, P.Date a, P.Quantity b) => a - b, doNullProp: true); - // t.Add("binary.-", (object f, P.Quantity a, P.Quantity b) => a - b, doNullProp: true); - - t.Add("binary.div", (object f, int a, int b) => b != 0 ? a / b : (int?)null, doNullProp: true); - t.Add("binary.div", (object f, long a, long b) => b != 0 ? a / b : (long?)null, doNullProp: true); - t.Add("binary.div", (object f, decimal a, decimal b) => b != 0 ? (long?)Math.Truncate(a / b) : null, doNullProp: true); - - t.Add("binary.mod", (object f, int a, int b) => b != 0 ? a % b : (int?)null, doNullProp: true); - t.Add("binary.mod", (object f, long a, long b) => b != 0 ? a % b : (long?)null, doNullProp: true); - t.Add("binary.mod", (object f, decimal a, decimal b) => b != 0 ? a % b : (decimal?)null, doNullProp: true); - - t.Add("binary.>", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">"), doNullProp: true); - t.Add("binary.<", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<"), doNullProp: true); - t.Add("binary.<=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<="), doNullProp: true); - t.Add("binary.>=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">="), doNullProp: true); - - t.Add("single", (IEnumerable f) => f.Single(), doNullProp: true); - t.Add("skip", (IEnumerable f, long a) => f.Skip((int)a), doNullProp: true); - t.Add("first", (IEnumerable f) => f.First(), doNullProp: true); - t.Add("last", (IEnumerable f) => f.Last(), doNullProp: true); - t.Add("tail", (IEnumerable f) => f.Tail(), doNullProp: true); - t.Add("take", (IEnumerable f, long a) => f.Take((int)a), doNullProp: true); - t.Add("builtin.item", (IEnumerable f, long a) => f.Item((int)a), doNullProp: true); - - t.Add("toBoolean", (P.Any f) => f.ToBoolean(), doNullProp: true); - t.Add("convertsToBoolean", (P.Any f) => f.ConvertsToBoolean(), doNullProp: true); - t.Add("toInteger", (P.Any f) => f.ToInteger(), doNullProp: true); - t.Add("convertsToInteger", (P.Any f) => f.ConvertsToInteger(), doNullProp: true); - t.Add("toLong", (P.Any f) => f.ToLong(), doNullProp: true); - t.Add("convertsToLong", (P.Any f) => f.ConvertsToLong(), doNullProp: true); - t.Add("toDecimal", (P.Any f) => f.ToDecimal(), doNullProp: true); - t.Add("convertsToDecimal", (P.Any f) => f.ConvertsToDecimal(), doNullProp: true); - t.Add("toQuantity", (P.Any f) => f.ToQuantity(), doNullProp: true); - t.Add("convertsToQuantity", (P.Any f) => f.ConvertsToQuantity(), doNullProp: true); - t.Add("toString", (P.Any f) => f.ToStringRepresentation(), doNullProp: true); - t.Add("convertsToString", (P.Any f) => f.ConvertsToString(), doNullProp: true); - t.Add("toDate", (P.Any f) => f.ToDate(), doNullProp: true); - t.Add("convertsToDate", (P.Any f) => f.ConvertsToDate(), doNullProp: true); - t.Add("toDateTime", (P.Any f) => f.ToDateTime(), doNullProp: true); - t.Add("convertsToDateTime", (P.Any f) => f.ConvertsToDateTime(), doNullProp: true); - t.Add("toTime", (P.Any f) => f.ToTime(), doNullProp: true); - t.Add("convertsToTime", (P.Any f) => f.ConvertsToTime(), doNullProp: true); - - t.Add("upper", (string f) => f.ToUpper(), doNullProp: true); - t.Add("lower", (string f) => f.ToLower(), doNullProp: true); - t.Add("toChars", (string f) => f.ToChars(), doNullProp: true); - t.Add("substring", (string f, int a) => f.FpSubstring(a, null), doNullProp: true); - t.Add("trim", (string f) => f.Trim(), doNullProp: true); - t.Add("encode", (string f, string enc) => f.FpEncode(enc), doNullProp: true); - t.Add("decode", (string f, string enc) => f.FpDecode(enc), doNullProp: true); - t.Add("escape", (string f, string enc) => f.FpEscape(enc), doNullProp: true); - t.Add("unescape", (string f, string enc) => f.FpUnescape(enc), doNullProp: true); - - //special case: only focus should be Null propagated: - t.Add(new CallSignature("substring", typeof(string), typeof(string), typeof(int), typeof(int?)), - InvokeeFactory.WrapWithPropNullForFocus((string f, int a, int? b) => f.FpSubstring(a, b))); - t.Add("startsWith", (string f, string fragment) => f.StartsWith(fragment), doNullProp: true); - t.Add("endsWith", (string f, string fragment) => f.EndsWith(fragment), doNullProp: true); - t.Add("matches", (string f, string regex) => Regex.IsMatch(f, regex), doNullProp: true); - t.Add("indexOf", (string f, string fragment) => f.FpIndexOf(fragment), doNullProp: true); - t.Add("contains", (string f, string fragment) => f.Contains(fragment), doNullProp: true); - t.Add("replaceMatches", (string f, string regex, string subst) => Regex.Replace(f, regex, subst), doNullProp: true); - t.Add("replace", (string f, string regex, string subst) => f.FpReplace(regex, subst), doNullProp: true); - t.Add("length", (string f) => f.Length, doNullProp: true); - t.Add("split", (string f, string seperator) => f.FpSplit(seperator), doNullProp: true); - t.Add("join", (IEnumerable f, string separator) => f.FpJoin(separator), doNullProp: true); - t.Add("join", (IEnumerable f) => f.FpJoin(), doNullProp: true); - t.Add("indexOf", (IEnumerable f, PocoNode elem, int start) => f.IndexOf(elem, start), doNullProp: true); - t.Add("indexOf", (IEnumerable f, PocoNode elem) => f.IndexOf(elem), doNullProp: true); - t.Add("lastIndexOf", (IEnumerable f, PocoNode elem, int start) => f.LastIndexOf(elem, start), doNullProp: true); - t.Add("lastIndexOf", (IEnumerable f, PocoNode elem) => f.LastIndexOf(elem), doNullProp: true); - - // Math functions - t.Add("abs", (decimal f) => Math.Abs(f), doNullProp: true); - t.Add("abs", (P.Quantity f) => new P.Quantity(Math.Abs(f.Value), f.Unit), doNullProp: true); - t.Add("ceiling", (decimal f) => Math.Ceiling(f), doNullProp: true); - t.Add("exp", (decimal f) => Math.Exp((double)f), doNullProp: true); - t.Add("floor", (decimal f) => Math.Floor(f), doNullProp: true); - t.Add("ln", (decimal f) => Math.Log((double)f), doNullProp: true); - t.Add("log", (decimal f, decimal @base) => Math.Log((double)f, (double)@base), doNullProp: true); - t.Add("power", (decimal f, decimal exponent) => f.Power(exponent), doNullProp: true); - t.Add("round", (decimal f, long precision) => Math.Round(f, (int)precision), doNullProp: true); - t.Add("round", (decimal f) => Math.Round(f), doNullProp: true); - t.Add("sqrt", (decimal f) => f.Sqrt(), doNullProp: true); - t.Add("truncate", (decimal f) => Math.Truncate((double)f), doNullProp: true); - - // The next two functions existed pre-normative, so we have kept them. - t.Add("is", (PocoNode f, string name) => f.Is(name), doNullProp: true); - t.Add("as", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); - - t.Add("ofType", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); - t.Add("binary.is", (object f, PocoNode left, string name) => left.Is(name), doNullProp: true); - t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); - - // Kept for backwards compatibility, but no longer part of the spec - t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); - - t.Add("extension", (IEnumerable f, string url) => f.Extension(url), doNullProp: true); - - // Logic operators do not use null propagation and may do short-cut eval - t.AddLogic("binary.and", (a, b) => a.And(b)); - t.AddLogic("binary.or", (a, b) => a.Or(b)); - t.AddLogic("binary.xor", (a, b) => a.XOr(b)); - t.AddLogic("binary.implies", (a, b) => a.Implies(b)); - - // Special late-bound functions - t.Add(new CallSignature("where", typeof(IEnumerable), typeof(object), typeof(Invokee)), runWhere); - t.Add(new CallSignature("select", typeof(IEnumerable), typeof(object), typeof(Invokee)), runSelect); - t.Add(new CallSignature("all", typeof(bool), typeof(object), typeof(Invokee)), runAll); - t.Add(new CallSignature("any", typeof(bool), typeof(object), typeof(Invokee)), runAny); - t.Add(new CallSignature("exists", typeof(bool), typeof(object), typeof(Invokee)), runAny); - t.Add(new CallSignature("repeat", typeof(IEnumerable), typeof(object), typeof(Invokee)), runRepeat); - t.Add(new CallSignature("trace", typeof(IEnumerable), typeof(string), typeof(object), typeof(Invokee)), Trace); - t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string)), DefineVariable); - t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string), typeof(Invokee)), DefineVariable); - - t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee)), runAggregate); - t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate); - - t.AddVar("sct", "http://snomed.info/sct"); - t.AddVar("loinc", "http://loinc.org"); - t.AddVar("ucum", "http://unitsofmeasure.org"); - - t.Add("builtin.coreexturl", (object f, string id) => getCoreExtensionUrl(id)); - t.Add("builtin.corevsurl", (object f, string id) => getCoreValueSetUrl(id)); - - return t; - } + // Functions that operate on the focus, without null propagation + t.Add("empty", (IEnumerable f) => !f.Any()); + t.Add("exists", (IEnumerable f) => f.Any()); + + t.Add("count", (IEnumerable f) => f.Count()); + t.Add("trace", (IEnumerable f, string name, EvaluationContext ctx) + => f.Trace(name, ctx)); + + t.Add("allTrue", (IEnumerable f) => f.All(e => e.GetValue() is true)); + t.Add("anyTrue", (IEnumerable f) => f.Any(e => e.GetValue() is true)); + t.Add("allFalse", (IEnumerable f) => f.All(e => e.GetValue() is false)); + t.Add("anyFalse", (IEnumerable f) => f.Any(e => e.GetValue() is false)); + t.Add("combine", (IEnumerable l, IEnumerable r) => l.Concat(r)); + t.Add("binary.|", (object _, IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); + t.Add("union", (IEnumerable l, IEnumerable r) => l.DistinctUnion(r)); + t.Add("binary.contains", (object _, IEnumerable a, PocoNode b) => a.Contains(b)); + t.Add("binary.in", (object _, PocoNode a, IEnumerable b) => b.Contains(a)); + t.Add("distinct", (IEnumerable f) => f.Distinct()); + t.Add("isDistinct", (IEnumerable f) => f.IsDistinct()); + t.Add("subsetOf", (IEnumerable f, IEnumerable a) => f.SubsetOf(a)); + t.Add("supersetOf", (IEnumerable f, IEnumerable a) => a.SubsetOf(f)); + t.Add("intersect", (IEnumerable f, IEnumerable a) => f.Intersect(a)); + t.Add("exclude", (IEnumerable f, IEnumerable a) => f.Exclude(a)); + + t.Add("today", (object _) => P.Date.Today()); + t.Add("now", (object _) => P.DateTime.Now()); + t.Add("timeOfDay", (object _) => P.Time.Now()); + + t.Add("binary.&", (object _, string a, string b) => (a ?? "") + (b ?? "")); + + t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee), typeof(Invokee)), runIif); + t.Add(new CallSignature("iif", typeof(IEnumerable), typeof(object), typeof(bool?), typeof(Invokee)), runIif); + + // Functions that use normal null propagation and work with the focus (buy may ignore it) + t.Add("not", (IEnumerable f) => f.Not(), doNullProp: true); + // t.Add("builtin.children", (IEnumerable f, string a) => f.Navigate(a), doNullProp: true); + t.AddBuiltinChildren(); + + t.Add("children", (IEnumerable f) => f.SelectMany(node => node.Children().SelectMany(n => n)), doNullProp: true); + t.Add("descendants", (IEnumerable f) => f.Descendants(), doNullProp: true); + + t.Add("binary.=", (object f, IEnumerable a, IEnumerable b) => a.IsEqualTo(b), doNullProp: true); + t.Add("binary.!=", (object f, IEnumerable a, IEnumerable b) => !a.IsEqualTo(b), doNullProp: true); + t.Add("binary.~", (object f, IEnumerable a, IEnumerable b) => a.IsEquivalentTo(b), doNullProp: false); + t.Add("binary.!~", (object f, IEnumerable a, IEnumerable b) => !a.IsEquivalentTo(b), doNullProp: false); + + t.Add("unary.-", (object f, int a) => -a, doNullProp: true); + t.Add("unary.-", (object f, long a) => -a, doNullProp: true); + t.Add("unary.-", (object f, decimal a) => -a, doNullProp: true); + t.Add("unary.-", (object f, P.Quantity a) => new P.Quantity(-a.Value, a.Unit), doNullProp: true); + t.Add("unary.+", (object f, int a) => a, doNullProp: true); + t.Add("unary.+", (object f, long a) => a, doNullProp: true); + t.Add("unary.+", (object f, decimal a) => a, doNullProp: true); + t.Add("unary.+", (object f, P.Quantity a) => a, doNullProp: true); + + t.Add("binary.*", (object f, int a, int b) => a * b, doNullProp: true); + t.Add("binary.*", (object f, long a, long b) => a * b, doNullProp: true); + t.Add("binary.*", (object f, decimal a, decimal b) => a * b, doNullProp: true); + // t.Add("binary.*", (object f, P.Quantity a, P.Quantity b) => a * b, doNullProp: true); + + t.Add("binary./", (object f, decimal a, decimal b) => b != 0 ? a / b : (decimal?)null, doNullProp: true); + // t.Add("binary./", (object f, P.Quantity a, P.Quantity b) => a / b, doNullProp: true); + + t.Add("binary.+", (object f, int a, int b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, long a, long b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, decimal a, decimal b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, string a, string b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, P.DateTime a, P.Quantity b) => a + b, doNullProp: true); + t.Add("binary.+", (object f, P.Date a, P.Quantity b) => a + b, doNullProp: true); + // t.Add("binary.+", (object f, P.Quantity a, P.Quantity b) => a + b, doNullProp: true); + + t.Add("binary.-", (object f, int a, int b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, long a, long b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, decimal a, decimal b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, P.DateTime a, P.Quantity b) => a - b, doNullProp: true); + t.Add("binary.-", (object f, P.Date a, P.Quantity b) => a - b, doNullProp: true); + // t.Add("binary.-", (object f, P.Quantity a, P.Quantity b) => a - b, doNullProp: true); + + t.Add("binary.div", (object f, int a, int b) => b != 0 ? a / b : (int?)null, doNullProp: true); + t.Add("binary.div", (object f, long a, long b) => b != 0 ? a / b : (long?)null, doNullProp: true); + t.Add("binary.div", (object f, decimal a, decimal b) => b != 0 ? (long?)Math.Truncate(a / b) : null, doNullProp: true); + + t.Add("binary.mod", (object f, int a, int b) => b != 0 ? a % b : (int?)null, doNullProp: true); + t.Add("binary.mod", (object f, long a, long b) => b != 0 ? a % b : (long?)null, doNullProp: true); + t.Add("binary.mod", (object f, decimal a, decimal b) => b != 0 ? a % b : (decimal?)null, doNullProp: true); + + t.Add("binary.>", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">"), doNullProp: true); + t.Add("binary.<", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<"), doNullProp: true); + t.Add("binary.<=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, "<="), doNullProp: true); + t.Add("binary.>=", (object f, P.Any a, P.Any b) => EqualityOperators.Compare(a, b, ">="), doNullProp: true); + + t.Add("single", (IEnumerable f) => f.Single(), doNullProp: true); + t.Add("skip", (IEnumerable f, long a) => f.Skip((int)a), doNullProp: true); + t.Add("first", (IEnumerable f) => f.First(), doNullProp: true); + t.Add("last", (IEnumerable f) => f.Last(), doNullProp: true); + t.Add("tail", (IEnumerable f) => f.Tail(), doNullProp: true); + t.Add("take", (IEnumerable f, long a) => f.Take((int)a), doNullProp: true); + t.Add("builtin.item", (IEnumerable f, long a) => f.Item((int)a), doNullProp: true); + + t.Add("toBoolean", (P.Any f) => f.ToBoolean(), doNullProp: true); + t.Add("convertsToBoolean", (P.Any f) => f.ConvertsToBoolean(), doNullProp: true); + t.Add("toInteger", (P.Any f) => f.ToInteger(), doNullProp: true); + t.Add("convertsToInteger", (P.Any f) => f.ConvertsToInteger(), doNullProp: true); + t.Add("toLong", (P.Any f) => f.ToLong(), doNullProp: true); + t.Add("convertsToLong", (P.Any f) => f.ConvertsToLong(), doNullProp: true); + t.Add("toDecimal", (P.Any f) => f.ToDecimal(), doNullProp: true); + t.Add("convertsToDecimal", (P.Any f) => f.ConvertsToDecimal(), doNullProp: true); + t.Add("toQuantity", (P.Any f) => f.ToQuantity(), doNullProp: true); + t.Add("convertsToQuantity", (P.Any f) => f.ConvertsToQuantity(), doNullProp: true); + t.Add("toString", (P.Any f) => f.ToStringRepresentation(), doNullProp: true); + t.Add("convertsToString", (P.Any f) => f.ConvertsToString(), doNullProp: true); + t.Add("toDate", (P.Any f) => f.ToDate(), doNullProp: true); + t.Add("convertsToDate", (P.Any f) => f.ConvertsToDate(), doNullProp: true); + t.Add("toDateTime", (P.Any f) => f.ToDateTime(), doNullProp: true); + t.Add("convertsToDateTime", (P.Any f) => f.ConvertsToDateTime(), doNullProp: true); + t.Add("toTime", (P.Any f) => f.ToTime(), doNullProp: true); + t.Add("convertsToTime", (P.Any f) => f.ConvertsToTime(), doNullProp: true); + + t.Add("upper", (string f) => f.ToUpper(), doNullProp: true); + t.Add("lower", (string f) => f.ToLower(), doNullProp: true); + t.Add("toChars", (string f) => f.ToChars(), doNullProp: true); + t.Add("substring", (string f, int a) => f.FpSubstring(a, null), doNullProp: true); + t.Add("trim", (string f) => f.Trim(), doNullProp: true); + t.Add("encode", (string f, string enc) => f.FpEncode(enc), doNullProp: true); + t.Add("decode", (string f, string enc) => f.FpDecode(enc), doNullProp: true); + t.Add("escape", (string f, string enc) => f.FpEscape(enc), doNullProp: true); + t.Add("unescape", (string f, string enc) => f.FpUnescape(enc), doNullProp: true); + + //special case: only focus should be Null propagated: + t.Add(new CallSignature("substring", typeof(string), typeof(string), typeof(int), typeof(int?)), + InvokeeFactory.WrapWithPropNullForFocus((string f, int a, int? b) => f.FpSubstring(a, b))); + t.Add("startsWith", (string f, string fragment) => f.StartsWith(fragment), doNullProp: true); + t.Add("endsWith", (string f, string fragment) => f.EndsWith(fragment), doNullProp: true); + t.Add("matches", (string f, string regex) => Regex.IsMatch(f, regex), doNullProp: true); + t.Add("indexOf", (string f, string fragment) => f.FpIndexOf(fragment), doNullProp: true); + t.Add("contains", (string f, string fragment) => f.Contains(fragment), doNullProp: true); + t.Add("replaceMatches", (string f, string regex, string subst) => Regex.Replace(f, regex, subst), doNullProp: true); + t.Add("replace", (string f, string regex, string subst) => f.FpReplace(regex, subst), doNullProp: true); + t.Add("length", (string f) => f.Length, doNullProp: true); + t.Add("split", (string f, string seperator) => f.FpSplit(seperator), doNullProp: true); + t.Add("join", (IEnumerable f, string separator) => f.FpJoin(separator), doNullProp: true); + t.Add("join", (IEnumerable f) => f.FpJoin(), doNullProp: true); + t.Add("indexOf", (IEnumerable f, PocoNode elem, int start) => f.IndexOf(elem, start), doNullProp: true); + t.Add("indexOf", (IEnumerable f, PocoNode elem) => f.IndexOf(elem), doNullProp: true); + t.Add("lastIndexOf", (IEnumerable f, PocoNode elem, int start) => f.LastIndexOf(elem, start), doNullProp: true); + t.Add("lastIndexOf", (IEnumerable f, PocoNode elem) => f.LastIndexOf(elem), doNullProp: true); + + // Math functions + t.Add("abs", (decimal f) => Math.Abs(f), doNullProp: true); + t.Add("abs", (P.Quantity f) => new P.Quantity(Math.Abs(f.Value), f.Unit), doNullProp: true); + t.Add("ceiling", (decimal f) => Math.Ceiling(f), doNullProp: true); + t.Add("exp", (decimal f) => Math.Exp((double)f), doNullProp: true); + t.Add("floor", (decimal f) => Math.Floor(f), doNullProp: true); + t.Add("ln", (decimal f) => Math.Log((double)f), doNullProp: true); + t.Add("log", (decimal f, decimal @base) => Math.Log((double)f, (double)@base), doNullProp: true); + t.Add("power", (decimal f, decimal exponent) => f.Power(exponent), doNullProp: true); + t.Add("round", (decimal f, long precision) => Math.Round(f, (int)precision), doNullProp: true); + t.Add("round", (decimal f) => Math.Round(f), doNullProp: true); + t.Add("sqrt", (decimal f) => f.Sqrt(), doNullProp: true); + t.Add("truncate", (decimal f) => Math.Truncate((double)f), doNullProp: true); + + // The next two functions existed pre-normative, so we have kept them. + t.Add("is", (PocoNode f, string name) => f.Is(name), doNullProp: true); + t.Add("as", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); + + t.Add("ofType", (IEnumerable f, string name) => f.FilterType(name), doNullProp: true); + t.Add("binary.is", (object f, PocoNode left, string name) => left.Is(name), doNullProp: true); + t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); + + // Kept for backwards compatibility, but no longer part of the spec + t.Add("binary.as", (object f, IEnumerable left, string name) => left.FilterType(name), doNullProp: true); + + t.Add("extension", (IEnumerable f, string url) => f.Extension(url), doNullProp: true); + + // Logic operators do not use null propagation and may do short-cut eval + t.AddLogic("binary.and", (a, b) => a.And(b)); + t.AddLogic("binary.or", (a, b) => a.Or(b)); + t.AddLogic("binary.xor", (a, b) => a.XOr(b)); + t.AddLogic("binary.implies", (a, b) => a.Implies(b)); + + // Special late-bound functions + t.Add(new CallSignature("where", typeof(IEnumerable), typeof(object), typeof(Invokee)), runWhere); + t.Add(new CallSignature("select", typeof(IEnumerable), typeof(object), typeof(Invokee)), runSelect); + t.Add(new CallSignature("all", typeof(bool), typeof(object), typeof(Invokee)), runAll); + t.Add(new CallSignature("any", typeof(bool), typeof(object), typeof(Invokee)), runAny); + t.Add(new CallSignature("exists", typeof(bool), typeof(object), typeof(Invokee)), runAny); + t.Add(new CallSignature("repeat", typeof(IEnumerable), typeof(object), typeof(Invokee)), runRepeat); + t.Add(new CallSignature("trace", typeof(IEnumerable), typeof(string), typeof(object), typeof(Invokee)), Trace); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string)), DefineVariable); + t.Add(new CallSignature("defineVariable", typeof(IEnumerable), typeof(object), typeof(string), typeof(Invokee)), DefineVariable); + + t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee)), runAggregate); + t.Add(new CallSignature("aggregate", typeof(IEnumerable), typeof(Invokee), typeof(Invokee), typeof(Invokee)), runAggregate); + + t.AddVar("sct", "http://snomed.info/sct"); + t.AddVar("loinc", "http://loinc.org"); + t.AddVar("ucum", "http://unitsofmeasure.org"); + + t.Add("builtin.coreexturl", (object f, string id) => getCoreExtensionUrl(id)); + t.Add("builtin.corevsurl", (object f, string id) => getCoreValueSetUrl(id)); + + return t; + } - /// - /// With the regular Add extension methods, a Wrap is added to each argument to turn it into IEnumerable<PocoNode>. - /// For 'builtin.children' we know that the focus and the result are already of the correct type, - /// so we created an optimized implementation avoiding the Wrap. - /// - /// - internal static void AddBuiltinChildren(this SymbolTable table) + /// + /// With the regular Add extension methods, a Wrap is added to each argument to turn it into IEnumerable<PocoNode>. + /// For 'builtin.children' we know that the focus and the result are already of the correct type, + /// so we created an optimized implementation avoiding the Wrap. + /// + /// + internal static void AddBuiltinChildren(this SymbolTable table) + { + table.Add(new CallSignature("builtin.children", + typeof(IEnumerable), + typeof(IEnumerable), + typeof(string)), ( + ctx, invokees) => { - table.Add(new CallSignature("builtin.children", - typeof(IEnumerable), - typeof(IEnumerable), - typeof(string)), ( - ctx, invokees) => - { - var iks = invokees.ToArray(); - var focus = iks[0].Invoke(ctx, InvokeeFactory.EmptyArgs); - var name = iks[1].Invoke(ctx, InvokeeFactory.EmptyArgs).First().GetValue() as string; - var result= focus.Navigate(name); + var iks = invokees.ToArray(); + var focus = iks[0].Invoke(ctx, InvokeeFactory.EmptyArgs); + var name = iks[1].Invoke(ctx, InvokeeFactory.EmptyArgs).First().GetValue() as string; + var result= focus.Navigate(name); - return result; - }); - } + return result; + }); + } - private static string getCoreExtensionUrl(string id) - { - return "http://hl7.org/fhir/StructureDefinition/" + id; - } + private static string getCoreExtensionUrl(string id) + { + return "http://hl7.org/fhir/StructureDefinition/" + id; + } - private static string getCoreValueSetUrl(string id) - { - return "http://hl7.org/fhir/ValueSet/" + id; - } + private static string getCoreValueSetUrl(string id) + { + return "http://hl7.org/fhir/ValueSet/" + id; + } - private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) + private static IEnumerable runAggregate(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var incrExpre = arguments.Skip(1).First(); + IEnumerable initialValue = []; + if (arguments.Count() > 2) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var incrExpre = arguments.Skip(1).First(); - IEnumerable initialValue = []; - if (arguments.Count() > 2) - { - var initialValueExpr = arguments.Skip(2).First(); - initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs); - } - - var totalContext = ctx.Nest(); - totalContext.SetTotal(initialValue); + var initialValueExpr = arguments.Skip(2).First(); + initialValue = initialValueExpr(ctx, InvokeeFactory.EmptyArgs); + } - foreach (PocoNode element in focus) - { - IEnumerable newFocus = [element]; - var newContext = totalContext.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetTotal(totalContext.GetTotal()); - var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); - totalContext.SetTotal(newTotalResult); - } + var totalContext = ctx.Nest(); + totalContext.SetTotal(initialValue); - return totalContext.GetTotal(); + foreach (PocoNode element in focus) + { + IEnumerable newFocus = [element]; + var newContext = totalContext.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetTotal(totalContext.GetTotal()); + var newTotalResult = incrExpre(newContext, InvokeeFactory.EmptyArgs); + totalContext.SetTotal(newTotalResult); } - private static IEnumerable Trace(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - string name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.GetValue() as string; + return totalContext.GetTotal(); + } - List selectArgs = [arguments.First(), .. arguments.Skip(2)]; - var selectResults = runSelect(ctx, selectArgs); - ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); + private static IEnumerable Trace(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + string name = arguments.Skip(1).First()(ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.GetValue() as string; - return focus; - } + List selectArgs = [arguments.First(), .. arguments.Skip(2)]; + var selectResults = runSelect(ctx, selectArgs); + ctx?.EvaluationContext?.Tracer?.Invoke(name, selectResults); + + return focus; + } - private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments) - { - Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); - var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); - string name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.GetValue() as string; + private static IEnumerable DefineVariable(Closure ctx, IEnumerable arguments) + { + Invokee[] enumerable = arguments as Invokee[] ?? arguments.ToArray(); + var focus = enumerable[0](ctx, InvokeeFactory.EmptyArgs); + string name = enumerable[1](ctx, InvokeeFactory.EmptyArgs).FirstOrDefault()?.GetValue() as string; - if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); + if(ctx.ResolveValue(name) is not null) throw new InvalidOperationException($"Variable {name} is already defined in this scope"); - if (enumerable.Length == 2) - { - ctx.SetValue(name, focus); - } - else - { - var newContext = ctx.Nest(focus); - newContext.SetThis(focus); - var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); - ctx.SetValue(name, result); - } - - return focus; + if (enumerable.Length == 2) + { + ctx.SetValue(name, focus); } - - private static IEnumerable runIif(Closure ctx, IEnumerable arguments) + else { - // iif(criterion: expression, true-result: collection [, otherwise-result: collection]) : collection - // note: short-circuit behavior is expected in this function - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var newContext = ctx.Nest(focus); newContext.SetThis(focus); - - var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs); - var trueResult = arguments.Skip(2).First(); - var otherResult = arguments.Skip(3).FirstOrDefault(); + var result = enumerable[2](newContext, InvokeeFactory.EmptyArgs); + ctx.SetValue(name, result); + } - if (expression.Count() > 1) - throw Error.InvalidOperation($"Result of {nameof(expression)} is not of type boolean"); + return focus; + } - return (expression.BooleanEval() ?? false) - ? trueResult(newContext, InvokeeFactory.EmptyArgs) // share focus with this function - : otherResult == null ? [] : otherResult(newContext, InvokeeFactory.EmptyArgs); - } + private static IEnumerable runIif(Closure ctx, IEnumerable arguments) + { + // iif(criterion: expression, true-result: collection [, otherwise-result: collection]) : collection + // note: short-circuit behavior is expected in this function + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - private static IEnumerable runWhere(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); + var newContext = ctx.Nest(focus); + newContext.SetThis(focus); + + var expression = arguments.Skip(1).First()(newContext, InvokeeFactory.EmptyArgs); + var trueResult = arguments.Skip(2).First(); + var otherResult = arguments.Skip(3).FirstOrDefault(); - return CachedEnumerable.Create(runForeach()); + if (expression.Count() > 1) + throw Error.InvalidOperation($"Result of {nameof(expression)} is not of type boolean"); - IEnumerable runForeach() - { - var index = 0; - - foreach (PocoNode element in focus) - { - PocoNode[] newFocus = [element]; - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(PocoNode.ForPrimitive(index)); - index++; - - if (lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval() == true) - yield return element; - } - } - } + return (expression.BooleanEval() ?? false) + ? trueResult(newContext, InvokeeFactory.EmptyArgs) // share focus with this function + : otherResult == null ? [] : otherResult(newContext, InvokeeFactory.EmptyArgs); + } - private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) - { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); + private static IEnumerable runWhere(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); - return CachedEnumerable.Create(runForeach()); + return CachedEnumerable.Create(runForeach()); - IEnumerable runForeach() - { - var index = 0; - - foreach (PocoNode element in focus) - { - IEnumerable newFocus = [element]; - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(PocoNode.ForPrimitive(index)); - index++; - - var result = lambda(newContext, InvokeeFactory.EmptyArgs); - foreach (var resultElement in result) // implement SelectMany() - yield return resultElement; - } - } - } - - private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments) + IEnumerable runForeach() { - var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); - var lambda = arguments.Skip(1).First(); - - var fullResult = new List(); + var index = 0; - while (newNodes.Any()) + foreach (PocoNode element in focus) { - var index = 0; - var current = newNodes; - newNodes = []; - - foreach (PocoNode element in current) - { - IEnumerable newFocus = [element]; - var newContext = ctx.Nest(newFocus); - newContext.SetThis(newFocus); - newContext.SetIndex(PocoNode.ForPrimitive(index)); - index++; - - var candidates = lambda(newContext, InvokeeFactory.EmptyArgs); - var uniqeNewNodes = candidates.Except(fullResult, EqualityOperators.TypedElementEqualityComparer); - - newNodes.AddRange(uniqeNewNodes); - } + PocoNode[] newFocus = [element]; + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(PocoNode.ForPrimitive(index)); + index++; - fullResult.AddRange(newNodes); + if (lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval() == true) + yield return element; } - - return fullResult; } + } + + private static IEnumerable runSelect(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + + return CachedEnumerable.Create(runForeach()); - private static IEnumerable runAll(Closure ctx, IEnumerable arguments) + IEnumerable runForeach() { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); var index = 0; foreach (PocoNode element in focus) @@ -445,21 +391,27 @@ private static IEnumerable runAll(Closure ctx, IEnumerable ar newContext.SetIndex(PocoNode.ForPrimitive(index)); index++; - var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); - if (result == null) return []; - if (result == false) return PocoNode.ForPrimitive(false); + var result = lambda(newContext, InvokeeFactory.EmptyArgs); + foreach (var resultElement in result) // implement SelectMany() + yield return resultElement; } - - return PocoNode.ForPrimitive(true); } + } + + private static IEnumerable runRepeat(Closure ctx, IEnumerable arguments) + { + var newNodes = arguments.First()(ctx, InvokeeFactory.EmptyArgs).ToList(); + var lambda = arguments.Skip(1).First(); + + var fullResult = new List(); - private static IEnumerable runAny(Closure ctx, IEnumerable arguments) + while (newNodes.Any()) { - var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); - var lambda = arguments.Skip(1).First(); var index = 0; + var current = newNodes; + newNodes = []; - foreach (PocoNode element in focus) + foreach (PocoNode element in current) { IEnumerable newFocus = [element]; var newContext = ctx.Nest(newFocus); @@ -467,11 +419,58 @@ private static IEnumerable runAny(Closure ctx, IEnumerable ar newContext.SetIndex(PocoNode.ForPrimitive(index)); index++; - var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); - if (result == true) return PocoNode.ForPrimitive(true); + var candidates = lambda(newContext, InvokeeFactory.EmptyArgs); + var uniqeNewNodes = candidates.Except(fullResult, EqualityOperators.TypedElementEqualityComparer); + + newNodes.AddRange(uniqeNewNodes); } - - return PocoNode.Root(new FhirBoolean(false)); + + fullResult.AddRange(newNodes); + } + + return fullResult; + } + + private static IEnumerable runAll(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + var index = 0; + + foreach (PocoNode element in focus) + { + IEnumerable newFocus = [element]; + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(PocoNode.ForPrimitive(index)); + index++; + + var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); + if (result == null) return []; + if (result == false) return PocoNode.ForPrimitive(false); + } + + return PocoNode.ForPrimitive(true); + } + + private static IEnumerable runAny(Closure ctx, IEnumerable arguments) + { + var focus = arguments.First()(ctx, InvokeeFactory.EmptyArgs); + var lambda = arguments.Skip(1).First(); + var index = 0; + + foreach (PocoNode element in focus) + { + IEnumerable newFocus = [element]; + var newContext = ctx.Nest(newFocus); + newContext.SetThis(newFocus); + newContext.SetIndex(PocoNode.ForPrimitive(index)); + index++; + + var result = lambda(newContext, InvokeeFactory.EmptyArgs).BooleanEval(); + if (result == true) return PocoNode.ForPrimitive(true); } + + return PocoNode.Root(new FhirBoolean(false)); } } \ No newline at end of file diff --git a/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs b/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs index 8ef960c8db..9c76c590aa 100644 --- a/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs +++ b/src/Hl7.Fhir.Base/FhirPath/FhirPathCompiler.cs @@ -10,57 +10,57 @@ using Hl7.FhirPath.Expressions; using Hl7.FhirPath.Parser; using Hl7.FhirPath.Sprache; +using System; -namespace Hl7.FhirPath +namespace Hl7.FhirPath; + +public class FhirPathCompiler { - public class FhirPathCompiler - { - private static Lazy _defaultSymbolTable = new(() => new SymbolTable().AddStandardFP()); + private static Lazy _defaultSymbolTable = new(() => new SymbolTable().AddStandardFP()); - public static void SetDefaultSymbolTable(Lazy st) - { - _defaultSymbolTable = st; - } + public static void SetDefaultSymbolTable(Lazy st) + { + _defaultSymbolTable = st; + } - public static SymbolTable DefaultSymbolTable - { - get { return _defaultSymbolTable.Value; } - } + public static SymbolTable DefaultSymbolTable + { + get { return _defaultSymbolTable.Value; } + } - public SymbolTable Symbols { get; private set; } + public SymbolTable Symbols { get; private set; } - public FhirPathCompiler(SymbolTable symbols) - { - Symbols = symbols; - } + public FhirPathCompiler(SymbolTable symbols) + { + Symbols = symbols; + } - public FhirPathCompiler() : this(DefaultSymbolTable) - { - } + public FhirPathCompiler() : this(DefaultSymbolTable) + { + } #pragma warning disable CA1822 // Mark members as static - public Expression Parse(string expression) + public Expression Parse(string expression) #pragma warning restore CA1822 // This might access instance data in the future. - { - var parse = Grammar.Expression.End().TryParse(expression); - - return parse.WasSuccessful ? parse.Value : throw new FormatException("Compilation failed: " + parse.ToString()); - } + { + var parse = Grammar.Expression.End().TryParse(expression); - public CompiledExpression Compile(Expression expression) - { - Invokee inv = expression.ToEvaluator(Symbols); + return parse.WasSuccessful ? parse.Value : throw new FormatException("Compilation failed: " + parse.ToString()); + } - return (focus, ctx) => - { - var closure = Closure.Root(focus, ctx); - return inv(closure, InvokeeFactory.EmptyArgs); - }; - } + public CompiledExpression Compile(Expression expression) + { + Invokee inv = expression.ToEvaluator(Symbols); - public CompiledExpression Compile(string expression) + return (focus, ctx) => { - return Compile(Parse(expression)); - } + var closure = Closure.Root(focus, ctx); + return inv(closure, InvokeeFactory.EmptyArgs); + }; + } + + public CompiledExpression Compile(string expression) + { + return Compile(Parse(expression)); } -} +} \ No newline at end of file From 2c003b62e05d1f35c0d57446a4e97cb159f716c9 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Wed, 26 Feb 2025 18:02:35 +0100 Subject: [PATCH 13/17] did not build --- .../Hl7.Fhir.Specification.R4.Tests.csproj | 1 - .../Hl7.Fhir.Specification.R4B.Tests.csproj | 1 - .../Hl7.Fhir.Specification.R5.Tests.csproj | 1 - src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj | 1 - src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj | 1 - src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs | 3 ++- src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj | 1 - 7 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj index ef3703d451..e744d03e9c 100644 --- a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj index beddd7f3c1..b69fe3ee62 100644 --- a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj @@ -17,7 +17,6 @@ - diff --git a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj index 0698c4d777..5d73bfeb3f 100644 --- a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj @@ -22,7 +22,6 @@ - diff --git a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj index b6b16fa499..5a1d37e3a0 100644 --- a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj +++ b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj index 7e05da129c..038deb69f1 100644 --- a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj +++ b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj @@ -22,7 +22,6 @@ - diff --git a/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs index 27332569fb..6d0c6cb0d0 100644 --- a/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/StringFunctionsTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Hl7.Fhir.ElementModel; using Hl7.FhirPath.Functions; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; @@ -13,7 +14,7 @@ public class StringFunctionsTests [TestMethod] public void StringSplit() { - CollectionAssert.AreEqual(new[] { "A", "B", "C" }, StringOperators.FpSplit("A,B,C", ",").Select(r => r.Value.ToString()).ToArray()); + CollectionAssert.AreEqual(new[] { "A", "B", "C" }, StringOperators.FpSplit("A,B,C", ",").Select(r => r.GetValue()!.ToString()).ToArray()); // verify the empty string CollectionAssert.AreEqual(new[] { "A", "", "C" }, StringOperators.FpSplit("A,,C", ",").Select(r => r.GetValue()!.ToString()).ToArray()); diff --git a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj index 312d91edaa..e25f7d3a92 100644 --- a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj +++ b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj @@ -10,7 +10,6 @@ - From ab59d587bc9ceb538c96be07b429a1c937900a00 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 27 Feb 2025 13:56:56 +0100 Subject: [PATCH 14/17] changed TestFramework version to match TestAdapter version. The unit tests run again! --- .../FhirPath/Expressions/SymbolTableExtensions.cs | 2 +- .../Hl7.Fhir.ElementModel.R4.Tests.csproj | 2 +- .../Hl7.Fhir.ElementModel.R4B.Tests.csproj | 2 +- .../Hl7.Fhir.ElementModel.R5.Tests.csproj | 2 +- .../Hl7.Fhir.ElementModel.STU3.Tests.csproj | 2 +- src/Hl7.Fhir.R4.Tests/Hl7.Fhir.R4.Tests.csproj | 2 +- src/Hl7.Fhir.R4B.Tests/Hl7.Fhir.R4B.Tests.csproj | 2 +- src/Hl7.Fhir.R5.Tests/Hl7.Fhir.R5.Tests.csproj | 2 +- src/Hl7.Fhir.STU3.Tests/Hl7.Fhir.STU3.Tests.csproj | 2 +- .../Hl7.Fhir.Serialization.R4.Tests.csproj | 2 +- .../Hl7.Fhir.Serialization.R4B.Tests.csproj | 2 +- .../Hl7.Fhir.Serialization.R5.Tests.csproj | 2 +- .../Hl7.Fhir.Serialization.STU3.Tests.csproj | 2 +- .../Hl7.Fhir.Specification.R4.Tests.csproj | 4 ++-- .../Hl7.Fhir.Specification.R4B.Tests.csproj | 4 ++-- .../Hl7.Fhir.Specification.R5.Tests.csproj | 4 ++-- .../Hl7.Fhir.Specification.STU3.Tests.csproj | 2 +- .../Hl7.Fhir.Support.Poco.Tests.csproj | 2 +- src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj | 4 ++-- src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj | 4 ++-- src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj | 4 ++-- 21 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs index 5672151eea..462c872be8 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/SymbolTableExtensions.cs @@ -65,7 +65,7 @@ public static void AddLogic(this SymbolTable table, string name, Func - + \ No newline at end of file diff --git a/src/Hl7.Fhir.ElementModel.R4B.Tests/Hl7.Fhir.ElementModel.R4B.Tests.csproj b/src/Hl7.Fhir.ElementModel.R4B.Tests/Hl7.Fhir.ElementModel.R4B.Tests.csproj index e7d037220b..ed5b099036 100644 --- a/src/Hl7.Fhir.ElementModel.R4B.Tests/Hl7.Fhir.ElementModel.R4B.Tests.csproj +++ b/src/Hl7.Fhir.ElementModel.R4B.Tests/Hl7.Fhir.ElementModel.R4B.Tests.csproj @@ -27,6 +27,6 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.ElementModel.R5.Tests/Hl7.Fhir.ElementModel.R5.Tests.csproj b/src/Hl7.Fhir.ElementModel.R5.Tests/Hl7.Fhir.ElementModel.R5.Tests.csproj index 56a551e7e7..3783fcfe38 100644 --- a/src/Hl7.Fhir.ElementModel.R5.Tests/Hl7.Fhir.ElementModel.R5.Tests.csproj +++ b/src/Hl7.Fhir.ElementModel.R5.Tests/Hl7.Fhir.ElementModel.R5.Tests.csproj @@ -27,6 +27,6 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.ElementModel.STU3.Tests/Hl7.Fhir.ElementModel.STU3.Tests.csproj b/src/Hl7.Fhir.ElementModel.STU3.Tests/Hl7.Fhir.ElementModel.STU3.Tests.csproj index 6521d4c5ea..7295259015 100644 --- a/src/Hl7.Fhir.ElementModel.STU3.Tests/Hl7.Fhir.ElementModel.STU3.Tests.csproj +++ b/src/Hl7.Fhir.ElementModel.STU3.Tests/Hl7.Fhir.ElementModel.STU3.Tests.csproj @@ -23,7 +23,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.R4.Tests/Hl7.Fhir.R4.Tests.csproj b/src/Hl7.Fhir.R4.Tests/Hl7.Fhir.R4.Tests.csproj index 67680013c8..101f7d8698 100644 --- a/src/Hl7.Fhir.R4.Tests/Hl7.Fhir.R4.Tests.csproj +++ b/src/Hl7.Fhir.R4.Tests/Hl7.Fhir.R4.Tests.csproj @@ -30,7 +30,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.R4B.Tests/Hl7.Fhir.R4B.Tests.csproj b/src/Hl7.Fhir.R4B.Tests/Hl7.Fhir.R4B.Tests.csproj index 70918f893e..b1d1bc291d 100644 --- a/src/Hl7.Fhir.R4B.Tests/Hl7.Fhir.R4B.Tests.csproj +++ b/src/Hl7.Fhir.R4B.Tests/Hl7.Fhir.R4B.Tests.csproj @@ -30,7 +30,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.R5.Tests/Hl7.Fhir.R5.Tests.csproj b/src/Hl7.Fhir.R5.Tests/Hl7.Fhir.R5.Tests.csproj index 9395968dfc..91bc440475 100644 --- a/src/Hl7.Fhir.R5.Tests/Hl7.Fhir.R5.Tests.csproj +++ b/src/Hl7.Fhir.R5.Tests/Hl7.Fhir.R5.Tests.csproj @@ -30,7 +30,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.STU3.Tests/Hl7.Fhir.STU3.Tests.csproj b/src/Hl7.Fhir.STU3.Tests/Hl7.Fhir.STU3.Tests.csproj index abbc481433..77b71deb7b 100644 --- a/src/Hl7.Fhir.STU3.Tests/Hl7.Fhir.STU3.Tests.csproj +++ b/src/Hl7.Fhir.STU3.Tests/Hl7.Fhir.STU3.Tests.csproj @@ -27,7 +27,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R4.Tests/Hl7.Fhir.Serialization.R4.Tests.csproj b/src/Hl7.Fhir.Serialization.R4.Tests/Hl7.Fhir.Serialization.R4.Tests.csproj index 42ce41628f..44bff7cd84 100644 --- a/src/Hl7.Fhir.Serialization.R4.Tests/Hl7.Fhir.Serialization.R4.Tests.csproj +++ b/src/Hl7.Fhir.Serialization.R4.Tests/Hl7.Fhir.Serialization.R4.Tests.csproj @@ -27,7 +27,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R4B.Tests/Hl7.Fhir.Serialization.R4B.Tests.csproj b/src/Hl7.Fhir.Serialization.R4B.Tests/Hl7.Fhir.Serialization.R4B.Tests.csproj index a390f7c68a..61b6e07e6f 100644 --- a/src/Hl7.Fhir.Serialization.R4B.Tests/Hl7.Fhir.Serialization.R4B.Tests.csproj +++ b/src/Hl7.Fhir.Serialization.R4B.Tests/Hl7.Fhir.Serialization.R4B.Tests.csproj @@ -23,7 +23,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.R5.Tests/Hl7.Fhir.Serialization.R5.Tests.csproj b/src/Hl7.Fhir.Serialization.R5.Tests/Hl7.Fhir.Serialization.R5.Tests.csproj index 2b3e1b33f2..7243465d0a 100644 --- a/src/Hl7.Fhir.Serialization.R5.Tests/Hl7.Fhir.Serialization.R5.Tests.csproj +++ b/src/Hl7.Fhir.Serialization.R5.Tests/Hl7.Fhir.Serialization.R5.Tests.csproj @@ -27,7 +27,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Serialization.STU3.Tests/Hl7.Fhir.Serialization.STU3.Tests.csproj b/src/Hl7.Fhir.Serialization.STU3.Tests/Hl7.Fhir.Serialization.STU3.Tests.csproj index 831f722ca3..4d776f8005 100644 --- a/src/Hl7.Fhir.Serialization.STU3.Tests/Hl7.Fhir.Serialization.STU3.Tests.csproj +++ b/src/Hl7.Fhir.Serialization.STU3.Tests/Hl7.Fhir.Serialization.STU3.Tests.csproj @@ -32,6 +32,6 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj index e744d03e9c..c1df206cf1 100644 --- a/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4.Tests/Hl7.Fhir.Specification.R4.Tests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj index b69fe3ee62..fe0cd5de90 100644 --- a/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R4B.Tests/Hl7.Fhir.Specification.R4B.Tests.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj index 5d73bfeb3f..1004ef2d1e 100644 --- a/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj +++ b/src/Hl7.Fhir.Specification.R5.Tests/Hl7.Fhir.Specification.R5.Tests.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/src/Hl7.Fhir.Specification.STU3.Tests/Hl7.Fhir.Specification.STU3.Tests.csproj b/src/Hl7.Fhir.Specification.STU3.Tests/Hl7.Fhir.Specification.STU3.Tests.csproj index 099d7dfd1f..887c491335 100644 --- a/src/Hl7.Fhir.Specification.STU3.Tests/Hl7.Fhir.Specification.STU3.Tests.csproj +++ b/src/Hl7.Fhir.Specification.STU3.Tests/Hl7.Fhir.Specification.STU3.Tests.csproj @@ -20,7 +20,7 @@ - + \ No newline at end of file diff --git a/src/Hl7.Fhir.Support.Poco.Tests/Hl7.Fhir.Support.Poco.Tests.csproj b/src/Hl7.Fhir.Support.Poco.Tests/Hl7.Fhir.Support.Poco.Tests.csproj index 184145b36b..337ae6e138 100644 --- a/src/Hl7.Fhir.Support.Poco.Tests/Hl7.Fhir.Support.Poco.Tests.csproj +++ b/src/Hl7.Fhir.Support.Poco.Tests/Hl7.Fhir.Support.Poco.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj index 5a1d37e3a0..5c344142b8 100644 --- a/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj +++ b/src/Hl7.Fhir.Support.Tests/Hl7.Fhir.Support.Tests.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj index 038deb69f1..aee33fba6f 100644 --- a/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj +++ b/src/Hl7.FhirPath.R4.Tests/Hl7.FhirPath.R4.Tests.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj index e25f7d3a92..c7d8d21cca 100644 --- a/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj +++ b/src/Hl7.FhirPath.Tests/HL7.FhirPath.Tests.csproj @@ -9,8 +9,8 @@ - - + + From 727551d39b209f086b028101986318f3098f3862 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 27 Feb 2025 16:02:21 +0100 Subject: [PATCH 15/17] wip. note that functionsTests is temp changed --- src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs | 12 +++++++++--- src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs index 24e1f2ad11..ca05c0d320 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs @@ -59,7 +59,7 @@ private static Cast getImplicitCast(object f, Type to) // this check seems weird, but PocoElementNode both implements PocoNode and IEnumerable for the sake of backwards compatibility bool fromElemList = from.CanBeTreatedAsType(typeof(IEnumerable)) && !from.CanBeTreatedAsType(typeof(PocoNode)); - if (to.CanBeTreatedAsType(typeof(P.Any)) && from.CanBeTreatedAsType(typeof(PocoNode))) return tryQuantity; + if (to.CanBeTreatedAsType(typeof(P.Quantity)) && from.CanBeTreatedAsType(typeof(PocoNode))) return tryQuantity; if (to == typeof(PocoNode) && !fromElemList) return any2primitiveTypedElement; if (to == typeof(IEnumerable)) return any2SingleItemList; @@ -75,8 +75,14 @@ private static Cast getImplicitCast(object f, Type to) if (typeof(P.Any).IsAssignableFrom(to) && !fromElemList) { - if (f is PocoNode {Poco: Quantity} q) - return _ => q; + if (f is PrimitiveNode { Value: null }) + return _ => null; + + if (f is PocoNode {Poco: P.IToSystemPrimitive tsp} && tsp.TryConvertToSystemType(out var result)) + { + return _ => result; + } + return o => P.Any.Convert(o); } diff --git a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs index 167cb173bd..684a892aa8 100644 --- a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs @@ -589,14 +589,14 @@ public static IEnumerable EmptyStringValueTestcases() => public static IEnumerable EmptyDecimalValueTestcases() => new[]{ - "round()", + //"round()", "toBoolean()" }.Select(e => new object[] { e, typeof(FhirDecimal) }); public static IEnumerable EmptyValueTestcases() => EmptyStringValueTestcases().Union(EmptyDecimalValueTestcases()); [DataTestMethod] - [DynamicData(nameof(EmptyValueTestcases), DynamicDataSourceType.Method)] + [DynamicData(nameof(EmptyDecimalValueTestcases), DynamicDataSourceType.Method)] public void AssertEmptyValueTestcases(string expression, Type data) { // Create a primitive with no value, just an extension From 4f4358a2c06a81903410fc671d9c8a5a5b345211 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 27 Feb 2025 16:19:29 +0100 Subject: [PATCH 16/17] fixed minor bug in UnboxTo logic --- src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs index ca05c0d320..6ae8170fd7 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Typecasts.cs @@ -129,9 +129,9 @@ internal static object UnboxTo(object instance, Type to) if (to.CanBeTreatedAsType(typeof(PocoNode))) return instance; if (to == typeof(object)) return instance; - if (element is PrimitiveNode { Value: { } value }) + if (element is PrimitiveNode pn) { - instance = value; + instance = pn.Value; } } From 85547e21ae17d14fe3fa49394e87924a54338bb5 Mon Sep 17 00:00:00 2001 From: Kasdejong Date: Thu, 27 Feb 2025 16:44:50 +0100 Subject: [PATCH 17/17] Fixed null propagation inconsistencies between system and cql/fhir primitives --- .../FhirPath/Expressions/Invokee.cs | 39 +++++++------------ .../FhirPath/Functions/ConversionOperators.cs | 2 +- .../Functions/FunctionsTests.cs | 4 +- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs index 4b93d7414f..2c5407c764 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Expressions/Invokee.cs @@ -42,26 +42,17 @@ public static FocusCollection GetThat(Closure context, IEnumerable _) = public static FocusCollection GetIndex(Closure context, IEnumerable args) => context.GetIndex(); - - private static readonly Predicate PROPAGATE_WHEN_EMPTY = focus => !focus.Any(); + private static readonly Predicate PROPAGATE_NEVER = _ => false; - private static readonly Predicate PROPAGATE_EMPTY_PRIMITIVE = focus => + private static readonly Predicate PROPAGATE_EMPTY = focus => { var first = focus.FirstOrDefault(); return first is null or PrimitiveNode { Value: null }; }; - private static Predicate getPropagator(bool doNullProp, Type argType) => - doNullProp switch - { - true when isPrimitiveDotNetType(argType) => PROPAGATE_EMPTY_PRIMITIVE, - true => PROPAGATE_WHEN_EMPTY, - _ => PROPAGATE_NEVER - }; - - private static bool isPrimitiveDotNetType(Type t) => t.IsPrimitive || t == typeof(string) || t == typeof(Decimal); - + private static Predicate getPropagator(bool doNullProp) => + doNullProp ? PROPAGATE_EMPTY : PROPAGATE_NEVER; public static Invokee Wrap(Func func) { @@ -75,7 +66,7 @@ public static Invokee Wrap(Func func, bool propNull) if (typeof(A) != typeof(EvaluationContext)) { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return []; + if (getPropagator(propNull)(focus)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus))); } @@ -93,7 +84,7 @@ internal static Invokee WrapWithPropNullForFocus(Func fu { // propagate only null for focus var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(true,typeof(A))(focus)) return []; + if (getPropagator(true)(focus)) return []; return Wrap(func, false)(ctx, args); }; @@ -104,12 +95,12 @@ public static Invokee Wrap(Func func, bool propNull) return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return []; + if (getPropagator(propNull)(focus)) return []; if (typeof(B) != typeof(EvaluationContext)) { var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return []; + if (getPropagator(propNull)(argA)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA))); } @@ -126,15 +117,15 @@ public static Invokee Wrap(Func func, bool propNull) return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull,typeof(A))(focus)) return []; + if (getPropagator(propNull)(focus)) return []; var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return []; + if (getPropagator(propNull)(argA)) return []; if (typeof(C) != typeof(EvaluationContext)) { var argB = args.Skip(2).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(C))(argB)) return []; + if (getPropagator(propNull)(argB)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), Typecasts.CastTo(argB))); @@ -153,17 +144,17 @@ public static Invokee Wrap(Func func, bool propNul return (ctx, args) => { var focus = args.First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(A))(focus)) return []; + if (getPropagator(propNull)(focus)) return []; var argA = args.Skip(1).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(B))(argA)) return []; + if (getPropagator(propNull)(argA)) return []; var argB = args.Skip(2).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(C))(argB)) return []; + if (getPropagator(propNull)(argB)) return []; if (typeof(D) != typeof(EvaluationContext)) { var argC = args.Skip(3).First()(ctx, EmptyArgs); - if (getPropagator(propNull, typeof(D))(argC)) return []; + if (getPropagator(propNull)(argC)) return []; return Typecasts.CastTo(func(Typecasts.CastTo(focus), Typecasts.CastTo(argA), Typecasts.CastTo(argB), Typecasts.CastTo(argC))); diff --git a/src/Hl7.Fhir.Base/FhirPath/Functions/ConversionOperators.cs b/src/Hl7.Fhir.Base/FhirPath/Functions/ConversionOperators.cs index 9f8c81b7ec..66814d408e 100644 --- a/src/Hl7.Fhir.Base/FhirPath/Functions/ConversionOperators.cs +++ b/src/Hl7.Fhir.Base/FhirPath/Functions/ConversionOperators.cs @@ -22,7 +22,7 @@ internal static class ConversionOperators /// /// /// - public static bool? ToBoolean(this Any focus) => + public static bool? ToBoolean(this Any focus) => focus.TryConvertTo(out var result) ? result.Value : null; /// diff --git a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs index 684a892aa8..167cb173bd 100644 --- a/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/FunctionsTests.cs @@ -589,14 +589,14 @@ public static IEnumerable EmptyStringValueTestcases() => public static IEnumerable EmptyDecimalValueTestcases() => new[]{ - //"round()", + "round()", "toBoolean()" }.Select(e => new object[] { e, typeof(FhirDecimal) }); public static IEnumerable EmptyValueTestcases() => EmptyStringValueTestcases().Union(EmptyDecimalValueTestcases()); [DataTestMethod] - [DynamicData(nameof(EmptyDecimalValueTestcases), DynamicDataSourceType.Method)] + [DynamicData(nameof(EmptyValueTestcases), DynamicDataSourceType.Method)] public void AssertEmptyValueTestcases(string expression, Type data) { // Create a primitive with no value, just an extension