Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1442: Added Quantity arithmetic #2595

Merged
merged 4 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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