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..5c8ca6c1a 100644 --- a/Cql/CoreTests/PrimitiveTests.cs +++ b/Cql/CoreTests/PrimitiveTests.cs @@ -32,8 +32,8 @@ public class PrimitiveTests 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); + var result = baseDate.Subtract(new CqlQuantity(25m, "month")); + Assert.AreEqual(2011, result.Value.Year); Assert.AreEqual(DateTimePrecision.Year, result.Precision); } @@ -55,7 +55,7 @@ public void CqlDateTime_Add_Year_By_Units() 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()); + Assert.AreEqual("1960", plus365days.ToString()); var plus366days = baseDate.Add(new CqlQuantity(366, "day")); Assert.AreEqual(DateTimePrecision.Year, plus366days.Value.Precision); @@ -70,7 +70,7 @@ public void CqlDateTime_Add_Year_By_Units() 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()); + Assert.AreEqual("1960", plus365DaysInSeconds.ToString()); } [TestMethod] @@ -93,6 +93,11 @@ public void CqlDateTime_Add_Month() 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] @@ -115,6 +120,28 @@ public void CqlDateTime_Subtract_Month() 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 CqlDateTime_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] @@ -1076,7 +1103,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 +1244,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 +3013,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); } diff --git a/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs b/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs index 8067e66bb..5abe9aa1d 100644 --- a/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs +++ b/Cql/Cql.Abstractions/Abstractions/UCUMUnits.cs @@ -79,26 +79,14 @@ public static class UCUMUnits /// 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, - }; - } + /// Defines days per year + /// + public const double DaysPerYearDouble = 365.25d; + /// + /// Defines days per month + /// + public const double DaysPerMonthDouble = 30.4375d; } 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..be71ea07c 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDate.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDate.cs @@ -94,45 +94,57 @@ public static bool TryParse(string s, out CqlDate? cqlDate) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = quantity.value!.Value; var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + switch (quantity.unit) { - case 'a': + case "a": + dto = dto.AddDays(UCUMUnits.DaysPerYearDouble); + break; + case "year": + case "years": 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"); - } - } + case "mo": + dto = dto.AddDays(UCUMUnits.DaysPerMonthDouble); break; - case 'd': - dto = dto.AddDays((int)value!); + case "month": + case "months": + dto = dto.AddMonths((int)value); break; - case 'w': + case "wk": + case "week": + case "weeks": dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); break; - case 'h': + case "d": + case "day": + case "days": + dto = dto.AddDays((int)value!); + break; + case "h": + case "hour": + case "hours": dto = dto.AddHours(Math.Truncate((double)value)); break; - case 's': + case "min": + case "minute": + case "minutes": + dto = dto.AddMinutes(Math.Truncate((double)value)); + break; + case "s": + case "second": + case "seconds": dto = dto.AddSeconds(Math.Truncate((double)value)); break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); + case "ms": + case "millisecond": + case "milliseconds": + dto = dto.AddMilliseconds(Math.Truncate((double)value)); + break; + default: + throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); } var newIsoDate = new DateIso8601(dto, Value.Precision); @@ -150,47 +162,57 @@ public static bool TryParse(string s, out CqlDate? cqlDate) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = -1 * quantity.value!.Value; var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + switch (quantity.unit) { - case 'a': + case "a": + dto = dto.AddDays(-1 * UCUMUnits.DaysPerYearDouble); + break; + case "year": + case "years": 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"); - } - } + case "mo": + dto = dto.AddDays(-1 * UCUMUnits.DaysPerMonthDouble); break; - case 'd': - dto = dto.AddDays((int)value!); + case "month": + case "months": + dto = dto.AddMonths((int)value); break; - case 'w': + case "wk": + case "week": + case "weeks": dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); break; - case 'h': + case "d": + case "day": + case "days": + dto = dto.AddDays((int)value!); + break; + case "hour": + case "hours": dto = dto.AddHours(Math.Truncate((double)value)); break; - case 's': + case "min": + case "minute": + case "minutes": + dto = dto.AddMinutes(Math.Truncate((double)value)); + break; + case "s": + case "second": + case "seconds": dto = dto.AddSeconds(Math.Truncate((double)value)); break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); + case "ms": + case "millisecond": + case "milliseconds": + dto = dto.AddMilliseconds(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; @@ -203,15 +225,13 @@ public static bool TryParse(string s, out CqlDate? cqlDate) /// 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) { - case UCUMUnits.Year: + case "year": return Value.Year; - case UCUMUnits.Month: + case "month": return Value.Month; - case UCUMUnits.Day: + case "day": return Value.Day; default: return null; @@ -261,10 +281,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 +305,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 +341,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)); } } diff --git a/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs b/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs index e72156441..580372436 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDateTime.cs @@ -147,45 +147,57 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = quantity.value!.Value; var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + switch (quantity.unit) { - case 'a': + case "a": + dto = dto.AddDays(UCUMUnits.DaysPerYearDouble); + break; + case "year": + case "years": 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"); - } - } + case "mo": + dto = dto.AddDays(UCUMUnits.DaysPerMonthDouble); break; - case 'd': - dto = dto.AddDays((int)value!); + case "month": + case "months": + dto = dto.AddMonths((int)value); break; - case 'w': + case "wk": + case "week": + case "weeks": dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); break; - case 'h': + case "d": + case "day": + case "days": + dto = dto.AddDays((int)value!); + break; + case "h": + case "hour": + case "hours": dto = dto.AddHours(Math.Truncate((double)value)); break; - case 's': + case "min": + case "minute": + case "minutes": + dto = dto.AddMinutes(Math.Truncate((double)value)); + break; + case "s": + case "second": + case "seconds": dto = dto.AddSeconds(Math.Truncate((double)value)); break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); + case "ms": + case "millisecond": + case "milliseconds": + dto = dto.AddMilliseconds(Math.Truncate((double)value)); + break; + default: + throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); } var newIsoDate = new DateTimeIso8601(dto, Value.Precision); @@ -203,45 +215,57 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = -1 * quantity.value!.Value; var dto = Value.DateTimeOffset; - switch (quantity.unit![0]) + switch (quantity.unit) { - case 'a': + case "a": + dto = dto.AddDays(-1 * UCUMUnits.DaysPerYearDouble); + break; + case "year": + case "years": 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"); - } - } + case "mo": + dto = dto.AddDays(-1 * UCUMUnits.DaysPerMonthDouble); break; - case 'd': - dto = dto.AddDays((int)value!); + case "month": + case "months": + dto = dto.AddMonths((int)value); break; - case 'w': + case "wk": + case "week": + case "weeks": dto = dto.AddDays((int)(value! * CqlDateTimeMath.DaysPerWeek)); break; - case 'h': + case "d": + case "day": + case "days": + dto = dto.AddDays((int)value!); + break; + case "h": + case "hour": + case "hours": dto = dto.AddHours(Math.Truncate((double)value)); break; - case 's': + case "min": + case "minute": + case "minutes": + dto = dto.AddMinutes(Math.Truncate((double)value)); + break; + case "s": + case "second": + case "seconds": dto = dto.AddSeconds(Math.Truncate((double)value)); break; - default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); + case "ms": + case "millisecond": + case "milliseconds": + dto = dto.AddMilliseconds(Math.Truncate((double)value)); + break; + default: + throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); } var newIsoDate = new DateTimeIso8601(dto, Value.Precision); @@ -256,23 +280,21 @@ public static bool TryParse(string s, out CqlDateTime? cqlDateTime) /// 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) { - case UCUMUnits.Year: + case "year": return Value.Year; - case UCUMUnits.Month: + case "month": return Value.Month; - case UCUMUnits.Day: + case "day": return Value.Day; - case UCUMUnits.Hour: + case "hour": return Value.Hour; - case UCUMUnits.Minute: + case "minute": return Value.Minute; - case UCUMUnits.Second: + case "second": return Value.Second; - case UCUMUnits.Millisecond: + case "millisecond": return Value.Millisecond; default: return null; @@ -340,10 +362,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 +386,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: diff --git a/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs b/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs index 072903c19..204a029eb 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlDateTimeMath.cs @@ -30,20 +30,18 @@ internal static class CqlDateTimeMath { if (low == null || high == null || precision == 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": + 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 +52,7 @@ 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 +64,7 @@ internal static class CqlDateTimeMath } else return asInt; } - case "h": + case "hour": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalHours; @@ -78,7 +76,7 @@ internal static class CqlDateTimeMath } else return asInt; } - case "min": + case "minute": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalMinutes; @@ -90,7 +88,7 @@ internal static class CqlDateTimeMath } else return asInt; } - case "s": + case "second": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalSeconds; @@ -102,7 +100,7 @@ internal static class CqlDateTimeMath } else return asInt; } - case "ms": + case "millisecond": { var span = secondDto.Subtract(firstDto); var asInt = (int)span.TotalMilliseconds; @@ -122,15 +120,13 @@ internal static class CqlDateTimeMath { if (low == null || high == null || 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": + case "year": var yearDiff = secondDto.Year - firstDto.Year; var firstDayInYear = firstDto.DayOfYear; var secondDayInYear = secondDto.DayOfYear; @@ -196,32 +192,32 @@ 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; + 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") }, }; /// diff --git a/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs b/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs index dd9b1c975..9210ada68 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlQuantity.cs @@ -30,7 +30,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 +51,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}'"); } diff --git a/Cql/Cql.Abstractions/Primitives/CqlTime.cs b/Cql/Cql.Abstractions/Primitives/CqlTime.cs index 9735ec57c..044858399 100644 --- a/Cql/Cql.Abstractions/Primitives/CqlTime.cs +++ b/Cql/Cql.Abstractions/Primitives/CqlTime.cs @@ -98,36 +98,39 @@ public static bool TryParse(string s, out CqlTime? time) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = quantity.value!.Value; var span = Value.TimeSpan; - switch (quantity.unit![0]) + switch (quantity.unit) { - 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"); - } - } + case "min": + case "minute": + case "minutes": + span = span.Add(TimeSpan.FromMinutes(Math.Truncate((double)value))); + break; + case "ms": + case "millisecond": + case "milliseconds": + span = span.Add(TimeSpan.FromMilliseconds(Math.Truncate((double)value))); break; - case 'd': + case "d": + case "day": + case "days": span = span.Add(TimeSpan.FromDays(Math.Truncate((double)value))); break; - case 'w': + case "wk": + case "week": + case "weeks": span = span.Add(TimeSpan.FromDays(Math.Truncate((double)value) * CqlDateTimeMath.DaysPerWeekDouble)); break; - case 'h': + case "h": + case "hour": + case "hours": span = span.Add(TimeSpan.FromHours(Math.Truncate((double)value))); break; - case 's': + case "s": + case "second": + case "seconds": span = span.Add(TimeSpan.FromSeconds(Math.Truncate((double)value))); break; default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); @@ -148,36 +151,39 @@ public static bool TryParse(string s, out CqlTime? time) { if (quantity == null || quantity.value == null || quantity.unit == null) return null; - quantity = quantity.NormalizeTo(Precision); + //quantity = quantity.NormalizeTo(Precision); var value = quantity.value!.Value; var span = Value.TimeSpan; - switch (quantity.unit![0]) + switch (quantity.unit) { - 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"); - } - } + case "min": + case "minute": + case "minutes": + span = span.Subtract(TimeSpan.FromMinutes(Math.Truncate((double)value))); + break; + case "ms": + case "millisecond": + case "milliseconds": + span = span.Subtract(TimeSpan.FromMilliseconds(Math.Truncate((double)value))); break; - case 'd': + case "d": + case "day": + case "days": span = span.Subtract(TimeSpan.FromDays(Math.Truncate((double)value))); break; - case 'w': + case "wk": + case "week": + case "weeks": span = span.Subtract(TimeSpan.FromDays(Math.Truncate((double)value) * CqlDateTimeMath.DaysPerWeekDouble)); break; - case 'h': + case "h": + case "hour": + case "hours": span = span.Subtract(TimeSpan.FromHours(Math.Truncate((double)value))); break; - case 's': + case "s": + case "second": + case "seconds": span = span.Subtract(TimeSpan.FromSeconds(Math.Truncate((double)value))); break; default: throw new ArgumentException($"Unknown date unit {quantity.unit} supplied"); @@ -195,17 +201,15 @@ public static bool TryParse(string s, out CqlTime? time) /// 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) { - case UCUMUnits.Hour: + case "hour": return Value.Hour; - case UCUMUnits.Minute: + case "minute": return Value.Minute; - case UCUMUnits.Second: + case "second": return Value.Second; - case UCUMUnits.Millisecond: + case "millisecond": return Value.Millisecond; default: return null; @@ -273,12 +277,10 @@ 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) { @@ -367,7 +369,7 @@ public static bool TryParse(string s, out CqlTime? time) case DateTimePrecision.Month: case DateTimePrecision.Day: default: - throw new ArgumentException($"Invalid UCUM precision {precision}", nameof(precision)); + throw new ArgumentException($"Invalid precision {precision}", nameof(precision)); } } diff --git a/Cql/Cql.Abstractions/PublicAPI.Shipped.txt b/Cql/Cql.Abstractions/PublicAPI.Shipped.txt index 6efd1e08a..f8f07c2ef 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! @@ -265,7 +267,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 +289,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! 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/Iso8601/DateTimePrecision.cs b/Cql/Iso8601/DateTimePrecision.cs index d8636ed81..459963b33 100644 --- a/Cql/Iso8601/DateTimePrecision.cs +++ b/Cql/Iso8601/DateTimePrecision.cs @@ -11,31 +11,32 @@ namespace Hl7.Cql.Iso8601 { public static class DateTimePrecisionExtensions { - public static DateTimePrecision? ToDateTimePrecision(this string? ucumUnit) + public static DateTimePrecision? ToDateTimePrecision(this string? unit) { - if (ucumUnit == null) + if (unit == null) return null; - else switch (ucumUnit[0]) + else switch (unit) { - case 'a': // year + case "year": + case "years": 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': + case "month": + case "months": + return DateTimePrecision.Month; + case "minute": + case "minutes": + return DateTimePrecision.Minute; + case "millisecond": + case "milliseconds": + return DateTimePrecision.Millisecond; + case "day": + case "days": return DateTimePrecision.Day; - case 'h': + case "hour": + case "hours": return DateTimePrecision.Hour; - case 's': + case "second": + case "seconds": return DateTimePrecision.Second; default: break; 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!