diff --git a/Tests/Medication/ImporterTest.cs b/Tests/Medication/ImporterTest.cs new file mode 100755 index 0000000..2a69659 --- /dev/null +++ b/Tests/Medication/ImporterTest.cs @@ -0,0 +1,45 @@ +/* ImporterTest.cs + * part of zaaReloaded2 + * + * Copyright 2015 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; + +namespace Tests.Medication +{ + [TestFixture] + class ImporterTest + { + [Test] + public void ImportDrugsTwoColumns() + { + string s = + "Aktuelle Medikation:\r" + + "Advagraf 1 mg 2-0-0 CellCept 500 mg 1-0-1\r" + + "CellCept 250 mg 1-0-1 Decortin 10 mg 1-0-0\r" + + "Beloc-Zok mite 1-0-1 Ramipril 5 mg 0-0-1 (neu)\r" + + "Pantozol 40 mg 0-0-1 Decostriol 0,5 µg 2-0-0\r" + + "Euthyrox 200 µg 1-1-1 (gesteigert) Ossofortin forte 1-0-1\r" + + "Vfend 200 mg 2-0-2 CPS-Pulver 0-1-0\r" + + "Cyklokapron 500 mg 1-1-1 Tamsulosin 0,4 mg 1-0-0 "; + zaaReloaded2.Medication.Importer i = new zaaReloaded2.Medication.Importer(s); + Assert.AreEqual(14, i.Prescriptions.Count); + } + } +} diff --git a/Tests/Medication/PrescriptionTest.cs b/Tests/Medication/PrescriptionTest.cs new file mode 100755 index 0000000..e84fd85 --- /dev/null +++ b/Tests/Medication/PrescriptionTest.cs @@ -0,0 +1,128 @@ +/* PrescriptionTest.cs + * part of zaaReloaded2 + * + * Copyright 2015 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.Medication; + +namespace Tests.Medication +{ + [TestFixture] + class PrescriptionTest + { + [Test] + [TestCase("Ramipril 5 mg 1-2-3", "Ramipril 5 mg", "1", "2", "3")] + [TestCase("Ramipril 5 mg 1 -2 - 3", "Ramipril 5 mg", "1", "2", "3")] + [TestCase("Ramipril 5 mg 1 - 2 - 3", "Ramipril 5 mg", "1", "2", "3")] + [TestCase("Ramipril 5 mg 1 - 2 - 3", "Ramipril 5 mg", "1", "2", "3")] + [TestCase("Ramipril 5 mg 1 1/2-2-3", "Ramipril 5 mg", "1 1/2", "2", "3")] + [TestCase("Ramipril 5 mg 1 1/2 - 2 - 3", "Ramipril 5 mg", "1 1/2", "2", "3")] + [TestCase("Ramipril 5 mg ½-⅓-¼", "Ramipril 5 mg", "½", "⅓", "¼")] + public void ParseLine(string line, string drug, string morning, + string noon, string evening) + { + Prescription p = Prescription.FromLine(line); + Assert.AreEqual(drug, p.Drug, "Drug should be " + drug); + Assert.AreEqual(morning, p.Morning, "Morning should be " + morning); + Assert.AreEqual(noon, p.Noon, "Noon should be " + noon); + Assert.AreEqual(evening, p.Evening, "Evening should be " + evening); + } + + [Test] + public void MultiplePrescriptions() + { + IEnumerable list = Prescription.ManyFromLine( + "Ramipril 5 mg 1-0-0 \t Prograf 1 mg 1-0-1"); + Assert.AreEqual(2, list.Count()); + Assert.AreEqual("Ramipril 5 mg\t1-0-0", list.First().ToString()); + Assert.AreEqual("Prograf 1 mg\t1-0-1", list.Last().ToString()); + } + + [Test] + [TestCase("Ramipril 5 mg", "1", "0", "0", "0", "Ramipril 5 mg\t1-0-0-0")] + [TestCase("Ramipril 5 mg", "1", "0", "0", "", "Ramipril 5 mg\t1-0-0")] + [TestCase("Ramipril 5 mg", "1", "0", "", "", "Ramipril 5 mg\t1-0")] + [TestCase("Ramipril 5 mg", "1", "", "", "", "Ramipril 5 mg\t1")] + [TestCase("Ramipril 5 mg", "1", "", "0", "0", "Ramipril 5 mg\t1-0-0-0")] + [TestCase("Ramipril 5 mg", "1", "0", "", "0", "Ramipril 5 mg\t1-0-0-0")] + [TestCase("Ramipril 5 mg", "1", "", "", "0", "Ramipril 5 mg\t1-0-0-0")] + [TestCase("Ramipril 5 mg", "", "", "", "", "Ramipril 5 mg\t")] + public void PrescriptionToString(string drug, string morning, string noon, + string evening, string night, string formatted) + { + Prescription p = new Prescription(drug, morning, noon, evening, night); + Assert.AreEqual(formatted, p.ToString()); + } + + [Test] + public void PrescriptionWithComment() + { + Prescription p = Prescription.FromLine("Ramipril 5 mg 1-0-2 (gesteigert)"); + Assert.AreEqual("Ramipril 5 mg", p.Drug); + Assert.AreEqual("1", p.Morning); + Assert.AreEqual("0", p.Noon); + Assert.AreEqual("2", p.Evening); + Assert.AreEqual("(gesteigert)", p.Comment); + Assert.AreEqual("Ramipril 5 mg\t1-0-2 (gesteigert)", p.ToString()); + } + + [Test] + public void PrescriptionsLineWithComment() + { + IList list = Prescription.ManyFromLine( + "Ramipril 5 mg 1-0-2 (gesteigert) \t Concor 2,5 mg 3-2-1-0 neu"); + Assert.AreEqual(2, list.Count); + Assert.AreEqual("Ramipril 5 mg", list[0].Drug); + Assert.AreEqual("1", list[0].Morning); + Assert.AreEqual("0", list[0].Noon); + Assert.AreEqual("2", list[0].Evening); + Assert.AreEqual("", list[0].Night); + Assert.AreEqual("(gesteigert)", list[0].Comment); + Assert.AreEqual("Concor 2,5 mg", list[1].Drug); + Assert.AreEqual("3", list[1].Morning); + Assert.AreEqual("2", list[1].Noon); + Assert.AreEqual("1", list[1].Evening); + Assert.AreEqual("0", list[1].Night); + Assert.AreEqual("neu", list[1].Comment); + } + + [Test] + public void PrescriptionWithoutTypicalDosing() + { + Prescription p = Prescription.FromLine("Eusaprim forte\t alle zwei Tage"); + Assert.AreEqual("Eusaprim forte", p.Drug); + Assert.AreEqual("alle zwei Tage", p.Comment); + Assert.AreEqual("Eusaprim forte\talle zwei Tage", p.ToString(), "ToString"); + } + + [Test] + [TestCase("CellCept 500 mg", true)] + [TestCase("Cell CEpt 500 mg", true)] + [TestCase("Myfortic", true)] + [TestCase("Mycophenolatmofetil 500 mg", true)] + [TestCase("Cellophan 5 g", false)] + [TestCase("MMF 500 mg", true)] + public void MmfProperty(string drug, bool isMmf) + { + Prescription p = new Prescription(drug); + Assert.AreEqual(isMmf, p.IsMmf); + } + } +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index c65c868..91145c5 100755 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -83,6 +83,8 @@ + + diff --git a/gimp/m.xcf b/gimp/m.xcf new file mode 100644 index 0000000..bf91a15 Binary files /dev/null and b/gimp/m.xcf differ diff --git a/gimp/mm.xcf b/gimp/mm.xcf new file mode 100644 index 0000000..b4162ed Binary files /dev/null and b/gimp/mm.xcf differ diff --git a/publish/zaaReloaded2.iss b/publish/zaaReloaded2.iss index 9988d0d..0188315 100755 --- a/publish/zaaReloaded2.iss +++ b/publish/zaaReloaded2.iss @@ -3,7 +3,7 @@ ; Apache License Version 2.0 [Setup] -; #define DEBUG +#define DEBUG ; Read the semantic and the installer file version from the VERSION file #define FILE_HANDLE FileOpen("..\zaaReloaded2\VERSION") diff --git a/zaaReloaded2/Commands.cs b/zaaReloaded2/Commands.cs index 6cd54ec..4fbe14e 100755 --- a/zaaReloaded2/Commands.cs +++ b/zaaReloaded2/Commands.cs @@ -138,6 +138,47 @@ namespace zaaReloaded2 Globals.ThisAddIn.Application.Selection); } + public static void FormatDrugs(int columns) + { + if (columns < 1 || columns > 2) + { + throw new ArgumentOutOfRangeException("Can only format 1 or 2 columns, not " + columns); + } + + // If no "real" selection exists, attempt to auto-detect the drugs section. + // (NB Technically, there is never _no_ selection in a document.) + Word.Window activeWindow = Globals.ThisAddIn.Application.ActiveWindow; + Word.Selection sel = activeWindow.Selection; + if (!(sel.Paragraphs.Count > 1 + || (sel.Text.Length > 1 && sel.Text.EndsWith("\r")))) + { + if (!Medication.Importer.AutoDetect(activeWindow.Document)) + { + NotificationAction a = new NotificationAction(); + a.Caption = "Formatieren nicht möglich"; + a.Message = "Das Dokument scheint keine Medikationsliste zu enthalten."; + a.OkButtonLabel = "Schließen"; + a.Invoke(); + return; + } + } + + Medication.Importer importer = new Medication.Importer(activeWindow.Selection.Text); + Medication.Formatter formatter = new Medication.Formatter(importer.Prescriptions); + + switch (columns) + { + case 1: + formatter.FormatOneColumn(activeWindow.Document); + break; + case 2: + formatter.FormatTwoColumns(activeWindow.Document); + break; + default: + break; + } + } + #endregion #region Private methods diff --git a/zaaReloaded2/Demo/Demo.docx b/zaaReloaded2/Demo/Demo.docx index ac0714c..7957a04 100755 Binary files a/zaaReloaded2/Demo/Demo.docx and b/zaaReloaded2/Demo/Demo.docx differ diff --git a/zaaReloaded2/Formatter/DocumentWriter.cs b/zaaReloaded2/Formatter/DocumentWriter.cs index 460ca9f..29548c2 100755 --- a/zaaReloaded2/Formatter/DocumentWriter.cs +++ b/zaaReloaded2/Formatter/DocumentWriter.cs @@ -171,6 +171,14 @@ namespace zaaReloaded2.Formatter _buffer.AppendLine(text); } + /// + /// Appends a newline to the buffer. + /// + public void WriteLine() + { + _buffer.AppendLine(); + } + /// /// Inserts text at the start of the buffer. /// diff --git a/zaaReloaded2/Formatter/Formatter.cs b/zaaReloaded2/Formatter/Formatter.cs index 2d39b55..27c0c2b 100755 --- a/zaaReloaded2/Formatter/Formatter.cs +++ b/zaaReloaded2/Formatter/Formatter.cs @@ -142,13 +142,7 @@ namespace zaaReloaded2.Formatter // Create undo record and styles prior to iterating over the elements // because a column switching element might trigger output to the // document. - bool hasAddin = Globals.ThisAddIn != null; - if (hasAddin) - { - Globals.ThisAddIn.Application.UndoRecord.StartCustomRecord( - String.Format("Laborformatierung ({0})", Properties.Settings.Default.AddinName) - ); - } + Helpers.StartUndo("Laborformatierung"); CreateStyles(); int current = 0; @@ -177,10 +171,7 @@ namespace zaaReloaded2.Formatter } _secondaryBuffer.Flush(); - if (hasAddin) - { - Globals.ThisAddIn.Application.UndoRecord.EndCustomRecord(); - } + Helpers.EndUndo(); } /// diff --git a/zaaReloaded2/Helpers.cs b/zaaReloaded2/Helpers.cs new file mode 100755 index 0000000..b658361 --- /dev/null +++ b/zaaReloaded2/Helpers.cs @@ -0,0 +1,69 @@ +/* Helpers.cs + * part of zaaReloaded2 + * + * Copyright 2015 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; + +namespace zaaReloaded2 +{ + /// + /// Common helper methods. + /// + public static class Helpers + { + /// + /// Splits a text into paragraphs. + /// + /// Text to split. + /// Array of paragraphs in the text. + /// + /// This implementation relies on the fact that the order of + /// splitting strings in C#'s String.Split() method is + /// important; see http://stackoverflow.com/a/8664639/270712 + /// + public static string[] SplitParagraphs(string text) + { + return text.Split( + new string[] { "\r\n", "\n\r", "\r", "\n" }, + StringSplitOptions.RemoveEmptyEntries); + } + + /// + /// Starts a custom undo record. + /// + /// + public static void StartUndo(string message) + { + if (Globals.ThisAddIn != null) + { + Globals.ThisAddIn.Application.UndoRecord.StartCustomRecord( + String.Format("{0} ({1})", message, Properties.Settings.Default.AddinName) + ); + } + } + + /// + /// Ends an undo record. + /// + public static void EndUndo() + { + if (Globals.ThisAddIn != null) + { + Globals.ThisAddIn.Application.UndoRecord.EndCustomRecord(); + } + } + } +} diff --git a/zaaReloaded2/Icons/m.png b/zaaReloaded2/Icons/m.png new file mode 100644 index 0000000..b1c5cba Binary files /dev/null and b/zaaReloaded2/Icons/m.png differ diff --git a/zaaReloaded2/Icons/mm.png b/zaaReloaded2/Icons/mm.png new file mode 100644 index 0000000..0fceba4 Binary files /dev/null and b/zaaReloaded2/Icons/mm.png differ diff --git a/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs b/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs index aea8b8a..3377e3d 100755 --- a/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs +++ b/zaaReloaded2/Importer/ZaaImporter/ZaaImporter.cs @@ -67,12 +67,7 @@ namespace zaaReloaded2.Importer.ZaaImporter /// ZAA-formatted Lauris output to import. public void Import(string text) { - // Split the text into parargraphs. This implementation relies on the fact - // that the order or splitting strings in C#'s String.Split() method is - // important; see http://stackoverflow.com/a/8664639/270712 - string[] paragraphs = text.Split( - new string[] { "\r\n", "\n\r", "\r", "\n" }, - StringSplitOptions.RemoveEmptyEntries); + string[] paragraphs = Helpers.SplitParagraphs(text); LaurisTimePoint timePoint = null; foreach (string paragraph in paragraphs) diff --git a/zaaReloaded2/Medication/Formatter.cs b/zaaReloaded2/Medication/Formatter.cs new file mode 100755 index 0000000..498896b --- /dev/null +++ b/zaaReloaded2/Medication/Formatter.cs @@ -0,0 +1,210 @@ +using Microsoft.Office.Interop.Word; +/* Formatter.cs + * part of zaaReloaded2 + * + * Copyright 2015 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; + +namespace zaaReloaded2.Medication +{ + /// + /// Formats prescriptions + /// + public class Formatter + { + #region Properties + + public IList Prescriptions { get; set; } + + #endregion + + #region Constructor + + public Formatter() { } + + public Formatter(IList prescriptions) + : this() + { + Prescriptions = prescriptions; + } + + #endregion + + #region Methods + + /// + /// Writes a block of prescriptions with one column to a + /// Word document. + /// + /// + public void FormatOneColumn(Document document) + { + DoFormat("Medikation einspaltig formatieren", + document, + writer => + { + foreach (Prescription p in Prescriptions) + { + writer.WriteLine(p.ToString()); + } + }); + } + + /// + /// Writes a block of prescriptions with two columns to a + /// Word document. + /// + /// + public void FormatTwoColumns(Document document) + { + DoFormat("Medikation zweispaltig formatieren", + document, + writer => + { + int half = Prescriptions.Count / 2 + Prescriptions.Count % 2; + for (int i = 0; i < half; i++) + { + writer.Write(Prescriptions[i].ToString()); + if (i + half < Prescriptions.Count) + { + writer.Write("\t" + Prescriptions[i + half].ToString()); + } + writer.WriteLine(); + } + }); + } + + /// + /// Creates a table containing all prescriptions and copies it to + /// the clipboard. + /// + public void CreatePrescriptionsTable() + { + throw new NotImplementedException(); + } + + #endregion + + #region Private methods + + void AddDisclaimer(zaaReloaded2.Formatter.DocumentWriter writer) + { + if (HasMMF()) + { + writer.WriteLine(); + writer.WriteLine("Hinweis: Während und nach Therapie mit Mycophenolsäurederivaten wie CellCept\u00ae " + + "und Myfortic\u00ae müssen Frauen und Männer eine Schwangerschaft sicher verhüten (siehe Rote-Hand-Brief zu " + + "CellCept\u00ae vom 10.11.2015)."); + writer.WriteLine(); + } + writer.WriteLine("Bitte Medikation überprüfen!"); + } + + /// + /// Creates a paragraph and character styles in the document. + /// + void CreateStyles(Document document) + { + if (document != null) + { + Style style; + // Don't see a better way to check for the existence of a particular + // paragraph style than by using a try...catch construction. + try + { + style = document.Styles[Properties.Settings.Default.DrugsParagraph]; + } + catch + { + // Add default paragraph style for laboratory + style = document.Styles.Add(Properties.Settings.Default.DrugsParagraph); + style.Font.Size = 10; // pt + style.Font.Bold = 0; + style.Font.Italic = 0; + style.Font.Underline = 0; + style.ParagraphFormat.SpaceAfter = 0; + style.ParagraphFormat.SpaceBefore = 0; + style.ParagraphFormat.LeftIndent = 0; // pt + style.ParagraphFormat.FirstLineIndent = 0; // pt + style.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphLeft; + style.ParagraphFormat.TabStops.ClearAll(); + int tabStop = 108; // 108 pt = 2.5 in = 3.8 cm + int halfWay = 227; // 227 pt = 3.15 in = 8 cm + style.ParagraphFormat.TabStops.Add(tabStop); + style.ParagraphFormat.TabStops.Add(halfWay); + style.ParagraphFormat.TabStops.Add(halfWay + tabStop); + } + + // try + // { + // style = document.Styles[Properties.Settings.Default.DrugsHeader]; + // } + // catch + // { + // // Add header paragraph style for laboratory + // style = document.Styles.Add(Properties.Settings.Default.DrugsHeader); + // style.Font.Size = 10; // pt + // style.Font.Bold = 1; + // style.Font.Italic = 0; + // style.Font.Underline = WdUnderline.wdUnderlineSingle; + // style.ParagraphFormat.SpaceAfter = 0; + // style.ParagraphFormat.SpaceBefore = 12; + // style.ParagraphFormat.LeftIndent = 36; // pt + // style.ParagraphFormat.FirstLineIndent = -36; // pt + // style.ParagraphFormat.Alignment = WdParagraphAlignment.wdAlignParagraphJustify; + // style.set_NextParagraphStyle(document.Styles[Properties.Settings.Default.DrugsParagraph]); + // } + } + } + + /// + /// Does the heavy lifting in a DRY way. + /// + void DoFormat(string description, Document document, + Action outputAction) + { + if (document == null) + { + throw new ArgumentNullException( + "Cannot format prescriptions because no document was given."); + } + + Helpers.StartUndo(description); + zaaReloaded2.Formatter.DocumentWriter writer = new zaaReloaded2.Formatter.DocumentWriter(document); + CreateStyles(document); + writer.Write(String.Format("", Properties.Settings.Default.DrugsParagraph)); + outputAction(writer); + AddDisclaimer(writer); + // writer.Write(""); // causes COM exceptions, needs fix + writer.Flush(); + Helpers.EndUndo(); + } + + /// + /// Determines whether MMF or MPA is contained in the prescriptions. + /// + /// True if MMF or MPA is prescribed. + bool HasMMF() + { + return Prescriptions.FirstOrDefault(p => p.IsMmf) != null; + } + + #endregion + } +} diff --git a/zaaReloaded2/Medication/Importer.cs b/zaaReloaded2/Medication/Importer.cs new file mode 100755 index 0000000..393ac34 --- /dev/null +++ b/zaaReloaded2/Medication/Importer.cs @@ -0,0 +1,208 @@ +using Microsoft.Office.Interop.Word; +/* Importer.cs + * part of zaaReloaded2 + * + * Copyright 2015 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; + +namespace zaaReloaded2.Medication +{ + /// + /// Imports prescriptions from a physician's letter. + /// + public class Importer + { + #region Static methods + + /// + /// Attempts to automatically detect a block of prescriptions + /// in a document. The document is screened from end to start. + /// The detected block is selected. + /// + /// True if a block was detected, false if not. + /// + /// + /// Autodetection works by examining the document paragraph by + /// paragraph, starting at the end of the document. The first + /// block of at least two lines that are identified as prescription + /// lines is selected. + /// + /// + /// It should be noted that every paragraph (a.k.a. line) may + /// be regarded as one of three things: + /// + /// + /// A typical prescription line (in the form "Ramipril 5 mg 1-0-0") + /// A typical non-prescription text line + /// Something inbetween, e.g. a line with tab stops as in + /// "Ramipril 5 mg \t alle zwei Tage" or in "Prof. B. Oss \t Dr. A. Sistent" + /// + /// + /// + /// It is the third type of line that may cause confusion. If such a line + /// is encountered at the start of a putative block of prescriptions, we + /// therefore enter a "fuzzy" state in the detection algorithm and take + /// it from there, i.e. disregard the block if there are no lines that + /// are clearly prescriptions lines, or accept the block if we do detect + /// adjacent lines with unequivocal prescriptions. + /// + /// + /// + public static bool AutoDetect(Document document) + { + Paragraph start = null; + Paragraph end = null; + bool insideBlock = false; + bool fuzzy = false; + bool result = false; + int i = document.Paragraphs.Count; + + while (i > 1) + { + string line = document.Paragraphs[i].Range.Text; + + if (Prescription.IsCanonicalPrescriptionLine(line)) + { + // The current line is unequivocally a prescription line: + // If we're not inside a block already, mark the bottom + // of the block. + // If we are inside a block already, make sure to leave + // the 'fuzzy' state because this clearly now is a prescription + // block. + if (insideBlock) + { + fuzzy = false; + } + else + { + end = document.Paragraphs[i]; + insideBlock = true; + } + } + else if (Prescription.IsPotentialPrescriptionLine(line)) + { + // The current line is a putative prescription line: + // If we're not inside a block already, enter the + // "fuzzy" state. + // If we are inside a block, no special action is + // needed, we can continue with the next paragraph. + if (!insideBlock) + { + fuzzy = true; + insideBlock = true; + end = document.Paragraphs[i]; + } + } + else + { + // The current line is not a prescription line: + // If we are currently in a definitive block of prescriptions, + // mark the line below the current line as the start of the block. + // If we're in a putative block, discard the information + // about the bottom end of the block and reset all flags. + if (insideBlock) + { + if (!fuzzy) + { + start = document.Paragraphs[i + 1]; + break; + } + else + { + fuzzy = false; + insideBlock = false; + end = null; + } + } + } + + i--; + } + + if (end != null) + { + // If we don't have a start paragraph, + // but do have an end paragraph, we set the start paragraph to the + // first paragraph of the document. + if (start == null) + { + start = document.Paragraphs[1]; + } + document.Range(start.Range.Start, end.Range.End).Select(); + result = true; + } + return result; + } + + #endregion + + #region Properties + + public List Prescriptions { get; protected set; } + + #endregion + + #region Constructor + + public Importer() { } + + public Importer(string text) + : this() + { + Import(text); + } + + #endregion + + #region Private methods + + protected virtual void Import(string text) + { + List list = new List(); + IList addition; + int columns = 1; + string[] lines = Helpers.SplitParagraphs(text); + foreach (string line in lines) + { + if (Prescription.IsCanonicalPrescriptionLine(line)) + { + addition = Prescription.ManyFromLine(line); + columns = System.Math.Max(columns, addition.Count); + list.AddRange(addition); + } + } + + // If the input had several columns, sort the prescriptions by + // column. + // TODO: Make this more generic so it works with 3 or 4 columns as well. + if (columns == 2) + { + var firstCol = list.Where((item, index) => index % 2 == 0); + var secondCol = list.Where((item, index) => index % 2 != 0); + Prescriptions = firstCol.Concat(secondCol).ToList(); + } + else + { + Prescriptions = list; + } + } + + #endregion + } +} diff --git a/zaaReloaded2/Medication/Prescription.cs b/zaaReloaded2/Medication/Prescription.cs new file mode 100755 index 0000000..adbe5c7 --- /dev/null +++ b/zaaReloaded2/Medication/Prescription.cs @@ -0,0 +1,267 @@ +/* Prescription.cs + * part of zaaReloaded2 + * + * Copyright 2015 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.Text.RegularExpressions; + +namespace zaaReloaded2.Medication +{ + /// + /// Represents a prescription + /// + public class Prescription + { + #region Static methods + + /// + /// Determines whether a line contains prescriptions. + /// + /// Line to inspect. + /// True if the line contains prescriptions. + public static bool IsCanonicalPrescriptionLine(string line) + { + return canonicalRegex.IsMatch(line); + } + + public static bool IsPotentialPrescriptionLine(string line) + { + return alternativeRegex.IsMatch(line); + } + + #endregion + + #region Factory + + /// + /// Creates a new Prescription object by parsing a line (e.g., + /// from a physician's letter). + /// + /// Line to parse + /// Prescription created from the + public static Prescription FromLine(string line) + { + // Replace any runs of whitespace with a single space + // (from http://stackoverflow.com/a/206946/270712) + // line = Regex.Replace(line, @"\s+", " "); + Match m = unifiedRegex.Match(line); + int n = m.Groups[DOSE_GROUP].Captures.Count; + + return new Prescription( + spaceRegex.Replace(m.Groups["drug"].Value, " "), + n > 0 ? m.Groups[DOSE_GROUP].Captures[0].Value : String.Empty, + n > 1 ? m.Groups[DOSE_GROUP].Captures[1].Value : String.Empty, + n > 2 ? m.Groups[DOSE_GROUP].Captures[2].Value : String.Empty, + n > 3 ? m.Groups[DOSE_GROUP].Captures[3].Value : String.Empty, + m.Groups["comment"].Value + ); + } + + /// + /// Extracts several prescriptions from a given line. + /// + /// Line that contains several prescriptions. + /// Enumerable with s. + public static IList ManyFromLine(string line) + { + // line = Regex.Replace(line, @"\s+", " "); + MatchCollection mc = unifiedRegex.Matches(line); + List list = new List(); + foreach (Match m in mc) + { + int n = m.Groups[DOSE_GROUP].Captures.Count; + list.Add(new Prescription( + spaceRegex.Replace(m.Groups["drug"].Value, " "), + n > 0 ? m.Groups[DOSE_GROUP].Captures[0].Value : String.Empty, + n > 1 ? m.Groups[DOSE_GROUP].Captures[1].Value : String.Empty, + n > 2 ? m.Groups[DOSE_GROUP].Captures[2].Value : String.Empty, + n > 3 ? m.Groups[DOSE_GROUP].Captures[3].Value : String.Empty, + m.Groups["comment"].Value + ) + ); + } + return list; + } + + #endregion + + #region Properties + + public string Drug { get; set; } + + public string Morning { get; set; } + + public string Noon { get; set; } + + public string Evening { get; set; } + + public string Night { get; set; } + + public string Comment { get; set; } + + /// + /// Determines whether the drug is MMF or a derivative. + /// + public bool IsMmf + { + get + { + string d = Drug.ToLower(); + return + d.StartsWith("mmf") || + d.StartsWith("cellcept") || + d.StartsWith("cell cept") || + d.StartsWith("myfortic") || + d.StartsWith("mycophenol"); + } + } + + #endregion + + #region Overrides + + public override string ToString() + { + string s = Drug + "\t"; + if (!String.IsNullOrEmpty(Morning)) + { + s += Morning; + } + else + { + if (!(String.IsNullOrEmpty(Noon) && String.IsNullOrEmpty(Evening) && + String.IsNullOrEmpty(Night))) + { + s += "0"; + } + } + if (!String.IsNullOrEmpty(Noon)) + { + s += "-" + Noon; + } + else + { + if (!(String.IsNullOrEmpty(Evening) && String.IsNullOrEmpty(Night))) + { + s += "-0"; + } + } + if (!String.IsNullOrEmpty(Evening)) + { + s += "-" + Evening; + } + else + { + if (!String.IsNullOrEmpty(Night)) + { + s += "-0"; + } + } + if (!String.IsNullOrEmpty(Night)) + { + s += "-" + Night; + } + if (!String.IsNullOrEmpty(Comment)) + { + if (!s.EndsWith("\t")) + { + s += " "; + } + s += Comment; + } + return s; + } + + #endregion + + #region Constructors + + public Prescription() { } + + public Prescription(string drug) + : this() + { + Drug = drug.Trim(); + } + + public Prescription(string drug, string morning, string noon, + string evening, string night) + : this(drug) + { + Morning = morning.Trim(); + Noon = noon.Trim(); + Evening = evening.Trim(); + Night = night.Trim(); + } + + public Prescription(string drug, string morning, string noon, + string evening, string night, string comment) + : this(drug, morning, noon, evening, night) + { + Comment = comment.Trim(); + } + + #endregion + + #region Fields + + private const string DOSE_GROUP = "dose"; + private const string DOSE = @"(\d\s+1/[234]|(\d\s?)?[\u00bd\u2153\u00bc]|\d+)"; + private const string SPACER = @"(\s*[-\u2012\u2013\u2014]+\s*)"; + + /// + /// The 'canonical' regex matches a prescription the form "Ramipril 5 mg 1-0-0" + /// with or without trailing comment. + /// + /// + /// Enclose entire regular expression in parentheses so we can use it + /// with or without trailing comment. + /// + private const string canonicalPattern = + @"((?[^\t]+)\s+" + + @"(?" + DOSE + @")" + SPACER + + @"(?" + DOSE + @")" + SPACER + + @"(?" + DOSE + @")" + + @"(" + SPACER + @"(?" + DOSE + @"))?" + + @"( +(?[^\t]+))?\s*)"; + private static readonly Regex canonicalRegex = new Regex(canonicalPattern); + + /// + /// The 'alternative' regex matches prescriptions that do not contain regular + /// dosing intervals ("1-0-0"), but free-style comments: "Cotrim forte alle 2 Tage". + /// + /// + /// Because this alternative pattern matches other lines as well (e.g. with + /// signature names), it requires special handling. + /// + private const string alternativePattern = + @"((?[^\t]+)( +|\t+)(?[^\t]+))"; + private static readonly Regex alternativeRegex = new Regex(alternativePattern); + + private static readonly Regex unifiedRegex = new Regex( + "(" + canonicalPattern + "|" + alternativePattern + ")"); + + /// + /// A 'cached', reusable regex to match several whitespace characters. + /// + private static readonly Regex spaceRegex = new Regex(@"\s+"); + + #endregion + } +} diff --git a/zaaReloaded2/Properties/Settings.Designer.cs b/zaaReloaded2/Properties/Settings.Designer.cs index 53fccbd..3ef9b9e 100755 --- a/zaaReloaded2/Properties/Settings.Designer.cs +++ b/zaaReloaded2/Properties/Settings.Designer.cs @@ -276,5 +276,14 @@ namespace zaaReloaded2.Properties { this["NeedUpgrade"] = value; } } + + [global::System.Configuration.ApplicationScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("zaaReloaded2-Medikamente")] + public string DrugsParagraph { + get { + return ((string)(this["DrugsParagraph"])); + } + } } } diff --git a/zaaReloaded2/Properties/Settings.settings b/zaaReloaded2/Properties/Settings.settings index d448e89..0416614 100755 --- a/zaaReloaded2/Properties/Settings.settings +++ b/zaaReloaded2/Properties/Settings.settings @@ -80,5 +80,8 @@ True + + zaaReloaded2-Medikamente + \ No newline at end of file diff --git a/zaaReloaded2/Ribbon.cs b/zaaReloaded2/Ribbon.cs index d098166..f008fbc 100755 --- a/zaaReloaded2/Ribbon.cs +++ b/zaaReloaded2/Ribbon.cs @@ -87,7 +87,7 @@ namespace zaaReloaded2 { switch (control.Id) { - case "zrlFormat": + case "zrlFormatLab": Commands.Format(); break; case "zrlSettings": @@ -105,6 +105,12 @@ namespace zaaReloaded2 case "zrlDemo": Commands.LoadDemo(); break; + case "zrlFormatDrugsOneCol": + Commands.FormatDrugs(1); + break; + case "zrlFormatDrugsTwoCol": + Commands.FormatDrugs(2); + break; default: throw new InvalidOperationException("No operation defined for " + control.Id); } @@ -150,6 +156,11 @@ namespace zaaReloaded2 return Commands.CanFormat(); } + public bool CanFormatDrugs(Office.IRibbonControl control) + { + return Commands.CanFormat(); + } + #endregion #region Public methods diff --git a/zaaReloaded2/Ribbon.xml b/zaaReloaded2/Ribbon.xml index 0fb6d2e..db54938 100755 --- a/zaaReloaded2/Ribbon.xml +++ b/zaaReloaded2/Ribbon.xml @@ -23,12 +23,22 @@ - -