diff --git a/Cql/CoreTests/CqlDateTests.cs b/Cql/CoreTests/CqlDateTests.cs new file mode 100644 index 000000000..2f9c998c3 --- /dev/null +++ b/Cql/CoreTests/CqlDateTests.cs @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025, Firely, NCQA 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-cql-sdk/main/LICENSE + */ + +#nullable enable +using Hl7.Cql.Iso8601; +using Hl7.Cql.Primitives; + +namespace CoreTests; + +[TestClass] +[TestCategory("UnitTest")] +public class CqlDateTests +{ + [TestMethod] + public void Add_Years() + { + var date = new CqlDate(2020, 1, 1); + var quantity = new CqlQuantity(2, "year"); + var result = date.Add(quantity); + Assert.AreEqual(new CqlDate(2022, 1, 1), result); + } + + [TestMethod] + public void Add_Months() + { + var date = new CqlDate(2020, 1, 31); + var quantity = new CqlQuantity(1, "month"); + var result = date.Add(quantity); + Assert.AreEqual(new CqlDate(2020, 2, 29), result); // Leap year + } + + [TestMethod] + public void Add_Days() + { + var date = new CqlDate(2020, 2, 27); + var quantity = new CqlQuantity(2, "day"); + var result = date.Add(quantity); + Assert.AreEqual(new CqlDate(2020, 2, 29), result); + } + + [TestMethod] + public void Subtract_Years() + { + var date = new CqlDate(2020, 1, 1); + var quantity = new CqlQuantity(5, "year"); + var result = date.Subtract(quantity); + Assert.AreEqual(new CqlDate(2015, 1, 1), result); + } + + [TestMethod] + public void Subtract_Months() + { + var date = new CqlDate(2020, 3, 31); + var quantity = new CqlQuantity(1, "month"); + var result = date.Subtract(quantity); + Assert.AreEqual(new CqlDate(2020, 2, 29), result); // Leap year + } + + [TestMethod] + public void Subtract_Days() + { + var date = new CqlDate(2020, 3, 1); + var quantity = new CqlQuantity(1, "day"); + var result = date.Subtract(quantity); + Assert.AreEqual(new CqlDate(2020, 2, 29), result); + } + + [TestMethod] + public void Operator_Addition() + { + var date = new CqlDate(2021, 12, 31); + var quantity = new CqlQuantity(1, "day"); + var result = date + quantity; + Assert.AreEqual(new CqlDate(2022, 1, 1), result); + } + + [TestMethod] + public void Operator_Subtraction() + { + var date = new CqlDate(2022, 1, 1); + var quantity = new CqlQuantity(1, "day"); + var result = date - quantity; + Assert.AreEqual(new CqlDate(2021, 12, 31), result); + } + + [TestMethod] + public void Add_NullQuantity_ReturnsNull() + { + var date = new CqlDate(2020, 1, 1); + CqlQuantity? quantity = null; + var result = date.Add(quantity); + Assert.IsNull(result); + } + + [TestMethod] + public void Subtract_NullQuantity_ReturnsNull() + { + var date = new CqlDate(2020, 1, 1); + CqlQuantity? quantity = null; + var result = date.Subtract(quantity); + Assert.IsNull(result); + } + + [TestMethod] + public void Operator_Addition_NullDate_ReturnsNull() + { + CqlDate? date = null; + var quantity = new CqlQuantity(1, "day"); + var result = date + quantity; + Assert.IsNull(result); + } + + [TestMethod] + public void Operator_Subtraction_NullDate_ReturnsNull() + { + CqlDate? date = null; + var quantity = new CqlQuantity(1, "day"); + var result = date - quantity; + Assert.IsNull(result); + } +} \ No newline at end of file diff --git a/Cql/CoreTests/CqlDateTimeTests.cs b/Cql/CoreTests/CqlDateTimeTests.cs new file mode 100644 index 000000000..ce869d6e5 --- /dev/null +++ b/Cql/CoreTests/CqlDateTimeTests.cs @@ -0,0 +1,357 @@ +/* + * Copyright (c) 2025, Firely, NCQA 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-cql-sdk/main/LICENSE + */ + +#nullable enable +using System.Linq.Expressions; +using Hl7.Cql.Fhir; +using Hl7.Cql.Iso8601; +using Hl7.Cql.Operators; +using Hl7.Cql.Primitives; +using Hl7.Cql.Runtime; + +namespace CoreTests; + +[TestClass] +[TestCategory("UnitTest")] +public class CqlDateTimeTests +{ + private CqlContext GetNewContext() => FhirCqlContext.WithDataSource(); + + [TestMethod] + public void Add_Year_By_Units() + { + Assert.IsTrue(CqlDateTime.TryParse("1960", out var baseDate)); + Assert.AreEqual(DateTimePrecision.Year, baseDate.Value.Precision); + var plusOneYear = baseDate.Add(new CqlQuantity(1m, "year")); + Assert.AreEqual(DateTimePrecision.Year, plusOneYear.Value.Precision); + Assert.IsNull(plusOneYear.Value.Month); + Assert.AreEqual("1961", plusOneYear.ToString()); + + var plusTwelveMonths = baseDate.Add(new CqlQuantity(12m, "month")); + Assert.AreEqual(DateTimePrecision.Year, plusTwelveMonths.Value.Precision); + Assert.IsNull(plusTwelveMonths.Value.Month); + Assert.AreEqual("1961", plusTwelveMonths.ToString()); + + var plus365days = baseDate.Add(new CqlQuantity(365, "day")); + Assert.AreEqual(DateTimePrecision.Year, plus365days.Value.Precision); + Assert.IsNull(plus365days.Value.Month); + Assert.AreEqual("1960", plus365days.ToString()); + + var plus366days = baseDate.Add(new CqlQuantity(366, "day")); + Assert.AreEqual(DateTimePrecision.Year, plus366days.Value.Precision); + Assert.IsNull(plus366days.Value.Month); + Assert.AreEqual("1961", plus366days.ToString()); + + var plus366DaysInHours = baseDate.Add(new CqlQuantity(366 * 24, "hours")); + Assert.AreEqual(DateTimePrecision.Year, plus366DaysInHours.Value.Precision); + Assert.IsNull(plus366DaysInHours.Value.Month); + Assert.AreEqual("1961", plus366DaysInHours.ToString()); + + var plus365DaysInSeconds = baseDate.Add(new CqlQuantity(365 * 24 * 60 * 60, "seconds")); + Assert.AreEqual(DateTimePrecision.Year, plus365DaysInSeconds.Value.Precision); + Assert.IsNull(plus365DaysInSeconds.Value.Month); + Assert.AreEqual("1960", plus365DaysInSeconds.ToString()); + } + + [TestMethod] + public void Add_Month() + { + Assert.IsTrue(CqlDateTime.TryParse("2022-01-01", out var baseDate)); + + var plus1Month = baseDate.Add(new CqlQuantity(1m, "month")); + Assert.AreEqual(DateTimePrecision.Day, plus1Month.Value.Precision); + Assert.IsNull(plus1Month.Value.Hour); + Assert.AreEqual("2022-02-01", plus1Month.ToString()); + + var plus2Months = baseDate.Add(new CqlQuantity(2m, "month")); + Assert.AreEqual(DateTimePrecision.Day, plus2Months.Value.Precision); + Assert.IsNull(plus2Months.Value.Hour); + Assert.AreEqual("2022-03-01", plus2Months.ToString()); + + var plus2pt5Months = baseDate.Add(new CqlQuantity(2.5m, "month")); + Assert.AreEqual(DateTimePrecision.Day, plus2pt5Months.Value.Precision); + Assert.IsNull(plus2pt5Months.Value.Hour); + Assert.AreEqual("2022-03-01", plus2pt5Months.ToString()); + + var plus1UcumMonth = baseDate.Add(new CqlQuantity(1m, "mo")); + Assert.AreEqual(DateTimePrecision.Day, plus1UcumMonth.Value.Precision); + Assert.IsNull(plus1UcumMonth.Value.Hour); + Assert.AreEqual("2022-01-31", plus1UcumMonth.ToString()); + + } + + [TestMethod] + public void Subtract_Month() + { + Assert.IsTrue(CqlDateTime.TryParse("2022-03-01", out var baseDate)); + + var minus1Month = baseDate.Subtract(new CqlQuantity(1m, "month")); + Assert.AreEqual(DateTimePrecision.Day, minus1Month.Value.Precision); + Assert.IsNull(minus1Month.Value.Hour); + Assert.AreEqual("2022-02-01", minus1Month.ToString()); + + var minus2Months = baseDate.Subtract(new CqlQuantity(2m, "month")); + Assert.AreEqual(DateTimePrecision.Day, minus2Months.Value.Precision); + Assert.IsNull(minus2Months.Value.Hour); + Assert.AreEqual("2022-01-01", minus2Months.ToString()); + + var minus2pt5Months = baseDate.Subtract(new CqlQuantity(2.5m, "month")); + Assert.AreEqual(DateTimePrecision.Day, minus2pt5Months.Value.Precision); + Assert.IsNull(minus2pt5Months.Value.Hour); + Assert.AreEqual("2022-01-01", minus2pt5Months.ToString()); + + var minus1UcumMonth = baseDate.Subtract(new CqlQuantity(1m, "mo")); + Assert.AreEqual(DateTimePrecision.Day, minus1UcumMonth.Value.Precision); + Assert.IsNull(minus1UcumMonth.Value.Hour); + Assert.AreEqual("2022-01-29", minus1UcumMonth.ToString()); + + } + + [TestMethod] + public void Subtract_Months_From_Year() + { + Assert.IsTrue(CqlDateTime.TryParse("2014", out var baseDate)); + var result = baseDate.Subtract(new CqlQuantity(25m, "month")); + Assert.AreEqual(2011, result.Value.Year); + Assert.AreEqual(DateTimePrecision.Year, result.Precision); + } + + [TestMethod] + public void Subtract_Year() + { + Assert.IsTrue(CqlDateTime.TryParse("2025-03-01", out var baseDate)); + + var minus1Year = baseDate.Subtract(new CqlQuantity(1m, "year")); + Assert.AreEqual(DateTimePrecision.Day, minus1Year.Value.Precision); + Assert.IsNull(minus1Year.Value.Hour); + Assert.AreEqual("2024-03-01", minus1Year.ToString()); + + var minus1UcumYear = baseDate.Subtract(new CqlQuantity(1m, "a")); + Assert.AreEqual(DateTimePrecision.Day, minus1UcumYear.Value.Precision); + Assert.IsNull(minus1UcumYear.Value.Hour); + Assert.AreEqual("2024-02-29", minus1UcumYear.ToString()); + + } + + [TestMethod] + public void Subtract_Day_and_Days() + { + var threeDays = new CqlQuantity(3, "days"); + var oneDay = new CqlQuantity(1, "day"); + var method = typeof(ICqlOperators) + .GetMethods() + .Where(x => + x.Name == nameof(CqlOperators.Subtract) && + x.GetParameters().Count() == 2 && + x.GetParameters()[0].ParameterType == typeof(CqlQuantity) && + x.GetParameters()[1].ParameterType == typeof(CqlQuantity) + ).First(); + + + var tdExpr = Expression.Constant(threeDays); + var odExpr = Expression.Constant(oneDay); + + var rc = GetNewContext(); + var fcq = rc.Operators; + var memExpr = Expression.Constant(fcq); + + var call = Expression.Call(memExpr, method, tdExpr, odExpr); + var le = Expression.Lambda>(call); + var compiled = le.Compile(); + var result = compiled.Invoke(); + + + } + + [TestMethod] + public void BoundariesBetween_Months() + { + Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-04-01", out var cqlStartDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out var cqlEndDate)); + var boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "month"); + Assert.AreEqual(2, boundariesBetween); + boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlEndDate, "month"); + Assert.AreEqual(1, boundariesBetween); + + Assert.IsTrue(DateTimeIso8601.TryParse("2020-03-01", out startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-04-30", out cqlStartDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out cqlEndDate)); + boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "month"); + Assert.AreEqual(1, boundariesBetween); + + boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlEndDate, "month"); + Assert.AreEqual(0, boundariesBetween); + } + [TestMethod] + public void BoundariesBetween_Years() + { + Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out var cqlStartDate)); + var boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); + Assert.AreEqual(1, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2022-01-01", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); + Assert.AreEqual(2, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); + Assert.AreEqual(0, boundariesBetween); + } + + [TestMethod] + public void WholeCalendarPeriodsBetween_Years() + { + Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-06-30", out var cqlStartDate)); + + var boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(0, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(0, boundariesBetween); // 1 full year occurs on mar 1, not feb 28 + + Assert.IsTrue(CqlDateTime.TryParse("2021-03-01", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(1, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2021-06-30", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(1, boundariesBetween); + + Assert.IsTrue(DateTimeIso8601.TryParse("2008-04-11", out startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2024-04-10", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(15, boundariesBetween); + + // leap year + Assert.IsTrue(DateTimeIso8601.TryParse("2020-04-11", out startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2023-05-11", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(3, boundariesBetween); + + // leap day + Assert.IsTrue(DateTimeIso8601.TryParse("2003-03-01", out startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2024-02-29", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); + Assert.AreEqual(20, boundariesBetween); + } + + [TestMethod] + public void WholeCalendarPeriodsBetween_Months() + { + Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); + Assert.IsTrue(CqlDateTime.TryParse("2020-06-30", out var cqlStartDate)); + + var boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); + Assert.AreEqual(4, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); + Assert.AreEqual(11, boundariesBetween); // 1 full year occurs on mar 1, not feb 28 + + Assert.IsTrue(CqlDateTime.TryParse("2021-03-01", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); + Assert.AreEqual(12, boundariesBetween); + + Assert.IsTrue(CqlDateTime.TryParse("2021-06-30", out cqlStartDate)); + boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); + Assert.AreEqual(16, boundariesBetween); + + } + + [TestMethod] + public void Add_Years_OperatorAndMethod() + { + var dt = new CqlDateTime(2020, 1, 1, 0, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(2, "year"); + + var resultMethod = dt.Add(quantity); + var resultOperator = dt + quantity; + + Assert.IsNotNull(resultMethod); + Assert.IsNotNull(resultOperator); + Assert.AreEqual(2022, resultMethod.Value.Year); + Assert.AreEqual(2022, resultOperator.Value.Year); + Assert.AreEqual(dt.Value.Month, resultMethod.Value.Month); + Assert.AreEqual(dt.Value.Day, resultMethod.Value.Day); + } + + [TestMethod] + public void Subtract_Years_OperatorAndMethod() + { + var dt = new CqlDateTime(2020, 1, 1, 0, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(3, "year"); + + var resultMethod = dt.Subtract(quantity); + var resultOperator = dt - quantity; + + Assert.IsNotNull(resultMethod); + Assert.IsNotNull(resultOperator); + Assert.AreEqual(2017, resultMethod.Value.Year); + Assert.AreEqual(2017, resultOperator.Value.Year); + Assert.AreEqual(dt.Value.Month, resultMethod.Value.Month); + Assert.AreEqual(dt.Value.Day, resultMethod.Value.Day); + } + + [TestMethod] + public void Add_Months() + { + var dt = new CqlDateTime(2021, 5, 15, 0, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(7, "month"); + + var result = dt.Add(quantity); + + Assert.IsNotNull(result); + Assert.AreEqual(2021, result.Value.Year); + Assert.AreEqual(12, result.Value.Month); + Assert.AreEqual(15, result.Value.Day); + } + + [TestMethod] + public void Subtract_Days() + { + var dt = new CqlDateTime(2021, 1, 10, 0, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(5, "day"); + + var result = dt.Subtract(quantity); + + Assert.IsNotNull(result); + Assert.AreEqual(2021, result.Value.Year); + Assert.AreEqual(1, result.Value.Month); + Assert.AreEqual(5, result.Value.Day); + } + + [TestMethod] + public void Add_NullQuantity_ReturnsNull() + { + var dt = new CqlDateTime(2021, 1, 1, 0, 0, 0, 0, 0, 0); + CqlQuantity? quantity = null; + + var result = dt.Add(quantity); + var resultOp = dt + quantity; + + Assert.IsNull(result); + Assert.IsNull(resultOp); + } + + [TestMethod] + public void Operator_NullDateTime_ReturnsNull() + { + CqlDateTime? dt = null; + var quantity = new CqlQuantity(1, "year"); + + var resultAdd = dt + quantity; + var resultSub = dt - quantity; + + Assert.IsNull(resultAdd); + Assert.IsNull(resultSub); + } +} \ No newline at end of file diff --git a/Cql/CoreTests/CqlQuantityTests.cs b/Cql/CoreTests/CqlQuantityTests.cs new file mode 100644 index 000000000..91fb4bd96 --- /dev/null +++ b/Cql/CoreTests/CqlQuantityTests.cs @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2025, Firely, NCQA 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-cql-sdk/main/LICENSE + */ + +#nullable enable +using Hl7.Cql.Primitives; + +namespace CoreTests; + +[TestClass] +[TestCategory("UnitTest")] +public class CqlQuantityTests +{ + [TestMethod] + public void Negate_PositiveValue_ReturnsNegativeValue() + { + var quantity = new CqlQuantity(5.5m, "mg"); + var negated = CqlQuantity.Negate(quantity); + + Assert.IsNotNull(negated); + Assert.AreEqual(-5.5m, negated.value); + Assert.AreEqual("mg", negated.unit); + } + + [TestMethod] + public void Negate_NullValue_ReturnsNullValueWithUnit() + { + var quantity = new CqlQuantity(null, "mg"); + var negated = CqlQuantity.Negate(quantity); + + Assert.IsNotNull(negated); + Assert.IsNull(negated.value); + Assert.AreEqual("mg", negated.unit); + } + + [TestMethod] + public void Negate_NullQuantity_ReturnsNull() + { + CqlQuantity? quantity = null; + var negated = CqlQuantity.Negate(quantity); + + Assert.IsNull(negated); + } + + [TestMethod] + public void OperatorNegate_PositiveValue_ReturnsNegativeValue() + { + var quantity = new CqlQuantity(10m, "g"); + var negated = -quantity; + + Assert.IsNotNull(negated); + Assert.AreEqual(-10m, negated.value); + Assert.AreEqual("g", negated.unit); + } + + [TestMethod] + public void OperatorNegate_NullValue_ReturnsNullValueWithUnit() + { + var quantity = new CqlQuantity(null, "g"); + var negated = -quantity; + + Assert.IsNotNull(negated); + Assert.IsNull(negated.value); + Assert.AreEqual("g", negated.unit); + } + + [TestMethod] + public void OperatorNegate_NullQuantity_ReturnsNull() + { + CqlQuantity? quantity = null; + var negated = -quantity; + + Assert.IsNull(negated); + } +} \ No newline at end of file diff --git a/Cql/CoreTests/CqlTimeTests.cs b/Cql/CoreTests/CqlTimeTests.cs new file mode 100644 index 000000000..f67671ea9 --- /dev/null +++ b/Cql/CoreTests/CqlTimeTests.cs @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025, Firely, NCQA 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-cql-sdk/main/LICENSE + */ + +#nullable enable +using Hl7.Cql.Primitives; + +namespace CoreTests; + +[TestClass] +[TestCategory("UnitTest")] +public class CqlTimeTests +{ + [TestMethod] + public void AdditionOperator_AddsMinutes() + { + var time = new CqlTime(10, 15, 0, 0, 0, 0); + var quantity = new CqlQuantity(5, "minute"); + var result = time + quantity; + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Value.Hour); + Assert.AreEqual(20, result.Value.Minute); + Assert.AreEqual(0, result.Value.Second); + } + + [TestMethod] + public void AdditionOperator_AddsHours() + { + var time = new CqlTime(8, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(2, "hour"); + var result = time + quantity; + Assert.IsNotNull(result); + Assert.AreEqual(10, result.Value.Hour); + Assert.AreEqual(0, result.Value.Minute); + } + + [TestMethod] + public void SubtractionOperator_SubtractsMinutes() + { + var time = new CqlTime(12, 30, 0, 0, 0, 0); + var quantity = new CqlQuantity(15, "minute"); + var result = time - quantity; + Assert.IsNotNull(result); + Assert.AreEqual(12, result.Value.Hour); + Assert.AreEqual(15, result.Value.Minute); + } + + [TestMethod] + public void SubtractionOperator_SubtractsHours() + { + var time = new CqlTime(14, 0, 0, 0, 0, 0); + var quantity = new CqlQuantity(3, "hour"); + var result = time - quantity; + Assert.IsNotNull(result); + Assert.AreEqual(11, result.Value.Hour); + Assert.AreEqual(0, result.Value.Minute); + } + + [TestMethod] + public void AdditionOperator_NullQuantity_ReturnsNull() + { + var time = new CqlTime(10, 0, 0, 0, 0, 0); + CqlQuantity? quantity = null; + var result = time + quantity; + Assert.IsNull(result); + } + + [TestMethod] + public void SubtractionOperator_NullQuantity_ReturnsNull() + { + var time = new CqlTime(10, 0, 0, 0, 0, 0); + CqlQuantity? quantity = null; + var result = time - quantity; + Assert.IsNull(result); + } + + [TestMethod] + public void AdditionOperator_NullTime_ReturnsNull() + { + CqlTime? time = null; + var quantity = new CqlQuantity(1, "hour"); + var result = time + quantity; + Assert.IsNull(result); + } + + [TestMethod] + public void SubtractionOperator_NullTime_ReturnsNull() + { + CqlTime? time = null; + var quantity = new CqlQuantity(1, "hour"); + var result = time - quantity; + Assert.IsNull(result); + } +} \ No newline at end of file diff --git a/Cql/CoreTests/ModelTest.cs b/Cql/CoreTests/ModelTest.cs index 5ad004324..c648c7d0e 100644 --- a/Cql/CoreTests/ModelTest.cs +++ b/Cql/CoreTests/ModelTest.cs @@ -36,7 +36,7 @@ public void Age() var ctx = new CqlContext(CqlOperators.Create(new UnitTestTypeResolver(), dataSource: dataSource, now: new DateTimeIso8601(2023, 3, 28, null, null, null, null, null, null))); - var age = ctx.Operators.Age("a"); + var age = ctx.Operators.Age("year"); Assert.AreEqual(age, 39); } @@ -51,7 +51,7 @@ public void AgeAt() var ctx = new CqlContext(CqlOperators.Create(new UnitTestTypeResolver(), dataSource: dataSource, now: new DateTimeIso8601(2023, 3, 28, null, null, null, null, null, null))); - var age = ctx.Operators.AgeAt(new CqlDate(2013, 3, 28), "a"); + var age = ctx.Operators.AgeAt(new CqlDate(2013, 3, 28), "year"); Assert.AreEqual(age, 29); } diff --git a/Cql/CoreTests/PrimitiveTests.cs b/Cql/CoreTests/PrimitiveTests.cs index c6fbe707f..4ca7762d0 100644 --- a/Cql/CoreTests/PrimitiveTests.cs +++ b/Cql/CoreTests/PrimitiveTests.cs @@ -6,247 +6,22 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE */ -using Hl7.Cql.Abstractions; +#nullable enable using Hl7.Cql.CodeGeneration.NET.Toolkit; using Hl7.Cql.Compiler; -using Hl7.Cql.Elm; using Hl7.Cql.Fhir; -using Hl7.Cql.Iso8601; -using Hl7.Cql.Operators; using Hl7.Cql.Primitives; using Hl7.Cql.Runtime; using Hl7.Cql.Runtime.Hosting; namespace CoreTests { - using DateTimePrecision = Hl7.Cql.Iso8601.DateTimePrecision; - using Expression = System.Linq.Expressions.Expression; - [TestClass] [TestCategory("UnitTest")] public class PrimitiveTests { private CqlContext GetNewContext() => FhirCqlContext.WithDataSource(); - [TestMethod] - public void CqlDate_Subtract_Months_From_Year() - { - Assert.IsTrue(CqlDateTime.TryParse("2014", out var baseDate)); - var result = baseDate.Subtract(new CqlQuantity(25m, UCUMUnits.Month)); - Assert.AreEqual(2012, result.Value.Year); - Assert.AreEqual(DateTimePrecision.Year, result.Precision); - } - - [TestMethod] - public void CqlDateTime_Add_Year_By_Units() - { - Assert.IsTrue(CqlDateTime.TryParse("1960", out var baseDate)); - Assert.AreEqual(DateTimePrecision.Year, baseDate.Value.Precision); - var plusOneYear = baseDate.Add(new CqlQuantity(1m, "year")); - Assert.AreEqual(DateTimePrecision.Year, plusOneYear.Value.Precision); - Assert.IsNull(plusOneYear.Value.Month); - Assert.AreEqual("1961", plusOneYear.ToString()); - - var plusTwelveMonths = baseDate.Add(new CqlQuantity(12m, "month")); - Assert.AreEqual(DateTimePrecision.Year, plusTwelveMonths.Value.Precision); - Assert.IsNull(plusTwelveMonths.Value.Month); - Assert.AreEqual("1961", plusTwelveMonths.ToString()); - - var plus365days = baseDate.Add(new CqlQuantity(365, "day")); - Assert.AreEqual(DateTimePrecision.Year, plus365days.Value.Precision); - Assert.IsNull(plus365days.Value.Month); - Assert.AreEqual("1961", plus365days.ToString()); - - var plus366days = baseDate.Add(new CqlQuantity(366, "day")); - Assert.AreEqual(DateTimePrecision.Year, plus366days.Value.Precision); - Assert.IsNull(plus366days.Value.Month); - Assert.AreEqual("1961", plus366days.ToString()); - - var plus366DaysInHours = baseDate.Add(new CqlQuantity(366 * 24, "hours")); - Assert.AreEqual(DateTimePrecision.Year, plus366DaysInHours.Value.Precision); - Assert.IsNull(plus366DaysInHours.Value.Month); - Assert.AreEqual("1961", plus366DaysInHours.ToString()); - - var plus365DaysInSeconds = baseDate.Add(new CqlQuantity(365 * 24 * 60 * 60, "seconds")); - Assert.AreEqual(DateTimePrecision.Year, plus365DaysInSeconds.Value.Precision); - Assert.IsNull(plus365DaysInSeconds.Value.Month); - Assert.AreEqual("1961", plus365DaysInSeconds.ToString()); - } - - [TestMethod] - public void CqlDateTime_Add_Month() - { - Assert.IsTrue(CqlDateTime.TryParse("2022-01-01", out var baseDate)); - - var plus1Month = baseDate.Add(new CqlQuantity(1m, "month")); - Assert.AreEqual(DateTimePrecision.Day, plus1Month.Value.Precision); - Assert.IsNull(plus1Month.Value.Hour); - Assert.AreEqual("2022-02-01", plus1Month.ToString()); - - var plus2Months = baseDate.Add(new CqlQuantity(2m, "month")); - Assert.AreEqual(DateTimePrecision.Day, plus2Months.Value.Precision); - Assert.IsNull(plus2Months.Value.Hour); - Assert.AreEqual("2022-03-01", plus2Months.ToString()); - - var plus2pt5Months = baseDate.Add(new CqlQuantity(2.5m, "month")); - Assert.AreEqual(DateTimePrecision.Day, plus2pt5Months.Value.Precision); - Assert.IsNull(plus2pt5Months.Value.Hour); - Assert.AreEqual("2022-03-01", plus2pt5Months.ToString()); - - } - - [TestMethod] - public void CqlDateTime_Subtract_Month() - { - Assert.IsTrue(CqlDateTime.TryParse("2022-03-01", out var baseDate)); - - var minus1Month = baseDate.Subtract(new CqlQuantity(1m, "month")); - Assert.AreEqual(DateTimePrecision.Day, minus1Month.Value.Precision); - Assert.IsNull(minus1Month.Value.Hour); - Assert.AreEqual("2022-02-01", minus1Month.ToString()); - - var minus2Months = baseDate.Subtract(new CqlQuantity(2m, "month")); - Assert.AreEqual(DateTimePrecision.Day, minus2Months.Value.Precision); - Assert.IsNull(minus2Months.Value.Hour); - Assert.AreEqual("2022-01-01", minus2Months.ToString()); - - var minus2pt5Months = baseDate.Subtract(new CqlQuantity(2.5m, "month")); - Assert.AreEqual(DateTimePrecision.Day, minus2pt5Months.Value.Precision); - Assert.IsNull(minus2pt5Months.Value.Hour); - Assert.AreEqual("2022-01-01", minus2pt5Months.ToString()); - - } - - [TestMethod] - public void CqlDateTime_Subtract_Day_and_Days() - { - var threeDays = new CqlQuantity(3, "days"); - var oneDay = new CqlQuantity(1, "day"); - var method = typeof(ICqlOperators) - .GetMethods() - .Where(x => - x.Name == nameof(CqlOperators.Subtract) && - x.GetParameters().Count() == 2 && - x.GetParameters()[0].ParameterType == typeof(CqlQuantity) && - x.GetParameters()[1].ParameterType == typeof(CqlQuantity) - ).First(); - - - var tdExpr = Expression.Constant(threeDays); - var odExpr = Expression.Constant(oneDay); - - - var rc = GetNewContext(); - var fcq = rc.Operators; - var memExpr = Expression.Constant(fcq); - - var call = Expression.Call(memExpr, method, tdExpr, odExpr); - var le = Expression.Lambda>(call); - var compiled = le.Compile(); - var result = compiled.Invoke(); - - - } - - [TestMethod] - public void CqlDateTime_BoundariesBetween_Months() - { - Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-04-01", out var cqlStartDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out var cqlEndDate)); - var boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "month"); - Assert.AreEqual(2, boundariesBetween); - boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlEndDate, "month"); - Assert.AreEqual(1, boundariesBetween); - - Assert.IsTrue(DateTimeIso8601.TryParse("2020-03-01", out startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-04-30", out cqlStartDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out cqlEndDate)); - boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "month"); - Assert.AreEqual(1, boundariesBetween); - - boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlEndDate, "month"); - Assert.AreEqual(0, boundariesBetween); - } - [TestMethod] - public void CqlDateTime_BoundariesBetween_Years() - { - Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out var cqlStartDate)); - var boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); - Assert.AreEqual(1, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2022-01-01", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); - Assert.AreEqual(2, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2020-03-31", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).BoundariesBetween(cqlStartDate, "year"); - Assert.AreEqual(0, boundariesBetween); - } - - [TestMethod] - public void CqlDateTime_WholeCalendarPeriodsBetween_Years() - { - Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-06-30", out var cqlStartDate)); - - var boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(0, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(0, boundariesBetween); // 1 full year occurs on mar 1, not feb 28 - - Assert.IsTrue(CqlDateTime.TryParse("2021-03-01", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(1, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2021-06-30", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(1, boundariesBetween); - - Assert.IsTrue(DateTimeIso8601.TryParse("2008-04-11", out startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2024-04-10", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(15, boundariesBetween); - - // leap year - Assert.IsTrue(DateTimeIso8601.TryParse("2020-04-11", out startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2023-05-11", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(3, boundariesBetween); - - // leap day - Assert.IsTrue(DateTimeIso8601.TryParse("2003-03-01", out startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2024-02-29", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "year"); - Assert.AreEqual(20, boundariesBetween); - } - - [TestMethod] - public void CqlDateTime_WholeCalendarPeriodsBetween_Months() - { - Assert.IsTrue(DateTimeIso8601.TryParse("2020-02-29", out var startDate)); - Assert.IsTrue(CqlDateTime.TryParse("2020-06-30", out var cqlStartDate)); - - var boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); - Assert.AreEqual(4, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2021-02-28", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); - Assert.AreEqual(11, boundariesBetween); // 1 full year occurs on mar 1, not feb 28 - - Assert.IsTrue(CqlDateTime.TryParse("2021-03-01", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); - Assert.AreEqual(12, boundariesBetween); - - Assert.IsTrue(CqlDateTime.TryParse("2021-06-30", out cqlStartDate)); - boundariesBetween = new CqlDateTime(startDate).WholeCalendarPeriodsBetween(cqlStartDate, "month"); - Assert.AreEqual(16, boundariesBetween); - - } - /// /// Handles Interval[3,null) contains 5 = null /// @@ -1076,7 +851,7 @@ public void Expand_Interval_DateTime_Second() var end = new CqlDateTime(2022, 1, 1, 0, 0, 6, 0, 0, 0); var interval = new CqlInterval(start, end, true, true); - var quantity = new CqlQuantity(3, "secondd"); + var quantity = new CqlQuantity(3, "second"); List expected = [ new CqlDateTime(2022, 1, 1, 0, 0, 0, 0, 0, 0), @@ -1217,11 +992,11 @@ public void Expand_Interval_Time_Year() var end = new CqlTime(12, null, null, null, null, null); var interval = new CqlInterval(start, end, true, true); - var quantity = new CqlQuantity(2, "years"); + var perQuantity = new CqlQuantity(2, "year"); var rc = GetNewContext(); var fcq = rc.Operators; - var expand = fcq.Expand(interval, quantity); + var expand = fcq.Expand(interval, perQuantity); Assert.IsNotNull(expand); Assert.IsTrue(expand.Count() == 0); } @@ -2986,11 +2761,11 @@ public void Expand_List_Interval_Time_Year() var end = new CqlTime(12, null, null, null, null, null); List> interval = [new CqlInterval(start, end, true, true)]; - var quantity = new CqlQuantity(2, "years"); + var perQuantity = new CqlQuantity(2, "year"); var rc = GetNewContext(); var fcq = rc.Operators; - var expand = fcq.Expand(interval, quantity); + var expand = fcq.Expand(interval, perQuantity); Assert.IsNotNull(expand); Assert.IsTrue(expand.Count() == 0); } @@ -3560,7 +3335,7 @@ public void Add_Date_Quantity_To_MaxDate() { var rc = GetNewContext(); var fcq = rc.Operators; - + var quantity = new CqlQuantity(1, "day"); var inputDateMaxValue = CqlDate.MaxValue; var newDateAddMax = fcq.Add(inputDateMaxValue, quantity); @@ -3610,7 +3385,7 @@ public void Add_Integer_To_MaxInteger() { var rc = GetNewContext(); var fcq = rc.Operators; - + var addedValue = fcq.Add(int.MaxValue, 1); Assert.IsNull(addedValue); } @@ -3724,17 +3499,17 @@ public void Subtract_Decimal_To_MinDecimal() var subtractedValue = fcq.Subtract(decimal.MinValue, 1m); Assert.IsNull(subtractedValue); } - + #region Slice tests - /* Refer http://cql.hl7.org/09-b-cqlreference.html for operation details on Skip, Tail and Take cql operators + /* Refer http://cql.hl7.org/09-b-cqlreference.html for operation details on Skip, Tail and Take cql operators * These CQL operators uses Slice semantics from http://cql.hl7.org/04-logicalspecification.html#slice */ [TestMethod] public void SliceSkip2() { - //The Skip operator returns the elements in the list, skipping the first number elements. + //The Skip operator returns the elements in the list, skipping the first number elements. //define "Skip2": Skip({ 1, 2, 3, 4, 5 }, 2) // { 3, 4, 5 } var rtx = GetNewContext(); var inputList = new List { 1, 2, 3, 4, 5 }; @@ -3756,7 +3531,7 @@ public void SliceSkipNull() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceSkipEmpty() { @@ -3769,7 +3544,7 @@ public void SliceSkipEmpty() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceSkipIsNull() { @@ -3797,7 +3572,7 @@ public void SliceSkipZero() [TestMethod] public void SliceTail234() { - //The Tail operator returns all but the first element from the given list. + //The Tail operator returns all but the first element from the given list. //define "Tail234": Tail({ 1, 2, 3, 4 }) // { 2, 3, 4 } var rtx = GetNewContext(); var inputList = new List { 1, 2, 3, 4 }; @@ -3806,7 +3581,7 @@ public void SliceTail234() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceTailEmpty() { @@ -3819,7 +3594,7 @@ public void SliceTailEmpty() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceTailIsNull() { @@ -3832,7 +3607,7 @@ public void SliceTailIsNull() Assert.IsNull(slicedList); Assert.AreEqual(expectedList, slicedList); } - + [TestMethod] public void SliceTake2() { @@ -3845,7 +3620,7 @@ public void SliceTake2() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceTakeTooMany() { @@ -3858,7 +3633,7 @@ public void SliceTakeTooMany() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceTakeEmpty() { @@ -3871,7 +3646,7 @@ public void SliceTakeEmpty() Assert.IsNotNull(slicedList); CollectionAssert.AreEqual(expectedList, slicedList.ToList()); } - + [TestMethod] public void SliceTakeIsNull() { @@ -3893,12 +3668,12 @@ public void SliceEmptyEnumerableWithIEnumerableNotCollection() var rtx = GetNewContext(); var inputEnumerable = Enumerable.Empty().Where(x => true); // Creates IEnumerable not a collection var expectedList = new List(); - + // Test various slice operations on empty enumerable var slicedList1 = rtx.Operators.Slice(inputEnumerable, 0, 5); var slicedList2 = rtx.Operators.Slice(inputEnumerable, 2, null); var slicedList3 = rtx.Operators.Slice(inputEnumerable, null, null); - + Assert.IsNotNull(slicedList1); Assert.IsNotNull(slicedList2); Assert.IsNotNull(slicedList3); diff --git a/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs b/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs index 8067e66bb..149261f7f 100644 --- a/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs +++ b/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs @@ -6,8 +6,6 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE */ -using Hl7.Cql.Iso8601; - namespace Hl7.Cql.Abstractions { /// @@ -28,78 +26,90 @@ public static class UCUMUnits public const string Unary = "1"; /// - /// Years (annos in Latin). + /// Years ("Annos" in Latin). /// + /// + /// Note that this unit is the same as the value of which is 365.25 days. + /// public const string Year = "a"; + + /// + /// Defines days per year + /// + /// + /// Used when specifying . + /// + public const double DaysPerYearDouble = 365.25d; + /// /// Months /// + /// + /// Note that this unit is the same as the value of which is 30.4375 days. + /// public const string Month = "mo"; + + /// + /// Defines days per month + /// + /// + /// Used when specifying . + /// + public const double DaysPerMonthDouble = 30.4375d; + /// /// Days /// public const string Day = "d"; + /// /// Hours /// public const string Hour = "h"; + /// /// Minutes /// public const string Minute = "min"; + /// /// Seconds /// public const string Second = "s"; + /// /// Milliseconds /// public const string Millisecond = "ms"; + /// /// Weeks, equal to 7 . /// public const string Week = "wk"; + /// /// Imperial inches /// public const string Inch = "[in_i]"; + /// /// Imperial feet /// public const string Foot = "[ft_i]"; + /// /// Imperial yards /// public const string Yard = "[yd_i]"; + /// /// Meters /// public const string Meter = "m"; + /// /// Centimeters /// public const string Centimeter = "cm"; - - /// - /// Maps to the corresponding UCUM unit. - /// - /// The precision to map. - /// The corresponding UCUM units, or if no mapping is defined. - public static string? FromDateTimePrecision(DateTimePrecision dtp) - { - return dtp switch - { - DateTimePrecision.Year => Year, - DateTimePrecision.Month => Month, - DateTimePrecision.Day => Day, - DateTimePrecision.Hour => Hour, - DateTimePrecision.Minute => Minute, - DateTimePrecision.Second => Second, - DateTimePrecision.Millisecond => Millisecond, - _ => null, - }; - } } - - } diff --git a/Cql/Cql.Abstractions/Abstractions/Units.cs b/Cql/Cql.Abstractions/Abstractions/Units.cs index 784d625b2..6ce8892b8 100644 --- a/Cql/Cql.Abstractions/Abstractions/Units.cs +++ b/Cql/Cql.Abstractions/Abstractions/Units.cs @@ -9,47 +9,23 @@ namespace Hl7.Cql.Abstractions { /// - /// Utilities for converting between CQL and UCUM units. + /// Utilities for converting precision to cql units /// public static class Units { /// - /// Maps CQL unit keywords (singular or plural) to their corresponding UCUM unit codes. + /// Maps DateTime Precisions to their corresponding CQL units. /// - /// - public static readonly IDictionary CqlUnitsToUCUM = new Dictionary(StringComparer.OrdinalIgnoreCase) + public static readonly IDictionary DatePrecisionToCqlUnits = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "year", UCUMUnits.Year }, - { "years", UCUMUnits.Year }, - { "month", UCUMUnits.Month }, - { "months", UCUMUnits.Month }, - { "days", UCUMUnits.Day }, - { "day", UCUMUnits.Day }, - { "week", UCUMUnits.Week }, - { "weeks", UCUMUnits.Week }, - { "hour", UCUMUnits.Hour }, - { "hours", UCUMUnits.Hour }, - { "minute", UCUMUnits.Minute }, - { "minutes", UCUMUnits.Minute }, - { "second", UCUMUnits.Second }, - { "seconds", UCUMUnits.Second }, - { "millisecond", UCUMUnits.Millisecond }, - { "milliseconds", UCUMUnits.Millisecond }, - }; - - /// - /// Maps UCUM unit codes to their corresponding CQL keywords, singular. - /// - public static readonly IDictionary UCUMUnitsToCql = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - { UCUMUnits.Year, "year" }, - { UCUMUnits.Month, "month" }, - { UCUMUnits.Week, "week" }, - { UCUMUnits.Day, "day" }, - { UCUMUnits.Hour, "hour" }, - { UCUMUnits.Minute, "minute" }, - { UCUMUnits.Second, "second" }, - { UCUMUnits.Millisecond, "millisecond" }, + { "Year", "year" }, + { "Month", "month" }, + { "Day", "day" }, + { "Week", "week" }, + { "Hour", "hour" }, + { "Minute", "minute" }, + { "Second", "second" }, + { "Millisecond", "millisecond" } }; } diff --git a/Cql/Cql.Abstractions/Primitives/CqlDate.cs b/Cql/Cql.Abstractions/Primitives/CqlDate.cs index abfc4c8bc..b7f2c2386 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDate.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDate.cs @@ -17,7 +17,11 @@ namespace Hl7.Cql.Primitives /// /// [CqlPrimitiveType(CqlPrimitiveType.Date)] - public class CqlDate : ICqlComparable, IEquivalentable + public class CqlDate : + ICqlComparable, + IEquivalentable, + IAdditionOperators, + ISubtractionOperators { /// /// Defines the minimum value for System dates (@0001-01-01). @@ -92,48 +96,24 @@ public static bool TryParse(string s, out CqlDate? cqlDate) /// If the quantity is not expressed in supported units, or an overflow occurs. public CqlDate? Add(CqlQuantity? quantity) { - if (quantity == null || quantity.value == null || quantity.unit == null) + if (quantity is not { value: { } value, unit: { } unit }) return null; - quantity = quantity.NormalizeTo(Precision); - var value = quantity.value!.Value; + var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + dto = unit switch { - case 'a': - dto = dto.AddYears((int)value); - break; - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'o': - dto = dto.AddMonths((int)value); - break; - case 'i': - dto = dto.AddMinutes(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddMilliseconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - dto = dto.AddDays((int)value!); - break; - case 'w': - dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); - break; - case 'h': - dto = dto.AddHours(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddSeconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } + "a" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerYearDouble), + "year" or "years" => dto.AddYears((int)value), + "mo" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerMonthDouble), + "month" or "months" => dto.AddMonths((int)value), + "wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)), + "d" or "day" or "days" => dto.AddDays((int)value!), + "h" or "hour" or "hours" => dto.AddHours(Math.Truncate((double)value)), + "min" or "minute" or "minutes" => dto.AddMinutes(Math.Truncate((double)value)), + "s" or "second" or "seconds" => dto.AddSeconds(Math.Truncate((double)value)), + "ms" or "millisecond" or "milliseconds" => dto.AddMilliseconds(Math.Truncate((double)value)), + _ => throw new ArgumentException($"Unknown date unit {unit} supplied") + }; var newIsoDate = new DateIso8601(dto, Value.Precision); var result = new CqlDate(newIsoDate); @@ -146,77 +126,21 @@ public static bool TryParse(string s, out CqlDate? cqlDate) /// The quantity to subtract. /// A new date with subtracted from it. /// If the quantity is not expressed in supported units, or an overflow occurs. - public CqlDate? Subtract(CqlQuantity? quantity) - { - if (quantity == null || quantity.value == null || quantity.unit == null) - return null; - quantity = quantity.NormalizeTo(Precision); - var value = -1 * quantity.value!.Value; - var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) - { - case 'a': - dto = dto.AddYears((int)value); - break; - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'o': - dto = dto.AddMonths((int)value); - break; - case 'i': - dto = dto.AddMinutes(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddMilliseconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - dto = dto.AddDays((int)value!); - break; - case 'w': - dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); - break; - case 'h': - dto = dto.AddHours(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddSeconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - - var newIsoDate = new DateIso8601(dto, Value.Precision); - var result = new CqlDate(newIsoDate); - return result; - } + public CqlDate? Subtract(CqlQuantity? quantity) => Add(-quantity); /// /// Gets the component of this date. /// /// The CQL or UCUM unit precision. /// The individual component at the specified precision, or if this date is not expressed in those units. - public int? Component(string precision) - { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; - switch (precision) + public int? Component(string precision) => + precision switch { - case UCUMUnits.Year: - return Value.Year; - case UCUMUnits.Month: - return Value.Month; - case UCUMUnits.Day: - return Value.Day; - default: - return null; - } - } + "year" => Value.Year, + "month" => Value.Month, + "day" => Value.Day, + _ => null + }; /// /// Gets the number of distinct boundaries in between this date and . @@ -248,7 +172,7 @@ public static bool TryParse(string s, out CqlDate? cqlDate) /// If the value is greater than zero, this object is greater than . /// If the value is , this comparison is uncertain because of . /// - public int? CompareToValue(CqlDate other, string? precision) => + public int? CompareToValue(CqlDate other, string? precision) => CompareValues(Value, other.Value, precision); private static int? CompareValues( @@ -261,10 +185,8 @@ public static bool TryParse(string s, out CqlDate? cqlDate) dtp = (DateTimePrecision)Math.Max((byte)self.Precision, (byte)other.Precision); else { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; // weeks isn't part of the precision enumeration - if (precision[0] == 'w') + if (precision == "week" || precision == "weeks") { var yearComparison = CompareTemporalIntegers(self.Year, other.Year); if (yearComparison == 0) @@ -287,7 +209,7 @@ public static bool TryParse(string s, out CqlDate? cqlDate) dtp = precision.ToDateTimePrecision() ?? DateTimePrecision.Unknown; } if (dtp == DateTimePrecision.Unknown) - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); switch (dtp) { case DateTimePrecision.Year: @@ -323,7 +245,7 @@ public static bool TryParse(string s, out CqlDate? cqlDate) case DateTimePrecision.Millisecond: case DateTimePrecision.Unknown: default: - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); } } @@ -358,15 +280,35 @@ public bool EquivalentToValue(CqlDate other, string? precision) => /// Returns for . /// public override string ToString() => Value.ToString(); + /// /// Compares this object to for equality. /// /// The object to compare against this value. /// if equal. public override bool Equals(object? obj) => Value.Equals((obj as CqlDate)?.Value!); + /// /// Gets the value of for . /// public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Adds a specified quantity to a date, returning a new date that is offset by the given quantity. + /// + /// The date to which the quantity will be added. May be null. + /// The quantity to add to the date. May be null. + /// A new representing the result of adding to . Returns null if either argument is null. + public static CqlDate? operator +(CqlDate? left, CqlQuantity? right) => left?.Add(right); + + /// + /// Subtracts the specified quantity from the given date, returning a new date that is offset by the quantity. + /// + /// The date from which to subtract the quantity. May be null. + /// The quantity to subtract from the date. May be null. + /// A new representing the result of subtracting from . Returns null if either argument is null. + public static CqlDate? operator -(CqlDate? left, CqlQuantity? right) => left?.Subtract(right); } } diff --git a/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs b/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs index e72156441..ffa56950f 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs @@ -17,7 +17,11 @@ namespace Hl7.Cql.Primitives /// /// [CqlPrimitiveType(CqlPrimitiveType.DateTime)] - public class CqlDateTime : ICqlComparable, IEquivalentable + public class CqlDateTime : + ICqlComparable, + IEquivalentable, + IAdditionOperators, + ISubtractionOperators { /// /// Defines the minimum value for System date times (@0001-01-01T00:00:00.000Z). @@ -145,48 +149,24 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) /// If the quantity is not expressed in supported units, or an overflow occurs. public CqlDateTime? Add(CqlQuantity? quantity) { - if (quantity == null || quantity.value == null || quantity.unit == null) + if (quantity is not { value: { } value, unit: { } unit }) return null; - quantity = quantity.NormalizeTo(Precision); - var value = quantity.value!.Value; + var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + dto = unit switch { - case 'a': - dto = dto.AddYears((int)value); - break; - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'o': - dto = dto.AddMonths((int)value); - break; - case 'i': - dto = dto.AddMinutes(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddMilliseconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - dto = dto.AddDays((int)value!); - break; - case 'w': - dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); - break; - case 'h': - dto = dto.AddHours(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddSeconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } + UCUMUnits.Year => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerYearDouble), + "year" or "years" => dto.AddYears((int)value), + "mo" => dto.AddDays(Math.Sign(value) * UCUMUnits.DaysPerMonthDouble), + "month" or "months" => dto.AddMonths((int)value), + "wk" or "week" or "weeks" => dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)), + "d" or "day" or "days" => dto.AddDays((int)value!), + "h" or "hour" or "hours" => dto.AddHours(Math.Truncate((double)value)), + "min" or "minute" or "minutes" => dto.AddMinutes(Math.Truncate((double)value)), + "s" or "second" or "seconds" => dto.AddSeconds(Math.Truncate((double)value)), + "ms" or "millisecond" or "milliseconds" => dto.AddMilliseconds(Math.Truncate((double)value)), + _ => throw new ArgumentException($"Unknown date unit {unit} supplied") + }; var newIsoDate = new DateTimeIso8601(dto, Value.Precision); var result = new CqlDateTime(newIsoDate); @@ -199,85 +179,25 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) /// The quantity to subtract. /// A new date time with subtracted from it. /// If the quantity is not expressed in supported units, or an overflow occurs. - public CqlDateTime? Subtract(CqlQuantity quantity) - { - if (quantity == null || quantity.value == null || quantity.unit == null) - return null; - quantity = quantity.NormalizeTo(Precision); - var value = -1 * quantity.value!.Value; - var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) - { - case 'a': - dto = dto.AddYears((int)value); - break; - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'o': - dto = dto.AddMonths((int)value); - break; - case 'i': - dto = dto.AddMinutes(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddMilliseconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - dto = dto.AddDays((int)value!); - break; - case 'w': - dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); - break; - case 'h': - dto = dto.AddHours(Math.Truncate((double)value)); - break; - case 's': - dto = dto.AddSeconds(Math.Truncate((double)value)); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - - var newIsoDate = new DateTimeIso8601(dto, Value.Precision); - var result = new CqlDateTime(newIsoDate); - return result; - } + public CqlDateTime? Subtract(CqlQuantity? quantity) => Add(-quantity); /// /// Gets the component of this date time. /// /// The CQL or UCUM unit precision. /// The individual component at the specified precision, or if this date is not expressed in those units. - public int? Component(string precision) - { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; - switch (precision) + public int? Component(string precision) => + precision switch { - case UCUMUnits.Year: - return Value.Year; - case UCUMUnits.Month: - return Value.Month; - case UCUMUnits.Day: - return Value.Day; - case UCUMUnits.Hour: - return Value.Hour; - case UCUMUnits.Minute: - return Value.Minute; - case UCUMUnits.Second: - return Value.Second; - case UCUMUnits.Millisecond: - return Value.Millisecond; - default: - return null; - } - } + "year" => Value.Year, + "month" => Value.Month, + "day" => Value.Day, + "hour" => Value.Hour, + "minute" => Value.Minute, + "second" => Value.Second, + "millisecond" => Value.Millisecond, + _ => null + }; /// /// Gets the number of distinct boundaries in between this date time and . @@ -340,10 +260,8 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) dtp = (DateTimePrecision)Math.Max((byte)self.Precision, (byte)other.Precision); else { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; // weeks isn't part of the precision enumeration - if (precision[0] == 'w') + if (precision == "week" || precision == "weeks") { var yearComparison = CompareTemporalIntegers(self.Year, other.Year); if (yearComparison == 0) @@ -366,12 +284,12 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) dtp = precision.ToDateTimePrecision() ?? DateTimePrecision.Unknown; } if (dtp == DateTimePrecision.Unknown) - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); switch (dtp) { default: case DateTimePrecision.Unknown: - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); case DateTimePrecision.Year: return CompareTemporalIntegers(self.Year, other.Year); case DateTimePrecision.Month: @@ -565,15 +483,36 @@ public bool EquivalentToValue(CqlDateTime other, string? precision) => /// Returns for . /// public override string ToString() => Value.ToString(); + /// /// Compares this object to for equality. /// /// The object to compare against this value. /// if equal. public override bool Equals(object? obj) => Value.Equals((obj as CqlDateTime)?.Value!); + /// /// Gets the value of for . /// public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Adds a specified quantity to a CqlDateTime value, returning a new CqlDateTime that represents the result. + /// + /// The CqlDateTime value to which the quantity will be added. May be null. + /// The CqlQuantity representing the amount to add to the date and time. May be null. + /// A new CqlDateTime that is the result of adding the specified quantity to the original value, or null if + /// either operand is null. + public static CqlDateTime? operator +(CqlDateTime? left, CqlQuantity? right) => left?.Add(right); + + /// + /// Subtracts a specified quantity from a CqlDateTime value, returning a new CqlDateTime that represents the + /// result. + /// + /// The CqlDateTime value from which to subtract. May be null. + /// The CqlQuantity value to subtract from the CqlDateTime. May be null. + /// A new CqlDateTime representing the result of subtracting the specified quantity from the original date and + /// time, or null if either operand is null. + public static CqlDateTime? operator -(CqlDateTime? left, CqlQuantity? right) => left?.Subtract(right); } } diff --git a/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs b/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs index 072903c19..9f180c5b1 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs @@ -28,22 +28,23 @@ internal static class CqlDateTimeMath /// internal static int? BoundariesBetween(DateTimeOffset? low, DateTimeOffset? high, string? precision) { - if (low == null || high == null || precision == null) + if (low is not {} firstDto || high is not {} secondDto || precision is null) return null; - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; - var firstDto = low.Value; - var secondDto = high.Value; switch (precision) { - case "a": + // https://cql.hl7.org/09-b-cqlreference.html#difference + // UCUM units not supported here + + case "year": var yearDiff = (secondDto.Year - firstDto.Year); return yearDiff; - case "mo": + + case "month": var monthDiff = (12 * (secondDto.Year - firstDto.Year) + secondDto.Month - firstDto.Month); return monthDiff; - case "wk": + + case "week": { var span = secondDto.Subtract(firstDto); var weeks = span.TotalDays / 7d; @@ -54,7 +55,8 @@ internal static class CqlDateTimeMath return asInt + 1; else return asInt; } - case "d": + + case "day": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalDays; @@ -66,7 +68,8 @@ internal static class CqlDateTimeMath } else return asInt; } - case "h": + + case "hour": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalHours; @@ -78,7 +81,8 @@ internal static class CqlDateTimeMath } else return asInt; } - case "min": + + case "minute": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalMinutes; @@ -90,7 +94,8 @@ internal static class CqlDateTimeMath } else return asInt; } - case "s": + + case "second": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalSeconds; @@ -102,7 +107,8 @@ internal static class CqlDateTimeMath } else return asInt; } - case "ms": + + case "millisecond": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalMilliseconds; @@ -114,23 +120,23 @@ internal static class CqlDateTimeMath } else return asInt; } + default: throw new ArgumentException($"Unit '{precision}' is not supported."); } } - internal static int? WholeCalendarPeriodsBetween(DateTimeOffset? low, DateTimeOffset? high, string precision) + internal static int? WholeCalendarPeriodsBetween(DateTimeOffset? low, DateTimeOffset? high, string? precision) { - if (low == null || high == null || precision == null) + if (low is not {} firstDto || high is not {} secondDto || precision == null) return null; - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; var calendar = new GregorianCalendar(); - var firstDto = low.Value; - var secondDto = high.Value; switch (precision) { - case "a": + // https://cql.hl7.org/09-b-cqlreference.html#difference + // UCUM units not supported here + + case "year": var yearDiff = secondDto.Year - firstDto.Year; var firstDayInYear = firstDto.DayOfYear; var secondDayInYear = secondDto.DayOfYear; @@ -196,95 +202,97 @@ internal static class CqlDateTimeMath else if (yearDiff < 0 && firstDayInYear < secondDayInYear) yearDiff += 1; return yearDiff; - case "mo": + + case "month": var monthDiff = (12 * (secondDto.Year - firstDto.Year) + secondDto.Month - firstDto.Month); if (monthDiff > 0 && secondDto.Day < firstDto.Day) monthDiff -= 1; else if (monthDiff < 0 && firstDto.Day < secondDto.Day) monthDiff += 1; return monthDiff; - case "wk": return (int)(secondDto.Subtract(firstDto).TotalDays / DaysPerWeekDouble); - case "d": return (int)secondDto.Subtract(firstDto).TotalDays; - case "h": return (int)secondDto.Subtract(firstDto).TotalHours; - case "min": return (int)secondDto.Subtract(firstDto).TotalMinutes; - case "s": return (int)secondDto.Subtract(firstDto).TotalSeconds; - case "ms": return (int)secondDto.Subtract(firstDto).TotalMilliseconds; - default: throw new ArgumentException($"Unit '{precision}' is not supported."); + + case "week": return (int)(secondDto.Subtract(firstDto).TotalDays / DaysPerWeekDouble); + case "day": return (int)secondDto.Subtract(firstDto).TotalDays; + case "hour": return (int)secondDto.Subtract(firstDto).TotalHours; + case "minute": return (int)secondDto.Subtract(firstDto).TotalMinutes; + case "second": return (int)secondDto.Subtract(firstDto).TotalSeconds; + case "millisecond": return (int)secondDto.Subtract(firstDto).TotalMilliseconds; + default: throw new ArgumentException($"Unit '{precision}' is not supported."); } } internal static readonly IDictionary UnitDateTimeQuantity = new Dictionary { - { DateTimePrecision.Day, new CqlQuantity(1m, UCUMUnits.Day) }, - { DateTimePrecision.Hour, new CqlQuantity(1m, UCUMUnits.Hour) }, - { DateTimePrecision.Millisecond, new CqlQuantity(1m, UCUMUnits.Millisecond) }, - { DateTimePrecision.Minute, new CqlQuantity(1m, UCUMUnits.Minute) }, - { DateTimePrecision.Month, new CqlQuantity(1m, UCUMUnits.Month) }, - { DateTimePrecision.Second, new CqlQuantity(1m, UCUMUnits.Second) }, - { DateTimePrecision.Year, new CqlQuantity(1m, UCUMUnits.Year) }, + { DateTimePrecision.Day, new CqlQuantity(1m, "day") }, + { DateTimePrecision.Hour, new CqlQuantity(1m, "hour") }, + { DateTimePrecision.Millisecond, new CqlQuantity(1m, "millisecond") }, + { DateTimePrecision.Minute, new CqlQuantity(1m, "minute") }, + { DateTimePrecision.Month, new CqlQuantity(1m, "month") }, + { DateTimePrecision.Second, new CqlQuantity(1m, "second") }, + { DateTimePrecision.Year, new CqlQuantity(1m, "year") }, }; - /// - /// For datetime addition and subtraction, when quantity is more precise than the datetime, - /// the quantity has to be normalized to the lesser precision and truncated. - /// - /// - internal static CqlQuantity NormalizeTo(this CqlQuantity quantity, DateTimePrecision target) - { - // using the table found here: - // https://cql.hl7.org/09-b-cqlreference.html#equivalent - return (quantity.unit, target) switch - { - (null, _) => quantity, - ("mo", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 12)!, UCUMUnits.Year), - - ("d", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 365)!, UCUMUnits.Year), - ("d", DateTimePrecision.Month) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 30)!, UCUMUnits.Month), - - ("h", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 24) / 365)!, UCUMUnits.Year), - ("h", DateTimePrecision.Month) => - new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 24) / 30)!, UCUMUnits.Month), - ("h", DateTimePrecision.Day) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 24)!, UCUMUnits.Day), - - ("mi", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 24) / 365)!, UCUMUnits.Year), - ("mi", DateTimePrecision.Month) => - new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 24) / 30)!, UCUMUnits.Month), - ("mi", DateTimePrecision.Day) => - new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 60) / 24)!, UCUMUnits.Day), - ("mi", DateTimePrecision.Hour) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 60)!, UCUMUnits.Hour), - - ("s", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 60) / 60) / 24) / 365)!, UCUMUnits.Year), - ("s", DateTimePrecision.Month) => - new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 60) / 60) / 24) / 30)!, UCUMUnits.Month), - ("s", DateTimePrecision.Day) => - new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 60) / 24)!, UCUMUnits.Day), - ("s", DateTimePrecision.Hour) => - new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 60) / 60)!, UCUMUnits.Hour), - ("s", DateTimePrecision.Minute) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 60)!, UCUMUnits.Minute), - - ("ms", DateTimePrecision.Year) => - new CqlQuantity(Math.Truncate((((((quantity.value ?? 0) / 1000) / 60) / 60) / 24) / 365)!, UCUMUnits.Year), - ("ms", DateTimePrecision.Month) => - new CqlQuantity(Math.Truncate((((((quantity.value ?? 0) / 1000) / 60) / 60) / 24) / 30)!, UCUMUnits.Month), - ("ms", DateTimePrecision.Day) => - new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 1000) / 60) / 60) / 24)!, UCUMUnits.Day), - ("ms", DateTimePrecision.Hour) => - new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 1000) / 60) / 60)!, UCUMUnits.Hour), - ("ms", DateTimePrecision.Minute) => - new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 1000) / 60)!, UCUMUnits.Minute), - ("ms", DateTimePrecision.Second) => - new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 1000)!, UCUMUnits.Second), - (_,_) => quantity - }; - } + // /// + // /// For datetime addition and subtraction, when quantity is more precise than the datetime, + // /// the quantity has to be normalized to the lesser precision and truncated. + // /// + // /// + // internal static CqlQuantity NormalizeTo(this CqlQuantity quantity, DateTimePrecision target) + // { + // // using the table found here: + // // https://cql.hl7.org/09-b-cqlreference.html#equivalent + // return (quantity.unit, target) switch + // { + // (null, _) => quantity, + // ("mo", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 12)!, UCUMUnits.Year), + // + // ("d", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 365)!, UCUMUnits.Year), + // ("d", DateTimePrecision.Month) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 30)!, UCUMUnits.Month), + // + // ("h", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 24) / 365)!, UCUMUnits.Year), + // ("h", DateTimePrecision.Month) => + // new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 24) / 30)!, UCUMUnits.Month), + // ("h", DateTimePrecision.Day) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 24)!, UCUMUnits.Day), + // + // ("mi", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 24) / 365)!, UCUMUnits.Year), + // ("mi", DateTimePrecision.Month) => + // new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 24) / 30)!, UCUMUnits.Month), + // ("mi", DateTimePrecision.Day) => + // new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 60) / 24)!, UCUMUnits.Day), + // ("mi", DateTimePrecision.Hour) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 60)!, UCUMUnits.Hour), + // + // ("s", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 60) / 60) / 24) / 365)!, UCUMUnits.Year), + // ("s", DateTimePrecision.Month) => + // new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 60) / 60) / 24) / 30)!, UCUMUnits.Month), + // ("s", DateTimePrecision.Day) => + // new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 60) / 60) / 24)!, UCUMUnits.Day), + // ("s", DateTimePrecision.Hour) => + // new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 60) / 60)!, UCUMUnits.Hour), + // ("s", DateTimePrecision.Minute) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 60)!, UCUMUnits.Minute), + // + // ("ms", DateTimePrecision.Year) => + // new CqlQuantity(Math.Truncate((((((quantity.value ?? 0) / 1000) / 60) / 60) / 24) / 365)!, UCUMUnits.Year), + // ("ms", DateTimePrecision.Month) => + // new CqlQuantity(Math.Truncate((((((quantity.value ?? 0) / 1000) / 60) / 60) / 24) / 30)!, UCUMUnits.Month), + // ("ms", DateTimePrecision.Day) => + // new CqlQuantity(Math.Truncate(((((quantity.value ?? 0) / 1000) / 60) / 60) / 24)!, UCUMUnits.Day), + // ("ms", DateTimePrecision.Hour) => + // new CqlQuantity(Math.Truncate((((quantity.value ?? 0) / 1000) / 60) / 60)!, UCUMUnits.Hour), + // ("ms", DateTimePrecision.Minute) => + // new CqlQuantity(Math.Truncate(((quantity.value ?? 0) / 1000) / 60)!, UCUMUnits.Minute), + // ("ms", DateTimePrecision.Second) => + // new CqlQuantity(Math.Truncate((quantity.value ?? 0) / 1000)!, UCUMUnits.Second), + // (_,_) => quantity + // }; + // } } } diff --git a/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs b/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs index dd9b1c975..6c9fab160 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs @@ -6,8 +6,6 @@ * available at https://raw.githubusercontent.com/FirelyTeam/firely-cql-sdk/main/LICENSE */ -using Hl7.Cql.Abstractions; - namespace Hl7.Cql.Primitives { /// @@ -15,7 +13,7 @@ namespace Hl7.Cql.Primitives /// /// [CqlPrimitiveType(CqlPrimitiveType.Quantity)] - public class CqlQuantity + public class CqlQuantity : IUnaryNegationOperators { /// /// Creates an instance. @@ -30,7 +28,7 @@ public CqlQuantity() { } public CqlQuantity(decimal? value, string? unit) { this.value = value; - this.unit = unit != null && Units.CqlUnitsToUCUM.TryGetValue(unit, out var ucumUnits) ? ucumUnits : unit; + this.unit = unit; } /// @@ -51,10 +49,6 @@ public CqlQuantity(decimal? value, string? unit) if (value == null || unit == null) return null; var unitString = unit; - if (Units.UCUMUnitsToCql.TryGetValue(unit, out var cqlUnit)) - { - unitString = cqlUnit; - } return string.Create(CultureInfo.InvariantCulture, $"{value} '{unitString}'"); } @@ -102,5 +96,26 @@ public static bool TryParse(string s, out CqlQuantity? q) else return v; } + /// + /// Returns the negated value of the specified instance. + /// + /// The to negate. If , the result is . + /// A representing the negated value of ; or if is . + public static CqlQuantity? operator -(CqlQuantity? value) => Negate(value)!; + + /// + /// Returns a new CqlQuantity with the value negated, preserving the unit. If the input value is null, the + /// result will also have a null value. + /// + /// The quantity to negate. May be null. + /// A new CqlQuantity with the negated value and the same unit as the input; or null if the input is null. + public static CqlQuantity? Negate(CqlQuantity? cqlQuantity) => + cqlQuantity switch + { + { value: { } value, unit: var unit } => new CqlQuantity(-value, unit), + { value: null, unit: var unit } => new CqlQuantity(null, unit), + null => null, + }; } } diff --git a/Cql/Cql.Abstractions/Primitives/CqlTime.cs b/Cql/Cql.Abstractions/Primitives/CqlTime.cs index 9735ec57c..2824297e3 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlTime.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlTime.cs @@ -17,12 +17,17 @@ namespace Hl7.Cql.Primitives /// /// [CqlPrimitiveType(CqlPrimitiveType.Time)] - public class CqlTime : ICqlComparable, IEquivalentable + public class CqlTime : + ICqlComparable, + IEquivalentable, + IAdditionOperators, + ISubtractionOperators { /// /// Defines the minimum value for System times (00:00:00.000Z) /// public static readonly CqlTime MinValue = new(0, 0, 0, 0, 0, 0); + /// /// Defines the maximum value for System times (23:59:59.999Z) /// @@ -32,10 +37,12 @@ public class CqlTime : ICqlComparable, IEquivalentable /// Gets the value of this time. /// public TimeIso8601 Value { get; } + /// /// Gets the value of this time in UTC. /// public TimeIso8601 InUtc { get; } + /// /// Gets the precision in which this time is specified. /// @@ -50,10 +57,14 @@ public class CqlTime : ICqlComparable, IEquivalentable /// The day component, or to indicate millisecond precision. /// The time zone offset hours component, or to indicate UTC. /// The time zone offset minutes component, or to indicate UTC. - public CqlTime(int hour, int? minute, int? second, int? millisecond, int? offsetHour, int? offsetMinute) : - this(new TimeIso8601(hour, minute, second, millisecond, offsetHour, offsetMinute)) - { - } + public CqlTime( + int hour, + int? minute, + int? second, + int? millisecond, + int? offsetHour, + int? offsetMinute) : + this(new TimeIso8601(hour, minute, second, millisecond, offsetHour, offsetMinute)) { } /// /// Creates an instance for the given ISO time. @@ -94,44 +105,22 @@ public static bool TryParse(string s, out CqlTime? time) /// The quantity to add. /// A new time with added to it. /// If the quantity is not expressed in supported units, or an overflow occurs. - public CqlTime? Add(CqlQuantity quantity) + public CqlTime? Add(CqlQuantity? quantity) { - if (quantity == null || quantity.value == null || quantity.unit == null) + if (quantity is not { value: { } value, unit: { } unit }) return null; - quantity = quantity.NormalizeTo(Precision); - var value = quantity.value!.Value; + var span = Value.TimeSpan; - switch (quantity.unit![0]) + span = unit switch { - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'i': - span = span.Add(TimeSpan.FromMinutes(Math.Truncate((double)value))); - break; - case 's': - span = span.Add(TimeSpan.FromMilliseconds(Math.Truncate((double)value))); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - span = span.Add(TimeSpan.FromDays(Math.Truncate((double)value))); - break; - case 'w': - span = span.Add(TimeSpan.FromDays(Math.Truncate((double)value) * CqlDateTimeMath.DaysPerWeekDouble)); - break; - case 'h': - span = span.Add(TimeSpan.FromHours(Math.Truncate((double)value))); - break; - case 's': - span = span.Add(TimeSpan.FromSeconds(Math.Truncate((double)value))); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } + UCUMUnits.Minute or "minute" or "minutes" => span.Add(TimeSpan.FromMinutes(Math.Truncate((double)value))), + UCUMUnits.Millisecond or "millisecond" or "milliseconds" => span.Add(TimeSpan.FromMilliseconds(Math.Truncate((double)value))), + UCUMUnits.Day or "day" or "days" => span.Add(TimeSpan.FromDays(Math.Truncate((double)value))), + UCUMUnits.Week or "week" or "weeks" => span.Add(TimeSpan.FromDays(Math.Truncate((double)value) * CqlDateTimeMath.DaysPerWeekDouble)), + UCUMUnits.Hour or "hour" or "hours" => span.Add(TimeSpan.FromHours(Math.Truncate((double)value))), + UCUMUnits.Second or "second" or "seconds" => span.Add(TimeSpan.FromSeconds(Math.Truncate((double)value))), + _ => throw new ArgumentException($"Unknown date unit {unit} supplied") + }; var newIsoTime = new TimeIso8601(span, Value.OffsetHour, Value.OffsetMinute, Value.Precision); var result = new CqlTime(newIsoTime); @@ -144,73 +133,22 @@ public static bool TryParse(string s, out CqlTime? time) /// The quantity to subtract. /// A new time with subtracted from it. /// If the quantity is not expressed in supported units, or an overflow occurs. - public CqlTime? Subtract(CqlQuantity quantity) - { - if (quantity == null || quantity.value == null || quantity.unit == null) - return null; - quantity = quantity.NormalizeTo(Precision); - var value = quantity.value!.Value; - var span = Value.TimeSpan; - switch (quantity.unit![0]) - { - case 'm': - if (quantity.unit.Length > 1) - { - switch (quantity.unit[1]) - { - case 'i': - span = span.Subtract(TimeSpan.FromMinutes(Math.Truncate((double)value))); - break; - case 's': - span = span.Subtract(TimeSpan.FromMilliseconds(Math.Truncate((double)value))); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - } - break; - case 'd': - span = span.Subtract(TimeSpan.FromDays(Math.Truncate((double)value))); - break; - case 'w': - span = span.Subtract(TimeSpan.FromDays(Math.Truncate((double)value) * CqlDateTimeMath.DaysPerWeekDouble)); - break; - case 'h': - span = span.Subtract(TimeSpan.FromHours(Math.Truncate((double)value))); - break; - case 's': - span = span.Subtract(TimeSpan.FromSeconds(Math.Truncate((double)value))); - break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); - } - - var newIsoTime = new TimeIso8601(span, Value.OffsetHour, Value.OffsetMinute, Value.Precision); - var result = new CqlTime(newIsoTime); - return result; - } + public CqlTime? Subtract(CqlQuantity? quantity) => Add(-quantity); /// /// Gets the component of this time. /// /// The CQL or UCUM unit precision. /// The individual component at the specified precision, or if this date is not expressed in those units. - public int? Component(string precision) - { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; - switch (precision) + public int? Component(string precision) => + precision switch { - case UCUMUnits.Hour: - return Value.Hour; - case UCUMUnits.Minute: - return Value.Minute; - case UCUMUnits.Second: - return Value.Second; - case UCUMUnits.Millisecond: - return Value.Millisecond; - default: - return null; - } - } + "hour" => Value.Hour, + "minute" => Value.Minute, + "second" => Value.Second, + "millisecond" => Value.Millisecond, + _ => null + }; /// /// Gets the number of distinct boundaries in between this time and . @@ -218,7 +156,8 @@ public static bool TryParse(string s, out CqlTime? time) /// The high time bound against which to calculate. /// The boundary precision to count. /// The number of boundaries crossed between this time and . - public int? BoundariesBetween(CqlTime high, string precision) => CqlDateTimeMath.BoundariesBetween(Value.DateTimeOffset, high.Value.DateTimeOffset, precision); + public int? BoundariesBetween(CqlTime high, string precision) => + CqlDateTimeMath.BoundariesBetween(Value.DateTimeOffset, high.Value.DateTimeOffset, precision); /// /// Gets the number of whole calendar periods in between this time and . @@ -226,7 +165,8 @@ public static bool TryParse(string s, out CqlTime? time) /// The high time bound against which to calculate. /// The calendar precision to count. /// The number of whole calendar periods between this time and . - public int? WholeCalendarPointsBetween(CqlTime high, string precision) => CqlDateTimeMath.WholeCalendarPeriodsBetween(Value.DateTimeOffset, high.Value.DateTimeOffset, precision); + public int? WholeCalendarPointsBetween(CqlTime high, string precision) => + CqlDateTimeMath.WholeCalendarPeriodsBetween(Value.DateTimeOffset, high.Value.DateTimeOffset, precision); /// /// Gets the immediate predecessor of this value in its precision. @@ -273,101 +213,104 @@ public static bool TryParse(string s, out CqlTime? time) dtp = (DateTimePrecision)Math.Max((byte)self.Precision, (byte)other.Precision); else { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var converted)) - precision = converted; dtp = precision.ToDateTimePrecision() ?? DateTimePrecision.Unknown; } + if (dtp == DateTimePrecision.Unknown) - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); switch (dtp) { case DateTimePrecision.Hour: + { + var left = selfInUtc; + var right = otherInUtc; + if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) { - var left = selfInUtc; - var right = otherInUtc; - if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) - { - left = self; - right = other; - } - var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); - return hourComparison; + left = self; + right = other; } + + var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); + return hourComparison; + } case DateTimePrecision.Minute: + { + var left = selfInUtc; + var right = otherInUtc; + if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) { - var left = selfInUtc; - var right = otherInUtc; - if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) - { - left = self; - right = other; - } + left = self; + right = other; + } - var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); - if (hourComparison == 0) - { - var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); - return minuteComparison; - } - else return hourComparison; + var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); + if (hourComparison == 0) + { + var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); + return minuteComparison; } + else return hourComparison; + } case DateTimePrecision.Second: + { + var left = selfInUtc; + var right = otherInUtc; + if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) { - var left = selfInUtc; - var right = otherInUtc; - if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) - { - left = self; - right = other; - } - var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); - if (hourComparison == 0) + left = self; + right = other; + } + + var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); + if (hourComparison == 0) + { + var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); + if (minuteComparison == 0) { - var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); - if (minuteComparison == 0) - { - var secondComparison = CompareTemporalIntegers(left.Second, right.Second); - return secondComparison; - } - else return minuteComparison; + var secondComparison = CompareTemporalIntegers(left.Second, right.Second); + return secondComparison; } - else return hourComparison; - + else return minuteComparison; } + else return hourComparison; + + } case DateTimePrecision.Millisecond: + { + var left = selfInUtc; + var right = otherInUtc; + if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) { - var left = selfInUtc; - var right = otherInUtc; - if (self.RationalOffset.HasValue ^ other.RationalOffset.HasValue) - { - left = self; - right = other; - } - var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); - if (hourComparison == 0) + left = self; + right = other; + } + + var hourComparison = CompareTemporalIntegers(left.Hour, right.Hour); + if (hourComparison == 0) + { + var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); + if (minuteComparison == 0) { - var minuteComparison = CompareTemporalIntegers(left.Minute, right.Minute); - if (minuteComparison == 0) + var secondComparison = CompareTemporalIntegers(left.Second, right.Second); + if (secondComparison == 0) { - var secondComparison = CompareTemporalIntegers(left.Second, right.Second); - if (secondComparison == 0) - { - var milliComparison = CompareTemporalIntegers(left.Millisecond, right.Millisecond); - return milliComparison; - } - return secondComparison; + var milliComparison = CompareTemporalIntegers(left.Millisecond, right.Millisecond); + return milliComparison; } - else return minuteComparison; + + return secondComparison; } - else return hourComparison; + else return minuteComparison; } + else return hourComparison; + } case DateTimePrecision.Unknown: case DateTimePrecision.Year: case DateTimePrecision.Month: case DateTimePrecision.Day: default: - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); } } @@ -384,15 +327,36 @@ public bool EquivalentToValue(CqlTime other, string? precision) => /// Returns for . /// public override string ToString() => Value.ToString(); + /// /// Compares this object to for equality. /// /// The object to compare against this value. /// if equal. public override bool Equals(object? obj) => Value.Equals((obj as CqlTime)?.Value!); + /// /// Gets the value of for . /// public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Adds a specified quantity to a CqlTime value, returning a new CqlTime that represents the result. + /// + /// The CqlTime value to which the quantity will be added. May be null. + /// The CqlQuantity representing the amount of time to add. May be null. + /// A new CqlTime that is the result of adding the specified quantity to the given time. Returns null if either + /// parameter is null. + public static CqlTime? operator +(CqlTime? left, CqlQuantity? right) => left?.Add(right); + + /// + /// Subtracts the specified quantity from the given time value, returning a new time that is offset by the + /// quantity. + /// + /// The time value from which to subtract the quantity. May be null. + /// The quantity to subtract from the time value. May be null. + /// A new representing the result of subtracting from . Returns null if is null. + public static CqlTime? operator -(CqlTime? left, CqlQuantity? right) => left?.Subtract(right); } } diff --git a/Cql/Cql.Abstractions/PublicAPI.Shipped.txt b/Cql/Cql.Abstractions/PublicAPI.Shipped.txt index 6efd1e08a..a0da995e0 100644 --- a/Cql/Cql.Abstractions/PublicAPI.Shipped.txt +++ b/Cql/Cql.Abstractions/PublicAPI.Shipped.txt @@ -17,6 +17,8 @@ const Hl7.Cql.Abstractions.UCUMUnits.Unary = "1" -> string! const Hl7.Cql.Abstractions.UCUMUnits.Week = "wk" -> string! const Hl7.Cql.Abstractions.UCUMUnits.Yard = "[yd_i]" -> string! const Hl7.Cql.Abstractions.UCUMUnits.Year = "a" -> string! +const Hl7.Cql.Abstractions.UCUMUnits.DaysPerYearDouble = 365.25 -> double +const Hl7.Cql.Abstractions.UCUMUnits.DaysPerMonthDouble = 30.4375 -> double Hl7.Cql.Abstractions.CqlCodeDefinitionAttribute Hl7.Cql.Abstractions.CqlCodeDefinitionAttribute.CodeDisplay.get -> string? Hl7.Cql.Abstractions.CqlCodeDefinitionAttribute.CodeId.get -> string! @@ -141,7 +143,6 @@ Hl7.Cql.Primitives.CqlDateTime.EquivalentToValue(Hl7.Cql.Primitives.CqlDateTime! Hl7.Cql.Primitives.CqlDateTime.InUtc.get -> Hl7.Cql.Iso8601.DateTimeIso8601! Hl7.Cql.Primitives.CqlDateTime.Precision.get -> Hl7.Cql.Iso8601.DateTimePrecision Hl7.Cql.Primitives.CqlDateTime.Predecessor() -> Hl7.Cql.Primitives.CqlDateTime! -Hl7.Cql.Primitives.CqlDateTime.Subtract(Hl7.Cql.Primitives.CqlQuantity! quantity) -> Hl7.Cql.Primitives.CqlDateTime? Hl7.Cql.Primitives.CqlDateTime.Successor() -> Hl7.Cql.Primitives.CqlDateTime! Hl7.Cql.Primitives.CqlDateTime.TimeOnly.get -> Hl7.Cql.Primitives.CqlTime? Hl7.Cql.Primitives.CqlDateTime.Value.get -> Hl7.Cql.Iso8601.DateTimeIso8601! @@ -192,7 +193,6 @@ Hl7.Cql.Primitives.CqlRatio.denominator.init -> void Hl7.Cql.Primitives.CqlRatio.numerator.get -> Hl7.Cql.Primitives.CqlQuantity? Hl7.Cql.Primitives.CqlRatio.numerator.init -> void Hl7.Cql.Primitives.CqlTime -Hl7.Cql.Primitives.CqlTime.Add(Hl7.Cql.Primitives.CqlQuantity! quantity) -> Hl7.Cql.Primitives.CqlTime? Hl7.Cql.Primitives.CqlTime.BoundariesBetween(Hl7.Cql.Primitives.CqlTime! high, string! precision) -> int? Hl7.Cql.Primitives.CqlTime.CompareToValue(Hl7.Cql.Primitives.CqlTime! other, string? precision = null) -> int? Hl7.Cql.Primitives.CqlTime.Component(string! precision) -> int? @@ -202,7 +202,6 @@ Hl7.Cql.Primitives.CqlTime.EquivalentToValue(Hl7.Cql.Primitives.CqlTime! other, Hl7.Cql.Primitives.CqlTime.InUtc.get -> Hl7.Cql.Iso8601.TimeIso8601! Hl7.Cql.Primitives.CqlTime.Precision.get -> Hl7.Cql.Iso8601.DateTimePrecision Hl7.Cql.Primitives.CqlTime.Predecessor() -> Hl7.Cql.Primitives.CqlTime! -Hl7.Cql.Primitives.CqlTime.Subtract(Hl7.Cql.Primitives.CqlQuantity! quantity) -> Hl7.Cql.Primitives.CqlTime? Hl7.Cql.Primitives.CqlTime.Successor() -> Hl7.Cql.Primitives.CqlTime! Hl7.Cql.Primitives.CqlTime.Value.get -> Hl7.Cql.Iso8601.TimeIso8601! Hl7.Cql.Primitives.CqlTime.WholeCalendarPointsBetween(Hl7.Cql.Primitives.CqlTime! high, string! precision) -> int? @@ -265,7 +264,6 @@ override Hl7.Cql.Primitives.CqlVocabulary.GetHashCode() -> int override Hl7.Cql.Primitives.CqlVocabulary.ToString() -> string! override sealed Hl7.Cql.Primitives.CqlCodeSystem.Equals(Hl7.Cql.Primitives.CqlVocabulary? other) -> bool override sealed Hl7.Cql.Primitives.CqlValueSet.Equals(Hl7.Cql.Primitives.CqlVocabulary? other) -> bool -static Hl7.Cql.Abstractions.UCUMUnits.FromDateTimePrecision(Hl7.Cql.Iso8601.DateTimePrecision dtp) -> string? static Hl7.Cql.Comparers.CqlComparerExtensions.ToEqualityComparer(this Hl7.Cql.Comparers.ICqlComparer! comparer, string? precision = null, bool useEquivalence = false) -> System.Collections.Generic.IEqualityComparer! static Hl7.Cql.Primitives.CqlCode.operator !=(Hl7.Cql.Primitives.CqlCode? left, Hl7.Cql.Primitives.CqlCode? right) -> bool static Hl7.Cql.Primitives.CqlCode.operator ==(Hl7.Cql.Primitives.CqlCode? left, Hl7.Cql.Primitives.CqlCode? right) -> bool @@ -288,8 +286,7 @@ static Hl7.Cql.Primitives.CqlVocabulary.operator !=(Hl7.Cql.Primitives.CqlVocabu static Hl7.Cql.Primitives.CqlVocabulary.operator ==(Hl7.Cql.Primitives.CqlVocabulary? left, Hl7.Cql.Primitives.CqlVocabulary? right) -> bool static Hl7.Cql.Primitives.TypeExtensions.IsCqlInterval(this System.Type! type, out System.Type? elementType) -> bool static Hl7.Cql.Primitives.TypeExtensions.IsCqlValueTuple(this System.Type! type) -> bool -static readonly Hl7.Cql.Abstractions.Units.CqlUnitsToUCUM -> System.Collections.Generic.IDictionary! -static readonly Hl7.Cql.Abstractions.Units.UCUMUnitsToCql -> System.Collections.Generic.IDictionary! +static readonly Hl7.Cql.Abstractions.Units.DatePrecisionToCqlUnits -> System.Collections.Generic.IDictionary! static readonly Hl7.Cql.Primitives.CqlDate.MaxValue -> Hl7.Cql.Primitives.CqlDate! static readonly Hl7.Cql.Primitives.CqlDate.MinValue -> Hl7.Cql.Primitives.CqlDate! static readonly Hl7.Cql.Primitives.CqlDateTime.MaxValue -> Hl7.Cql.Primitives.CqlDateTime! @@ -311,3 +308,14 @@ virtual Hl7.Cql.Primitives.CqlValueSet.Equals(Hl7.Cql.Primitives.CqlValueSet? ot virtual Hl7.Cql.Primitives.CqlVocabulary.EqualityContract.get -> System.Type! virtual Hl7.Cql.Primitives.CqlVocabulary.Equals(Hl7.Cql.Primitives.CqlVocabulary? other) -> bool virtual Hl7.Cql.Primitives.CqlVocabulary.PrintMembers(System.Text.StringBuilder! builder) -> bool +Hl7.Cql.Primitives.CqlDateTime.Subtract(Hl7.Cql.Primitives.CqlQuantity? quantity) -> Hl7.Cql.Primitives.CqlDateTime? +Hl7.Cql.Primitives.CqlTime.Add(Hl7.Cql.Primitives.CqlQuantity? quantity) -> Hl7.Cql.Primitives.CqlTime? +Hl7.Cql.Primitives.CqlTime.Subtract(Hl7.Cql.Primitives.CqlQuantity? quantity) -> Hl7.Cql.Primitives.CqlTime? +static Hl7.Cql.Primitives.CqlDate.operator +(Hl7.Cql.Primitives.CqlDate? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlDate? +static Hl7.Cql.Primitives.CqlDate.operator -(Hl7.Cql.Primitives.CqlDate? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlDate? +static Hl7.Cql.Primitives.CqlDateTime.operator +(Hl7.Cql.Primitives.CqlDateTime? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlDateTime? +static Hl7.Cql.Primitives.CqlDateTime.operator -(Hl7.Cql.Primitives.CqlDateTime? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlDateTime? +static Hl7.Cql.Primitives.CqlQuantity.Negate(Hl7.Cql.Primitives.CqlQuantity? cqlQuantity) -> Hl7.Cql.Primitives.CqlQuantity? +static Hl7.Cql.Primitives.CqlQuantity.operator -(Hl7.Cql.Primitives.CqlQuantity? value) -> Hl7.Cql.Primitives.CqlQuantity? +static Hl7.Cql.Primitives.CqlTime.operator +(Hl7.Cql.Primitives.CqlTime? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlTime? +static Hl7.Cql.Primitives.CqlTime.operator -(Hl7.Cql.Primitives.CqlTime? left, Hl7.Cql.Primitives.CqlQuantity? right) -> Hl7.Cql.Primitives.CqlTime? diff --git a/Cql/Cql.CqlToElm/Visitors/TerminalParsers.cs b/Cql/Cql.CqlToElm/Visitors/TerminalParsers.cs index d6f75663e..d26a95158 100644 --- a/Cql/Cql.CqlToElm/Visitors/TerminalParsers.cs +++ b/Cql/Cql.CqlToElm/Visitors/TerminalParsers.cs @@ -77,22 +77,9 @@ public static (decimal value, string unit) Parse(this cqlParser.QuantityContext // This is either a unit, or a datetimeprecision (which we parse as text here) var unitText = context.unit().STRING().ParseString() ?? context.unit().GetText(); - var unit = translateUnit(unitText); - return (decimalValue, unit!); - - static string translateUnit(string u) => u switch - { - "year" or "years" => "a", - "month" or "months" => "mo", - "week" or "weeks" => "wk", - "day" or "days" => "d", - "hour" or "hours" => "h", - "minute" or "minutes" => "min", - "second" or "seconds" => "s", - "millisecond" or "milliseconds" => "ms", - _ => u - }; + // We should actually validate the range of units here, but for now we just return it as-is. + return (decimalValue, unitText!); } @@ -140,7 +127,7 @@ public static DateTimePrecision Parse(this cqlParser.DateTimePrecisionSpecifierC _ => throw new InvalidOperationException($"Encountered invalid date time precision {context.GetText()}.") }; - + // : (qualifier '.')* identifier diff --git a/Cql/Cql.Runtime/Comparers/CqlComparers.CqlQuantityCqlComparer.cs b/Cql/Cql.Runtime/Comparers/CqlComparers.CqlQuantityCqlComparer.cs index 385bebca7..44212d94f 100644 --- a/Cql/Cql.Runtime/Comparers/CqlComparers.CqlQuantityCqlComparer.cs +++ b/Cql/Cql.Runtime/Comparers/CqlComparers.CqlQuantityCqlComparer.cs @@ -14,7 +14,7 @@ namespace Hl7.Cql.Comparers; partial class CqlComparers { /// - /// A comparer that compares to instances, possibly by normalizing their values + /// A comparer that compares two instances, possibly by normalizing their values /// using the UCUM system. /// private class CqlQuantityCqlComparer( diff --git a/Cql/Cql.Runtime/Conversion/ConversionConstants.cs b/Cql/Cql.Runtime/Conversion/ConversionConstants.cs index 85fb5b9e3..e9d5aad13 100644 --- a/Cql/Cql.Runtime/Conversion/ConversionConstants.cs +++ b/Cql/Cql.Runtime/Conversion/ConversionConstants.cs @@ -20,14 +20,14 @@ namespace Hl7.Cql.Conversion internal static class ConversionConstants { /// - /// Defines 365 days per year in precision. + /// Defines 365.25 days per year in precision. /// - public const decimal DaysPerYear = 365m; + public const decimal DaysPerYear = 365.25m; /// - /// Defines 365 days per year in precision. + /// Defines 365.25 days per year in precision. /// - public const double DaysPerYearAsDouble = 365d; + public const double DaysPerYearAsDouble = 365.25d; /// diff --git a/Cql/Cql.Runtime/Conversion/UcumConversionExtensions.cs b/Cql/Cql.Runtime/Conversion/UcumConversionExtensions.cs index 72525cbc2..3245f9405 100644 --- a/Cql/Cql.Runtime/Conversion/UcumConversionExtensions.cs +++ b/Cql/Cql.Runtime/Conversion/UcumConversionExtensions.cs @@ -12,7 +12,7 @@ namespace Hl7.Cql.Conversion { /// - /// Utility functions for working with Fireky's UCUM library, which allows full support for conversions within the UCUM unit system. + /// Utility functions for working with Firely's UCUM library, which allows full support for conversions within the UCUM unit system. /// internal static class Ucum { diff --git a/Cql/Cql.Runtime/Conversion/UnitConverter.cs b/Cql/Cql.Runtime/Conversion/UnitConverter.cs index 97138cb9d..5a38787c3 100644 --- a/Cql/Cql.Runtime/Conversion/UnitConverter.cs +++ b/Cql/Cql.Runtime/Conversion/UnitConverter.cs @@ -99,12 +99,10 @@ public decimal ChangeUnits(decimal value, string fromUnit, string toUnit) return null; string fromUnit = source.unit ?? "1"; - if (Units.CqlUnitsToUCUM.TryGetValue(fromUnit, out var ucumUnit)) - fromUnit = ucumUnit; var newValue = ChangeUnits(source.value.Value, fromUnit, ucumUnits); - var newQuanitty = new CqlQuantity(newValue, ucumUnits); - return newQuanitty; + var newQuantity = new CqlQuantity(newValue, ucumUnits); + return newQuantity; } /// diff --git a/Cql/Cql.Runtime/Operators/CqlOperators.ArithmeticOperators.cs b/Cql/Cql.Runtime/Operators/CqlOperators.ArithmeticOperators.cs index 7c3219a7a..fd344516c 100644 --- a/Cql/Cql.Runtime/Operators/CqlOperators.ArithmeticOperators.cs +++ b/Cql/Cql.Runtime/Operators/CqlOperators.ArithmeticOperators.cs @@ -99,8 +99,16 @@ internal partial class CqlOperators else if (left.value == null || right.value == null) return null; else if (left.unit != right.unit) - throw new NotSupportedException("Mixed unit arithmetic is not supported."); - else return new CqlQuantity(Add(left.value, right.value), left.unit); + { + // Cql supports both singular and plural units such as day/days, year/years and are equivalent units + string? leftUnit = left.unit; + string? rightUnit = right.unit; + CompareNormalizedUnits(leftUnit, rightUnit); + + return new CqlQuantity(Add(left.value, right.value), leftUnit); + } + else + return new CqlQuantity(Add(left.value, right.value), left.unit); } #endregion @@ -464,7 +472,14 @@ internal partial class CqlOperators else if (left.value == null || right.value == null) return null; else if (left.unit != right.unit) - throw new NotSupportedException("Mixed unit arithmetic is not supported."); + { + // Cql supports both singular and plural units such as day/days, year/years and are equivalent units + string? leftUnit = left.unit; + string? rightUnit = right.unit; + CompareNormalizedUnits(leftUnit, rightUnit); + + return new CqlQuantity(Add(left.value, right.value), leftUnit); + } else return new CqlQuantity(Modulo(left.value, right.value), left.unit); } @@ -743,7 +758,14 @@ internal partial class CqlOperators else if (left.value == null || right.value == null) return null; else if (left.unit != right.unit) - throw new NotSupportedException("Mixed unit arithmetic is not supported."); + { + // Cql supports both singular and plural units such as day/days, year/years and are equivalent units + string? leftUnit = left.unit; + string? rightUnit = right.unit; + CompareNormalizedUnits(leftUnit, rightUnit); + + return new CqlQuantity(Add(left.value, right.value), leftUnit); + } else return new CqlQuantity(Subtract(left.value, right.value), left.unit); } @@ -837,6 +859,33 @@ internal partial class CqlOperators else return new CqlQuantity(TruncatedDivide(left.value, right.value), "1"); } + private static void CompareNormalizedUnits(string? leftUnit, string? rightUnit) + { + string normalizedLeftUnit = leftUnit ?? string.Empty; + string normalizedRightUnit = rightUnit ?? string.Empty; + + if (!string.IsNullOrEmpty(leftUnit) && leftUnit.EndsWith("s")) + { + var singularLeft = leftUnit.Substring(0, leftUnit.Length - 1); + if (Units.DatePrecisionToCqlUnits.TryGetValue(singularLeft, out _ )) + { + normalizedLeftUnit = singularLeft; + } + } + + if (!string.IsNullOrEmpty(rightUnit) && rightUnit.EndsWith("s")) + { + var singularRight = rightUnit.Substring(0, rightUnit.Length - 1); + if (Units.DatePrecisionToCqlUnits.TryGetValue(singularRight, out _)) + { + normalizedRightUnit = singularRight; + } + } + + if (normalizedLeftUnit != normalizedRightUnit) + throw new NotSupportedException("Mixed unit arithmetic is not supported."); + } + #endregion } } diff --git a/Cql/Cql.Runtime/Operators/CqlOperators.DateTimeOperators.cs b/Cql/Cql.Runtime/Operators/CqlOperators.DateTimeOperators.cs index 834fb0d1e..628e45743 100644 --- a/Cql/Cql.Runtime/Operators/CqlOperators.DateTimeOperators.cs +++ b/Cql/Cql.Runtime/Operators/CqlOperators.DateTimeOperators.cs @@ -459,8 +459,6 @@ internal partial class CqlOperators protected bool GreaterOrSamePrecision(DateTimePrecision left, string precision) { - if (Units.CqlUnitsToUCUM.TryGetValue(precision, out var ucum)) - precision = ucum; var right = precision.ToDateTimePrecision(); if (right == null || right == DateTimePrecision.Unknown) throw new ArgumentException($"Unknown precision {precision}", nameof(precision)); diff --git a/Cql/Cql.Runtime/Operators/CqlOperators.IntervalOperators.cs b/Cql/Cql.Runtime/Operators/CqlOperators.IntervalOperators.cs index abe94b79a..d45633308 100644 --- a/Cql/Cql.Runtime/Operators/CqlOperators.IntervalOperators.cs +++ b/Cql/Cql.Runtime/Operators/CqlOperators.IntervalOperators.cs @@ -667,20 +667,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -689,7 +689,7 @@ internal partial class CqlOperators { switch (per.unit) { - case "mo": + case "month": if (interval.low!.Precision < Iso8601.DateTimePrecision.Month && interval.high!.Precision < Iso8601.DateTimePrecision.Month) return expanded; @@ -701,8 +701,8 @@ internal partial class CqlOperators break; - case "d": - case "wk": + case "day": + case "week": if (interval.low!.Precision < Iso8601.DateTimePrecision.Day && interval.high!.Precision < Iso8601.DateTimePrecision.Day) return expanded; @@ -715,10 +715,10 @@ internal partial class CqlOperators break; // parsed as a time unit when it's a date so default to the coarsest // ex: Interval[2023-01-01, 2023-12-31] per minute - case "h": - case "min": - case "s": - case "ms": + case "hour": + case "minute": + case "second": + case "millisecond": return expanded; } } @@ -793,20 +793,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -815,7 +815,7 @@ internal partial class CqlOperators { switch (per.unit) { - case "mo": + case "month": if (interval.low!.Precision < Iso8601.DateTimePrecision.Month && interval.high!.Precision < Iso8601.DateTimePrecision.Month) return expanded; @@ -826,8 +826,8 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "d": - case "wk": + case "day": + case "week": if (interval.low!.Precision < Iso8601.DateTimePrecision.Day && interval.high!.Precision < Iso8601.DateTimePrecision.Day) return expanded; @@ -839,7 +839,7 @@ internal partial class CqlOperators break; // per has a coarser precision than the interval so nothing is added - case "h": + case "hour": if (interval.low!.Precision < Iso8601.DateTimePrecision.Hour && interval.high!.Precision < Iso8601.DateTimePrecision.Hour) return expanded; @@ -850,7 +850,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "min": + case "minute": if (interval.low!.Precision < Iso8601.DateTimePrecision.Minute && interval.high!.Precision < Iso8601.DateTimePrecision.Minute) return expanded; @@ -861,7 +861,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "s": + case "second": if (interval.low!.Precision < Iso8601.DateTimePrecision.Second && interval.high!.Precision < Iso8601.DateTimePrecision.Second) return expanded; @@ -968,20 +968,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -991,7 +991,7 @@ internal partial class CqlOperators switch (per.unit) { // per has a coarser precision than the interval so nothing is added - case "h": + case "hour": if (interval.low!.Precision < Iso8601.DateTimePrecision.Hour && interval.high!.Precision < Iso8601.DateTimePrecision.Hour) return expanded; @@ -1002,7 +1002,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "min": + case "minute": if (interval.low!.Precision < Iso8601.DateTimePrecision.Minute && interval.high!.Precision < Iso8601.DateTimePrecision.Minute) return expanded; @@ -1013,7 +1013,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "s": + case "second": if (interval.low!.Precision > Iso8601.DateTimePrecision.Second && interval.high!.Precision > Iso8601.DateTimePrecision.Second) return expanded; @@ -1026,10 +1026,10 @@ internal partial class CqlOperators break; // parsed as a date unit when it's a time so return empty list // ex: Interval[@T10, @T10] per month - case "a": - case "mo": - case "d": - case "wk": + case "year": + case "month": + case "day": + case "week": return expanded; } } @@ -1106,8 +1106,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type decimal + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) return expanded; } @@ -1142,8 +1142,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type integer + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) return expanded; } @@ -1180,8 +1180,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type long + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) return expanded; } diff --git a/Cql/Cql.Runtime/Operators/CqlOperators.ListOperators.cs b/Cql/Cql.Runtime/Operators/CqlOperators.ListOperators.cs index b8a96be64..e7af2bfe6 100644 --- a/Cql/Cql.Runtime/Operators/CqlOperators.ListOperators.cs +++ b/Cql/Cql.Runtime/Operators/CqlOperators.ListOperators.cs @@ -147,20 +147,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -169,7 +169,7 @@ internal partial class CqlOperators { switch (per.unit) { - case "mo": + case "month": if (interval.low!.Precision < Iso8601.DateTimePrecision.Month && interval.high!.Precision < Iso8601.DateTimePrecision.Month) return expanded; @@ -181,8 +181,8 @@ internal partial class CqlOperators break; - case "d": - case "wk": + case "day": + case "week": if (interval.low!.Precision < Iso8601.DateTimePrecision.Day && interval.high!.Precision < Iso8601.DateTimePrecision.Day) return expanded; @@ -195,10 +195,10 @@ internal partial class CqlOperators break; // parsed as a time unit when it's a date so default to the coarsest // ex: Interval[2023-01-01, 2023-12-31] per minute - case "h": - case "min": - case "s": - case "ms": + case "hour": + case "minute": + case "second": + case "millisecond": return expanded!; } } @@ -242,11 +242,11 @@ internal partial class CqlOperators do { - var precision = UCUMUnits.FromDateTimePrecision(listItem!.Precision); + Units.DatePrecisionToCqlUnits.TryGetValue(listItem!.Precision.ToString(), out var cqlunits); // high is one less than next grouping using the smallest precision of the interval // expand { Interval[@2022-01-01, @2024-03-01] } per 2 years returns { [2022-01-01, 2023-12-31], [2024-01-01, 2025-12-31] } - var onePrior = new CqlQuantity(1, precision); + var onePrior = new CqlQuantity(1, cqlunits); var next = listItem.Add(per); var high = next!.Subtract(onePrior); @@ -288,20 +288,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -310,7 +310,7 @@ internal partial class CqlOperators { switch (per.unit) { - case "mo": + case "month": if (interval.low!.Precision < Iso8601.DateTimePrecision.Month && interval.high!.Precision < Iso8601.DateTimePrecision.Month) return expanded; @@ -321,8 +321,8 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "d": - case "wk": + case "day": + case "week": if (interval.low!.Precision < Iso8601.DateTimePrecision.Day && interval.high!.Precision < Iso8601.DateTimePrecision.Day) return expanded; @@ -334,7 +334,7 @@ internal partial class CqlOperators break; // per has a coarser precision than the interval so nothing is added - case "h": + case "hour": if (interval.low!.Precision < Iso8601.DateTimePrecision.Hour && interval.high!.Precision < Iso8601.DateTimePrecision.Hour) return expanded; @@ -345,7 +345,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "min": + case "minute": if (interval.low!.Precision < Iso8601.DateTimePrecision.Minute && interval.high!.Precision < Iso8601.DateTimePrecision.Minute) return expanded; @@ -356,7 +356,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "s": + case "second": if (interval.low!.Precision < Iso8601.DateTimePrecision.Second && interval.high!.Precision < Iso8601.DateTimePrecision.Second) return expanded; @@ -433,10 +433,10 @@ internal partial class CqlOperators do { - var precision = UCUMUnits.FromDateTimePrecision(listItem!.Precision); + Units.DatePrecisionToCqlUnits.TryGetValue(listItem!.Precision.ToString(), out var cqlunits); // high is one less than next grouping using the smallest precision of the interval - var onePrior = new CqlQuantity(1, precision); + var onePrior = new CqlQuantity(1, cqlunits); var next = listItem.Add(per); var high = next!.Subtract(onePrior); @@ -478,20 +478,20 @@ internal partial class CqlOperators { if (interval.low!.Precision == interval.high!.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); } else if (interval.low.Precision < interval.high.Precision) { - Units.CqlUnitsToUCUM.TryGetValue(interval.low.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.low.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setHighPrecisionToPer = true; } else { - Units.CqlUnitsToUCUM.TryGetValue(interval.high.Precision.ToString(), out var ucmunits); - per = new CqlQuantity(1, ucmunits); + Units.DatePrecisionToCqlUnits.TryGetValue(interval.high.Precision.ToString(), out var cqlunits); + per = new CqlQuantity(1, cqlunits); setLowPrecisionToPer = true; } @@ -501,7 +501,7 @@ internal partial class CqlOperators switch (per.unit) { // per has a coarser precision than the interval so nothing is added - case "h": + case "hour": if (interval.low!.Precision < Iso8601.DateTimePrecision.Hour && interval.high!.Precision < Iso8601.DateTimePrecision.Hour) continue; @@ -512,7 +512,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "min": + case "minute": if (interval.low!.Precision < Iso8601.DateTimePrecision.Minute && interval.high!.Precision < Iso8601.DateTimePrecision.Minute) continue; @@ -523,7 +523,7 @@ internal partial class CqlOperators setHighPrecisionToPer = true; break; - case "s": + case "second": if (interval.low!.Precision < Iso8601.DateTimePrecision.Second && interval.high!.Precision < Iso8601.DateTimePrecision.Second) continue; @@ -537,10 +537,10 @@ internal partial class CqlOperators break; // parsed as a date unit when it's a time so return empty list // ex: Interval[@T10, @T10] per month - case "a": - case "mo": - case "d": - case "wk": + case "year": + case "month": + case "day": + case "week": continue; } } @@ -589,10 +589,10 @@ internal partial class CqlOperators do { - var precision = UCUMUnits.FromDateTimePrecision(listItem!.Precision); + Units.DatePrecisionToCqlUnits.TryGetValue(listItem!.Precision.ToString(), out var cqlunits); // high is one less than next grouping using the smallest precision of the interval - var onePrior = new CqlQuantity(1, precision); + var onePrior = new CqlQuantity(1, cqlunits); var next = listItem.Add(per); var high = next!.Subtract(onePrior); @@ -633,8 +633,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type decimal + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) continue; } @@ -681,8 +681,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type integer + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) continue; } @@ -729,8 +729,8 @@ internal partial class CqlOperators per = new CqlQuantity(1, "1"); else { - Units.UCUMUnitsToCql.TryGetValue(per.unit ?? "", out var ucumUnits); - if (ucumUnits != null) + // If the per quantity is a datetime, bypass the expansion of input interval of type long + if (per.unit is not null && Units.DatePrecisionToCqlUnits.Values.Contains(per.unit)) continue; } diff --git a/Cql/Cql.Runtime/Operators/CqlOperators.TypeOperators.cs b/Cql/Cql.Runtime/Operators/CqlOperators.TypeOperators.cs index d44ffdb07..944283be4 100644 --- a/Cql/Cql.Runtime/Operators/CqlOperators.TypeOperators.cs +++ b/Cql/Cql.Runtime/Operators/CqlOperators.TypeOperators.cs @@ -205,8 +205,7 @@ internal partial class CqlOperators { if (argument == null || argument.value == null || unit == null) return null; - if (Units.CqlUnitsToUCUM.TryGetValue(unit, out var converted)) - unit = converted; + var newQuantity = UnitConverter.ChangeUnits(argument, unit); return newQuantity; } diff --git a/Cql/CqlToElmTests/(tests)/LiteralTest.cs b/Cql/CqlToElmTests/(tests)/LiteralTest.cs index e19432bcd..3cd5276f6 100644 --- a/Cql/CqlToElmTests/(tests)/LiteralTest.cs +++ b/Cql/CqlToElmTests/(tests)/LiteralTest.cs @@ -1366,7 +1366,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(6m, quantity.value); - Assert.AreEqual("a", quantity.unit); + Assert.AreEqual("year", quantity.unit); } } @@ -1388,7 +1388,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(6m, quantity.value); - Assert.AreEqual("a", quantity.unit); + Assert.AreEqual("years", quantity.unit); } } @@ -1410,7 +1410,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(1.245671213m, quantity.value); - Assert.AreEqual("mo", quantity.unit); + Assert.AreEqual("month", quantity.unit); } } @@ -1432,7 +1432,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(1.245671213m, quantity.value); - Assert.AreEqual("mo", quantity.unit); + Assert.AreEqual("months", quantity.unit); } } @@ -1454,7 +1454,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0m, quantity.value); - Assert.AreEqual("d", quantity.unit); + Assert.AreEqual("day", quantity.unit); } } @@ -1476,7 +1476,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0m, quantity.value); - Assert.AreEqual("d", quantity.unit); + Assert.AreEqual("days", quantity.unit); } } @@ -1498,7 +1498,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0m, quantity.value); - Assert.AreEqual("h", quantity.unit); + Assert.AreEqual("hour", quantity.unit); } } @@ -1520,7 +1520,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0m, quantity.value); - Assert.AreEqual("h", quantity.unit); + Assert.AreEqual("hours", quantity.unit); } } @@ -1542,7 +1542,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0.25m, quantity.value); - Assert.AreEqual("min", quantity.unit); + Assert.AreEqual("minute", quantity.unit); } } @@ -1564,7 +1564,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(0.25m, quantity.value); - Assert.AreEqual("min", quantity.unit); + Assert.AreEqual("minutes", quantity.unit); } } @@ -1586,7 +1586,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(1m, quantity.value); - Assert.AreEqual("s", quantity.unit); + Assert.AreEqual("second", quantity.unit); } } @@ -1609,7 +1609,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(1m, quantity.value); - Assert.AreEqual("s", quantity.unit); + Assert.AreEqual("seconds", quantity.unit); } } @@ -1631,7 +1631,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(2000000m, quantity.value); - Assert.AreEqual("ms", quantity.unit); + Assert.AreEqual("millisecond", quantity.unit); } } @@ -1653,7 +1653,7 @@ library QuantityTest version '1.0.0' Assert.IsNotNull(quantity.localId); Assert.IsNotNull(quantity.locator); Assert.AreEqual(2000000m, quantity.value); - Assert.AreEqual("ms", quantity.unit); + Assert.AreEqual("milliseconds", quantity.unit); } } diff --git a/Cql/CqlToElmTests/Input/DQIC/CqlDateTimeOperatorsTest.xml b/Cql/CqlToElmTests/Input/DQIC/CqlDateTimeOperatorsTest.xml index e6303a2ac..9da26b613 100644 --- a/Cql/CqlToElmTests/Input/DQIC/CqlDateTimeOperatorsTest.xml +++ b/Cql/CqlToElmTests/Input/DQIC/CqlDateTimeOperatorsTest.xml @@ -1177,7 +1177,7 @@ DateTime(2014) - 25 months - @2012T + @2011T @T15:59:59.999 - 5 hours diff --git a/Cql/Iso8601/DateTimePrecision.cs b/Cql/Iso8601/DateTimePrecision.cs index d8636ed81..69aa87a83 100644 --- a/Cql/Iso8601/DateTimePrecision.cs +++ b/Cql/Iso8601/DateTimePrecision.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -/* +/* * Copyright (c) 2023, NCQA 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-cql-sdk/main/LICENSE */ @@ -11,37 +11,19 @@ namespace Hl7.Cql.Iso8601 { public static class DateTimePrecisionExtensions { - public static DateTimePrecision? ToDateTimePrecision(this string? ucumUnit) - { - if (ucumUnit == null) - return null; - else switch (ucumUnit[0]) - { - case 'a': // year - return DateTimePrecision.Year; - case 'm': // month - switch (ucumUnit[1]) - { - case 'o': // mo = month - return DateTimePrecision.Month; - case 'i': // min = minute - return DateTimePrecision.Minute; - case 's': - return DateTimePrecision.Millisecond; - default: break; - } - break; - case 'd': - return DateTimePrecision.Day; - case 'h': - return DateTimePrecision.Hour; - case 's': - return DateTimePrecision.Second; - default: - break; - } - return DateTimePrecision.Unknown; - } + public static DateTimePrecision? ToDateTimePrecision(this string? unit) => + unit switch + { + null => null, + "year" or "years" => DateTimePrecision.Year, + "month" or "months" => DateTimePrecision.Month, + "minute" or "minutes" => DateTimePrecision.Minute, + "millisecond" or "milliseconds" => DateTimePrecision.Millisecond, + "day" or "days" => DateTimePrecision.Day, + "hour" or "hours" => DateTimePrecision.Hour, + "second" or "seconds" => DateTimePrecision.Second, + _ => DateTimePrecision.Unknown + }; } public enum DateTimePrecision diff --git a/Cql/Iso8601/PublicAPI.Shipped.txt b/Cql/Iso8601/PublicAPI.Shipped.txt index 60b0e0ca5..e779a2b25 100644 --- a/Cql/Iso8601/PublicAPI.Shipped.txt +++ b/Cql/Iso8601/PublicAPI.Shipped.txt @@ -65,7 +65,7 @@ static Hl7.Cql.Iso8601.DateIso8601.TryParse(string! stringValue, out Hl7.Cql.Iso static Hl7.Cql.Iso8601.DateTimeIso8601.Now.get -> Hl7.Cql.Iso8601.DateTimeIso8601! static Hl7.Cql.Iso8601.DateTimeIso8601.TryParse(string! stringValue, out Hl7.Cql.Iso8601.DateTimeIso8601? dateTimeValue) -> bool static Hl7.Cql.Iso8601.DateTimeIso8601.UtcNow.get -> Hl7.Cql.Iso8601.DateTimeIso8601! -static Hl7.Cql.Iso8601.DateTimePrecisionExtensions.ToDateTimePrecision(this string? ucumUnit) -> Hl7.Cql.Iso8601.DateTimePrecision? +static Hl7.Cql.Iso8601.DateTimePrecisionExtensions.ToDateTimePrecision(this string? unit) -> Hl7.Cql.Iso8601.DateTimePrecision? static Hl7.Cql.Iso8601.TimeIso8601.TryParse(string! stringValue, out Hl7.Cql.Iso8601.TimeIso8601? timeValue) -> bool static readonly Hl7.Cql.Iso8601.DateIso8601.Expression -> System.Text.RegularExpressions.Regex! static readonly Hl7.Cql.Iso8601.DateTimeIso8601.Expression -> System.Text.RegularExpressions.Regex! diff --git a/Demo/Cql/Build/CqlToElm.Targets.xml b/Demo/Cql/Build/CqlToElm.Targets.xml index d396eb87f..f03a31099 100644 --- a/Demo/Cql/Build/CqlToElm.Targets.xml +++ b/Demo/Cql/Build/CqlToElm.Targets.xml @@ -38,15 +38,21 @@ - - --> + + + + + diff --git a/Demo/Measures.ecqm-content-qicore-2024/Measures.ecqm-content-qicore-2024.csproj b/Demo/Measures.ecqm-content-qicore-2024/Measures.ecqm-content-qicore-2024.csproj index b9b1f648f..03a5b4779 100644 --- a/Demo/Measures.ecqm-content-qicore-2024/Measures.ecqm-content-qicore-2024.csproj +++ b/Demo/Measures.ecqm-content-qicore-2024/Measures.ecqm-content-qicore-2024.csproj @@ -25,7 +25,7 @@ $(CqlSolutionDir)/submodules/Firely.Cql.Sdk.Integration.Runner/IntegrationRunner/CMS Resources - true + diff --git a/submodules/Firely.Cql.Sdk.Integration.Runner b/submodules/Firely.Cql.Sdk.Integration.Runner index 6f6d28995..20d09e746 160000 --- a/submodules/Firely.Cql.Sdk.Integration.Runner +++ b/submodules/Firely.Cql.Sdk.Integration.Runner @@ -1 +1 @@ -Subproject commit 6f6d289958a15c8b9f77e309129d044c266fa401 +Subproject commit 20d09e7462e166cb3c1398828779a0afa4e4baa5