From d7af047b5405e49c445d271a75149c6a3e18ef2c Mon Sep 17 00:00:00 2001 From: halcwb Date: Sat, 24 Jan 2026 10:49:45 +0100 Subject: [PATCH 1/3] fix: fix med from string missing properties --- src/Informedica.GenFORM.Lib/DoseLimit.fs | 2 + src/Informedica.GenORDER.Lib/Medication.fs | 36 ++++++++---- .../Scripts/Medication.fsx | 55 +++++++++++++++++++ src/Informedica.GenUNITS.Lib/ValueUnit.fs | 9 +++ 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/src/Informedica.GenFORM.Lib/DoseLimit.fs b/src/Informedica.GenFORM.Lib/DoseLimit.fs index ce7eda2e..9b894730 100644 --- a/src/Informedica.GenFORM.Lib/DoseLimit.fs +++ b/src/Informedica.GenFORM.Lib/DoseLimit.fs @@ -11,6 +11,7 @@ module DoseLimit = /// Field labels for deterministic parsing module FieldLabels = + let [] DoseUnit = "[dun]" let [] Quantity = "[qty]" let [] QuantityAdjust = "[qty-adj]" let [] PerTime = "[per-time]" @@ -163,6 +164,7 @@ module DoseLimit = let emptyS = "" [ $"%s{dl.DoseLimitTarget |> LimitTarget.toString}" + $"{FieldLabels.DoseUnit} %s{dl.DoseUnit |> Units.toStringEngShortWithoutGroup}" $"%s{dl.Rate |> printMinMaxDose FieldLabels.Rate emptyS}" $"%s{dl.RateAdjust |> printMinMaxDose FieldLabels.RateAdjust emptyS}" diff --git a/src/Informedica.GenORDER.Lib/Medication.fs b/src/Informedica.GenORDER.Lib/Medication.fs index 781d950a..b1d9d506 100644 --- a/src/Informedica.GenORDER.Lib/Medication.fs +++ b/src/Informedica.GenORDER.Lib/Medication.fs @@ -229,6 +229,7 @@ module Medication = else // All known field labels let allLabels = [ + DoseLimit.FieldLabels.DoseUnit DoseLimit.FieldLabels.Quantity DoseLimit.FieldLabels.QuantityAdjust DoseLimit.FieldLabels.PerTime @@ -314,17 +315,26 @@ module Medication = let valueStr = m.Groups[2].Value.Trim().TrimEnd(',').Trim() let fullLabel = $"[{labelContent}]" - let labelMatch = - fieldParsers - |> List.tryFind (fun (label, _, _) -> label = fullLabel) - - match labelMatch with - | Some (label, parser, setter) -> - match parser valueStr with - | Ok mm -> dl <- setter dl mm - | Error e -> errors <- $"{label}: {e}" :: errors - | None -> - errors <- $"Unknown field label: {fullLabel}" :: errors + // Skip empty values - they represent default/unset fields + if valueStr |> String.IsNullOrWhiteSpace then + () // Skip this field + elif fullLabel = DoseLimit.FieldLabels.DoseUnit then + let dun = valueStr |> Units.fromString + match dun with + | Some un -> dl <- { dl with DoseUnit = un } + | None -> errors <- $"Unknown dose unit: {valueStr}"::errors + else + let labelMatch = + fieldParsers + |> List.tryFind (fun (label, _, _) -> label = fullLabel) + + match labelMatch with + | Some (label, parser, setter) -> + match parser valueStr with + | Ok mm -> dl <- setter dl mm + | Error e -> errors <- $"{label}: {e}" :: errors + | None -> + errors <- $"Unknown field label: {fullLabel}" :: errors // If no labeled matches and we have constraintsStr, return error requiring labels if matches.Count = 0 && not (constraintsStr |> String.IsNullOrWhiteSpace) then @@ -353,6 +363,10 @@ module Medication = if not (valueStr |> String.IsNullOrWhiteSpace) then match label with + | "qts" -> + match parseValueUnitOpt valueStr with + | Ok vuOpt -> sl <- { sl with Quantities = vuOpt } + | Error e -> errors <- $"[qts]: {e}"::errors | "qty" -> match parseMinMax valueStr with | Ok mm -> sl <- { sl with Quantity = mm } diff --git a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx index dabc279d..bb099dbd 100644 --- a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx +++ b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx @@ -419,3 +419,58 @@ printfn "\n=== Demo: tpn ===" Scenarios.tpn |> Medication.toString |> print + + +""" +Informative +Medication created: Id: f1415ef2-eeb1-4020-89f4-e30eac94486a +Name: amoxicilline/clavulaanzuur +Quantity: +Quantities: +Route: INTRAVENEUS +OrderType: DiscontinuousOrder +Adjust: 17 kg +Frequencies: 3 x/day +Time: +Dose: [dun] ml +Div: +DoseCount: 1 x +Components: + + Name: amoxicilline/clavulaanzuur + Form: poeder voor injectievloeistof + Quantities: 1 ml + Divisible: 10 + Dose: + Solution: + Substances: + + Name: amoxicilline + Concentrations: 50 mg/ml + Dose: amoxicilline, [dun] mg, [per-time-adj] 100 mg/kg/day, [per-time] max 6000 mg/day + Solution: [conc] 20 mg/ml + + Name: clavulaanzuur + Concentrations: 5;10 mg/ml + Dose: clavulaanzuur, [dun] mg, [per-time-adj] 10 mg/kg/day, [per-time] max 600 mg/day + Solution: + + Name: NaCl 0,9% + Form: vloeistof + Quantities: 1 ml + Divisible: 10 + Dose: + Solution: + Substances: + + Name: natrium + Concentrations: 0.155 mmol/ml + Dose: + Solution: + + Name: chloor + Concentrations: 0.155 mmol/ml + Dose: + Solution: +""" +|> Medication.fromString diff --git a/src/Informedica.GenUNITS.Lib/ValueUnit.fs b/src/Informedica.GenUNITS.Lib/ValueUnit.fs index a19035d2..be3373db 100644 --- a/src/Informedica.GenUNITS.Lib/ValueUnit.fs +++ b/src/Informedica.GenUNITS.Lib/ValueUnit.fs @@ -1760,6 +1760,15 @@ module Units = let toStringEngShort = toString true English Short + /// + /// Turn a unit to an english short string without group annotation + /// + /// + /// toStringEngShort (Time (Day 1N)) = "day" + /// + let toStringEngShortWithoutGroup = + toString false English Short + /// /// Turn a unit to an english long string with group annotation /// From 5f9a8b8e20a4846908c05c5cbc20ab527197a7ac Mon Sep 17 00:00:00 2001 From: halcwb Date: Sun, 25 Jan 2026 11:06:40 +0100 Subject: [PATCH 2/3] fix: valueunit remove old units from string hack, improve layout medication logging, handle once without a dose case order printing, improve dose rule validation depending on schedule --- src/Informedica.GenFORM.Lib/DoseRule.fs | 27 +- src/Informedica.GenORDER.Lib/Order.fs | 12 +- src/Informedica.GenORDER.Lib/OrderLogging.fs | 2 +- .../Scripts/Medication.fsx | 233 +++++++++++++++--- src/Informedica.GenUNITS.Lib/Scripts/Api.fsx | 7 + src/Informedica.GenUNITS.Lib/ValueUnit.fs | 4 +- 6 files changed, 238 insertions(+), 47 deletions(-) diff --git a/src/Informedica.GenFORM.Lib/DoseRule.fs b/src/Informedica.GenFORM.Lib/DoseRule.fs index eb0841ef..4c64b97f 100644 --- a/src/Informedica.GenFORM.Lib/DoseRule.fs +++ b/src/Informedica.GenFORM.Lib/DoseRule.fs @@ -535,16 +535,25 @@ module DoseRule = createError "getDataResult" exn - let doseRuleDataIsValid (dd: DoseRuleData) = - dd.DoseType |> String.notEmpty && - (dd.Frequencies |> Array.length > 0 && dd.FreqUnit |> String.notEmpty || - dd.MaxQty |> Option.isSome || - dd.MaxQtyAdj |> Option.isSome || - dd.MaxPerTime |> Option.isSome || - dd.MaxPerTime |> Option.isSome || - dd.MaxRate |> Option.isSome || - dd.MaxRateAdj |> Option.isSome) + match dd.DoseText |> DoseType.fromString dd.DoseType with + | NoDoseType -> + // assume an empty dose type is deliberate + if dd.DoseType |> String.notEmpty then + $"Not valid dose rule data:\n{dd}\n" + |> ConsoleWriter.NewLineNoTime.writeWarningMessage + false + | Once _ -> true + | OnceTimed _ -> + dd.MaxTime.IsSome && dd.TimeUnit |> String.notEmpty + | Discontinuous _ -> + dd.Frequencies |> Array.length > 0 && dd.FreqUnit |> String.notEmpty + | Timed _ -> + dd.Frequencies |> Array.length > 0 && dd.FreqUnit |> String.notEmpty && + dd.MaxTime.IsSome && dd.TimeUnit |> String.notEmpty + | Continuous _ -> + dd.RateUnit |> String.notEmpty && + (dd.MaxRate.IsSome || dd.MaxRateAdj.IsSome) let processDoseRuleData prods routeMapping (data, msgs) : GenFormResult<_> = diff --git a/src/Informedica.GenORDER.Lib/Order.fs b/src/Informedica.GenORDER.Lib/Order.fs index 6bf8937f..d0cc882e 100644 --- a/src/Informedica.GenORDER.Lib/Order.fs +++ b/src/Informedica.GenORDER.Lib/Order.fs @@ -4118,9 +4118,14 @@ module Order = [| [| // the orderable dose quantity - ord.Orderable - |> Orderable.Print.doseQuantityTo printMd -1 - |> wrap Alert [ ord.Orderable.Dose.Quantity |> Quantity.toOrdVar ] + let doseQty = + ord.Orderable + |> Orderable.Print.doseQuantityTo printMd -1 + // special case when no dose quantity + if doseQty |> String.isNullOrWhiteSpace then "eenmalig" |> Valid + else + doseQty + |> wrap Alert [ ord.Orderable.Dose.Quantity |> Quantity.toOrdVar ] // the orderable dose adjust quantity if useAdj then @@ -4129,7 +4134,6 @@ module Order = ord.Orderable |> Orderable.Print.doseQuantityAdjustTo printMd -1 |> wrap Alert [ ord.Orderable.Dose.QuantityAdjust |> QuantityAdjust.toOrdVar ] - |] |] else diff --git a/src/Informedica.GenORDER.Lib/OrderLogging.fs b/src/Informedica.GenORDER.Lib/OrderLogging.fs index cfc132b3..6a2476e8 100644 --- a/src/Informedica.GenORDER.Lib/OrderLogging.fs +++ b/src/Informedica.GenORDER.Lib/OrderLogging.fs @@ -93,7 +93,7 @@ module OrderLogging = $"Scenario {oid}: {n |> Name.toString} = {v}" | Events.MedicationCreated m -> - $"Medication created: {m}" + $"Medication created:\n\n{m}\n" | Events.ComponentItemsHarmonized s -> s diff --git a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx index bb099dbd..40071a21 100644 --- a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx +++ b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx @@ -361,9 +361,6 @@ Components: runTestsWithCLIArgs [] [||] tests -"10 mg/kg - 20 mg/kg" -|> MinMax.parseMinMax - // Demo: Show the new labeled output format printfn "\n=== Demo: Labeled DoseLimit output ===" Scenarios.amfo @@ -421,56 +418,228 @@ Scenarios.tpn |> print +module MedicationTexts = + + + let onceSingleComponentSingleItemNoDose = """ +Id: 4b73efb9-97b3-44b1-af51-bfad909a9371 +Name: chloorhexidine +Quantity: +Quantities: +Route: CUTAAN +OrderType: OnceOrder +Adjust: 11 kg +Frequencies: +Time: +Dose: [dun] +Div: +DoseCount: 1 x +Components: + + Name: chloorhexidine + Form: creme + Quantities: 1 g + Divisible: + Dose: chloorhexidine, [dun] x + Solution: + Substances: + + Name: was + Concentrations: 150 mg/g + Dose: + Solution: + + Name: decyloleaat + Concentrations: 200 mg/g + Dose: + Solution: + + Name: sorbitol + Concentrations: 28 mg/g + Dose: + Solution: + + Name: chloorhexidine + Concentrations: 10 mg/g + Dose: + Solution: """ -Informative -Medication created: Id: f1415ef2-eeb1-4020-89f4-e30eac94486a -Name: amoxicilline/clavulaanzuur + + + // Once single component single item scenario + let onceSingleComponentSingleItem = """ +Id: 93e8c175-99a1-48d8-b2f4-90005fdb8ada +Name: paracetamol Quantity: Quantities: -Route: INTRAVENEUS -OrderType: DiscontinuousOrder -Adjust: 17 kg -Frequencies: 3 x/day +Route: RECTAAL +OrderType: OnceOrder +Adjust: 10 kg +Frequencies: Time: -Dose: [dun] ml +Dose: [dun], [qty] 1 stuk/dosis Div: DoseCount: 1 x Components: - Name: amoxicilline/clavulaanzuur - Form: poeder voor injectievloeistof - Quantities: 1 ml - Divisible: 10 + Name: paracetamol + Form: zetpil + Quantities: 1 stuk + Divisible: 1 Dose: Solution: Substances: - Name: amoxicilline - Concentrations: 50 mg/ml - Dose: amoxicilline, [dun] mg, [per-time-adj] 100 mg/kg/day, [per-time] max 6000 mg/day - Solution: [conc] 20 mg/ml - - Name: clavulaanzuur - Concentrations: 5;10 mg/ml - Dose: clavulaanzuur, [dun] mg, [per-time-adj] 10 mg/kg/day, [per-time] max 600 mg/day + Name: paracetamol + Concentrations: 120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk + Dose: paracetamol, [dun] mg, [qty-adj] 40 mg/kg/dosis, [qty] max 1000 mg/dosis Solution: +""" - Name: NaCl 0,9% - Form: vloeistof - Quantities: 1 ml + // OnceTimed single component single item scenario + let onceTimedSingleComponentSingleItem = """ +Id: 95c44266-84c5-4969-a815-9fbf2c9ed693 +Name: paracetamol +Quantity: +Quantities: +Route: INTRAVENEUS +OrderType: OnceTimedOrder +Adjust: 10 kg +Frequencies: +Time: 15 min - 20 min +Dose: [dun], [qty-adj] max 20 ml/kg/dosis, [qty] max 1000 ml/dosis +Div: +DoseCount: 1 x +Components: + + Name: paracetamol + Form: infusievloeistof + Quantities: 100;50 ml Divisible: 10 Dose: Solution: Substances: - Name: natrium - Concentrations: 0.155 mmol/ml - Dose: + Name: paracetamol + Concentrations: 10 mg/ml + Dose: paracetamol, [dun] mg, [qty-adj] 20 mg/kg/dosis, [qty] max 1000 mg/dosis Solution: +""" - Name: chloor - Concentrations: 0.155 mmol/ml - Dose: + // Discontinuous single component single item scenario + let discontinuousSingleComponentSingleItem = """ +d: e043a880-70ec-4600-8e5a-109e5ef39108 +Name: paracetamol +Quantity: +Quantities: +Route: RECTAAL +OrderType: DiscontinuousOrder +Adjust: 10 kg +Frequencies: 3;4 x/day +Time: +Dose: [dun], [qty] 1 stuk/dosis +Div: +DoseCount: 1 x +Components: + + Name: paracetamol + Form: zetpil + Quantities: 1 stuk + Divisible: 1 + Dose: + Solution: + Substances: + + Name: paracetamol + Concentrations: 120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk + Dose: paracetamol, [dun] mg, [qty-adj] 10 mg/kg - 20 mg/kg/dosis Solution: """ + + // Timed single component single item scenario + let timedSingleComponentSingleItem = """ +Id: a9e18942-f879-4df1-bc21-6375c3291ed7 +Name: paracetamol +Quantity: +Quantities: +Route: INTRAVENEUS +OrderType: TimedOrder +Adjust: 10 kg +Frequencies: 4 x/day +Time: 15 min - 20 min +Dose: [dun], [qty-adj] max 20 ml/kg/dosis, [qty] max 1000 ml/dosis +Div: +DoseCount: 1 x +Components: + + Name: paracetamol + Form: infusievloeistof + Quantities: 100;50 ml + Divisible: 10 + Dose: + Solution: + Substances: + + Name: paracetamol + Concentrations: 10 mg/ml + Dose: paracetamol, [dun] mg, [per-time-adj] 60 mg/kg/day, [per-time] max 4000 mg/day, [qty] max 1000 mg/dosis + Solution: +""" + +MedicationTexts.onceSingleComponentSingleItem |> Medication.fromString +|> Result.map (fun med -> + [ + OrderCommand.CalcMinMax + ] + |> run (Some logger) med +) +|> ignore + + +MedicationTexts.discontinuousSingleComponentSingleItem +|> Medication.fromString +|> Result.map (fun med -> + [ + OrderCommand.CalcMinMax + ] + |> run (Some logger) med +) +|> ignore + + +MedicationTexts.onceTimedSingleComponentSingleItem +|> Medication.fromString +|> Result.map (fun med -> + [ + OrderCommand.CalcMinMax + ] + |> run (Some logger) med +) +|> ignore + + +MedicationTexts.timedSingleComponentSingleItem +|> Medication.fromString +|> Result.map (fun med -> + [ + OrderCommand.CalcMinMax + ] + |> run (Some logger) med +) +|> ignore + + +MedicationTexts.onceSingleComponentSingleItemNoDose +|> Medication.fromString +|> Result.bind (fun med -> + [ + OrderCommand.CalcMinMax + ] + |> run (Some logger) med + |> Result.mapError (fun _ -> []) +) +|> function + | Ok ord -> ord |> Order.Print.printOrderToTableFormat false true [||] + | Error _ -> "cannot run order" |> failwith +|> ignore diff --git a/src/Informedica.GenUNITS.Lib/Scripts/Api.fsx b/src/Informedica.GenUNITS.Lib/Scripts/Api.fsx index ec0ce94c..53587d27 100644 --- a/src/Informedica.GenUNITS.Lib/Scripts/Api.fsx +++ b/src/Informedica.GenUNITS.Lib/Scripts/Api.fsx @@ -44,3 +44,10 @@ open Informedica.Utils.Lib.BCL "0,8 mL/uur" //|> String.replace "," "." |> ValueUnit.fromString + + +"x" +|> FParsec.CharParsers.run Parser.parseUnit + +"x" +|> Units.fromString diff --git a/src/Informedica.GenUNITS.Lib/ValueUnit.fs b/src/Informedica.GenUNITS.Lib/ValueUnit.fs index be3373db..18b9c8b0 100644 --- a/src/Informedica.GenUNITS.Lib/ValueUnit.fs +++ b/src/Informedica.GenUNITS.Lib/ValueUnit.fs @@ -216,7 +216,7 @@ module Parser = ) ] ) - // need to change nan to xxx to avoid getting a float 'nan' + // need to change nan to nnn to avoid getting a float 'nan' |> List.map (fun r -> {| r with unit = r.unit |> String.replace "nan" "nnn" |}) |> List.distinctBy (fun r -> r.unit, r.grp) |> List.filter (fun r -> @@ -1630,9 +1630,11 @@ module Units = // TODO: ugly hack need to fix this s + (* |> String.replace "x[Count]" "#" |> String.replace "x" "/" |> String.replace "#" "x[Count]" + *) |> String.split "/" |> function | us when us |> List.length >= 1 && (us |> List.length <= 3) -> From 51b4cb408b6eac6f5d915fb9ec1504a121d88f76 Mon Sep 17 00:00:00 2001 From: halcwb Date: Sun, 25 Jan 2026 11:21:09 +0100 Subject: [PATCH 3/3] fix(genform): remove empty dose unit from dose limit to string --- src/Informedica.GenFORM.Lib/DoseLimit.fs | 4 +++- src/Informedica.GenORDER.Lib/Scripts/Medication.fsx | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Informedica.GenFORM.Lib/DoseLimit.fs b/src/Informedica.GenFORM.Lib/DoseLimit.fs index 9b894730..15080602 100644 --- a/src/Informedica.GenFORM.Lib/DoseLimit.fs +++ b/src/Informedica.GenFORM.Lib/DoseLimit.fs @@ -162,9 +162,11 @@ module DoseLimit = [ let perDose = "/dosis" let emptyS = "" + let dun = dl.DoseUnit |> Units.toStringEngShortWithoutGroup [ $"%s{dl.DoseLimitTarget |> LimitTarget.toString}" - $"{FieldLabels.DoseUnit} %s{dl.DoseUnit |> Units.toStringEngShortWithoutGroup}" + if dun |> String.notEmpty then + $"{FieldLabels.DoseUnit} %s{dun}" $"%s{dl.Rate |> printMinMaxDose FieldLabels.Rate emptyS}" $"%s{dl.RateAdjust |> printMinMaxDose FieldLabels.RateAdjust emptyS}" diff --git a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx index 40071a21..07854020 100644 --- a/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx +++ b/src/Informedica.GenORDER.Lib/Scripts/Medication.fsx @@ -431,7 +431,7 @@ OrderType: OnceOrder Adjust: 11 kg Frequencies: Time: -Dose: [dun] +Dose: Div: DoseCount: 1 x Components: @@ -586,6 +586,7 @@ Components: Solution: """ + MedicationTexts.onceSingleComponentSingleItem |> Medication.fromString |> Result.map (fun med ->