From d3b71d3507e6bac3ee1b8d01b31fe301e7e49637 Mon Sep 17 00:00:00 2001 From: iacsha <122627209+iacsha@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:19:00 +0000 Subject: [PATCH 1/2] Parse every HL7 DTM precision in TSComponentOne.GetAsDate (fixes #411) GetAsDate only accepted five ParseExact formats, so valid DTM values such as 20230211140114.0000+0800 (fractional seconds with a time zone offset, sent by Mindray V9 monitors) threw HL7Exception, as did hour, month and year precision and an offset on minute precision. Add the missing YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ] forms after the existing formats, so values that already parsed are unchanged; an offset converts to local time as the existing offset format already does. --- .../Model/Primitive/TSComponentOne.cs | 25 +++++- tests/NHapi.NUnit/DateTypeTest26.cs | 86 +++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tests/NHapi.NUnit/DateTypeTest26.cs diff --git a/src/NHapi.Base/Model/Primitive/TSComponentOne.cs b/src/NHapi.Base/Model/Primitive/TSComponentOne.cs index 2a12073fa..9e5a14ddd 100644 --- a/src/NHapi.Base/Model/Primitive/TSComponentOne.cs +++ b/src/NHapi.Base/Model/Primitive/TSComponentOne.cs @@ -28,6 +28,7 @@ namespace NHapi.Base.Model.Primitive { using System; using System.Globalization; + using System.Linq; using System.Threading; /// @@ -57,6 +58,26 @@ namespace NHapi.Base.Model.Primitive /// public class TSComponentOne : AbstractPrimitive { + /// + /// The remaining DTM precisions, YYYY[MM[DD[HH[MM[SS[.S[S[S[S]]]]]]]]][+/-ZZZZ], that the + /// DateTimeFormat properties below do not cover, for example fractional seconds with a time zone offset + /// (20230211140114.0000+0800). Tried after those formats, so values that already + /// parsed keep their result. A time zone offset is converted to local time, as it already is + /// for . + /// + private static readonly string[] AdditionalDtmFormats = + { + "yyyy", + "yyyyMM", + "yyyyMMddHH", + "yyyyzzz", + "yyyyMMzzz", + "yyyyMMddzzz", + "yyyyMMddHHzzz", + "yyyyMMddHHmmzzz", + "yyyyMMddHHmmss.FFFFzzz", + }; + private CommonTS myDetail; /// message to which this Type belongs. @@ -246,7 +267,9 @@ public virtual DateTime GetAsDate() { try { - var dateFormats = new string[] { LongDateTimeFormat, ShortDateTimeFormat, LongDateTimeFormatWithSecond, LongDateTimeFormatWithOffset, LongDateTimeFormatWithFractionOfSecond }; + var dateFormats = new string[] { LongDateTimeFormat, ShortDateTimeFormat, LongDateTimeFormatWithSecond, LongDateTimeFormatWithOffset, LongDateTimeFormatWithFractionOfSecond } + .Concat(AdditionalDtmFormats) + .ToArray(); var val = DateTime.MinValue; var culture = Thread.CurrentThread.CurrentCulture; if (Value != null && Value.Length > 0) diff --git a/tests/NHapi.NUnit/DateTypeTest26.cs b/tests/NHapi.NUnit/DateTypeTest26.cs new file mode 100644 index 000000000..60fbc919f --- /dev/null +++ b/tests/NHapi.NUnit/DateTypeTest26.cs @@ -0,0 +1,86 @@ +namespace NHapi.NUnit +{ + using System; + + using global::NUnit.Framework; + + using NHapi.Base; + using NHapi.Base.Parser; + using NHapi.Model.V26.Datatype; + using NHapi.Model.V26.Message; + + [TestFixture] + public class DateTypeTest26 + { + private static readonly TimeSpan PlusEight = TimeSpan.FromHours(8); + + private static readonly object[] OffsetCases = + { + // Fractional seconds with an offset: the Mindray V9 value from issue #411. + new object[] { "20230211140114.0000+0800", new DateTimeOffset(2023, 2, 11, 14, 1, 14, PlusEight) }, + new object[] { "20230211140114.1234+0800", new DateTimeOffset(2023, 2, 11, 14, 1, 14, 123, PlusEight).AddTicks(4000) }, + new object[] { "20230211140114.5-0530", new DateTimeOffset(2023, 2, 11, 14, 1, 14, 500, TimeSpan.FromMinutes(-330)) }, + new object[] { "202302111401+0800", new DateTimeOffset(2023, 2, 11, 14, 1, 0, PlusEight) }, + new object[] { "2023021114+0800", new DateTimeOffset(2023, 2, 11, 14, 0, 0, PlusEight) }, + new object[] { "20230211+0800", new DateTimeOffset(2023, 2, 11, 0, 0, 0, PlusEight) }, + }; + + private static readonly object[] PrecisionCases = + { + new object[] { "2023021114", new DateTime(2023, 2, 11, 14, 0, 0) }, + new object[] { "202302", new DateTime(2023, 2, 1) }, + new object[] { "2023", new DateTime(2023, 1, 1) }, + }; + + [TestCaseSource(nameof(OffsetCases))] + public void GetAsDate_DtmWithOffset_ConvertsToLocalTime(string value, DateTimeOffset expected) + { + var dtm = new DTM(new ACK()) { Value = value }; + + Assert.That(dtm.GetAsDate(), Is.EqualTo(expected.LocalDateTime)); + } + + [TestCaseSource(nameof(PrecisionCases))] + public void GetAsDate_ReducedPrecisionDtm_Parses(string value, DateTime expected) + { + var dtm = new DTM(new ACK()) { Value = value }; + + Assert.That(dtm.GetAsDate(), Is.EqualTo(expected)); + } + + [TestCase("20230211140114.12345")] + [TestCase("2023021114011")] + [TestCase("20231311")] + [TestCase("not a date")] + public void GetAsDate_InvalidDtm_StillThrows(string value) + { + var dtm = new DTM(new ACK()) { Value = value }; + + Assert.Throws(() => dtm.GetAsDate()); + } + + [Test] + public void GetAsDate_MindrayOruR01_ParsesMessageAndObservationTimes() + { + var message = + "MSH|^~\\&|MINDRAY_^00B0353000000^EUI-64|MINDRAY|||20230211140114.0000+0800||ORU^R01^ORU_R01|10|P|2.6|||AL|NE||UNICODE UTF-8|||IHE_PCD_001^IHE PCD^1.3.6.1.4.1.19376.1.6.1.1.1^ISO\r" + + "PID|||1234^^^Hospital^PI||Uui^Kki^^^^^L|||M\r" + + "PV1||I|ED^^RED VS9\r" + + "OBR|1|10^MINDRAY_^00B0353000000^EUI-64|10^MINDRAY_^00A037000000^EUI-64|182777000^monitoring of patient^SCT|||20230211134735.0000+0800\r" + + "OBX|1|NM|150456^MDC_PULS_OXIM_SAT_O2^MDC|1.3.1.150456|97|262688^MDC_DIM_PERCENT^MDC|||||R|||20230211134735.0000+0800||||00A037000000^^00A037000000^EUI-64\r"; + + var oru = (ORU_R01)new PipeParser().Parse(message); + var obx = oru.GetPATIENT_RESULT().GetORDER_OBSERVATION().GetOBSERVATION().OBX; + + Assert.Multiple(() => + { + Assert.That( + oru.MSH.DateTimeOfMessage.GetAsDate(), + Is.EqualTo(new DateTimeOffset(2023, 2, 11, 14, 1, 14, PlusEight).LocalDateTime)); + Assert.That( + obx.DateTimeOfTheObservation.GetAsDate(), + Is.EqualTo(new DateTimeOffset(2023, 2, 11, 13, 47, 35, PlusEight).LocalDateTime)); + }); + } + } +} From 388f3867f001ca0fdb83c2653a70236d2598091e Mon Sep 17 00:00:00 2001 From: iacsha <122627209+iacsha@users.noreply.github.com> Date: Sun, 8 Nov 2026 19:07:25 +0000 Subject: [PATCH 2/2] Use an implicitly typed array for the DTM formats --- src/NHapi.Base/Model/Primitive/TSComponentOne.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHapi.Base/Model/Primitive/TSComponentOne.cs b/src/NHapi.Base/Model/Primitive/TSComponentOne.cs index 9e5a14ddd..726749346 100644 --- a/src/NHapi.Base/Model/Primitive/TSComponentOne.cs +++ b/src/NHapi.Base/Model/Primitive/TSComponentOne.cs @@ -267,7 +267,7 @@ public virtual DateTime GetAsDate() { try { - var dateFormats = new string[] { LongDateTimeFormat, ShortDateTimeFormat, LongDateTimeFormatWithSecond, LongDateTimeFormatWithOffset, LongDateTimeFormatWithFractionOfSecond } + var dateFormats = new[] { LongDateTimeFormat, ShortDateTimeFormat, LongDateTimeFormatWithSecond, LongDateTimeFormatWithOffset, LongDateTimeFormatWithFractionOfSecond } .Concat(AdditionalDtmFormats) .ToArray(); var val = DateTime.MinValue;