From f77c14b86db8f68d23811e8962a38689c3ef95c6 Mon Sep 17 00:00:00 2001 From: Lucero Velasco Date: Tue, 31 Dec 2024 16:14:50 -0700 Subject: [PATCH 1/4] added fix for parsing high precision datetime string --- .../src/main/kotlin/common/DateUtilities.kt | 1 + .../test/kotlin/common/DateUtilitiesTests.kt | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/prime-router/src/main/kotlin/common/DateUtilities.kt b/prime-router/src/main/kotlin/common/DateUtilities.kt index e8ab8de32b2..1faa69329fb 100644 --- a/prime-router/src/main/kotlin/common/DateUtilities.kt +++ b/prime-router/src/main/kotlin/common/DateUtilities.kt @@ -42,6 +42,7 @@ object DateUtilities { /** wraps around all the possible variations of a date for finding something that matches */ const val variableDateTimePattern = "[yyyyMMdd]" + + "[yyyyMMddHHmmss.SSSSxx]" + "[yyyyMMdd[HHmm][ss][.S][Z]]" + "[yyyy-MM-dd HH:mm:ss.ZZZ]" + // nano seconds diff --git a/prime-router/src/test/kotlin/common/DateUtilitiesTests.kt b/prime-router/src/test/kotlin/common/DateUtilitiesTests.kt index c6528bab86c..800f649dbd0 100644 --- a/prime-router/src/test/kotlin/common/DateUtilitiesTests.kt +++ b/prime-router/src/test/kotlin/common/DateUtilitiesTests.kt @@ -405,6 +405,7 @@ class DateUtilitiesTests { "12/1/1900" to "19001201000000", "2/3/02" to "20020203000000", "2/3/02 8:00" to "20020203080000", + "20241220194528.4230+0000" to "20241220194528" ).forEach { (input, expected) -> val parsed = DateUtilities.parseDate(input) assertThat( @@ -417,6 +418,24 @@ class DateUtilitiesTests { ) ).isEqualTo(expected) } + + // test high precision offset + mapOf( + "1975-08-01T11:39:00Z" to "19750801113900.0000+0000", + "2022-04-29T15:43:02.307Z" to "20220429154302.3070+0000", + "20241220194528.4230+0000" to "20241220194528.4230+0000" + ).forEach { (input, expected) -> + val parsed = DateUtilities.parseDate(input) + assertThat( + DateUtilities.formatDateForReceiver( + parsed, + DateUtilities.utcZone, + DateUtilities.DateTimeFormat.HIGH_PRECISION_OFFSET, + false, + false + ) + ).isEqualTo(expected) + } } @Test From 10b08fd3982d72d703a770be41a1286140fd38de Mon Sep 17 00:00:00 2001 From: Lucero Velasco Date: Tue, 31 Dec 2024 16:17:37 -0700 Subject: [PATCH 2/4] added ability to pass dateTimeFormat and other formatting params into changeTimeZone, added tests --- .../hl7/utils/CustomFHIRFunctions.kt | 40 ++++++----- .../hl7/utils/CustomFHIRFunctionsTests.kt | 70 +++++++++++++++++++ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt b/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt index 819df5d2def..c1b3c726220 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt @@ -14,7 +14,9 @@ import org.hl7.fhir.r4.model.HumanName import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.StringType import java.time.DateTimeException +import java.time.LocalDate import java.time.ZoneId +import java.time.format.DateTimeParseException import java.util.TimeZone /** @@ -446,10 +448,19 @@ object CustomFHIRFunctions : FhirPathFunctions { throw SchemaException("Must call changeTimezone on a single element") } - if (parameters == null || parameters[0].size != 1) { + if (parameters == null || parameters.first().isEmpty()) { throw SchemaException("Must pass a timezone as the parameter") } + var dateTimeFormat = DateUtilities.DateTimeFormat.OFFSET + if (parameters.first().size > 1) { + try { + dateTimeFormat = DateUtilities.DateTimeFormat.valueOf(parameters.first()[1].primitiveValue()) + } catch (e: IllegalArgumentException) { + throw SchemaException("Date time format not found.") + } + } + val inputTimeZone = parameters.first().first().primitiveValue() val timezonePassed = try { TimeZone.getTimeZone(ZoneId.of(inputTimeZone)) @@ -462,27 +473,24 @@ object CustomFHIRFunctions : FhirPathFunctions { } return if (focus[0] is StringType) { - if (focus[0].toString().length <= 8) { // we don't want to convert Date-only strings - return mutableListOf(StringType(focus[0].toString())) + val inputDate = try { + DateUtilities.parseDate((focus[0].toString())) + } catch (e: DateTimeParseException) { + throw SchemaException("Error trying to change time zone: " + e.message) } - // TODO: find a way to pass in these values from receiver settings - - val dateTimeFormat = null - val convertPositiveDateTimeOffsetToNegative = null - val useHighPrecisionHeaderDateTimeFormat = null + if (inputDate is LocalDate) { + return mutableListOf(StringType(focus[0].toString())) + } val formattedDate = DateUtilities.formatDateForReceiver( - DateUtilities.parseDate((focus[0].toString())), + inputDate, ZoneId.of(inputTimeZone), - dateTimeFormat ?: DateUtilities.DateTimeFormat.OFFSET, - convertPositiveDateTimeOffsetToNegative ?: false, - useHighPrecisionHeaderDateTimeFormat ?: false - ) - - mutableListOf( - StringType(formattedDate) + dateTimeFormat, + parameters.first().getOrNull(2)?.primitiveValue()?.toBoolean() ?: false, + parameters.first().getOrNull(3)?.primitiveValue()?.toBoolean() ?: false ) + mutableListOf(StringType(formattedDate)) } else { val inputDate = focus[0] as? BaseDateTimeType ?: throw SchemaException( "Must call changeTimezone on a dateTime, instant, or date; " + diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt index b81f4143a72..fe9f45bd890 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt @@ -506,6 +506,76 @@ class CustomFHIRFunctionsTests { }.hasClass(SchemaException::class.java) } + @Test + fun `test changeTimezone with date as string - success`() { + val date = StringType("20241220194528.4230+0000") + val timezone = StringType("UTC") + val timezoneFormat = StringType("HIGH_PRECISION_OFFSET") + val convertToNegative = StringType("true") + val useHighPrecision = StringType("true") + var parameters: MutableList = mutableListOf(timezone, timezoneFormat, convertToNegative, useHighPrecision) + + // test format + var outputDate = CustomFHIRFunctions.changeTimezone( + mutableListOf(date), + mutableListOf(parameters) + ) + assertThat(outputDate[0]).isInstanceOf(StringType::class.java) + assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220194528.4230-0000") + + // test timezone change with offset format + timezone.value = "America/Phoenix" + timezoneFormat.value = "OFFSET" + convertToNegative.value = "false" + useHighPrecision.value = "false" + + outputDate = CustomFHIRFunctions.changeTimezone( + mutableListOf(date), + mutableListOf(parameters) + ) + assertThat(outputDate[0]).isInstanceOf(StringType::class.java) + assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220124528-0700") + + // test different format for input date + val date2 = StringType("2021-08-09T08:52:34-04:00") + timezone.value = "America/Phoenix" + timezoneFormat.value = "OFFSET" + convertToNegative.value = "false" + useHighPrecision.value = "false" + + outputDate = CustomFHIRFunctions.changeTimezone( + mutableListOf(date2), + mutableListOf(parameters) + ) + assertThat(outputDate[0]).isInstanceOf(StringType::class.java) + assertThat(outputDate[0].primitiveValue()).isEqualTo("20210809055234-0700") + + // test date without time + val date3 = StringType("2021-08-09") + timezone.value = "America/Phoenix" + timezoneFormat.value = "OFFSET" + convertToNegative.value = "false" + useHighPrecision.value = "false" + + outputDate = CustomFHIRFunctions.changeTimezone( + mutableListOf(date3), + mutableListOf(parameters) + ) + assertThat(outputDate[0]).isInstanceOf(StringType::class.java) + assertThat(outputDate[0].primitiveValue()).isEqualTo("2021-08-09") + + // test timezone change with required param only + timezone.value = "America/Phoenix" + parameters = mutableListOf(timezone) + + outputDate = CustomFHIRFunctions.changeTimezone( + mutableListOf(date), + mutableListOf(parameters) + ) + assertThat(outputDate[0]).isInstanceOf(StringType::class.java) + assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220124528-0700") + } + @Test fun `test deidentifies a human name`() { val name = HumanName() From 16c414857088e2da46a18081dedfb139aa847484 Mon Sep 17 00:00:00 2001 From: Lucero Velasco Date: Thu, 2 Jan 2025 18:11:56 -0700 Subject: [PATCH 3/4] fix for accessing optional params --- .../hl7/utils/CustomFHIRFunctions.kt | 16 ++++++---- .../hl7/utils/CustomFHIRFunctionsTests.kt | 32 ++++++++++++------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt b/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt index c1b3c726220..3ee64310671 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctions.kt @@ -111,9 +111,13 @@ object CustomFHIRFunctions : FhirPathFunctions { CustomFHIRFunctionNames.ChangeTimezone -> { FunctionDetails( - "changes the timezone of a dateTime, instant, or date resource to the timezone passed in", + "changes the timezone of a dateTime, instant, or date resource to the timezone passed in. " + + "optional params: " + + "dateTimeFormat ('OFFSET', 'LOCAL', 'HIGH_PRECISION_OFFSET', 'DATE_ONLY')(default: 'OFFSET')," + + " convertPositiveDateTimeOffsetToNegative (boolean)(default: false)," + + " useHighPrecisionHeaderDateTimeFormat (boolean)(default: false)", 1, - 1 + 4 ) } @@ -453,9 +457,9 @@ object CustomFHIRFunctions : FhirPathFunctions { } var dateTimeFormat = DateUtilities.DateTimeFormat.OFFSET - if (parameters.first().size > 1) { + if (parameters.size > 1) { try { - dateTimeFormat = DateUtilities.DateTimeFormat.valueOf(parameters.first()[1].primitiveValue()) + dateTimeFormat = DateUtilities.DateTimeFormat.valueOf(parameters.get(1).first().primitiveValue()) } catch (e: IllegalArgumentException) { throw SchemaException("Date time format not found.") } @@ -487,8 +491,8 @@ object CustomFHIRFunctions : FhirPathFunctions { inputDate, ZoneId.of(inputTimeZone), dateTimeFormat, - parameters.first().getOrNull(2)?.primitiveValue()?.toBoolean() ?: false, - parameters.first().getOrNull(3)?.primitiveValue()?.toBoolean() ?: false + parameters.getOrNull(2)?.first()?.primitiveValue()?.toBoolean() ?: false, + parameters.getOrNull(3)?.first()?.primitiveValue()?.toBoolean() ?: false ) mutableListOf(StringType(formattedDate)) } else { diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt index fe9f45bd890..31545581aab 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt @@ -512,13 +512,15 @@ class CustomFHIRFunctionsTests { val timezone = StringType("UTC") val timezoneFormat = StringType("HIGH_PRECISION_OFFSET") val convertToNegative = StringType("true") - val useHighPrecision = StringType("true") - var parameters: MutableList = mutableListOf(timezone, timezoneFormat, convertToNegative, useHighPrecision) + val useHighPrecision = StringType("false") // test format var outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date), - mutableListOf(parameters) + mutableListOf( + mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) ) assertThat(outputDate[0]).isInstanceOf(StringType::class.java) assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220194528.4230-0000") @@ -531,7 +533,10 @@ class CustomFHIRFunctionsTests { outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date), - mutableListOf(parameters) + mutableListOf( + mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) ) assertThat(outputDate[0]).isInstanceOf(StringType::class.java) assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220124528-0700") @@ -539,18 +544,21 @@ class CustomFHIRFunctionsTests { // test different format for input date val date2 = StringType("2021-08-09T08:52:34-04:00") timezone.value = "America/Phoenix" - timezoneFormat.value = "OFFSET" + timezoneFormat.value = "LOCAL" convertToNegative.value = "false" useHighPrecision.value = "false" outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date2), - mutableListOf(parameters) + mutableListOf( + mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) ) assertThat(outputDate[0]).isInstanceOf(StringType::class.java) - assertThat(outputDate[0].primitiveValue()).isEqualTo("20210809055234-0700") + assertThat(outputDate[0].primitiveValue()).isEqualTo("20210809055234") - // test date without time + // test date without time should return same date string val date3 = StringType("2021-08-09") timezone.value = "America/Phoenix" timezoneFormat.value = "OFFSET" @@ -559,18 +567,20 @@ class CustomFHIRFunctionsTests { outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date3), - mutableListOf(parameters) + mutableListOf( + mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) ) assertThat(outputDate[0]).isInstanceOf(StringType::class.java) assertThat(outputDate[0].primitiveValue()).isEqualTo("2021-08-09") // test timezone change with required param only timezone.value = "America/Phoenix" - parameters = mutableListOf(timezone) outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date), - mutableListOf(parameters) + mutableListOf(mutableListOf(timezone)) ) assertThat(outputDate[0]).isInstanceOf(StringType::class.java) assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220124528-0700") From 7f501092ee9d1b839d3f2582efd794b05d04e4a8 Mon Sep 17 00:00:00 2001 From: Lucero Velasco Date: Thu, 2 Jan 2025 18:29:20 -0700 Subject: [PATCH 4/4] added test coverage for changeTimeZone exceptions --- .../hl7/utils/CustomFHIRFunctionsTests.kt | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt index 31545581aab..be39c04419b 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/CustomFHIRFunctionsTests.kt @@ -510,7 +510,7 @@ class CustomFHIRFunctionsTests { fun `test changeTimezone with date as string - success`() { val date = StringType("20241220194528.4230+0000") val timezone = StringType("UTC") - val timezoneFormat = StringType("HIGH_PRECISION_OFFSET") + val dateTimeFormat = StringType("HIGH_PRECISION_OFFSET") val convertToNegative = StringType("true") val useHighPrecision = StringType("false") @@ -518,7 +518,7 @@ class CustomFHIRFunctionsTests { var outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date), mutableListOf( - mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), mutableListOf(useHighPrecision) ) ) @@ -527,14 +527,14 @@ class CustomFHIRFunctionsTests { // test timezone change with offset format timezone.value = "America/Phoenix" - timezoneFormat.value = "OFFSET" + dateTimeFormat.value = "OFFSET" convertToNegative.value = "false" useHighPrecision.value = "false" outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date), mutableListOf( - mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), mutableListOf(useHighPrecision) ) ) @@ -544,14 +544,14 @@ class CustomFHIRFunctionsTests { // test different format for input date val date2 = StringType("2021-08-09T08:52:34-04:00") timezone.value = "America/Phoenix" - timezoneFormat.value = "LOCAL" + dateTimeFormat.value = "LOCAL" convertToNegative.value = "false" useHighPrecision.value = "false" outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date2), mutableListOf( - mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), mutableListOf(useHighPrecision) ) ) @@ -561,14 +561,14 @@ class CustomFHIRFunctionsTests { // test date without time should return same date string val date3 = StringType("2021-08-09") timezone.value = "America/Phoenix" - timezoneFormat.value = "OFFSET" + dateTimeFormat.value = "OFFSET" convertToNegative.value = "false" useHighPrecision.value = "false" outputDate = CustomFHIRFunctions.changeTimezone( mutableListOf(date3), mutableListOf( - mutableListOf(timezone), mutableListOf(timezoneFormat), mutableListOf(convertToNegative), + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), mutableListOf(useHighPrecision) ) ) @@ -586,6 +586,40 @@ class CustomFHIRFunctionsTests { assertThat(outputDate[0].primitiveValue()).isEqualTo("20241220124528-0700") } + @Test + fun `test changeTimezone with date as string - exception`() { + val date = StringType("20241220194528.4230+0000") + val timezone = StringType("UTC") + val dateTimeFormat = StringType("HIGH_PRECISION_OFFSET") + val convertToNegative = StringType("true") + val useHighPrecision = StringType("false") + + assertFailure { + dateTimeFormat.value = "HIGH_PRECISION_OFFS" + // test invalid dateTime format + CustomFHIRFunctions.changeTimezone( + mutableListOf(date), + mutableListOf( + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) + ) + }.hasClass(SchemaException::class.java) + + assertFailure { + dateTimeFormat.value = "HIGH_PRECISION_OFFSET" + date.value = "2021-08-09T" + // test invalid dateTime string input + CustomFHIRFunctions.changeTimezone( + mutableListOf(date), + mutableListOf( + mutableListOf(timezone), mutableListOf(dateTimeFormat), mutableListOf(convertToNegative), + mutableListOf(useHighPrecision) + ) + ) + }.hasClass(SchemaException::class.java) + } + @Test fun `test deidentifies a human name`() { val name = HumanName()