Skip to content

Commit

Permalink
Merge branch 'develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
mmsmits authored Sep 21, 2023
2 parents 7b540d0 + e661642 commit e2484d0
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 6 deletions.
69 changes: 65 additions & 4 deletions src/Hl7.Fhir.Base/ElementModel/Types/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,72 @@ Quantity normalizeToUcum(Quantity orig)
/// to specify comparison behaviour for date comparisons.</remarks>
public Result<int> 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<Quantity> Add(Quantity a, Quantity b)
{
var (left, right) = alignQuantityUnits(a, b);

return (left!.Unit == right!.Unit)
? Ok<Quantity>(new(left.Value + right.Value, left.Unit))
: Fail<Quantity>(Error.InvalidOperation($"The add operation cannot be performed on quantities with units '{left.Unit}' and '{right.Unit}'."));
}

internal static Result<Quantity> Substract(Quantity a, Quantity b)
{
var (left, right) = alignQuantityUnits(a, b);

return (left!.Unit == right!.Unit)
? Ok<Quantity>(new(left.Value - right.Value, left.Unit))
: Fail<Quantity>(Error.InvalidOperation($"The substract operation cannot be performed on quantities with units '{left.Unit}' and '{right.Unit}'."));
}

internal static Result<Quantity> Multiply(Quantity a, Quantity b)
{
var (left, right) = alignQuantityUnits(a, b);

return Ok<Quantity>(new(left.Value * right.Value, Ucum.PerformMetricOperation(left.Unit, right.Unit, (a, b) => a * b)));
}

internal static Result<Quantity> Divide(Quantity a, Quantity b)
{
if (b.Value == 0) return Fail<Quantity>(Error.InvalidOperation("Cannot divide by zero."));

var (left, right) = alignQuantityUnits(a, b);

return Ok<Quantity>(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();

Expand Down
7 changes: 7 additions & 0 deletions src/Hl7.Fhir.Base/ElementModel/Types/Ucum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Metric.Axis>());
return new M.Quantity(value, metric);
}

internal static string PerformMetricOperation(string unit1, string unit2, Func<Metric, Metric, Metric> operation)
{
var a = SYSTEM.Value.Metric(unit1);
var b = SYSTEM.Value.Metric(unit2);
return operation(a, b).ToString();
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/Hl7.Fhir.Base/FhirPath/ElementNavFhirExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
58 changes: 57 additions & 1 deletion src/Hl7.Fhir.Support.Tests/ElementModel/QuantityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -146,5 +151,56 @@ public void QuantityCompareTests(string left, string right, Comparison expectedR
break;
}
}

public static IEnumerable<object?[]> 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<Quantity, Quantity, Result<Quantity>> 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<IFailed>();
}
}
}
}
}

#nullable restore
6 changes: 6 additions & 0 deletions src/Hl7.FhirPath.R4.Tests/PocoTests/FhirPathTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,12 @@ public static IEnumerable<object[]> 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 };
Expand Down
74 changes: 74 additions & 0 deletions src/Hl7.FhirPath.Tests/Functions/MathOperatorsTests.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -22,5 +29,72 @@ public void Power()

2m.Power(2m).Should().BeOfType(typeof(decimal));
}

private static IEnumerable<object[]> 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<object[]> 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<object[]> 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<object[]> 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<object[]> AllQuantityOperations()
{
return
Enumerable.Empty<object[]>()
.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<Exception>();
}
else
{
dummy.IsBoolean(expression, expected)
.Should().BeTrue(because: $"The expression was supposed to result in {expected}.");
}
}
}


}
#nullable restore

0 comments on commit e2484d0

Please sign in to comment.