From cf765c4c407ec7f2feddc0e67ff20f8fea5231cc Mon Sep 17 00:00:00 2001 From: Daniel Kraus Date: Sun, 17 Sep 2017 13:28:12 +0200 Subject: [PATCH] Add importer for clinic system. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neu: Laborwerte, die vom Ambulanzsystem ausgegeben wurden, können jetzt auch verarbeitet werden. --- .../ClinicImporter/ClinicImporterTest.cs | 55 ++++ .../Importer/ClinicImporter/ClinicItemTest.cs | 56 ++++ .../Importer/ClinicImporter/ClinicLineTest.cs | 45 +++ .../Importer/ClinicImporter/TimePointTest.cs | 67 +++++ Tests/TestHelpers.cs | 14 + Tests/Tests.csproj | 8 + Tests/demo-output-clinic.txt | 90 ++++++ zaaReloaded2/Commands.cs | 3 +- zaaReloaded2/Demo/Demo.docx | Bin 24444 -> 27329 bytes .../AutoDetect.cs => AutoDetector.cs} | 84 +++++- zaaReloaded2/Importer/BaseImporter.cs | 170 ++++++++++++ .../Importer/ClinicImporter/ClinicImporter.cs | 68 +++++ .../Importer/ClinicImporter/ClinicLine.cs | 138 ++++++++++ .../ClinicImporter/ClinicTimePoint.cs | 258 ++++++++++++++++++ .../Importer/ZaaImporter/LaurisTimePoint.cs | 2 +- .../Importer/ZaaImporter/ZaaImporter.cs | 110 ++------ zaaReloaded2/LabModel/TimePoint.cs | 20 +- zaaReloaded2/zaaReloaded2.csproj | 7 +- 18 files changed, 1091 insertions(+), 104 deletions(-) create mode 100755 Tests/Importer/ClinicImporter/ClinicImporterTest.cs create mode 100755 Tests/Importer/ClinicImporter/ClinicItemTest.cs create mode 100755 Tests/Importer/ClinicImporter/ClinicLineTest.cs create mode 100755 Tests/Importer/ClinicImporter/TimePointTest.cs create mode 100755 Tests/demo-output-clinic.txt rename zaaReloaded2/Importer/{ZaaImporter/AutoDetect.cs => AutoDetector.cs} (56%) create mode 100755 zaaReloaded2/Importer/BaseImporter.cs create mode 100755 zaaReloaded2/Importer/ClinicImporter/ClinicImporter.cs create mode 100755 zaaReloaded2/Importer/ClinicImporter/ClinicLine.cs create mode 100755 zaaReloaded2/Importer/ClinicImporter/ClinicTimePoint.cs diff --git a/Tests/Importer/ClinicImporter/ClinicImporterTest.cs b/Tests/Importer/ClinicImporter/ClinicImporterTest.cs new file mode 100755 index 0000000..4ad5609 --- /dev/null +++ b/Tests/Importer/ClinicImporter/ClinicImporterTest.cs @@ -0,0 +1,55 @@ +/* ClinicImporterTest.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using zaa = zaaReloaded2.Importer.ClinicImporter; +using NUnit.Framework; + +namespace Tests.Importer.ClinicImporter +{ + [TestFixture] + class ClinicImporterTest + { + /// + /// Parses the demo-output.txt file that is built into the assembly. + /// Note that this demo text contains seven time points, only six of + /// which are distinct. Thus, the two time points with identical time + /// stamps should be merged in the . + /// + [Test] + public void ParseTimePoints() + { + zaa.ClinicImporter importer = TestHelpers.ClinicImporterFromResource(); + // Only 6 distinct time points (see method documentation above). + Assert.AreEqual(5, importer.Laboratory.TimePoints.Count); + } + + [Test] + public void ParseInvalidInput() + { + zaa.ClinicImporter importer = new zaa.ClinicImporter(); + importer.Import("some arbitrary text\r\nthat does not represent\r\na valid lab"); + Assert.IsFalse(importer.Success); + importer.Import("(03.03.1930 13:30:00)\r\n\tNatrium 135 [135 - 145] mmol/l;"); + Assert.IsTrue(importer.Success); + } + } +} diff --git a/Tests/Importer/ClinicImporter/ClinicItemTest.cs b/Tests/Importer/ClinicImporter/ClinicItemTest.cs new file mode 100755 index 0000000..40a06c9 --- /dev/null +++ b/Tests/Importer/ClinicImporter/ClinicItemTest.cs @@ -0,0 +1,56 @@ +/* LaurisItemTest.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using zaaReloaded2.LabModel; +using zaaReloaded2.Importer.ZaaImporter; + +namespace Tests.Importer.ClinicImporter +{ + /// + /// The ClinicItemTest really just tests the zaaReloaded2.Importer.ZaaImporter.LaurisItem + /// class with text formatted by the outpatient clinic system to make sure LaurisItem works + /// with clinic-formatted lab data as well. + /// + [TestFixture] + class ClinicItemTest + { + [Test] + [TestCase(" Natrium: 137 [135 - 145] mmol/l", "Natrium", 137, "mmol/l", 135, 145, true)] + public void ParseClinicWithBothLimits( + string laurisString, string name, double value, + string unit, double lowerLimit, double upperLimit, bool isNormal) + { + LaurisItem i = new LaurisItem(laurisString); + Assert.AreEqual(name, i.Name, "Name"); + Assert.AreEqual(unit, i.Unit, "Unit"); + Assert.IsFalse(i.IsExtreme, "IsExtreme"); + Assert.IsTrue(i.IsNumerical, "IsNumerical"); + Assert.AreEqual(value, i.NumericalValue, "NumericalValue"); + Assert.AreEqual(lowerLimit, i.LowerLimit, "Lower limit"); + Assert.AreEqual(upperLimit, i.UpperLimit, "Upper limit"); + Assert.AreEqual(isNormal, i.IsNormal, "IsNormal"); + Assert.IsTrue(i.HasLimits, "HasLimits"); + Assert.IsTrue(i.HasLowerLimit, "HasLowerLimit"); + Assert.IsTrue(i.HasUpperLimit, "HasUpperLimit"); + } + } +} diff --git a/Tests/Importer/ClinicImporter/ClinicLineTest.cs b/Tests/Importer/ClinicImporter/ClinicLineTest.cs new file mode 100755 index 0000000..f0bbdd1 --- /dev/null +++ b/Tests/Importer/ClinicImporter/ClinicLineTest.cs @@ -0,0 +1,45 @@ +/* ClinicLineTest.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using NUnit.Framework; +using zaaReloaded2.LabModel; +using zaaReloaded2.Importer.ClinicImporter; + +namespace Tests.Importer.ClinicImporter +{ + [TestFixture] + class ClinicLineTest + { + [Test] + public void ParseLine() + { + string demo = "\tNatrium:\t137\t[135 - 145]\tmmol/l"; + ClinicLine line = new ClinicLine(demo); + Assert.IsTrue(line.IsClinicLine); + Assert.AreEqual(137, line.Item.NumericalValue); + } + + [Test] + public void ParseInvalidLine() + { + // Missing leading tab + string demo = "Natrium:\t137\t[135 - 145]\tmmol/l"; + ClinicLine line = new ClinicLine(demo); + Assert.IsFalse(line.IsClinicLine); + } + } +} diff --git a/Tests/Importer/ClinicImporter/TimePointTest.cs b/Tests/Importer/ClinicImporter/TimePointTest.cs new file mode 100755 index 0000000..3569617 --- /dev/null +++ b/Tests/Importer/ClinicImporter/TimePointTest.cs @@ -0,0 +1,67 @@ +/* TimePointTest.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using zaaReloaded2.LabModel; +using zaaReloaded2.Importer.ClinicImporter; + +namespace Tests.Importer.ClinicImporter +{ + [TestFixture] + class ClinicTimePointTest + { + [Test] + public void ParseValidClinicTimePoint() + { + ClinicTimePoint tp = new ClinicTimePoint( + "(06.09.2017 09:45:00)" + Environment.NewLine + + "\tKalium:\t4.6\t[3.5 - 5]\tmmol/l"); + Assert.IsTrue(tp.IsValidTimePoint); + } + + [Test] + public void ParseInvalidClinicTimePoints() + { + ClinicTimePoint tp = new ClinicTimePoint("Mit freundlichen Grüßen"); + Assert.IsFalse(tp.IsValidTimePoint, + "Bogus paragraph should be invalid LaurisTimePoint"); + + tp.ClinicText = "(22.10.2013 12:30:00)"; + Assert.IsFalse(tp.IsValidTimePoint, + "LaurisTimePoint should be invalid if it consists of time stamp only."); + + } + + [Test] + public void ParseClinicTimePointWithDuplicateItems() + { + ClinicTimePoint tp = new ClinicTimePoint( + "(22.10.2013 12:30:00)" + Environment.NewLine + + "\tNatrium:\t137\t[135 - 145]\tmmol/l" + Environment.NewLine + + "\tNatrium:\t140\t[135 - 145]\tmmol/l" + ); + Assert.IsTrue(tp.Items.ContainsKey("Natrium"), + "LaurisTimePoint should contain 'Natrium' item."); + Assert.AreEqual(140, tp.Items["Natrium"].NumericalValue, + "LaurisTimePoint does not use last occurrence of 'Natrium'."); + } + } +} diff --git a/Tests/TestHelpers.cs b/Tests/TestHelpers.cs index a95db83..fa8fff0 100755 --- a/Tests/TestHelpers.cs +++ b/Tests/TestHelpers.cs @@ -21,6 +21,7 @@ using System.IO; using System.Linq; using System.Text; using zaaReloaded2.Importer.ZaaImporter; +using zaaReloaded2.Importer.ClinicImporter; namespace Tests { @@ -38,5 +39,18 @@ namespace Tests importer.Import(r.ReadToEnd()); return importer; } + + /// + /// Creates a ClinicImporter object and imports demo-output-clinic.txt. + /// + /// + public static ClinicImporter ClinicImporterFromResource() + { + Stream s = typeof(TestHelpers).Assembly.GetManifestResourceStream("Tests.demo-output-clinic.txt"); + StreamReader r = new StreamReader(s); + ClinicImporter importer = new ClinicImporter(); + importer.Import(r.ReadToEnd()); + return importer; + } } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 4c7e441..5ac530b 100755 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -91,6 +91,10 @@ + + + + @@ -144,6 +148,10 @@ + + + + diff --git a/Tests/demo-output-clinic.txt b/Tests/demo-output-clinic.txt new file mode 100755 index 0000000..c36f1da --- /dev/null +++ b/Tests/demo-output-clinic.txt @@ -0,0 +1,90 @@ +# This file contains 5 distinct time points. +# This leading text should be ignored. +# DO NOT CHANGE THE FOLLOWING TEXT, LEST THE TESTS WILL FAIL! +(06.09.2017 09:54:00) + Gesamt-Eiweiss (PU): 54 [<= 120] mg/l + Gesamt-Eiweiss/Creatinin (PU): 59 [<= 70] mg/g Crea + Albumin (PU): 4 [<= 30] mg/l + Albumin/Creatinin (PU): 4.4 [<= 30] mg/g Crea + Creatinin (PU): 91.1 [29 - 226] mg/dl + +(06.09.2017 09:54:00) + Erythrozyten (U): negativ [negativ] + Leukozyten (U): + [negativ] + Nitrit (U): negativ [negativ] + Protein (U): negativ [negativ] + Glucose (U): negativ [negativ] + Ketonkörper (U): + [negativ] + Bilirubin (U): negativ [negativ] + Urobilinogen (U): negativ [negativ] + pH (U): 6.500 [4.8 - 7.4] + spezifisches Gewicht (U): 1.015 [1.00 - 1.04] g/ml + Erythrozyten (U): 3 [<= 25] Ery/µl + Leukozyten (U): 4 [<= 20] Leu/µl + Bakterien (U): 193 Bak/µl + Plattenepithelien (U): 25 Epi/µl + Übergangsepithelien (U): 0 Uge/µl + hyaline Zylinder (U): 0 Zyh/µl + +(06.09.2017 09:52:00) + Gesamt-Eiweiss (SU): < 40 [<= 120] mg/l + Albumin (SU): < 3 mg/l + a1-Microglobulin (SU): < 5 mg/l + Immunglobulin G (SU): < 4 mg/l + Sammelzeit (U): 24 h + Sammelmenge (U): 3200 ml + Calcium (SU): 0.26 mmol/l + Calcium (SU)/die: 0.83 [2.5 - 8] mmol/d + Creatinin (SU): 30.2 mg/dl + Harnstoff (SU): 674 [900 - 3000] mg/dl + Harnstoff (SU)/die: 21.6 [<= 35] g/d + Kalium (SU): 45.4 [20 - 80] mmol/l + Kalium (SU)/die: 145.28 [25 - 125] mmol/d + Natrium (SU): 28.9 [54 - 150] mmol/l + Natrium (SU)/die: 92.5 [40 - 220] mmol/d + +(06.09.2017 09:50:00) + Cystatin C (Latex Gen. 2): 1.04 [0.61 - 0.95] mg/l + +(06.09.2017 09:45:00) + Natrium: 137 [135 - 145] mmol/l + Kalium: 4.6 [3.5 - 5] mmol/l + Calcium: 2.4 [2.0 - 2.7] mmol/l + anorg. Phosphat: 1.29 [0.87 - 1.45] mmol/l + Calcium-Phosphat-Produkt: 3.10 [<= 4.4] mmol²/l² + glomerul. Filtrationsr. CKD-EP: 62 ml/min /1,73qm + glomerul. Filtrationsr. (MDRD): 59 ml/min /1,73qm + Creatinin: 1.06 [0 - 0.95] mg/dl + Harnstoff: 52.1 [10 - 50] mg/dl + Lactat Dehydrogenase: 175 [<= 250] U/l + Cholesterin: 180 [130 - 220] mg/dl + Triglyceride: 51 [74 - 172] mg/dl + LDL - Cholesterin: 91 [0 - 150] mg/dl + HDL - Cholesterin: 79 [>= 35] mg/dl + Albumin: 4.5 [3.5 - 5.5] g/dl + Leukozyten: 4.7 [5 - 10] n*1000/µl + Erythrozyten: 4.09 [4 - 5] n*10E6/µl + Hämoglobin: 11.9 [12 - 16] g/dl + Hämatokrit: 36.6 [35 - 47] % + MCV: 89.5 [82 - 96] fl + MCH (HbE): 29.1 [27 - 33] pg + MCHC: 32.5 [32 - 36] g/dl + Thrombozyten: 302 [150 - 450] n*1000/µl + Mittleres Plättchenvolumen: 11.3 [9.6 - 12.0] fl + Neutrophile: 2.60 [1.8 - 7.2] n*1000/µl + Lymphozyten: 1.35 [1 - 4.05] n*1000/µl + Monozyten: 0.47 [0.08 - 0.8] n*1000/µl + Eosinophile: 0.2 [0.04 - 0.36] n*1000/µl + Basophile: 0.1 [0 - 0.08] n*1000/µl + % Neutrophile: 55.1 [41 - 70] % + % Lymphozyten: 28.7 [25 - 40] % + % Monozyten: 10.0 [2 - 8] % + % Eosinophile: 4.7 [0.8 - 6.2] % + % Basophile: 1.3 [0 - 1] % + Unreife Granulozyten: 0.01 n*1000/µl + % Unreife Granulozyten: 0.2 % + C-reaktives Protein: 0.02 [0 - 0.5] mg/dl + +(06.09.2017 09:15:00) + TSH: 1.30 [0.3 - 4.0] mIU/l + PTH intakt: 106.0 [12 - 65] ng/l diff --git a/zaaReloaded2/Commands.cs b/zaaReloaded2/Commands.cs index c6b34d1..c40a37d 100755 --- a/zaaReloaded2/Commands.cs +++ b/zaaReloaded2/Commands.cs @@ -208,7 +208,8 @@ namespace zaaReloaded2 { Logger.Info("DoFormat: Attempting to auto-detect"); Word.Document doc = activeWindow.Document; - if (!AutoDetect.Detect(doc)) + Importer.AutoDetector autoDetector = new Importer.AutoDetector(); + if (!autoDetector.Detect(doc)) { Logger.Info("DoFormat: Automatic detection failed"); NotificationAction a = new NotificationAction(); diff --git a/zaaReloaded2/Demo/Demo.docx b/zaaReloaded2/Demo/Demo.docx index 6206475a3356c47100b5154642e42c1093dd8918..a81f68bd422ba5e769b0c91151eb9223611e938f 100755 GIT binary patch delta 16536 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%0A;w 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 delta 13652 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 Dyow@p diff --git a/zaaReloaded2/Importer/ZaaImporter/AutoDetect.cs b/zaaReloaded2/Importer/AutoDetector.cs similarity index 56% rename from zaaReloaded2/Importer/ZaaImporter/AutoDetect.cs rename to zaaReloaded2/Importer/AutoDetector.cs index 3bf9905..b1798eb 100755 --- a/zaaReloaded2/Importer/ZaaImporter/AutoDetect.cs +++ b/zaaReloaded2/Importer/AutoDetector.cs @@ -20,11 +20,15 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using zaaReloaded2.Importer.ZaaImporter; +using zaaReloaded2.Importer.ClinicImporter; -namespace zaaReloaded2.Importer.ZaaImporter +namespace zaaReloaded2.Importer { - static class AutoDetect + class AutoDetector { + #region Public methods + /// /// Attempts to automatically detect laboratory data in the Word /// document. @@ -34,7 +38,7 @@ namespace zaaReloaded2.Importer.ZaaImporter /// True if laboratory data was detected, false if not. /// if /// is null. - public static bool Detect(Document document) + public bool Detect(Document document) { if (document == null) { @@ -89,17 +93,68 @@ namespace zaaReloaded2.Importer.ZaaImporter return false; } + #endregion + + #region Private methods + + /// + /// Returns true if a paragraph is a time stamp line. + /// + private bool IsTimeStampParagraph(Paragraph paragraph) + { + string text = paragraph.Range.Text; + bool isCinicTimePoint = ClinicTimePoint.IsTimeStampLine(text); + bool isZaaTimePoint = LaurisTimePoint.IsTimeStampLine(text); + // If the line is a ZAA time point, but not a clinic timepoint, we can deduct that + // the lab mode *must* be ZAA, because it will be a line in the form + // "(17.09.2015-201710:44:00) Cyclosporin-A vor Gabe: 130 µg/l;" which does not + // occur in the clinic format. + if ((_mode == Mode.Undefined) && isZaaTimePoint && !isCinicTimePoint) + { + _mode = Mode.Zaa; + } + return isCinicTimePoint || isZaaTimePoint; + } + /// /// Returns true if a paragraph is either a time stamp line /// or a paragraph with laboratory items. /// + /// + /// This method determines the mode: either ZAA-generated output or clinic system-generated + /// output. ZAA is given priority over clinic. Once a mode is detected, it will stick to + /// that mode. + /// /// /// - private static bool IsLabParagraph(Paragraph paragraph) + private bool IsLabParagraph(Paragraph paragraph) { string text = paragraph.Range.Text; - return (LaurisParagraph.ResemblesLaurisParagraph(text) - || LaurisTimePoint.IsTimeStampLine(text)); + bool isLabParagraph = false; + switch (_mode) + { + case Mode.Undefined: + if (LaurisParagraph.ResemblesLaurisParagraph(text) || LaurisTimePoint.IsTimeStampLine(text)) + { + _mode = Mode.Zaa; + isLabParagraph = true; + } + else if (ClinicLine.ResemblesClinicLine(text) || ClinicTimePoint.IsTimeStampLine(text)) + { + _mode = Mode.Clinic; + isLabParagraph = true; + } + break; + case Mode.Zaa: + isLabParagraph = LaurisParagraph.ResemblesLaurisParagraph(text) || LaurisTimePoint.IsTimeStampLine(text); + break; + case Mode.Clinic: + isLabParagraph = ClinicLine.ResemblesClinicLine(text) || ClinicTimePoint.IsTimeStampLine(text); + break; + default: + break; + } + return isLabParagraph; } /// @@ -110,9 +165,24 @@ namespace zaaReloaded2.Importer.ZaaImporter /// /// Paragraph whose index to return. /// Index of the paragraph. - private static int GetParagraphIndex(Document document, Paragraph paragraph) + private int GetParagraphIndex(Document document, Paragraph paragraph) { return document.Range(0, paragraph.Range.Start).Paragraphs.Count; } + + #endregion + + #region Fields + + private enum Mode + { + Undefined, + Zaa, + Clinic + } + + private Mode _mode; + + #endregion } } diff --git a/zaaReloaded2/Importer/BaseImporter.cs b/zaaReloaded2/Importer/BaseImporter.cs new file mode 100755 index 0000000..374085f --- /dev/null +++ b/zaaReloaded2/Importer/BaseImporter.cs @@ -0,0 +1,170 @@ +/* ZaaImporter.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Bovender.Extensions; +using zaaReloaded2.LabModel; +using zaaReloaded2.Thesaurus; + +namespace zaaReloaded2.Importer +{ + /// + /// Base class for certain importers such as ZaaImporter, ClinicImporter. + /// + public class BaseImporter : IImporter + { + #region IImporter implementation + + public Laboratory Laboratory + { + [DebuggerStepThrough] + get + { + if (_laboratory == null) + { + _laboratory = new Laboratory(); + } + return _laboratory; + } + [DebuggerStepThrough] + set + { + _laboratory = value; + } + } + + public bool Success + { + get + { + return Laboratory.TimePoints.Count > 0; + } + } + + /// + /// Splits the into individual time points + /// and creates objects from them. + /// + public void Import(string text) + { + Logger.Info("Import: \"{0}\"", text.TruncateWithEllipsis(120)); + string[] paragraphs = Helpers.SplitParagraphs(text); + Logger.Info("Import: {0} paragraph(s)", paragraphs.Length); + TimePoint timePoint = null; + + foreach (string paragraph in paragraphs) + { + Logger.Info("Import: \"{0}\"", paragraph.TruncateWithEllipsis(40)); + // If the current paragraph looks like a time stamp, + // create a new time point. + if (IsTimeStamp(paragraph)) + { + Logger.Info("Import: Time stamp detected", paragraph); + timePoint = CreateTimePoint(paragraph, _parameters, _units); + // Add the time point to the laboratory only if none + // with the same time stamp exists yet. + TimePoint existing = null; + if (Laboratory.TryGetTimePoint(timePoint.TimeStamp, ref existing)) + { + timePoint = existing; + } + else + { + Laboratory.AddTimePoint(timePoint); + } + } + // If the current paragraph looks like a paragraph with + // laboratory items, add it to the current time point; + // if no time point exists yet, create one. + else if (IsItemsParagraph(paragraph)) + { + Logger.Info("Import: Paragraph with lab items detected"); + if (timePoint == null) + { + timePoint = CreateTimePoint(paragraph, _parameters, _units); + Laboratory.AddTimePoint(timePoint); + } + else + { + timePoint.Parse(paragraph); + } + } + else + { + Logger.Debug("Import: Neither time stamp, nor Lauris paragraph"); + } + } + } + + #endregion + + #region Constructor + + public BaseImporter() + { + _parameters = Parameters.Default; + _units = Units.Default; + } + + #endregion + + #region Virtual methods + + protected virtual TimePoint CreateTimePoint(string paragraph, Parameters parameters, Units units) + { + NotImplementedException e = new NotImplementedException("Cannot create TimePoint in base class - derived class must override CreateTimePoint"); + Logger.Fatal(e); + throw e; + } + + protected virtual bool IsTimeStamp(string paragraph) + { + NotImplementedException e = new NotImplementedException("Cannot test time stamp in base class - derived class must override IsTimeStamp"); + Logger.Fatal(e); + throw e; + } + + protected virtual bool IsItemsParagraph(string paragraph) + { + NotImplementedException e = new NotImplementedException("Cannot test items in base class - derived class must override IsItemsParagraph"); + Logger.Fatal(e); + throw e; + } + + #endregion + + #region Fields + + Laboratory _laboratory; + Parameters _parameters; + Units _units; + + #endregion + + #region Class logger + + private static NLog.Logger Logger { get { return _logger.Value; } } + + private static readonly Lazy _logger = new Lazy(() => NLog.LogManager.GetCurrentClassLogger()); + + #endregion + } +} diff --git a/zaaReloaded2/Importer/ClinicImporter/ClinicImporter.cs b/zaaReloaded2/Importer/ClinicImporter/ClinicImporter.cs new file mode 100755 index 0000000..024985e --- /dev/null +++ b/zaaReloaded2/Importer/ClinicImporter/ClinicImporter.cs @@ -0,0 +1,68 @@ +/* ZaaImporter.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Bovender.Extensions; +using zaaReloaded2.LabModel; +using zaaReloaded2.Thesaurus; + +namespace zaaReloaded2.Importer.ClinicImporter +{ + /// + /// Imports laboratory items by parsing the Lauris data from a + /// physician's letter. + /// + public class ClinicImporter : BaseImporter + { + #region Constructor + + public ClinicImporter() : base() { } + + #endregion + + #region Class logger + + private static NLog.Logger Logger { get { return _logger.Value; } } + + private static readonly Lazy _logger = new Lazy(() => NLog.LogManager.GetCurrentClassLogger()); + + #endregion + + #region Implementation of BaseImporter + + protected override TimePoint CreateTimePoint(string paragraph, Parameters parameters, Units units) + { + return new ClinicTimePoint(paragraph, parameters, units); + } + + protected override bool IsTimeStamp(string paragraph) + { + return ClinicTimePoint.IsTimeStampLine(paragraph); + } + + protected override bool IsItemsParagraph(string paragraph) + { + return ClinicLine.ResemblesClinicLine(paragraph); + } + + #endregion + } +} diff --git a/zaaReloaded2/Importer/ClinicImporter/ClinicLine.cs b/zaaReloaded2/Importer/ClinicImporter/ClinicLine.cs new file mode 100755 index 0000000..43459c9 --- /dev/null +++ b/zaaReloaded2/Importer/ClinicImporter/ClinicLine.cs @@ -0,0 +1,138 @@ +/* ClinicLine.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using zaaReloaded2.Thesaurus; +using zaaReloaded2.LabModel; +using zaaReloaded2.Importer.ZaaImporter; +using Bovender.Extensions; + +namespace zaaReloaded2.Importer.ClinicImporter +{ + /// + /// Parses a line in a lab section produced by the outpatients clinic system, + /// and creates a list of s. + /// + public class ClinicLine + { + #region Static methods + + /// + /// Investigates a paragraph and determines whether it looks + /// like a clinic laboratory items line. + /// + public static bool ResemblesClinicLine(string line) + { + return _expectedFormat.IsMatch(line); + } + + #endregion + + #region Public properties + + public LaurisItem Item { get; private set; } + + /// + /// Gets the original line that this object was constructed from. + /// + public string OriginalLine{ get; private set; } + + /// + /// Is true if the matches the expected + /// format and contains s. + /// + public bool IsClinicLine{ get; private set; } + + #endregion + + #region Constructor + + public ClinicLine(string line) + { + OriginalLine = line; + Parse(); + } + + /// + /// Constructs a object from a given line, + /// using a and a + /// to translate the individual + /// items' properties. + /// + /// line to parse. + /// ParameterDictionary that contains + /// canonical names and material types. + /// Unit dictionary that contains canonical + /// unit names. + public ClinicLine(string line, + Thesaurus.Parameters parameterDictionary, + Thesaurus.Units unitDictionary) + { + OriginalLine = line; + _parameterDictionary = parameterDictionary; + _unitDictionary = unitDictionary; + Parse(); + } + + #endregion + + #region Private methods + + /// + /// Attempts to parse a line. + /// + void Parse() + { + Logger.Info("Parse: \"{0}\"", OriginalLine.TruncateWithEllipsis(40).Replace("\t", " ")); + Match m = _expectedFormat.Match(OriginalLine); + if (m.Success) + { + Logger.Info("Parse: Matches clinic line format"); + Item = new LaurisItem(m.Groups["item"].Value, _parameterDictionary, _unitDictionary); + IsClinicLine = true; + } + else + { + Logger.Info("Parse: Does not match clinic line format"); + IsClinicLine = false; + } + } + + #endregion + + #region Fields + + static readonly Regex _expectedFormat = new Regex(@"\t(?[^:]+:(\t[^\t]+){3})"); + Thesaurus.Parameters _parameterDictionary; + Thesaurus.Units _unitDictionary; + + #endregion + + #region Class logger + + private static NLog.Logger Logger { get { return _logger.Value; } } + + private static readonly Lazy _logger = new Lazy(() => NLog.LogManager.GetCurrentClassLogger()); + + #endregion + } +} diff --git a/zaaReloaded2/Importer/ClinicImporter/ClinicTimePoint.cs b/zaaReloaded2/Importer/ClinicImporter/ClinicTimePoint.cs new file mode 100755 index 0000000..8ca5aeb --- /dev/null +++ b/zaaReloaded2/Importer/ClinicImporter/ClinicTimePoint.cs @@ -0,0 +1,258 @@ +/* ClinicTimePoint.cs + * part of zaaReloaded2 + * + * Copyright 2017 Daniel Kraus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using zaaReloaded2.Thesaurus; +using zaaReloaded2.LabModel; + +namespace zaaReloaded2.Importer.ClinicImporter +{ + /// + /// Holds all laboratory items for a given time point. + /// + class ClinicTimePoint : TimePoint + { + #region Static methods + + /// + /// Examines a string and returns true if it resembles + /// a time stamp line in the clinic output. + /// + /// Line to examine. + /// True if line resembles a time stamp line + /// in the clinic output. + static public bool IsTimeStampLine(string line) + { + return _timeStampRegex.IsMatch(line); + } + + /// + /// Gets a Regex object that matches a clinic time stamp + /// line. + /// + static public Regex TimeStampRegex + { + get + { + return _timeStampRegex; + } + } + + #endregion + + #region Properties + + /// + /// Gets an array of lines in this ClinicText. + /// + public IList Lines + { + [DebuggerStepThrough] + get + { + if (_lines == null) + { + _lines = new List(); + } + return _lines; + } + set + { + _lines = value; + ParseLines(); + } + } + + /// + /// Is true if the LaurisText has time stamp in the first + /// paragraph and s in the others. + /// + public bool IsValidTimePoint + { + get + { + return Items.Count > 0; + } + } + + /// + /// Gets or sets the original Lauris text for this timepoint. + /// + public string ClinicText + { + [DebuggerStepThrough] + get + { + return String.Join(Environment.NewLine, Lines); + } + set + { + if (!String.IsNullOrEmpty(value)) + { + Lines = value.Split( + new string[] { Environment.NewLine }, + StringSplitOptions.None).ToList(); + } + } + } + + #endregion + + #region Constructors + + public ClinicTimePoint() { } + + public ClinicTimePoint( + string clinicText, + Parameters parameterDictionary, + Units unitDictionary) + : this() + { + _parameterDictionary = parameterDictionary; + _unitDictionary = unitDictionary; + ClinicText = clinicText; + } + + public ClinicTimePoint(string clinicText) + : this(clinicText, null, null) + { } + + public ClinicTimePoint( + IList lines, + Parameters parameterDictionary, + Units unitDictionary) + : this(parameterDictionary, unitDictionary) + { + Lines = lines; + } + + public ClinicTimePoint(IList lines) + : this(lines, null, null) + { + } + + public ClinicTimePoint( + Parameters parameterDictionary, + Units unitDictionary) + : this() + { + _parameterDictionary = parameterDictionary; + _unitDictionary = unitDictionary; + } + + #endregion + + #region Public methods + + /// + /// Adds a new line to this time point by parsing + /// the line for a laboratory item. + /// + /// Line to add. + public override void Parse(string paragraph) + { + Lines.Add(paragraph); + ParseLine(paragraph); + } + + #endregion + + #region Private methods + + /// + /// Analyzes each clinic line in this time point, sets the date + /// and time, and collects LabItem data. + /// + void ParseLines() + { + if (Lines != null) + { + foreach (string paragraph in Lines) + { + ParseLine(paragraph); + } + } + } + + void ParseLine(string line) + { + Match m = _timeStampRegex.Match(line); + if (m.Success) + { + Logger.Info("ParseLine: Matches time stamp"); + DateTime dt; + if (DateTime.TryParseExact( + m.Groups["datetime"].Value, + "dd.MM.yyyy HH:mm", + CultureInfo.InvariantCulture, + DateTimeStyles.AllowWhiteSpaces, + out dt)) + { + TimeStamp = dt; + } + } + else + { + Logger.Info("ParseLine: Not a time stamp"); + ClinicLine clinicLine = new ClinicLine( + line, + _parameterDictionary, + _unitDictionary); + if (clinicLine.IsClinicLine) + { + Logger.Debug("ParseParagraph: Merging Lauris paragraph"); + Items[clinicLine.Item.QualifiedName] = clinicLine.Item; + } + } + } + + void AddItems(IItemDictionary items) + { + Items.Merge(items); + } + + #endregion + + #region Private fields + + /// + /// A regular expression that matches the time stamp in the first + /// paragraph of a LaurisText. + /// + static readonly Regex _timeStampRegex = new Regex( + @"^(Labor:?)?\s*\(?\s*(?\d\d\.\d\d\.\d\d\d\d\s+\d\d:\d\d)(:00)?\)\s*$"); + IList _lines; + Parameters _parameterDictionary; + Units _unitDictionary; + + #endregion + + #region Class logger + + private static NLog.Logger Logger { get { return _logger.Value; } } + + private static readonly Lazy _logger = new Lazy(() => NLog.LogManager.GetCurrentClassLogger()); + + #endregion + } +} diff --git a/zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs b/zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs index 3d5d1ef..27978bd 100755 --- a/zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs +++ b/zaaReloaded2/Importer/ZaaImporter/LaurisTimePoint.cs @@ -169,7 +169,7 @@ namespace zaaReloaded2.Importer.ZaaImporter /// the paragraph for laboratory items. /// /// Paragraph to add. - public void AddParagraph(string paragraph) + public override void Parse(string paragraph) { Paragraphs.Add(paragraph); ParseParagraph(paragraph); diff --git a/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs b/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs index fdc869f..48f490c 100755 --- a/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs +++ b/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs @@ -30,106 +30,30 @@ namespace zaaReloaded2.Importer.ZaaImporter /// Imports laboratory items by parsing the Lauris data from a /// physician's letter. /// - public class ZaaImporter : IImporter + public class ZaaImporter : BaseImporter { - #region IImporter implementation - - public Laboratory Laboratory - { - [DebuggerStepThrough] - get - { - if (_laboratory == null) - { - _laboratory = new Laboratory(); - } - return _laboratory; - } - [DebuggerStepThrough] - set - { - _laboratory = value; - } - } - - public bool Success - { - get - { - return Laboratory.TimePoints.Count > 0; - } - } - - /// - /// Splits the into individual time points - /// and creates objects from them. - /// - /// ZAA-formatted Lauris output to import. - public void Import(string text) - { - Logger.Info("Import: \"{0}\"", text.TruncateWithEllipsis(120)); - string[] paragraphs = Helpers.SplitParagraphs(text); - Logger.Info("Import: {0} paragraph(s)", paragraphs.Length); - LaurisTimePoint timePoint = null; - - foreach (string paragraph in paragraphs) - { - Logger.Info("Import: \"{0}\"", paragraph.TruncateWithEllipsis(40)); - // If the current paragraph looks like a Lauris time stamp, - // create a new time point. - if (LaurisTimePoint.IsTimeStampLine(paragraph)) - { - Logger.Info("Import: Time stamp detected", paragraph); - timePoint = new LaurisTimePoint(paragraph, _parameters, _units); - // Add the time point to the laboratory only if none - // with the same time stamp exists yet. - TimePoint existing = null; - if (Laboratory.TryGetTimePoint(timePoint.TimeStamp, ref existing)) - { - timePoint = existing as LaurisTimePoint; - } - else - { - Laboratory.AddTimePoint(timePoint); - } - } - // If the current paragraph looks like a paragraph with - // laboratory items, add it to the current time point; - // if no time point exists yet, create one. - else if (LaurisParagraph.ResemblesLaurisParagraph(paragraph)) - { - Logger.Info("Import: Lauris paragraph detected"); - if (timePoint == null) - { - timePoint = new LaurisTimePoint(_parameters, _units); - Laboratory.AddTimePoint(timePoint); - } - timePoint.AddParagraph(paragraph); - } - else - { - Logger.Debug("Import: Neither time stamp, nor Lauris paragraph"); - } - } - } - - #endregion - #region Constructor - public ZaaImporter() - { - _parameters = Parameters.Default; - _units = Units.Default; - } + public ZaaImporter() : base() { } #endregion - #region Fields + #region Implementation of BaseImporter - Laboratory _laboratory; - Parameters _parameters; - Units _units; + protected override TimePoint CreateTimePoint(string paragraph, Parameters parameters, Units units) + { + return new LaurisTimePoint(paragraph, parameters, units); + } + + protected override bool IsTimeStamp(string paragraph) + { + return LaurisTimePoint.IsTimeStampLine(paragraph); + } + + protected override bool IsItemsParagraph(string paragraph) + { + return LaurisParagraph.ResemblesLaurisParagraph(paragraph); + } #endregion diff --git a/zaaReloaded2/LabModel/TimePoint.cs b/zaaReloaded2/LabModel/TimePoint.cs index a96ffb7..f5c65fe 100755 --- a/zaaReloaded2/LabModel/TimePoint.cs +++ b/zaaReloaded2/LabModel/TimePoint.cs @@ -68,6 +68,7 @@ namespace zaaReloaded2.LabModel { if (String.IsNullOrEmpty(item.QualifiedName)) { + Logger.Fatal("Cannot add item without QualifiedName!"); throw new ArgumentException("Cannot add item that has no qualified name."); } Items.Add(item.QualifiedName, item); @@ -84,8 +85,10 @@ namespace zaaReloaded2.LabModel public void MergeItems(TimePoint otherTimePoint) { if (otherTimePoint == null) + { + Logger.Fatal("Cannot merge null!"); throw new ArgumentNullException("otherTimePoint"); - + } Items.Merge(otherTimePoint.Items); } @@ -101,6 +104,21 @@ namespace zaaReloaded2.LabModel return Items.ContainsKey(item); } + public virtual void Parse(string paragraph) + { + NotImplementedException e = new NotImplementedException("Cannot parse paragraph in base class - derived class must override Parse"); + Logger.Fatal(e); + throw e; + } + + #endregion + + #region Class logger + + private static NLog.Logger Logger { get { return _logger.Value; } } + + private static readonly Lazy _logger = new Lazy(() => NLog.LogManager.GetCurrentClassLogger()); + #endregion } } diff --git a/zaaReloaded2/zaaReloaded2.csproj b/zaaReloaded2/zaaReloaded2.csproj index fd33e25..26438d8 100755 --- a/zaaReloaded2/zaaReloaded2.csproj +++ b/zaaReloaded2/zaaReloaded2.csproj @@ -204,7 +204,11 @@ - + + + + + @@ -466,6 +470,7 @@ + 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)