diff --git a/src/Hl7.Fhir.Base/ElementModel/Types/Quantity.cs b/src/Hl7.Fhir.Base/ElementModel/Types/Quantity.cs index ea3c0f3762..a6856ab0d3 100644 --- a/src/Hl7.Fhir.Base/ElementModel/Types/Quantity.cs +++ b/src/Hl7.Fhir.Base/ElementModel/Types/Quantity.cs @@ -304,11 +304,72 @@ Quantity normalizeToUcum(Quantity orig) /// to specify comparison behaviour for date comparisons. public Result TryCompareTo(Any other) => TryCompareTo(other, CQL_EQUIVALENCE_COMPARISON); - public static bool operator +(Quantity a, Quantity b) => throw Error.NotSupported("Adding two quantites is not yet supported"); - public static bool operator -(Quantity a, Quantity b) => throw Error.NotSupported("Subtracting two quantites is not yet supported"); - public static bool operator *(Quantity a, Quantity b) => throw Error.NotSupported("Multiplying two quantites is not yet supported"); - public static bool operator /(Quantity a, Quantity b) => throw Error.NotSupported("Dividing two quantites is not yet supported"); + private static (Quantity, Quantity) alignQuantityUnits(Quantity a, Quantity b) + { + if (a.System != QuantityUnitSystem.UCUM || b.System != QuantityUnitSystem.UCUM) + { + Error.NotSupported("Arithmetic operations on quantities using systems other than UCUM are not supported."); + } + + Quantity? left = a; + Quantity? right = b; + + if (a.Unit != b.Unit) + { + // align units with each other + if (!a.TryCanonicalize(out left)) left = a; + if (!b.TryCanonicalize(out right)) right = b; + } + + return (left!, right!); + } + + public static Quantity? operator +(Quantity a, Quantity b) => + Add(a, b).ValueOrDefault(); + + public static Quantity? operator -(Quantity a, Quantity b) => + Substract(a, b).ValueOrDefault(); + + public static Quantity operator *(Quantity a, Quantity b) => + Multiply(a, b).ValueOrDefault(); + + public static Quantity? operator /(Quantity a, Quantity b) => + Divide(a, b).ValueOrDefault(); + + internal static Result Add(Quantity a, Quantity b) + { + var (left, right) = alignQuantityUnits(a, b); + + return (left!.Unit == right!.Unit) + ? Ok(new(left.Value + right.Value, left.Unit)) + : Fail(Error.InvalidOperation($"The add operation cannot be performed on quantities with units '{left.Unit}' and '{right.Unit}'.")); + } + + internal static Result Substract(Quantity a, Quantity b) + { + var (left, right) = alignQuantityUnits(a, b); + + return (left!.Unit == right!.Unit) + ? Ok(new(left.Value - right.Value, left.Unit)) + : Fail(Error.InvalidOperation($"The substract operation cannot be performed on quantities with units '{left.Unit}' and '{right.Unit}'.")); + } + + internal static Result Multiply(Quantity a, Quantity b) + { + var (left, right) = alignQuantityUnits(a, b); + + return Ok(new(left.Value * right.Value, Ucum.PerformMetricOperation(left.Unit, right.Unit, (a, b) => a * b))); + } + + internal static Result Divide(Quantity a, Quantity b) + { + if (b.Value == 0) return Fail(Error.InvalidOperation("Cannot divide by zero.")); + + var (left, right) = alignQuantityUnits(a, b); + + return Ok(new(left.Value / right.Value, left.Unit == right.Unit ? "1" : Ucum.PerformMetricOperation(left.Unit, right.Unit, (a, b) => a / b))); + } public override int GetHashCode() => (Unit, Value).GetHashCode(); diff --git a/src/Hl7.Fhir.Base/ElementModel/Types/Ucum.cs b/src/Hl7.Fhir.Base/ElementModel/Types/Ucum.cs index 52747af53e..7b29a3764b 100644 --- a/src/Hl7.Fhir.Base/ElementModel/Types/Ucum.cs +++ b/src/Hl7.Fhir.Base/ElementModel/Types/Ucum.cs @@ -47,6 +47,13 @@ private static M.Quantity toUnitsOfMeasureQuantity(this decimal value, string un Metric metric = (unit != null) ? SYSTEM.Value.Metric(unit) : new Metric(new List()); return new M.Quantity(value, metric); } + + internal static string PerformMetricOperation(string unit1, string unit2, Func operation) + { + var a = SYSTEM.Value.Metric(unit1); + var b = SYSTEM.Value.Metric(unit2); + return operation(a, b).ToString(); + } } } diff --git a/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs b/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs index e66eb4fe7b..468bfb6f6b 100644 --- a/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs +++ b/src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs @@ -283,7 +283,7 @@ internal static P.Time BoundaryTime(P.Time time, long? precision, int minutes, i "code" when input is ScopedNode sn => inParams.WithCode(code: sn.Value as string, context: sn.LocalLocation), "Coding" => inParams.WithCoding(input.ParseCoding()), "CodeableConcept" => inParams.WithCodeableConcept(input.ParseCodeableConcept()), - "System.String" => inParams.WithCode(code: input.Value as string, context: "No context available"), + "string" or "System.String" => inParams.WithCode(code: input.Value as string, context: "No context available"), _ => null, }; diff --git a/src/Hl7.Fhir.Support.Tests/ElementModel/QuantityTests.cs b/src/Hl7.Fhir.Support.Tests/ElementModel/QuantityTests.cs index fc7e22e2fa..71fdde70fb 100644 --- a/src/Hl7.Fhir.Support.Tests/ElementModel/QuantityTests.cs +++ b/src/Hl7.Fhir.Support.Tests/ElementModel/QuantityTests.cs @@ -7,10 +7,15 @@ */ using FluentAssertions; +using Hl7.Fhir.ElementModel.Types; +using Hl7.Fhir.Utility; using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using P = Hl7.Fhir.ElementModel.Types; +#nullable enable + namespace Hl7.Fhir.ElementModel.Tests { [TestClass] @@ -146,5 +151,56 @@ public void QuantityCompareTests(string left, string right, Comparison expectedR break; } } + + public static IEnumerable ArithmeticTestdata => new[] + { + new object[] { "25 'kg'", "5 'kg'", "30 'kg'" , (object)Quantity.Add }, + new object[] { "25 'kg'", "1000 'g'", "26000 'g'", (object)Quantity.Add }, + new object[] { "3 '[in_i]'", "2 '[in_i]'", "5 '[in_i]'", (object)Quantity.Add }, + new object[] { "4.0 'kg.m/s2'", "2000 'g.m.s-2'", "6000 'g.m.s-2'", (object)Quantity.Add } , + new object[] { "3 'm'", "3 'cm'", "303 'cm'", (object)Quantity.Add }, + new object[] { "3 'm'", "0 'cm'","300 'cm'", (object)Quantity.Add }, + new object[] { "3 'm'", "-80 'cm'", "220 'cm'", (object)Quantity.Add }, + + new object?[] { "3 'm'", "0 'kg'", null, (object)Quantity.Add }, + + new object[] { "25 'kg'", "500 'g'", "24500 'g'", (object)Quantity.Substract }, + new object[] { "25 'kg'", "25001 'g'", "-1 'g'", (object)Quantity.Substract}, + new object[] { "1 '[in_i]'", "2 'cm'", "0.005400 'm'", (object)Quantity.Substract }, + + new object?[] { "1 '[in_i]'", "2 'kg'", null, (object)Quantity.Substract }, + + new object[] { "25 'km'", "20 'cm'", "5000 'm2'", (object)Quantity.Multiply }, + new object[] { "2.0 'cm'", "2.0 'm'", "0.040 'm2'", (object)Quantity.Multiply }, + new object[] { "2.0 'cm'", "9 'kg'", "180 'g.m'", (object)Quantity.Multiply }, + + + new object[] { "14.4 'km'", "2.0 'h'", "2 'm.s-1'", (object)Quantity.Divide }, + new object[] { "9 'm2'", "3 'm'", "3 'm'", (object)Quantity.Divide }, + new object[] { "6 'm'", "3 'm'", "2 '1'", (object)Quantity.Divide }, + new object?[] { "3 'm'", "0 'cm'", null, (object)Quantity.Divide }, + }; + + + [TestMethod] + [DynamicData(nameof(ArithmeticTestdata))] + public void ArithmeticOperationsTests(string left, string right, object result, Func> operation) + { + _ = Quantity.TryParse(left, out var q1); + _ = Quantity.TryParse(right, out var q2); + + var opResult = operation(q1, q2); + + if (result is string r && Quantity.TryParse(r, out var q3)) + { + opResult.ValueOrDefault().Should().Be(q3); + } + else + { + opResult.Should().BeAssignableTo(); + } + } } -} \ No newline at end of file +} + +#nullable restore \ No newline at end of file diff --git a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathTest.cs b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathTest.cs index d9b9f5c926..fb2810ce09 100644 --- a/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathTest.cs +++ b/src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathTest.cs @@ -383,6 +383,12 @@ public static IEnumerable MemberOfTestData() }, "memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')", true }; // memberOf with string objects + yield return new object[] { new FhirString("85353-1"), "memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')", true }; + yield return new object[] { new FhirString("male"), "memberOf('http://hl7.org/fhir/ValueSet/administrative-gender')", true }; + yield return new object[] { new FhirString("female"), "memberOf('http://hl7.org/fhir/ValueSet/administrative-gender')", true }; + yield return new object[] { new FhirString("no-idea"), "memberOf('http://hl7.org/fhir/ValueSet/administrative-gender')", false }; + + // memberOf with inline string objects yield return new object[] { new FhirBoolean(), "'85353-1'.memberOf('http://hl7.org/fhir/ValueSet/observation-vitalsignresult')", true }; yield return new object[] { new FhirBoolean(), "'male'.memberOf('http://hl7.org/fhir/ValueSet/administrative-gender')", true }; yield return new object[] { new FhirBoolean(), "'female'.memberOf('http://hl7.org/fhir/ValueSet/administrative-gender')", true }; diff --git a/src/Hl7.FhirPath.Tests/Functions/MathOperatorsTests.cs b/src/Hl7.FhirPath.Tests/Functions/MathOperatorsTests.cs index c6df5224f4..d91c9b506f 100644 --- a/src/Hl7.FhirPath.Tests/Functions/MathOperatorsTests.cs +++ b/src/Hl7.FhirPath.Tests/Functions/MathOperatorsTests.cs @@ -1,6 +1,13 @@ using FluentAssertions; +using Hl7.Fhir.ElementModel; +using Hl7.FhirPath; using Hl7.FhirPath.FhirPath.Functions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; + +#nullable enable namespace HL7.FhirPath.Tests.Functions { @@ -22,5 +29,72 @@ public void Power() 2m.Power(2m).Should().BeOfType(typeof(decimal)); } + + private static IEnumerable QuantityAddOperations() => + new (string expression, bool expected, bool invalid)[] + { + ("25 'kg' + 5 'kg' = 30 'kg'", true, false), + ("3 '[in_i]' + 2 '[in_i]' = 5 '[in_i]'", true, false), + ("3 'm' + 0 'cm' = 300 'cm'", true, false), + ("(3 'm' + 0 'kg').empty()", true, false), + }.Select(t => new object[] { t.expression, t.expected, t.invalid }); + + private static IEnumerable QuantitySubstractOperations() => + new (string expression, bool expected, bool invalid)[] + { + ("25 'kg' - 500 'g' = 24500 'g'", true, false), + ("25 'kg' - 25001 'g' = -1 'g'", true, false), + ("1 '[in_i]' - 2 'cm' = 0.005400 'm'", true, false), + ("(3 '[in_i]' - 0 'kg').empty()", true, false), + }.Select(t => new object[] { t.expression, t.expected, t.invalid }); + + private static IEnumerable QuantityMultiplyOperations() => + new (string expression, bool expected, bool invalid)[] + { + ("25 'km' * 20 'cm' = 5000 'm2'", true, false), + ("2 'cm' * 2 'm' = 0.040 'm2'", true, false), + ("2 'cm' * 9 'kg' = 180 'g.m'", true, false), + }.Select(t => new object[] { t.expression, t.expected, t.invalid }); + + private static IEnumerable QuantityDivideOperations() => + new (string expression, bool expected, bool invalid)[] + { + ("14.4 'km' / 2 'h' = 2 'm.s-1'", true, false), + ("9 'm2' / 3 'm' = 3 'm'", true, false), + ("6 'm' / 3 'm' = 2 '1'", true, false), + ("(3 'm' / 0 'cm').empty()", true, false), + }.Select(t => new object[] { t.expression, t.expected, t.invalid }); + + public static IEnumerable AllQuantityOperations() + { + return + Enumerable.Empty() + .Union(QuantityAddOperations()) + .Union(QuantitySubstractOperations()) + .Union(QuantityMultiplyOperations()) + .Union(QuantityDivideOperations()) + ; + } + + [DataTestMethod] + [DynamicData(nameof(AllQuantityOperations), DynamicDataSourceType.Method)] + public void AssertTestcases(string expression, bool expected, bool invalid = false) + { + ITypedElement dummy = ElementNode.ForPrimitive(true); + + if (invalid) + { + Action act = () => dummy.IsBoolean(expression, expected); + act.Should().Throw(); + } + else + { + dummy.IsBoolean(expression, expected) + .Should().BeTrue(because: $"The expression was supposed to result in {expected}."); + } + } } + + } +#nullable restore \ No newline at end of file