From 086710028e0ca8f95630ed9bed41d3cd2fb47200 Mon Sep 17 00:00:00 2001 From: Will Yochum Date: Wed, 17 Jan 2024 10:03:15 -0500 Subject: [PATCH 1/5] create filter to format time with a colon --- .../Filters/DateFiltersTests.cs | 32 +++++++++++++++++++ .../Filters/DateFilters.cs | 27 ++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs index 0a18aea77..930a7008d 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs @@ -220,6 +220,22 @@ public static IEnumerable GetInvalidDataForAddSeconds() yield return new object[] { @"2001-01T" }; } + public static IEnumerable GetValidDataForTimeHandling() + { + yield return new object[] { @"0730", "07:30:00" }; + yield return new object[] { @"1730", "17:30:00" }; + yield return new object[] { @"18:45", "18:45" }; + yield return new object[] { @"09:45:50", "09:45:50" }; + yield return new object[] { @"1", "1" }; + yield return new object[] { @"abc", "abc" }; + yield return new object[] { null, null }; + } + + public static IEnumerable GetInvalidDataForTimeHandling() + { + yield return new object[] { @"9999" }; + } + [Theory] [MemberData(nameof(GetValidDataForAddSeconds))] public void GivenSeconds_WhenAddOnValidDateTime_CorrectDateTimeStringShouldBeReturned(string originalDateTime, double seconds, string timeZoneHandling, string expectedDateTime) @@ -356,6 +372,22 @@ public void GivenAnInvalidDateTime_WhenFormatAsHl7DateTime_ExceptionShouldBeThro Assert.Equal(FhirConverterErrorCode.InvalidDateTimeFormat, exception.FhirConverterErrorCode); } + [Theory] + [MemberData(nameof(GetValidDataForTimeHandling))] + public void GivenAValidData_WhenFormatTimeWithColon_FormattedTimeOrOriginalInputShouldBeReturned(string input, string expectedDateTime) + { + var result = Filters.FormatTimeWithColon(input); + Assert.Equal(expectedDateTime, result); + } + + [Theory] + [MemberData(nameof(GetInvalidDataForTimeHandling))] + public void GivenInvalidData_WhenFormatAsTimeInterval_InputTimeShouldBeReturnedAndNoExceptionThrown(string input) + { + var exception = Assert.Throws(() => Filters.FormatTimeWithColon(input)); + Assert.Equal(FhirConverterErrorCode.InvalidDateTimeFormat, exception.FhirConverterErrorCode); + } + [Fact] public void NowTest() { diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs index d89c92cd0..d08a60683 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------- using System; +using System.Globalization; +using System.Linq; using Microsoft.Health.Fhir.Liquid.Converter.Exceptions; using Microsoft.Health.Fhir.Liquid.Converter.Models; @@ -96,6 +98,31 @@ public static string Now(string input, string format = "yyyy-MM-ddTHH:mm:ss.FFFZ return DateTime.UtcNow.ToString(format); } + /* Converts an HL7v2 time interval to the FHIR format for time + / HL7v2 - 2.8.35.2 Explicit time interval (ST) + / FHIR - time https://build.fhir.org/datatypes.html#time + */ + public static string FormatTimeWithColon(string input, string inputFormat = "HHmm") + { + // For backwards compatibility, if the input is not a 4 digit numeric value then just return the value + if (string.IsNullOrEmpty(input) || (!(input.All(char.IsDigit) && (input.Length == 4)))) + { + return input; + } + + var dt = TimeSpan.Zero; + try + { + dt = DateTime.ParseExact(input, inputFormat, CultureInfo.InvariantCulture).TimeOfDay; + } + catch (Exception) + { + throw new RenderException(FhirConverterErrorCode.InvalidDateTimeFormat, string.Format(Resources.InvalidDateTimeFormat, input)); + } + + return dt.ToString(); + } + public static string FormatAsHl7v2DateTime(string input, string timeZoneHandling = "preserve") { if (string.IsNullOrEmpty(input)) From 5ab839a59342d0eab982d49cc9e50b6a64ea9469 Mon Sep 17 00:00:00 2001 From: Will Yochum Date: Tue, 30 Jan 2024 11:38:09 -0500 Subject: [PATCH 2/5] update time filter to be more generic --- .../Filters/DateFiltersTests.cs | 19 +++++++++++++++++++ .../Filters/DateFilters.cs | 9 +++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs index 930a7008d..e817162fe 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs @@ -231,6 +231,17 @@ public static IEnumerable GetValidDataForTimeHandling() yield return new object[] { null, null }; } + public static IEnumerable GetValidDataAndFormatForTimeHandling() + { + yield return new object[] { @"0730", "HHmm", "07:30:00" }; + yield return new object[] { @"1730", "HHmm", "17:30:00" }; + yield return new object[] { @"12010000", "HHmmssff", "12:01:00" }; + yield return new object[] { @"12010001", "HHmmssff", "12:01:00.0100000" }; + yield return new object[] { @"094010", "HHmmss", "09:40:10" }; + yield return new object[] { @"09:45:50", "HH:mm:ss", "09:45:50" }; + yield return new object[] { @"18:45", "HHmm", "18:45" }; // time format does not match format string + } + public static IEnumerable GetInvalidDataForTimeHandling() { yield return new object[] { @"9999" }; @@ -380,6 +391,14 @@ public void GivenAValidData_WhenFormatTimeWithColon_FormattedTimeOrOriginalInput Assert.Equal(expectedDateTime, result); } + [Theory] + [MemberData(nameof(GetValidDataAndFormatForTimeHandling))] + public void GivenAValidData_WhenFormatTimeWithColonAndInputFormatProvided_FormattedTimeOrOriginalInputShouldBeReturned(string input, string inputFormat, string expectedDateTime) + { + var result = Filters.FormatTimeWithColon(input, inputFormat); + Assert.Equal(expectedDateTime, result); + } + [Theory] [MemberData(nameof(GetInvalidDataForTimeHandling))] public void GivenInvalidData_WhenFormatAsTimeInterval_InputTimeShouldBeReturnedAndNoExceptionThrown(string input) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs index d08a60683..83eeab7a8 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs @@ -98,14 +98,15 @@ public static string Now(string input, string format = "yyyy-MM-ddTHH:mm:ss.FFFZ return DateTime.UtcNow.ToString(format); } - /* Converts an HL7v2 time interval to the FHIR format for time - / HL7v2 - 2.8.35.2 Explicit time interval (ST) + /* Converts an HL7v2 time to the FHIR format for time + / HL7v2 - 2.8.35.2 Explicit time interval (ST). Format: HHmm + / HL7v2 - 2.4.5.6 TM time. Format: HHMM[SS[.SSSS]][+/-ZZZZ] / FHIR - time https://build.fhir.org/datatypes.html#time */ public static string FormatTimeWithColon(string input, string inputFormat = "HHmm") { - // For backwards compatibility, if the input is not a 4 digit numeric value then just return the value - if (string.IsNullOrEmpty(input) || (!(input.All(char.IsDigit) && (input.Length == 4)))) + // For backwards compatibility + if (string.IsNullOrEmpty(input) || (input.Length != inputFormat.Length)) { return input; } From 22a2a5e6f4a2726b65ae8e21a9047561feecd0a5 Mon Sep 17 00:00:00 2001 From: Will Yochum Date: Wed, 31 Jan 2024 10:43:16 -0500 Subject: [PATCH 3/5] remove inputFormat validation, add arg for error suppression --- .../Filters/DateFiltersTests.cs | 44 ++++++------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs index e817162fe..ac4feacb6 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs @@ -220,31 +220,21 @@ public static IEnumerable GetInvalidDataForAddSeconds() yield return new object[] { @"2001-01T" }; } - public static IEnumerable GetValidDataForTimeHandling() - { - yield return new object[] { @"0730", "07:30:00" }; - yield return new object[] { @"1730", "17:30:00" }; - yield return new object[] { @"18:45", "18:45" }; - yield return new object[] { @"09:45:50", "09:45:50" }; - yield return new object[] { @"1", "1" }; - yield return new object[] { @"abc", "abc" }; - yield return new object[] { null, null }; - } - public static IEnumerable GetValidDataAndFormatForTimeHandling() { - yield return new object[] { @"0730", "HHmm", "07:30:00" }; - yield return new object[] { @"1730", "HHmm", "17:30:00" }; - yield return new object[] { @"12010000", "HHmmssff", "12:01:00" }; - yield return new object[] { @"12010001", "HHmmssff", "12:01:00.0100000" }; - yield return new object[] { @"094010", "HHmmss", "09:40:10" }; - yield return new object[] { @"09:45:50", "HH:mm:ss", "09:45:50" }; - yield return new object[] { @"18:45", "HHmm", "18:45" }; // time format does not match format string + yield return new object[] { @"0730", "HHmm", false, "07:30:00" }; + yield return new object[] { @"1730", "HHmm", false, "17:30:00" }; + yield return new object[] { @"12010000", "HHmmssff", false, "12:01:00" }; + yield return new object[] { @"12010001", "HHmmssff", false, "12:01:00.0100000" }; + yield return new object[] { @"094010", "HHmmss", false, "09:40:10" }; + yield return new object[] { @"09:45:50", "HH:mm:ss", false, "09:45:50" }; + yield return new object[] { @"144100.0000+0300", "HHmmss.ffffzzzz", false, "06:41:00" }; + yield return new object[] { @"18:45", "HHmm", true, "18:45" }; // time format does not match format string, but date parsing error is suppressed } public static IEnumerable GetInvalidDataForTimeHandling() { - yield return new object[] { @"9999" }; + yield return new object[] { @"9999", "HHmm" }; } [Theory] @@ -383,27 +373,19 @@ public void GivenAnInvalidDateTime_WhenFormatAsHl7DateTime_ExceptionShouldBeThro Assert.Equal(FhirConverterErrorCode.InvalidDateTimeFormat, exception.FhirConverterErrorCode); } - [Theory] - [MemberData(nameof(GetValidDataForTimeHandling))] - public void GivenAValidData_WhenFormatTimeWithColon_FormattedTimeOrOriginalInputShouldBeReturned(string input, string expectedDateTime) - { - var result = Filters.FormatTimeWithColon(input); - Assert.Equal(expectedDateTime, result); - } - [Theory] [MemberData(nameof(GetValidDataAndFormatForTimeHandling))] - public void GivenAValidData_WhenFormatTimeWithColonAndInputFormatProvided_FormattedTimeOrOriginalInputShouldBeReturned(string input, string inputFormat, string expectedDateTime) + public void GivenAValidData_WhenFormatTimeWithColonAndInputFormatProvided_FormattedTimeOrOriginalInputShouldBeReturned(string input, string inputFormat, bool suppressParsingError, string expectedDateTime) { - var result = Filters.FormatTimeWithColon(input, inputFormat); + var result = Filters.FormatTimeWithColon(input, inputFormat, suppressParsingError); Assert.Equal(expectedDateTime, result); } [Theory] [MemberData(nameof(GetInvalidDataForTimeHandling))] - public void GivenInvalidData_WhenFormatAsTimeInterval_InputTimeShouldBeReturnedAndNoExceptionThrown(string input) + public void GivenInvalidData_WhenFormatAsTimeInterval_ExceptionThrown(string input, string inputFormat) { - var exception = Assert.Throws(() => Filters.FormatTimeWithColon(input)); + var exception = Assert.Throws(() => Filters.FormatTimeWithColon(input, inputFormat)); Assert.Equal(FhirConverterErrorCode.InvalidDateTimeFormat, exception.FhirConverterErrorCode); } From a521f550ba1e428dcec0b7d30b7cb2ca75d0633f Mon Sep 17 00:00:00 2001 From: Will Yochum Date: Wed, 31 Jan 2024 10:50:24 -0500 Subject: [PATCH 4/5] add missing file updates --- .../Filters/DateFilters.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs index 83eeab7a8..8852e02fd 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs @@ -103,14 +103,8 @@ public static string Now(string input, string format = "yyyy-MM-ddTHH:mm:ss.FFFZ / HL7v2 - 2.4.5.6 TM time. Format: HHMM[SS[.SSSS]][+/-ZZZZ] / FHIR - time https://build.fhir.org/datatypes.html#time */ - public static string FormatTimeWithColon(string input, string inputFormat = "HHmm") + public static string FormatTimeWithColon(string input, string inputFormat, bool suppressParsingError = false) { - // For backwards compatibility - if (string.IsNullOrEmpty(input) || (input.Length != inputFormat.Length)) - { - return input; - } - var dt = TimeSpan.Zero; try { @@ -118,6 +112,11 @@ public static string FormatTimeWithColon(string input, string inputFormat = "HHm } catch (Exception) { + if (suppressParsingError) + { + return input; + } + throw new RenderException(FhirConverterErrorCode.InvalidDateTimeFormat, string.Format(Resources.InvalidDateTimeFormat, input)); } From 12d100b36c59f87ea6d087cf77e30430f52944a0 Mon Sep 17 00:00:00 2001 From: Will Yochum Date: Wed, 31 Jan 2024 15:01:18 -0500 Subject: [PATCH 5/5] fix code to handle timezones --- .../Filters/DateFiltersTests.cs | 21 ++++++++++--------- .../Filters/DateFilters.cs | 14 +++++++++++-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs index ac4feacb6..6d953f06c 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/DateFiltersTests.cs @@ -222,14 +222,15 @@ public static IEnumerable GetInvalidDataForAddSeconds() public static IEnumerable GetValidDataAndFormatForTimeHandling() { - yield return new object[] { @"0730", "HHmm", false, "07:30:00" }; - yield return new object[] { @"1730", "HHmm", false, "17:30:00" }; - yield return new object[] { @"12010000", "HHmmssff", false, "12:01:00" }; - yield return new object[] { @"12010001", "HHmmssff", false, "12:01:00.0100000" }; - yield return new object[] { @"094010", "HHmmss", false, "09:40:10" }; - yield return new object[] { @"09:45:50", "HH:mm:ss", false, "09:45:50" }; - yield return new object[] { @"144100.0000+0300", "HHmmss.ffffzzzz", false, "06:41:00" }; - yield return new object[] { @"18:45", "HHmm", true, "18:45" }; // time format does not match format string, but date parsing error is suppressed + yield return new object[] { @"0730", "HHmm", "preserve", false, "07:30:00" }; + yield return new object[] { @"1730", "HHmm", "preserve", false, "17:30:00" }; + yield return new object[] { @"12010000", "HHmmssff", "preserve", false, "12:01:00" }; + yield return new object[] { @"12010001", "HHmmssff", "preserve", false, "12:01:00.0100000" }; + yield return new object[] { @"094010", "HHmmss", "preserve", false, "09:40:10" }; + yield return new object[] { @"09:45:50", "HH:mm:ss", "preserve", false, "09:45:50" }; + yield return new object[] { @"144100.0000+0300", "HHmmss.ffffzzz", "utc", false, "11:41:00" }; + yield return new object[] { @"144100.0000+0300", "HHmmss.ffffzzz", "preserve", false, "14:41:00" }; + yield return new object[] { @"18:45", "HHmm", "preserve", true, "18:45" }; // time format does not match format string, but date parsing error is suppressed } public static IEnumerable GetInvalidDataForTimeHandling() @@ -375,9 +376,9 @@ public void GivenAnInvalidDateTime_WhenFormatAsHl7DateTime_ExceptionShouldBeThro [Theory] [MemberData(nameof(GetValidDataAndFormatForTimeHandling))] - public void GivenAValidData_WhenFormatTimeWithColonAndInputFormatProvided_FormattedTimeOrOriginalInputShouldBeReturned(string input, string inputFormat, bool suppressParsingError, string expectedDateTime) + public void GivenAValidData_WhenFormatTimeWithColonAndInputFormatProvided_FormattedTimeOrOriginalInputShouldBeReturned(string input, string inputFormat, string timeZoneHandling, bool suppressParsingError, string expectedDateTime) { - var result = Filters.FormatTimeWithColon(input, inputFormat, suppressParsingError); + var result = Filters.FormatTimeWithColon(input, inputFormat, timeZoneHandling, suppressParsingError); Assert.Equal(expectedDateTime, result); } diff --git a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs index 8852e02fd..977250143 100644 --- a/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs +++ b/src/Microsoft.Health.Fhir.Liquid.Converter/Filters/DateFilters.cs @@ -103,12 +103,22 @@ public static string Now(string input, string format = "yyyy-MM-ddTHH:mm:ss.FFFZ / HL7v2 - 2.4.5.6 TM time. Format: HHMM[SS[.SSSS]][+/-ZZZZ] / FHIR - time https://build.fhir.org/datatypes.html#time */ - public static string FormatTimeWithColon(string input, string inputFormat, bool suppressParsingError = false) + public static string FormatTimeWithColon(string input, string inputFormat, string timeZoneHandling = "preserve", bool suppressParsingError = false) { + if (!Enum.TryParse(timeZoneHandling, true, out TimeZoneHandlingMethod outputTimeZoneHandling)) + { + throw new RenderException(FhirConverterErrorCode.InvalidTimeZoneHandling, Resources.InvalidTimeZoneHandling); + } + var dt = TimeSpan.Zero; try { - dt = DateTime.ParseExact(input, inputFormat, CultureInfo.InvariantCulture).TimeOfDay; + dt = outputTimeZoneHandling switch + { + TimeZoneHandlingMethod.Preserve => DateTimeOffset.ParseExact(input, inputFormat, CultureInfo.InvariantCulture).TimeOfDay, + TimeZoneHandlingMethod.Utc => DateTimeOffset.ParseExact(input, inputFormat, CultureInfo.InvariantCulture).ToUniversalTime().TimeOfDay, + _ => throw new ArgumentException(Resources.InvalidTimeZoneHandling), + }; } catch (Exception) {