From 1f7a941949380b19cfff55683ea99f17e3ab36f2 Mon Sep 17 00:00:00 2001 From: Casper Bollen Date: Sat, 2 Dec 2023 18:34:21 +0100 Subject: [PATCH] chore: implementing new valueunit use in genform --- src/Informedica.GenForm.Lib/DoseRule.fs | 300 +- src/Informedica.GenForm.Lib/Filter.fs | 50 +- .../Informedica.GenForm.Lib.fsproj | 2 +- src/Informedica.GenForm.Lib/Mapping.fs | 45 +- src/Informedica.GenForm.Lib/MinMax.fs | 87 - src/Informedica.GenForm.Lib/Patient.fs | 205 +- .../PrescriptionRule.fs | 19 +- src/Informedica.GenForm.Lib/Product.fs | 121 +- .../Scripts/Update.fsx | 3059 +---------------- src/Informedica.GenForm.Lib/SolutionRule.fs | 149 +- src/Informedica.GenForm.Lib/Types.fs | 116 +- src/Informedica.GenForm.Lib/Utils.fs | 127 +- src/Informedica.GenForm.Lib/VenousAccess.fs | 20 + src/Informedica.GenOrder.Lib/Api.fs | 108 +- src/Informedica.GenOrder.Lib/DrugOrder.fs | 270 +- src/Informedica.GenOrder.Lib/Patient.fs | 159 +- src/Informedica.GenOrder.Lib/Scripts/Api2.fsx | 37 +- src/Informedica.GenOrder.Lib/Types.fs | 138 +- src/Informedica.GenOrder.Lib/ValueUnit.fs | 12 + src/Informedica.GenUnits.Lib/ValueUnit.fs | 5 +- src/Server/Formulary.fs | 27 +- src/Server/ScenarioResult.fs | 18 +- tests/Informedica.GenForm.Tests/Tests.fs | 507 ++- 23 files changed, 1583 insertions(+), 3998 deletions(-) delete mode 100644 src/Informedica.GenForm.Lib/MinMax.fs create mode 100644 src/Informedica.GenForm.Lib/VenousAccess.fs diff --git a/src/Informedica.GenForm.Lib/DoseRule.fs b/src/Informedica.GenForm.Lib/DoseRule.fs index 308e235..b57e7e7 100644 --- a/src/Informedica.GenForm.Lib/DoseRule.fs +++ b/src/Informedica.GenForm.Lib/DoseRule.fs @@ -1,33 +1,31 @@ namespace Informedica.GenForm.Lib - module DoseRule = open System open MathNet.Numerics open Informedica.Utils.Lib open Informedica.Utils.Lib.BCL - - + open Informedica.GenCore.Lib.Ranges module DoseLimit = + open Informedica.GenUnits.Lib /// An empty DoseLimit. let limit = { DoseLimitTarget = NoDoseLimitTarget - DoseUnit = "" - RateUnit = "" - Quantity = MinMax.none + DoseUnit = NoUnit + Quantity = MinMax.empty NormQuantityAdjust = None - QuantityAdjust = MinMax.none - PerTime = MinMax.none + QuantityAdjust = MinMax.empty + PerTime = MinMax.empty NormPerTimeAdjust = None - PerTimeAdjust = MinMax.none - Rate = MinMax.none - RateAdjust = MinMax.none + PerTimeAdjust = MinMax.empty + Rate = MinMax.empty + RateAdjust = MinMax.empty } @@ -42,10 +40,10 @@ module DoseRule = let useAdjust (dl : DoseLimit) = [ dl.NormQuantityAdjust = None - dl.QuantityAdjust = MinMax.none + dl.QuantityAdjust = MinMax.empty dl.NormPerTimeAdjust = None - dl.PerTimeAdjust = MinMax.none - dl.RateAdjust = MinMax.none + dl.PerTimeAdjust = MinMax.empty + dl.RateAdjust = MinMax.empty ] |> List.forall id |> not @@ -84,68 +82,65 @@ module DoseRule = module Print = - let printFreqs (r : DoseRule) = - let frs = - r.Frequencies - |> Array.map BigRational.ToInt32 - |> Array.map string - |> String.concat ", " + open Informedica.GenUnits.Lib - if frs |> String.isNullOrWhiteSpace then "" - else - if r.FreqTimeUnit |> String.isNullOrWhiteSpace then $"{frs} x" - else - $"{frs} x / {r.FreqTimeUnit}" + let printFreqs (r : DoseRule) = + r.Frequencies + |> Option.map (fun vu -> + vu + |> Utils.ValueUnit.toString 0 + ) + |> Option.defaultValue "" let printInterval (dr: DoseRule) = - if dr.IntervalTimeUnit |> String.isNullOrWhiteSpace then "" + if dr.IntervalTime = MinMax.empty then "" else - let s = dr.IntervalTime |> MinMax.toString - if s |> String.isNullOrWhiteSpace then "" - else - let s = - s - |> String.replace "≥" "min. interval " - |> String.replace "<" "max. interval" - |> fun s -> - if s |> String.contains "-" then $"elke {s}" - else s - $"{s} {dr.IntervalTimeUnit}" + dr.IntervalTime + |> MinMax.toString + "min. interval " + "min. interval " + "max. interval " + "max. interval " let printTime (dr: DoseRule) = - if dr.AdministrationTimeUnit |> String.isNullOrWhiteSpace then "" + if dr.AdministrationTime = MinMax.empty then "" else - let s = dr.AdministrationTime |> MinMax.toString - if s |> String.isNullOrWhiteSpace then "" - else - $"{s} {dr.AdministrationTimeUnit}" - |> String.replace "<" "max" + dr.AdministrationTime + |> MinMax.toString + "min. " + "min. " + "max. " + "max. " let printDuration (dr: DoseRule) = - if dr.DurationUnit |> String.isNullOrWhiteSpace then "" + if dr.Duration = MinMax.empty then "" else - let s = dr.Duration |> MinMax.toString - if s |> String.isNullOrWhiteSpace then "" - else - $"{s} {dr.DurationUnit}" - |> String.replace "<" "max" + dr.Duration + |> MinMax.toString + "min. duur " + "min. duur " + "max. duur " + "max. duur " - let printMinMaxDose u (minMax : MinMax) = - let s = minMax |> MinMax.toString - if s |> String.isNullOrWhiteSpace then "" + let printMinMaxDose (minMax : MinMax) = + if minMax = MinMax.empty then "" else - $"{s} {u}" - |> String.replace "<" "max" + minMax + |> MinMax.toString + "> " + "> " + "< " + "< " - let printNormDose u br = - match br with + let printNormDose vu = + match vu with | None -> "" - | Some br -> $"{br |> BigRational.toStringNl} {u}" + | Some vu -> $"{vu |> Utils.ValueUnit.toString 3}" let printDose wrap (dr : DoseRule) = @@ -162,25 +157,19 @@ module DoseRule = if useSubstDl then substDls else shapeDls |> Array.map (fun dl -> - let doseQtyAdjUnit = $"{dl.DoseUnit}/{dr.AdjustUnit}/keer" - let doseTotAdjUnit = $"{dl.DoseUnit}/{dr.AdjustUnit}/{dr.FreqTimeUnit}" - let doseTotUnit = $"{dl.DoseUnit}/{dr.FreqTimeUnit}" - let doseQtyUnit = $"{dl.DoseUnit}/keer" - let doseRateUnit = $"{dl.DoseUnit}/{dl.RateUnit}" - let doseRateAdjUnit = $"{dl.DoseUnit}/{dr.AdjustUnit}/{dl.RateUnit}" [ - $"{dl.Rate |> printMinMaxDose doseRateUnit}" - $"{dl.RateAdjust |> printMinMaxDose doseRateAdjUnit}" + $"{dl.Rate |> printMinMaxDose}" + $"{dl.RateAdjust |> printMinMaxDose}" - $"{dl.NormPerTimeAdjust |> printNormDose doseTotAdjUnit} " + - $"{dl.PerTimeAdjust |> printMinMaxDose doseTotAdjUnit}" + $"{dl.NormPerTimeAdjust |> printNormDose} " + + $"{dl.PerTimeAdjust |> printMinMaxDose}" - $"{dl.PerTime |> printMinMaxDose doseTotUnit}" + $"{dl.PerTime |> printMinMaxDose}" - $"{dl.NormQuantityAdjust |> printNormDose doseQtyAdjUnit} " + - $"{dl.QuantityAdjust |> printMinMaxDose doseQtyAdjUnit}" + $"{dl.NormQuantityAdjust |> printNormDose} " + + $"{dl.QuantityAdjust |> printMinMaxDose}" - $"{dl.Quantity |> printMinMaxDose doseQtyUnit}" + $"{dl.Quantity |> printMinMaxDose}" ] |> List.map String.trim |> List.filter (String.IsNullOrEmpty >> not) @@ -194,16 +183,16 @@ module DoseRule = /// fold: https://github.com/dotnet/fsharp/issues/6699 let toMarkdown (rules : DoseRule array) = let generic_md generic = - $"\n# {generic}\n---\n" + $"\n\n# {generic}\n\n---\n" let route_md route products = - $"\n### Route: {route}\n\n#### Producten\n%s{products}\n" + $"\n\n### Route: {route}\n\n#### Producten\n%s{products}\n" let product_md product = $"* {product}" - let indication_md indication = $"\n## Indicatie: %s{indication}\n---\n" + let indication_md indication = $"\n\n## Indicatie: %s{indication}\n\n---\n" - let doseCapt_md = "\n#### Doseringen\n" + let doseCapt_md = "\n\n#### Doseringen\n\n" let dose_md dt dose freqs intv time dur = let dt = dt |> DoseType.toString @@ -231,9 +220,9 @@ module DoseRule = let patient_md patient diagn = if diagn |> String.isNullOrWhiteSpace then - $"\n##### Patient: **%s{patient}**\n" + $"\n\n##### Patient: **%s{patient}**\n\n" else - $"\n##### Patient: **%s{patient}**\n\n%s{diagn}" + $"\n\n##### Patient: **%s{patient}**\n\n%s{diagn}" let printDoses (rules : DoseRule array) = ("", rules |> Array.groupBy (fun d -> d.DoseType)) @@ -321,7 +310,12 @@ module DoseRule = |> Array.collect (fun d -> d.Products) |> Array.sortBy (fun p -> p.Substances - |> Array.sumBy (fun s -> s.Quantity |> Option.defaultValue 0N) + |> Array.sumBy (fun s -> + s.Quantity + |> Option.map ValueUnit.getValue + |> Option.bind Array.tryHead + |> Option.defaultValue 0N + ) ) |> Array.map (fun p -> product_md p.Label) |> Array.distinct @@ -372,6 +366,7 @@ module DoseRule = open Utils + open Informedica.GenUnits.Lib /// @@ -471,23 +466,41 @@ module DoseRule = r.Department |> Some Diagnoses = [| r.Diagn |] |> Array.filter String.notEmpty Gender = r.Gender - Age = (r.MinAge, r.MaxAge) |> MinMax.fromTuple - Weight = (r.MinWeight, r.MaxWeight) |> MinMax.fromTuple - BSA = (r.MinBSA, r.MaxBSA) |> MinMax.fromTuple - GestAge = (r.MinGestAge, r.MaxGestAge) |> MinMax.fromTuple - PMAge = (r.MinPMAge, r.MaxPMAge) |> MinMax.fromTuple + Age = + (r.MinAge, r.MaxAge) + |> MinMax.fromTuple (Some Utils.Units.day) + Weight = + (r.MinWeight, r.MaxWeight) + |> MinMax.fromTuple (Some Utils.Units.weightGram) + BSA = + (r.MinBSA, r.MaxBSA) + |> MinMax.fromTuple (Some Utils.Units.bsaM2) + GestAge = + (r.MinGestAge, r.MaxGestAge) + |> MinMax.fromTuple (Some Utils.Units.day) + PMAge = + (r.MinPMAge, r.MaxPMAge) + |> MinMax.fromTuple (Some Utils.Units.day) Location = AnyAccess } - AdjustUnit = r.AdjustUnit DoseType = r.DoseType - Frequencies = r.Frequencies - FreqTimeUnit = r.FreqUnit - AdministrationTime = (r.MinTime, r.MaxTime) |> MinMax.fromTuple - AdministrationTimeUnit = r.TimeUnit - IntervalTime = (r.MinInterval, r.MaxInterval) |> MinMax.fromTuple - IntervalTimeUnit = r.IntervalUnit - Duration = (r.MinDur, r.MaxDur) |> MinMax.fromTuple - DurationUnit = r.DurUnit + AdjustUnit = r.AdjustUnit |> Units.adjustUnit + Frequencies = + match r.FreqUnit |> Units.freqUnit with + | None -> None + | Some u -> + r.Frequencies + |> ValueUnit.withUnit u + |> Some + AdministrationTime = + (r.MinTime, r.MaxTime) + |> MinMax.fromTuple (r.TimeUnit |> Utils.Units.timeUnit) + IntervalTime = + (r.MinInterval, r.MaxInterval) + |> MinMax.fromTuple (r.IntervalUnit |> Utils.Units.timeUnit) + Duration = + (r.MinDur, r.MaxDur) + |> MinMax.fromTuple (r.DurUnit |> Utils.Units.timeUnit) DoseLimits = [||] Products = [||] } @@ -496,38 +509,97 @@ module DoseRule = { dr with DoseLimits = let shapeLimits = - Mapping.filterRouteShapeUnit dr.Route dr.Shape "" + Mapping.filterRouteShapeUnit dr.Route dr.Shape NoUnit |> Array.map (fun rsu -> { DoseLimit.limit with DoseLimitTarget = dr.Shape |> ShapeDoseLimitTarget - DoseUnit = rsu.DoseUnit Quantity = - let min = rsu.MinDoseQty |> Option.bind BigRational.fromFloat - let max = rsu.MaxDoseQty |> Option.bind BigRational.fromFloat - (min, max) |> MinMax.fromTuple + { + Min = rsu.MinDoseQty |> Option.map Limit.Inclusive + Max = rsu.MaxDoseQty |> Option.map Limit.Inclusive + } } ) |> Array.distinct rs |> Array.map (fun r -> + // the adjust unit + let adj = r.AdjustUnit |> Utils.Units.adjustUnit + // the dose unit + let du = r.DoseUnit |> Units.fromString + // the adjusted dose unit + let duAdj = + match adj, du with + | Some adj, Some du -> + du + |> Units.per adj + |> Some + | _ -> None + // the time unit + let tu = r.FreqUnit |> Utils.Units.timeUnit + // the dose unit per time unit + let duTime = + match du, tu with + | Some du, Some tu -> + du + |> Units.per tu + |> Some + | _ -> None + // the adjusted dose unit per time unit + let duAdjTime = + match duAdj, tu with + | Some duAdj, Some tu -> + duAdj + |> Units.per tu + |> Some + | _ -> None + // the rate unit + let ru = r.RateUnit |> Units.fromString + // the dose unit per rate unit + let duRate = + match du, ru with + | Some du, Some ru -> + du + |> Units.per ru + |> Some + | _ -> None + // the adjusted dose unit per rate unit + let duAdjRate = + match duAdj, ru with + | Some duAdj, Some ru -> + duAdj + |> Units.per ru + |> Some + | _ -> None + { DoseLimitTarget = r.Substance |> SubstanceDoseLimitTarget - DoseUnit = - if r.Substance |> String.isNullOrWhiteSpace |> not then r.DoseUnit - else - match shapeLimits |> Array.tryHead with - | Some sl -> sl.DoseUnit - | None -> "" - RateUnit = r.RateUnit - Quantity = (r.MinQty, r.MaxQty) |> MinMax.fromTuple - NormQuantityAdjust = r.NormQtyAdj - QuantityAdjust = (r.MinQtyAdj, r.MaxQtyAdj) |> MinMax.fromTuple - PerTime = (r.MinPerTime, r.MaxPerTime) |> MinMax.fromTuple - NormPerTimeAdjust = r.NormPerTimeAdj - PerTimeAdjust = (r.MinPerTimeAdj, r.MaxPerTimeAdj) |> MinMax.fromTuple - Rate = (r.MinRate, r.MaxRate) |> MinMax.fromTuple - RateAdjust = (r.MinRateAdj, r.MaxRateAdj) |> MinMax.fromTuple + DoseUnit = du |> Option.defaultValue NoUnit + Quantity = + (r.MinQty, r.MaxQty) + |> MinMax.fromTuple du + NormQuantityAdjust = + r.NormQtyAdj + |> ValueUnit.withOptionalUnit duAdj + QuantityAdjust = + (r.MinQtyAdj, r.MaxQtyAdj) + |> MinMax.fromTuple duAdj + PerTime = + (r.MinPerTime, r.MaxPerTime) + |> MinMax.fromTuple duTime + NormPerTimeAdjust = + r.NormPerTimeAdj + |> ValueUnit.withOptionalUnit duAdjTime + PerTimeAdjust = + (r.MinPerTimeAdj, r.MaxPerTimeAdj) + |> MinMax.fromTuple duAdjTime + Rate = + (r.MinRate, r.MaxRate) + |> MinMax.fromTuple duRate + RateAdjust = + (r.MinRateAdj, r.MaxRateAdj) + |> MinMax.fromTuple duAdjRate } ) |> Array.append shapeLimits @@ -644,4 +716,6 @@ module DoseRule = let useAdjust (dr : DoseRule) = dr.DoseLimits |> Array.filter DoseLimit.isSubstanceLimit - |> Array.exists DoseLimit.useAdjust \ No newline at end of file + |> Array.exists DoseLimit.useAdjust + + diff --git a/src/Informedica.GenForm.Lib/Filter.fs b/src/Informedica.GenForm.Lib/Filter.fs index 8a555c1..f72fc39 100644 --- a/src/Informedica.GenForm.Lib/Filter.fs +++ b/src/Informedica.GenForm.Lib/Filter.fs @@ -1,6 +1,7 @@ namespace Informedica.GenForm.Lib + module Filter = @@ -11,16 +12,8 @@ module Filter = Generic = None Shape = None Route = None - Department = None - Diagnoses = [||] - Gender = AnyGender - AgeInDays = None - WeightInGram = None - HeightInCm = None - GestAgeInDays = None - PMAgeInDays = None DoseType = AnyDoseType - Location = AnyAccess + Patient = Patient.patient } @@ -36,42 +29,15 @@ module Filter = |> Patient.calcPMAge { filter with - Department = pat.Department |> Some - Diagnoses = pat.Diagnoses - Gender = pat.Gender - AgeInDays = pat.AgeInDays - WeightInGram = pat.WeightInGram - HeightInCm = pat.HeightInCm - GestAgeInDays = pat.GestAgeInDays - PMAgeInDays = pat.PMAgeInDays - Location = pat.VenousAccess - } - - - /// - /// Extract a Patient from a Filter. - /// - /// The Filter - /// The Patient - let getPatient (filter : Filter) = - { Patient.patient with - Department = filter.Department |> Option.defaultValue "" - Diagnoses = filter.Diagnoses - Gender = filter.Gender - AgeInDays = filter.AgeInDays - WeightInGram = filter.WeightInGram - HeightInCm = filter.HeightInCm - GestAgeInDays = filter.GestAgeInDays - PMAgeInDays = filter.PMAgeInDays - VenousAccess = filter.Location + Patient = pat } let calcPMAge (filter : Filter) = { filter with - PMAgeInDays = - filter - |> getPatient + Patient = + filter.Patient |> Patient.calcPMAge - |> fun pat -> pat.PMAgeInDays - } \ No newline at end of file + } + + diff --git a/src/Informedica.GenForm.Lib/Informedica.GenForm.Lib.fsproj b/src/Informedica.GenForm.Lib/Informedica.GenForm.Lib.fsproj index acb2e1e..077e9d1 100644 --- a/src/Informedica.GenForm.Lib/Informedica.GenForm.Lib.fsproj +++ b/src/Informedica.GenForm.Lib/Informedica.GenForm.Lib.fsproj @@ -7,8 +7,8 @@ + - diff --git a/src/Informedica.GenForm.Lib/Mapping.fs b/src/Informedica.GenForm.Lib/Mapping.fs index 9f5fe21..4993b90 100644 --- a/src/Informedica.GenForm.Lib/Mapping.fs +++ b/src/Informedica.GenForm.Lib/Mapping.fs @@ -5,6 +5,7 @@ module Mapping = open Informedica.Utils.Lib open Informedica.Utils.Lib.BCL + open Informedica.GenUnits.Lib /// Mapping of long Z-index route names to short names @@ -86,17 +87,48 @@ module Mapping = let getStr = getColumn Csv.getStringColumn r let getFlt = getColumn Csv.getFloatOptionColumn r + let un = getStr "Unit" |> Units.fromString |> Option.defaultValue NoUnit { Route = getStr "Route" Shape = getStr "Shape" - Unit = getStr "Unit" - DoseUnit = getStr "DoseUnit" - MinDoseQty = getFlt "MinDoseQty" - MaxDoseQty = getFlt "MaxDoseQty" + Unit = un + DoseUnit = getStr "DoseUnit" |> Units.fromString |> Option.defaultValue NoUnit + MinDoseQty = + if un = NoUnit then None + else + getFlt "MinDoseQty" + |> Option.bind BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit un) + MaxDoseQty = + if un = NoUnit then None + else + getFlt "MaxDoseQty" + |> Option.bind BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit un) Timed = getStr "Timed" |> String.equalsCapInsens "true" Reconstitute = getStr "Reconstitute" |> String.equalsCapInsens "true" IsSolution = getStr "IsSolution" |> String.equalsCapInsens "true" } + |> fun rs -> + match rs.DoseUnit with + | NoUnit -> rs + | du -> + { rs with + MinDoseQty = + getFlt "MinDoseQty" + |> Option.bind (fun v -> + v + |> BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit du) + ) + MaxDoseQty = + getFlt "MaxDoseQty" + |> Option.bind (fun v -> + v + |> BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit du) + ) + } ) @@ -116,9 +148,8 @@ module Mapping = xs.Route |> mapRoute |> Option.map (String.equalsCapInsens (rte |> String.trim)) |> Option.defaultValue false let eqsShp = shape |> String.isNullOrWhiteSpace || shape |> String.trim |> String.equalsCapInsens xs.Shape let eqsUnt = - unt |> String.isNullOrWhiteSpace || - unt |> String.trim |> String.equalsCapInsens xs.Unit || - xs.Unit |> mapUnit |> Option.map (String.equalsCapInsens (unt |> String.trim)) |> Option.defaultValue false + unt = NoUnit || + unt |> Units.eqsUnit xs.Unit eqsRte && eqsShp && eqsUnt ) diff --git a/src/Informedica.GenForm.Lib/MinMax.fs b/src/Informedica.GenForm.Lib/MinMax.fs deleted file mode 100644 index 182b363..0000000 --- a/src/Informedica.GenForm.Lib/MinMax.fs +++ /dev/null @@ -1,87 +0,0 @@ -namespace Informedica.GenForm.Lib - - - -module MinMax = - - open Informedica.Utils.Lib.BCL - - - /// Create a MinMax from a tuple - let fromTuple (min, max) = - { - Minimum = min - Maximum = max - } - - - /// An empty MinMax - let none = (None, None) |> fromTuple - - - /// - /// Map the minimum and maximum values of a MinMax - /// - /// Function to map the minimum value - /// Function to map the maximum value - /// The MinMax to map - let map fMin fMax (minMax : MinMax) = - { minMax with - Minimum = minMax.Minimum |> Option.map fMin - Maximum = minMax.Maximum |> Option.map fMax - } - - - /// Check if a MinMax is empty - let isNone (minMax : MinMax) = - minMax.Minimum |> Option.isNone && - minMax.Maximum |> Option.isNone - - /// - /// Check if a value is between the minimum and maximum of a MinMax - /// - /// - /// Assumes that the minimum is inclusive and the maximum is exclusive - /// and if either is None, it is ignored. If both are None, the result is - /// always true. If the optional value to check is None, the result is only - /// true when MinMax is also None. - /// - /// - /// - /// let minMax = MinMax.fromTuple (Some 1N, Some 10N) - /// let result = MinMax.isBetween minMax (Some 5N) - /// // result is true - /// let result = MinMax.isBetween minMax (Some 10N) - /// // result is false - /// let result = MinMax.isBetween minMax (Some 1N) - /// // result is true - /// let result = MinMax.isBetween minMax (Some 0N) - /// // result is false - /// let result = MinMax.isBetween minMax None - /// // result is false - /// - /// - let isBetween (minMax : MinMax) = function - | Some v -> - match minMax.Minimum, minMax.Maximum with - | None, None -> true - | Some min, None -> v >= min - | None, Some max -> v < max - | Some min, Some max -> v >= min && v < max - | None when minMax |> isNone -> true - | _ -> false - - - /// Get the string representation of a MinMax - let toString { Minimum = min; Maximum = max } = - let min = min |> Option.map BigRational.toStringNl - let max = max |> Option.map BigRational.toStringNl - - match min, max with - | None, None -> "" - | Some min, None -> $"≥ {min}" - | Some min, Some max -> - if min = max then $"{min}" - else - $"{min} - {max}" - | None, Some max -> $"< {max}" diff --git a/src/Informedica.GenForm.Lib/Patient.fs b/src/Informedica.GenForm.Lib/Patient.fs index 04ef452..f8f5eb6 100644 --- a/src/Informedica.GenForm.Lib/Patient.fs +++ b/src/Informedica.GenForm.Lib/Patient.fs @@ -25,9 +25,9 @@ module Gender = /// Check if a Filter contains a Gender. /// Note if AnyGender is specified, this will always return true. let filter gender (filter : Filter) = - match filter.Gender, gender with + match filter.Patient.Gender, gender with | _, AnyGender -> true - | _ -> filter.Gender = gender + | _ -> filter.Patient.Gender = gender @@ -36,11 +36,28 @@ module PatientCategory = open MathNet.Numerics open Informedica.Utils.Lib.BCL + open Informedica.GenUnits.Lib module BSA = Informedica.GenCore.Lib.Calculations.BSA + module Limit = Informedica.GenCore.Lib.Ranges.Limit + module MinMax = Informedica.GenCore.Lib.Ranges.MinMax module Conversions = Informedica.GenCore.Lib.Conversions + let patientCategory : PatientCategory = + { + Department = None + Diagnoses = [||] + Gender = AnyGender + Age = MinMax.empty + Weight = MinMax.empty + BSA = MinMax.empty + GestAge = MinMax.empty + PMAge = MinMax.empty + Location = AnyAccess + } + + /// /// Use a PatientCategory to get a sort value. /// @@ -54,13 +71,20 @@ module PatientCategory = /// let sortBy (pat : PatientCategory) = let toInt = function - | Some x -> x |> BigRational.ToInt32 + | Some x -> + x + |> Limit.getValueUnit + |> ValueUnit.getValue + |> Array.tryHead + |> function + | None -> 0 + | Some x -> x |> BigRational.ToInt32 | None -> 0 - (pat.Age.Minimum |> toInt |> fun i -> if i > 0 then i + 300 else i) + - (pat.GestAge.Minimum |> toInt) + - (pat.PMAge.Minimum |> toInt) + - (pat.Weight.Minimum |> Option.map (fun w -> w / 1000N) |> toInt) + (pat.Age.Min |> toInt |> fun i -> if i > 0 then i + 300 else i) + + (pat.GestAge.Min |> toInt) + + (pat.PMAge.Min |> toInt) + + (pat.Weight.Min |> toInt |> fun w -> w / 1000) /// @@ -78,49 +102,43 @@ module PatientCategory = ([| patCat |] |> Array.filter (fun p -> - if filter.Diagnoses |> Array.isEmpty then true + if filter.Patient.Diagnoses |> Array.isEmpty then true else p.Diagnoses |> Array.exists (fun d -> - filter.Diagnoses |> Array.exists (String.equalsCapInsens d) + filter.Patient.Diagnoses + |> Array.exists (String.equalsCapInsens d) ) ), [| - fun (p: PatientCategory) -> filter.Department |> eqs p.Department - fun (p: PatientCategory) -> filter.AgeInDays |> MinMax.isBetween p.Age - fun (p: PatientCategory) -> filter.WeightInGram |> MinMax.isBetween p.Weight + fun (p: PatientCategory) -> filter.Patient.Department |> eqs p.Department + fun (p: PatientCategory) -> filter.Patient.Age |> Utils.MinMax.inRange p.Age + fun (p: PatientCategory) -> filter.Patient.Weight |> Utils.MinMax.inRange p.Weight fun (p: PatientCategory) -> - match filter.WeightInGram, filter.HeightInCm with + match filter.Patient.Weight, filter.Patient.Height with | Some w, Some h -> - BSA.calcDuBois (Some 3) - (w |> BigRational.toDecimal |> Conversions.kgFromDecimal) - (h |> BigRational.toDecimal |> Conversions.cmFromDecimal) - |> decimal - |> BigRational.fromDecimal + Calculations.calcDuBois w h |> Some - | _ -> None - |> MinMax.isBetween p.BSA - if filter.AgeInDays |> Option.isSome then + |> Utils.MinMax.inRange p.BSA + | _ -> true + if filter.Patient.Age |> Option.isSome then yield! [| fun (p: PatientCategory) -> // defaults to normal gestation - filter.GestAgeInDays - |> Option.defaultValue 259N + filter.Patient.GestAge + |> Option.defaultValue Utils.ValueUnit.ageFullTerm |> Some - |> MinMax.isBetween p.GestAge + |> Utils.MinMax.inRange p.GestAge fun (p: PatientCategory) -> // defaults to normal postmenstrual age - filter.PMAgeInDays - |> Option.defaultValue 259N + filter.Patient.PMAge + |> Option.defaultValue Utils.ValueUnit.ageFullTerm |> Some - |> MinMax.isBetween p.PMAge + |> Utils.MinMax.inRange p.PMAge |] fun (p: PatientCategory) -> filter |> Gender.filter p.Gender fun (p: PatientCategory) -> - match p.Location, filter.Location with - | AnyAccess, _ - | _, AnyAccess -> true - | _ -> p.Location = filter.Location + VenousAccess.check p.Location filter.Patient.VenousAccess |]) ||> Array.fold(fun acc pred -> acc @@ -142,12 +160,20 @@ module PatientCategory = /// considered to be between the minimum and maximum. /// let checkAgeWeightMinMax age weight aMinMax wMinMax = - age |> MinMax.isBetween aMinMax && - weight |> MinMax.isBetween wMinMax + // TODO rename aMinMax and wMinMax to ageMinMax and weightMinMax + + age |> Utils.MinMax.inRange aMinMax && + weight |> Utils.MinMax.inRange wMinMax /// Prints an age in days as a string. let printAge age = + let age = + age + |> ValueUnit.convertTo Units.Time.day + |> ValueUnit.getValue + |> Array.tryHead + |> Option.defaultValue 0N let a = age |> BigRational.ToInt32 match a with | _ when a < 7 -> @@ -169,13 +195,21 @@ module PatientCategory = /// Print days as weeks. let printDaysToWeeks d = + let d = + d + |> ValueUnit.convertTo Units.Time.day + |> ValueUnit.getValue + |> Array.tryHead + |> Option.defaultValue 0N + let d = d |> BigRational.ToInt32 (d / 7) |> sprintf "%i weken" /// Print an MinMax age as a string. let printAgeMinMax (age : MinMax) = - match age.Minimum, age.Maximum with + let printAge = Limit.getValueUnit >> printAge + match age.Min, age.Max with | Some min, Some max -> let min = min |> printAge let max = max |> printAge @@ -199,10 +233,14 @@ module PatientCategory = let neonate = let s = - if pat.GestAge.Maximum.IsSome && pat.GestAge.Maximum.Value < 259N then "prematuren" + if pat.GestAge.Max.IsSome && + pat.GestAge.Max.Value + |> Limit.getValueUnit > printDaysToWeeks + + match pat.GestAge.Min, pat.GestAge.Max, pat.PMAge.Min, pat.PMAge.Max with | _, _, Some min, Some max -> let min = min |> printDaysToWeeks let max = max |> printDaysToWeeks @@ -229,18 +267,16 @@ module PatientCategory = | _ -> "" let weight = - let toStr (v : BigRational) = - let v = v / 1000N - if v.Denominator = 1I then v |> BigRational.ToInt32 |> sprintf "%i" - else - v - |> BigRational.ToDouble - |> sprintf "%A" - - match pat.Weight.Minimum, pat.Weight.Maximum with - | Some min, Some max -> $"gewicht %s{min |> toStr} tot %s{max |> toStr} kg" - | Some min, None -> $"gewicht vanaf %s{min |> toStr} kg" - | None, Some max -> $"gewicht tot %s{max |> toStr} kg" + let toStr lim = + lim + |> Limit.getValueUnit + |> ValueUnit.convertTo Units.Weight.kiloGram + |> Utils.ValueUnit.toString -1 + + match pat.Weight.Min, pat.Weight.Max with + | Some min, Some max -> $"gewicht %s{min |> toStr} tot %s{max |> toStr}" + | Some min, None -> $"gewicht vanaf %s{min |> toStr}" + | None, Some max -> $"gewicht tot %s{max |> toStr}" | None, None -> "" [ @@ -260,30 +296,31 @@ module Patient = open MathNet.Numerics open Informedica.Utils.Lib.BCL + open Informedica.GenUnits.Lib module BSA = Informedica.GenCore.Lib.Calculations.BSA module Conversions = Informedica.GenCore.Lib.Conversions - + module Limit = Informedica.GenCore.Lib.Ranges.Limit /// An empty Patient. let patient = { - Department = "" + Department = None Diagnoses = [||] Gender = AnyGender - AgeInDays = None - WeightInGram = None - HeightInCm = None - GestAgeInDays = None - PMAgeInDays = None - VenousAccess = AnyAccess + Age = None + Weight = None + Height = None + GestAge = None + PMAge = None + VenousAccess = [ ] } let calcPMAge (pat: Patient) = { pat with - PMAgeInDays = - match pat.AgeInDays, pat.GestAgeInDays with + PMAge = + match pat.Age, pat.GestAge with | Some ad, Some ga -> ad + ga |> Some | _ -> None } @@ -291,21 +328,9 @@ module Patient = /// Calculate the BSA of a Patient. let calcBSA (pat: Patient) = - match pat.WeightInGram, pat.HeightInCm with + match pat.Weight, pat.Height with | Some w, Some h -> - let w = - (w |> BigRational.toDouble) / 1000. - |> decimal - |> Conversions.kgFromDecimal - let h = - h - |> BigRational.toDouble - |> decimal - |> Conversions.cmFromDecimal - - BSA.calcDuBois (Some 2) w h - |> decimal - |> BigRational.fromDecimal + Utils.Calculations.calcDuBois w h |> Some | _ -> None @@ -313,19 +338,20 @@ module Patient = /// Get the string representation of a Patient. let rec toString (pat: Patient) = [ - pat.Department + pat.Department |> Option.defaultValue "" pat.Gender |> Gender.toString - pat.AgeInDays + pat.Age |> Option.map PatientCategory.printAge |> Option.defaultValue "" let printDaysToWeeks = PatientCategory.printDaysToWeeks let s = - if pat.GestAgeInDays.IsSome && pat.GestAgeInDays.Value < 259N then "prematuren" + if pat.GestAge.IsSome && + pat.GestAge.Value < Utils.ValueUnit.ageFullTerm then "prematuren" else "neonaten" - match pat.GestAgeInDays, pat.PMAgeInDays with + match pat.GestAge, pat.PMAge with | _, Some a -> let a = a |> printDaysToWeeks $"{s} postconceptie leeftijd %s{a}" @@ -334,29 +360,31 @@ module Patient = $"{s} zwangerschapsduur %s{a}" | _ -> "" - let toStr (v : BigRational) = - let v = v / 1000N + let toStr u vu = + let v = + vu + |> ValueUnit.convertTo u + |> ValueUnit.getValue + |> Array.tryHead + |> Option.defaultValue 0N if v.Denominator = 1I then v |> BigRational.ToInt32 |> sprintf "%i" else v - |> BigRational.toStringNl + |> BigRational.ToDouble + |> sprintf "%A" - pat.WeightInGram - |> Option.map (fun w -> $"gewicht %s{w |> toStr} kg") + pat.Weight + |> Option.map (fun w -> $"gewicht %s{w |> toStr Units.Mass.kiloGram } kg") |> Option.defaultValue "" - pat.HeightInCm - |> Option.map (fun h -> $"lengte {h |> BigRational.toStringNl} cm") + pat.Height + |> Option.map (fun h -> $"lengte {h |> toStr Units.Height.centiMeter} cm") |> Option.defaultValue "" pat |> calcBSA |> Option.map (fun bsa -> - let bsa = - bsa - |> BigRational.toDouble - |> Double.fixPrecision 2 - $"BSA {bsa} m2" + $"BSA {bsa |> Utils.ValueUnit.toString 3}" ) |> Option.defaultValue "" ] @@ -364,3 +392,4 @@ module Patient = |> List.filter (String.isNullOrWhiteSpace >> not) |> String.concat ", " + diff --git a/src/Informedica.GenForm.Lib/PrescriptionRule.fs b/src/Informedica.GenForm.Lib/PrescriptionRule.fs index 3c999b1..b37c03f 100644 --- a/src/Informedica.GenForm.Lib/PrescriptionRule.fs +++ b/src/Informedica.GenForm.Lib/PrescriptionRule.fs @@ -5,11 +5,12 @@ module PrescriptionRule = open Informedica.Utils.Lib open MathNet.Numerics + open Informedica.GenUnits.Lib /// Use a Filter to get matching PrescriptionRules. let filter (filter : Filter) = - let pat = filter |> Filter.getPatient + let pat = filter.Patient DoseRule.get () |> DoseRule.filter filter @@ -25,10 +26,7 @@ module PrescriptionRule = Generic = dr.Generic |> Some Shape = dr.Shape |> Some Route = dr.Route |> Some - //WeightInGram = pat.WeightInGram - //HeightInCm = pat.HeightInCm DoseType = dr.DoseType - //Location = pat.VenousAccess } } ) @@ -57,13 +55,21 @@ module PrescriptionRule = Products = pr.DoseRule.Products |> Array.filter (fun p -> + // TODO rewrite to compare valueunits p.ShapeQuantities + |> ValueUnit.getValue |> Array.exists (fun sq -> shapeQuantities |> Array.exists ((=) sq) ) && p.Substances - |> Array.map (fun s -> s.Name.ToLower(), s.Quantity) + // TODO rewrite to compare valueunits + |> Array.map (fun s -> + s.Name.ToLower(), + s.Quantity + |> Option.map ValueUnit.getValue + |> Option.bind Array.tryHead + ) |> Array.forall (fun sq -> substs |> Array.exists((=) sq) @@ -132,3 +138,6 @@ module PrescriptionRule = /// Get all frequencies of an array of PrescriptionRules. let frequencies = getDoseRules >> DoseRule.frequencies + + + diff --git a/src/Informedica.GenForm.Lib/Product.fs b/src/Informedica.GenForm.Lib/Product.fs index 33a3ba5..e099ce4 100644 --- a/src/Informedica.GenForm.Lib/Product.fs +++ b/src/Informedica.GenForm.Lib/Product.fs @@ -2,6 +2,7 @@ namespace Informedica.GenForm.Lib + module Product = open MathNet.Numerics @@ -34,6 +35,8 @@ module Product = module ShapeRoute = + open Informedica.GenUnits.Lib + let private get_ () = Web.getDataFromSheet Web.dataUrlIdGenPres "ShapeRoute" @@ -51,8 +54,14 @@ module Product = { Shape = get "Shape" Route = get "Route" - Unit = get "Unit" - DoseUnit = get "DoseUnit" + Unit = + get "Unit" + |> Units.fromString + |> Option.defaultValue NoUnit + DoseUnit = + get "DoseUnit" + |> Units.fromString + |> Option.defaultValue NoUnit Timed = get "Timed" = "TRUE" Reconstitute = get "Reconstitute" = "TRUE" IsSolution = get "IsSolution" = "TRUE" @@ -152,17 +161,21 @@ module Product = [| fun (r : Reconstitution) -> r.Route |> eqs filter.Route fun (r : Reconstitution) -> - if filter.Location = AnyAccess then true + if filter.Patient.VenousAccess = [AnyAccess] || + filter.Patient.VenousAccess |> List.isEmpty then true else match filter.DoseType with | AnyDoseType -> true | _ -> filter.DoseType = r.DoseType - fun (r : Reconstitution) -> r.Department |> eqs filter.Department + fun (r : Reconstitution) -> r.Department |> eqs filter.Patient.Department fun (r : Reconstitution) -> - match r.Location, filter.Location with + match r.Location, filter.Patient.VenousAccess with | AnyAccess, _ - | _, AnyAccess -> true - | _ -> r.Location = filter.Location + | _, [] + | _, [ AnyAccess ] -> true + | _ -> + filter.Patient.VenousAccess + |> List.exists ((=) r.Location) |] |> Array.fold (fun (acc : Reconstitution[]) pred -> acc |> Array.filter pred @@ -172,6 +185,9 @@ module Product = module Parenteral = + open Informedica.GenUnits.Lib + + let private get_ () = Web.getDataFromSheet Web.dataUrlIdGenPres "ParentMeds" |> fun data -> @@ -222,11 +238,15 @@ module Product = Label = r.Name Shape = "" Routes = [||] - ShapeQuantities = [| 1N |] - ShapeUnit = "mL" + ShapeQuantities = + 1N + |> ValueUnit.singleWithUnit + Units.Volume.milliLiter + ShapeUnit = + Units.Volume.milliLiter RequiresReconstitution = false Reconstitution = [||] - Divisible = 10N |> Some + Divisible = Some 10N Substances = r.Substances |> Array.map (fun (s, q) -> @@ -236,10 +256,19 @@ module Product = | _ -> failwith $"cannot parse substance {s}" { Name = n - Quantity = q - Unit = u + Quantity = + q + |> Option.bind (fun q -> + u + |> Units.fromString + |> function + | None -> None + | Some u -> + q + |> ValueUnit.singleWithUnit u + |> Some + ) MultipleQuantity = None - MultipleUnit = "" } ) } @@ -252,10 +281,13 @@ module Product = + open Informedica.GenUnits.Lib + + let private get_ () = // check if the shape is a solution let isSol = ShapeRoute.isSolution (ShapeRoute.get ()) - + // TODO make this a configuration let rename (subst : Informedica.ZIndex.Lib.Types.ProductSubstance) defN = if subst.SubstanceName |> String.startsWithCapsInsens "AMFOTERICINE B" || subst.SubstanceName |> String.startsWithCapsInsens "COFFEINE" then @@ -307,9 +339,11 @@ module Product = let atc = gp.ATC |> ATCGroup.findByATC5 - let su = + let shpUnit = gp.Substances[0].ShapeUnit |> String.toLower + |> Units.fromString + |> Option.defaultValue NoUnit { GPK = $"{gp.Id}" @@ -366,12 +400,10 @@ module Product = |> Array.distinct |> fun xs -> if xs |> Array.isEmpty then [| 1N |] else xs - ShapeUnit = - gp.Substances[0].ShapeUnit - |> Mapping.mapUnit - |> Option.defaultValue "" + |> ValueUnit.withUnit shpUnit + ShapeUnit = shpUnit RequiresReconstitution = - Mapping.requiresReconstitution (gp.Route, su, gp.Shape) + Mapping.requiresReconstitution (gp.Route, shpUnit, gp.Shape) Reconstitution = Reconstitution.get () |> Array.filter (fun r -> @@ -384,8 +416,15 @@ module Product = DoseType = r.DoseType Department = r.Dep Location = r.Location - DiluentVolume = r.DiluentVol.Value - ExpansionVolume = r.ExpansionVol + DiluentVolume = + r.DiluentVol.Value + |> ValueUnit.singleWithUnit Units.Volume.milliLiter + ExpansionVolume = + r.ExpansionVol + |> Option.map (fun v -> + v + |> ValueUnit.singleWithUnit Units.Volume.milliLiter + ) Diluents = r.Diluents |> String.splitAt ';' @@ -394,24 +433,31 @@ module Product = ) Divisible = // TODO: need to map this to a config setting - if gp.Shape |> String.containsCapsInsens "druppel" then Some 20N + if gp.Shape |> String.containsCapsInsens "druppel" then 20N else - if isSol gp.Shape then 10N |> Some - else Some 1N + if isSol gp.Shape then 10N + else 1N + |> Some Substances = gp.Substances |> Array.map (fun s -> + let su = + s.SubstanceUnit + |> Units.fromString + |> Option.map (fun u -> + u |> ValueUnit.per shpUnit + ) + |> Option.defaultValue NoUnit { Name = rename s s.SubstanceName Quantity = s.SubstanceQuantity |> BigRational.fromFloat - Unit = - s.SubstanceUnit - |> Mapping.mapUnit - |> Option.defaultValue "" + |> Option.map (fun q -> + q + |> ValueUnit.singleWithUnit su + ) MultipleQuantity = None - MultipleUnit = "" } ) } @@ -449,20 +495,25 @@ module Product = |> Array.filter (fun r -> (rte |> String.isNullOrWhiteSpace || r.Route |> String.equalsCapInsens rte) && (r.DoseType = AnyDoseType || r.DoseType = dtp) && - (dep |> String.isNullOrWhiteSpace || r.Department |> String.equalsCapInsens dep) && - (r.Location = AnyAccess || r.Location = loc) + (dep |> Option.map (fun dep -> r.Department |> String.equalsCapInsens dep) |> Option.defaultValue true) && + (loc |> List.isEmpty || loc |> List.exists ((=) r.Location) || loc |> List.exists ((=) AnyAccess)) ) |> Array.map (fun r -> { prod with - ShapeUnit = "milliliter" - ShapeQuantities = [| r.DiluentVolume |] + ShapeUnit = + Units.Volume.milliLiter + ShapeQuantities = r.DiluentVolume Substances = prod.Substances |> Array.map (fun s -> { s with Quantity = s.Quantity - |> Option.map (fun q -> q / r.DiluentVolume) + |> Option.map (fun q -> + // replace the old shapeunit with the new one + let one = 1N |> ValueUnit.singleWithUnit prod.ShapeUnit + (one * q) / r.DiluentVolume + ) } ) } diff --git a/src/Informedica.GenForm.Lib/Scripts/Update.fsx b/src/Informedica.GenForm.Lib/Scripts/Update.fsx index 6e3e1d0..9b8bcc1 100644 --- a/src/Informedica.GenForm.Lib/Scripts/Update.fsx +++ b/src/Informedica.GenForm.Lib/Scripts/Update.fsx @@ -4,7 +4,7 @@ #load "../Types.fs" #load "../Utils.fs" #load "../Mapping.fs" -#load "../MinMax.fs" +#load "../VenousAccess.fs" #load "../Patient.fs" #load "../DoseType.fs" #load "../Product.fs" @@ -14,3014 +14,67 @@ #load "../PrescriptionRule.fs" - -[] -module Types = - - open MathNet.Numerics - open Informedica.GenUnits.Lib - - type MinMax = Informedica.GenCore.Lib.Ranges.MinMax - - /// Associate a Route and a Shape - /// setting default values for the other fields - type RouteShape = - { - /// The Route - Route : string - /// The pharmacological Shape - Shape : string - /// The Unit of the Shape - Unit : Unit - /// The Dose Unit to use for Dose Limits - DoseUnit : Unit - /// The minimum Dose quantity - MinDoseQty : ValueUnit option - /// The maximum Dose quantity - MaxDoseQty : ValueUnit option - /// Whether a Dose runs over a Time - Timed : bool - /// Whether the Shape needs to be reconstituted - Reconstitute : bool - /// Whether the Shape is a solution - IsSolution : bool - } - - - /// The types for VenousAccess. - type VenousAccess = - /// Peripheral Venous Access - | PVL - /// Central Venous Access - | CVL - /// Any Venous Access - | AnyAccess - - - /// Possible Genders. - type Gender = Male | Female | AnyGender - - - /// Possible Dose Types. - type DoseType = - /// A Start Dose - | Start - /// A Once only Dose - | Once - /// A PRN Dose - | PRN - /// A Maintenance Dose - | Maintenance - /// A Continuous Dose - | Continuous - /// A Step Down Dose in a number of steps. - | StepDown of int - /// A Step Up Dose in a number of steps. - | StepUp of int - - | Contraindicated - /// Any Dosetype - | AnyDoseType - - - /// A Shape Route with associated attributes. - type ShapeRoute = - { - /// The pharmacological Shape - Shape : string - /// The Route of administration - Route : string - /// The Unit of the Shape - Unit : Unit - /// The Dose Unit to use for Dose Limits - DoseUnit : Unit - /// Whether a Dose runs over a Time - Timed : bool - /// Whether the Shape needs to be reconstituted - Reconstitute : bool - /// Whether the Shape is a solution - IsSolution : bool - } - - - /// A Substance type. - type Substance = - { - /// The name of the Substance - Name : string - /// The Quantity of the Substance - Quantity : ValueUnit option - /// The indivisible Quantity of the Substance - MultipleQuantity : ValueUnit option - } - - /// A Product type. - type Product = - { - /// The GPK id of the Product - GPK : string - /// The ATC code of the Product - ATC : string - /// The ATC main group of the Product - MainGroup : string - /// The ATC sub group of the Product - SubGroup : string - /// The Generic name of the Product - Generic : string - /// A tall-man representation of the Generic name of the Product - TallMan : string - /// Synonyms for the Product - Synonyms : string array - /// The full product name of the Product - Product : string - /// The label of the Product - Label : string - /// The pharmacological Shape of the Product - Shape : string - /// The possible Routes of administration of the Product - Routes : string [] - /// The possible quantities of the Shape of the Product - ShapeQuantities : ValueUnit - /// The uid of the Shape of the Product - ShapeUnit : Unit - /// Whether the Shape of the Product requires reconstitution - RequiresReconstitution : bool - /// The possible reconstitution rules for the Product - Reconstitution : Reconstitution [] - /// The division factor of the Product - Divisible : ValueUnit - /// The Substances in the Product - Substances : Substance array - } - and Reconstitution = - { - /// The route for the reconstitution - Route : string - /// The DoseType for the reconstitution - DoseType: DoseType - /// The department for the reconstitution - Department : string - /// The location for the reconstitution - Location : VenousAccess - /// The volume of the reconstitution - DiluentVolume : ValueUnit - /// An optional expansion volume of the reconstitution - ExpansionVolume : ValueUnit option - /// The Diluents for the reconstitution - Diluents : string [] - } - - - /// A DoseLimit for a Shape or Substance. - type DoseLimit = - { - /// The Target for the Doselimit - DoseLimitTarget : DoseLimitTarget - /// A MinMax Dose Quantity for the DoseLimit - Quantity : MinMax - /// An optional Dose Quantity Adjust for the DoseLimit. - /// Note: if this is specified a min and max QuantityAdjust - /// will be assumed to be 10% minus and plus the normal value - NormQuantityAdjust : ValueUnit option - /// A MinMax Quantity Adjust for the DoseLimit - QuantityAdjust : MinMax - /// An optional Dose Per Time for the DoseLimit - PerTime : MinMax - /// An optional Per Time Adjust for the DoseLimit - /// Note: if this is specified a min and max NormPerTimeAdjust - /// will be assumed to be 10% minus and plus the normal value - NormPerTimeAdjust : ValueUnit option - /// A MinMax Per Time Adjust for the DoseLimit - PerTimeAdjust : MinMax - /// A MinMax Rate for the DoseLimit - Rate : MinMax - /// A MinMax Rate Adjust for the DoseLimit - RateAdjust : MinMax - } - and DoseLimitTarget = - | NoDoseLimitTarget - | SubstanceDoseLimitTarget of string - | ShapeDoseLimitTarget of string - - - /// A PatientCategory to which a DoseRule applies. - type PatientCategory = - { - Department : string option - Diagnoses : string [] - Gender : Gender - Age : MinMax - Weight : MinMax - BSA : MinMax - GestAge : MinMax - PMAge : MinMax - Location : VenousAccess - } - - - /// A specific Patient to filter DoseRules. - type Patient = - { - /// The Department of the Patient - Department : string option - /// A list of Diagnoses of the Patient - Diagnoses : string [] - /// The Gender of the Patient - Gender : Gender - /// The Age in days of the Patient - Age : ValueUnit option - /// The Weight in grams of the Patient - Weight : ValueUnit option - /// The Height in cm of the Patient - Height : ValueUnit option - /// The Gestational Age in days of the Patient - GestAge : ValueUnit option - /// The Post Menstrual Age in days of the Patient - PMAge : ValueUnit option - /// The Venous Access of the Patient - VenousAccess : VenousAccess list - } - static member Gender_ = - (fun (p : Patient) -> p.Gender), (fun g (p : Patient) -> { p with Gender = g}) - - static member Age_ = - (fun (p : Patient) -> p.Age), (fun a (p : Patient) -> { p with Age = a}) - - static member Weight_ = - (fun (p : Patient) -> p.Weight), (fun w (p : Patient) -> { p with Weight = w}) - - static member Height_ = - (fun (p : Patient) -> p.Height), (fun b (p : Patient) -> { p with Height = b}) - - static member GestAge_ = - (fun (p : Patient) -> p.GestAge), (fun a (p : Patient) -> { p with GestAge = a}) - - static member PMAge_ = - (fun (p : Patient) -> p.PMAge), (fun a (p : Patient) -> { p with PMAge = a}) - - static member Department_ = - (fun (p : Patient) -> p.Department), (fun d (p : Patient) -> { p with Department = d}) - - - /// The DoseRule type. Identifies exactly one DoseRule - /// for a specific PatientCategory, Indication, Generic, Shape, Route and DoseType. - type DoseRule = - { - /// The Indication of the DoseRule - Indication : string - /// The Generic of the DoseRule - Generic : string - /// The pharmacological Shape of the DoseRule - Shape : string - /// The Route of administration of the DoseRule - Route : string - /// The PatientCategory of the DoseRule - PatientCategory : PatientCategory - /// The DoseType of the DoseRule - DoseType : DoseType - /// The possible Frequencies of the DoseRule - Frequencies : ValueUnit option - /// The MinMax Administration Time of the DoseRule - AdministrationTime : MinMax - /// The MinMax Interval Time of the DoseRule - IntervalTime : MinMax - /// The MinMax Duration of the DoseRule - Duration : MinMax - /// The list of associated DoseLimits of the DoseRule. - /// In principle for the Shape and each Substance . - DoseLimits : DoseLimit array - /// The list of associated Products of the DoseRule. - Products : Product array - } - - - /// A SolutionLimit for a Substance. - type SolutionLimit = - { - /// The Substance for the SolutionLimit - Substance : string - /// The MinMax Quantity of the Substance for the SolutionLimit - Quantity : MinMax - /// A list of possible Quantities of the Substance for the SolutionLimit - Quantities : ValueUnit option - /// The Minmax Concentration of the Substance for the SolutionLimit - Concentration : MinMax - } - - - /// A SolutionRule for a specific Generic, Shape, Route, DoseType, Department - /// Venous Access Location, Age range, Weight range, Dose range and Generic Products. - type SolutionRule = - { - /// The Generic of the SolutionRule - Generic : string - /// The Shape of the SolutionRule - Shape : string - /// The Route of the SolutionRule - Route : string - /// The DoseType of the SolutionRule - DoseType : DoseType - /// The Department of the SolutionRule - Department : string - /// The Venous Access Location of the SolutionRule - Location : VenousAccess - /// The MinMax Age range of the SolutionRule - Age : MinMax - /// The MinMax Weight range of the SolutionRule - Weight : MinMax - /// The MinMax Dose range of the SolutionRule - Dose : MinMax - /// The Products the SolutionRule applies to - Products : Product [] - /// The possible Solutions to use - Solutions : string [] - /// The possible Volumes to use - Volumes : ValueUnit option - /// A MinMax Volume range to use - Volume : MinMax - /// The percentage to be use as a DoseQuantity - DosePerc : MinMax - /// The SolutionLimits for the SolutionRule - SolutionLimits : SolutionLimit [] - } - - - /// A Filter to get the DoseRules for a specific Patient. - type Filter = - { - /// The Indication to filter on - Indication : string option - /// The Generic to filter on - Generic : string option - /// The Shape to filter on - Shape : string option - /// The Route to filter on - Route : string option - /// The DoseType to filter on - DoseType : DoseType - /// The patient to filter on - Patient : Patient - } - - - /// A PrescriptionRule for a specific Patient - /// with a DoseRule and a list of SolutionRules. - type PrescriptionRule = - { - Patient : Patient - DoseRule : DoseRule - SolutionRules : SolutionRule [] - } - - - -[] -module Utils = - - open System - open System.IO - open System.Net.Http - - open Informedica.Utils.Lib - open Informedica.Utils.Lib.BCL - - - - module Web = - - - /// The url to the data sheet for Constraints - let [] dataUrlIdConstraints = "1nny8rn9zWtP8TMawB3WeNWhl5d4ofbWKbGzGqKTd49g" - - - /// The url to the data sheet for GenPRES - /// https://docs.google.com/spreadsheets/d/1AEVYnqjAbVniu3VuczeoYvMu3RRBu930INhr3QzSDYQ/edit?usp=sharing - let [] dataUrlIdGenPres = "1AEVYnqjAbVniu3VuczeoYvMu3RRBu930INhr3QzSDYQ" - - - /// - /// Get data from a web sheet - /// - /// The Url Id of the web sheet - /// The specific sheet - /// The data as a table of string array array - let getDataFromSheet urlId sheet = - fun () -> Web.GoogleSheets.getDataFromSheet urlId sheet - |> StopWatch.clockFunc $"loaded {sheet} from web sheet" - - - - module BigRational = - - - /// - /// Parse an array of strings in float format to an array of BigRational - /// - /// - /// Uses ; as separator. Filters out non parsable strings. - /// - /// - /// - /// let brs = toBrs "1.0;2.0;3.0" - /// // returns [|1N; 2N; 3N|] - /// let brs = toBrs "1.0;2.0;3.0;abc" - /// // returns [|1N; 2N; 3N|] - /// - /// - let toBrs s = - s - |> String.splitAt ';' - |> Array.choose Double.tryParse - |> Array.choose BigRational.fromFloat - - - /// - /// Return 2 BigRational arrays as a tuple of optional first BigRational - /// of the first and second array. A None is returned for an empty array. - /// - /// - /// - /// let brs1 = [|1N|] - /// let brs2 = [|4N|] - /// tupleBrOpt brs1 brs2 - /// // returns (Some 1N, Some 4N) - /// let brs1 = [|1N|] - /// let brs2 = [||] - /// tupleBrOpt brs1 brs2 - /// // returns (Some 1N, None) - /// - /// - let tupleBrOpt brs1 brs2 = - brs1 |> Array.tryHead, - brs2 |> Array.tryHead - - - module Calculations = - - open MathNet.Numerics - open Informedica.GenUnits.Lib - - module Conversions = Informedica.GenCore.Lib.Conversions - module BSA = Informedica.GenCore.Lib.Calculations.BSA - - let calcDuBois weight height = - let w = - weight - |> ValueUnit.convertTo Units.Mass.kiloGram - |> ValueUnit.getValue - |> Array.tryHead - |> Option.defaultValue 0N - |> BigRational.toDecimal - |> Conversions.kgFromDecimal - let h = - height - |> ValueUnit.convertTo Units.Height.centiMeter - |> ValueUnit.getValue - |> Array.tryHead - |> Option.defaultValue 0N - |> BigRational.toDecimal - |> Conversions.cmFromDecimal - - BSA.calcDuBois (Some 2) w h - |> decimal - |> BigRational.fromDecimal - |> ValueUnit.singleWithUnit Units.BSA.m2 - - - module Units = - - open Informedica.GenUnits.Lib - - let week = Units.Time.week - - let day = Units.Time.day - - let weightGram = Units.Weight.gram - - let heightCm = Units.Height.centiMeter - - let bsaM2 = Units.BSA.m2 - - let timeUnit s = - if s |> String.isNullOrWhiteSpace then None - else - $"{s}[Time]" |> Units.fromString - - let freqUnit s = - if s |> String.isNullOrWhiteSpace then None - else - $"times[Count]/{s}[Time]" |> Units.fromString - - let adjustUnit s = - match s with - | _ when s |> String.equalsCapInsens "kg" -> Units.Weight.kiloGram |> Some - | _ when s |> String.equalsCapInsens "m2" -> bsaM2 |> Some - | _ -> None - - - let mL = Units.Volume.milliLiter - - - - module ValueUnit = - - open MathNet.Numerics - open Informedica.GenUnits.Lib - - - /// The full term age for a neonate - /// which is 37 weeks - let ageFullTerm = 37N |> ValueUnit.singleWithUnit Units.Time.week - - - let withOptionalUnit u v = - match v, u with - | Some v, Some u -> - v - |> ValueUnit.singleWithUnit u +open MathNet.Numerics +open Informedica.Utils.Lib.BCL +open Informedica.GenUnits.Lib +open Informedica.GenForm.Lib +open Informedica.GenCore.Lib.Ranges + + +{ PatientCategory.patientCategory with + Department = None + Age = + { PatientCategory.patientCategory.Age with + Min = + 90N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive |> Some - | _ -> None - - - let toString prec vu = - ValueUnit.toStringDecimalDutchShortWithPrec prec vu - |> String.replace ";" ", " - - - module MinMax = - - open Informedica.GenUnits.Lib - open Informedica.GenCore.Lib.Ranges - - let fromTuple u (min, max) = - match u with - | None -> MinMax.empty - | Some u -> - { - Min = - min - |> Option.map (ValueUnit.singleWithUnit u) - |> Option.map Inclusive - Max = - max - |> Option.map (ValueUnit.singleWithUnit u) - |> Option.map Inclusive - } - - - let inRange minMax vu = - vu - |> Option.map (fun v -> - minMax |> MinMax.inRange v - ) - |> Option.defaultValue true - - - -module VenousAccess = - - - let check location venousAccess = - match location, venousAccess with - | AnyAccess, xs - | _, xs when xs |> List.isEmpty -> true - | _ -> - venousAccess - |> List.exists ((=) location) - - -module Mapping = - - open Informedica.Utils.Lib - open Informedica.Utils.Lib.BCL - open Informedica.GenUnits.Lib - - - /// Mapping of long Z-index route names to short names - let routeMapping = - Web.getDataFromSheet Web.dataUrlIdGenPres "Routes" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - - {| - Long = get "ZIndex" - Short = get "ShortDutch" - |} - ) - - - /// Mapping of long Z-index unit names to short names - let unitMapping = - Web.getDataFromSheet Web.dataUrlIdGenPres "Units" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - - {| - Long = get "ZIndexUnitLong" - Short = get "Unit" - MV = get "MetaVisionUnit" - |} - ) - - - /// Try to find mapping for a route - let mapRoute rte = - routeMapping - |> Array.tryFind (fun r -> - r.Long |> String.equalsCapInsens rte || - r.Short |> String.equalsCapInsens rte - - ) - |> Option.map (fun r -> r.Short) - - - /// Try to map a unit to a short name - let mapUnit unt = - unitMapping - |> Array.tryFind (fun r -> - r.Long |> String.equalsCapInsens unt || - r.Short |> String.equalsCapInsens unt - ) - |> Option.map (fun r -> r.Short) - - - /// Get the array of RouteShape records - let mappingRouteShape = - Web.getDataFromSheet Web.dataUrlIdGenPres "ShapeRoute" - |> fun data -> - let inline getColumn get = - data - |> Array.head - |> get - - data - |> Array.tail - |> Array.map (fun r -> - let getStr = getColumn Csv.getStringColumn r - let getFlt = getColumn Csv.getFloatOptionColumn r - - { - Route = getStr "Route" - Shape = getStr "Shape" - Unit = getStr "Unit" |> Units.fromString |> Option.defaultValue NoUnit - DoseUnit = getStr "DoseUnit" |> Units.fromString |> Option.defaultValue NoUnit - MinDoseQty = None // getFlt "MinDoseQty" - MaxDoseQty = None //getFlt "MaxDoseQty" - Timed = getStr "Timed" |> String.equalsCapInsens "true" - Reconstitute = getStr "Reconstitute" |> String.equalsCapInsens "true" - IsSolution = getStr "IsSolution" |> String.equalsCapInsens "true" - } - |> fun rs -> - match rs.DoseUnit with - | NoUnit -> rs - | du -> - { rs with - MinDoseQty = - getFlt "MinDoseQty" - |> Option.bind (fun v -> - v - |> BigRational.fromFloat - |> Option.map (ValueUnit.singleWithUnit du) - ) - MaxDoseQty = - getFlt "MaxDoseQty" - |> Option.bind (fun v -> - v - |> BigRational.fromFloat - |> Option.map (ValueUnit.singleWithUnit du) - ) - } - ) - - - /// - /// Filter the mappingRouteShape array on route, shape and unit - /// - /// The Route - /// The Shape - /// The Unit - /// An array of RouteShape records - let filterRouteShapeUnit rte shape unt = - mappingRouteShape - |> Array.filter (fun xs -> - let eqsRte = - rte |> String.isNullOrWhiteSpace || - rte |> String.trim |> String.equalsCapInsens xs.Route || - xs.Route |> mapRoute |> Option.map (String.equalsCapInsens (rte |> String.trim)) |> Option.defaultValue false - let eqsShp = shape |> String.isNullOrWhiteSpace || shape |> String.trim |> String.equalsCapInsens xs.Shape - let eqsUnt = - unt = NoUnit || - unt |> Units.eqsUnit xs.Unit - eqsRte && eqsShp && eqsUnt - ) - - - let private requires_ (rtes, unt, shape) = - rtes - |> Array.collect (fun rte -> - filterRouteShapeUnit rte shape unt - ) - |> Array.map (fun xs -> xs.Reconstitute) - |> Array.exists id - - - /// Check if reconstitution is required for a route, shape and unit - let requiresReconstitution = - Memoization.memoize requires_ - - - -module Gender = - - open Informedica.Utils.Lib.BCL - - - /// Map a string to a Gender. - let fromString s = - let s = s |> String.toLower |> String.trim - match s with - | "man" -> Male - | "vrouw" -> Female - | _ -> AnyGender - - - /// Get the string representation of a Gender. - let toString = function - | Male -> "man" - | Female -> "vrouw" - | AnyGender -> "" - - - /// Check if a Filter contains a Gender. - /// Note if AnyGender is specified, this will always return true. - let filter gender (filter : Filter) = - match filter.Patient.Gender, gender with - | _, AnyGender -> true - | _ -> filter.Patient.Gender = gender - - - -module PatientCategory = - - - open MathNet.Numerics - open Informedica.Utils.Lib.BCL - open Informedica.GenUnits.Lib - - module BSA = Informedica.GenCore.Lib.Calculations.BSA - module Limit = Informedica.GenCore.Lib.Ranges.Limit - module MinMax = Informedica.GenCore.Lib.Ranges.MinMax - module Conversions = Informedica.GenCore.Lib.Conversions - - - /// - /// Use a PatientCategory to get a sort value. - /// - /// - /// The order will be based on the following: - /// - Age - /// - Weight - /// - Gestational Age - /// - Post Menstrual Age - /// The first will receive the highest weight and the last the lowest. - /// - let sortBy (pat : PatientCategory) = - let toInt = function - | Some x -> - x - |> Limit.getValueUnit - |> ValueUnit.getValue - |> Array.tryHead - |> function - | None -> 0 - | Some x -> x |> BigRational.ToInt32 - | None -> 0 - - (pat.Age.Min |> toInt |> fun i -> if i > 0 then i + 300 else i) + - (pat.GestAge.Min |> toInt) + - (pat.PMAge.Min |> toInt) + - (pat.Weight.Min |> toInt |> fun w -> w / 1000) - - - /// - /// Filters a PatientCategory using a Filter. - /// Returns true if the PatientCategory matches the Filter criteria. - /// - /// The Filter to filter the PatientCategory with - /// The Patient Category - let filter (filter : Filter) (patCat : PatientCategory) = - let eqs a b = - match a, b with - | None, _ - | _, None -> true - | Some a, Some b -> a = b - - ([| patCat |] - |> Array.filter (fun p -> - if filter.Patient.Diagnoses |> Array.isEmpty then true - else - p.Diagnoses - |> Array.exists (fun d -> - filter.Patient.Diagnoses - |> Array.exists (String.equalsCapInsens d) - ) - ), - [| - fun (p: PatientCategory) -> filter.Patient.Department |> eqs p.Department - fun (p: PatientCategory) -> filter.Patient.Age |> Utils.MinMax.inRange p.Age - fun (p: PatientCategory) -> filter.Patient.Weight |> Utils.MinMax.inRange p.Weight - fun (p: PatientCategory) -> - match filter.Patient.Weight, filter.Patient.Height with - | Some w, Some h -> - Utils.Calculations.calcDuBois w h - |> Some - | _ -> None - |> Utils.MinMax.inRange p.BSA - if filter.Patient.Age |> Option.isSome then - yield! [| - fun (p: PatientCategory) -> - // defaults to normal gestation - filter.Patient.GestAge - |> Option.defaultValue Utils.ValueUnit.ageFullTerm - |> Some - |> Utils.MinMax.inRange p.GestAge - fun (p: PatientCategory) -> - // defaults to normal postmenstrual age - filter.Patient.PMAge - |> Option.defaultValue Utils.ValueUnit.ageFullTerm - |> Some - |> Utils.MinMax.inRange p.PMAge - |] - fun (p: PatientCategory) -> filter |> Gender.filter p.Gender - fun (p: PatientCategory) -> VenousAccess.check p.Location filter.Patient.VenousAccess - |]) - ||> Array.fold(fun acc pred -> - acc - |> Array.filter pred - ) - |> fun xs -> xs |> Array.length > 0 - - - /// - /// Check whether an age and weight are between the - /// specified age MinMax and weight MinMax. - /// - /// An optional age - /// An optional weight - /// The age MinMax - /// The weight MinMax - /// - /// When age and or weight are not specified, they are - /// considered to be between the minimum and maximum. - /// - let checkAgeWeightMinMax age weight aMinMax wMinMax = - // TODO rename aMinMax and wMinMax to ageMinMax and weightMinMax - - age |> Utils.MinMax.inRange aMinMax && - weight |> Utils.MinMax.inRange wMinMax - - - /// Prints an age in days as a string. - let printAge age = - let age = - age - |> ValueUnit.convertTo Units.Time.day - |> ValueUnit.getValue - |> Array.tryHead - |> Option.defaultValue 0N - let a = age |> BigRational.ToInt32 - match a with - | _ when a < 7 -> - if a = 1 then $"%i{a} dag" - else $"%i{a} dagen" - | _ when a <= 30 -> - let a = a / 7 - if a = 1 then $"%i{a} week" - else $"%i{a} weken" - | _ when a < 365 -> - let a = a / 30 - if a = 1 then $"%i{a} maand" - else $"%i{a} maanden" - | _ -> - let a = a / 365 - if a = 1 then $"%A{a} jaar" - else $"%A{a} jaar" - - - /// Print days as weeks. - let printDaysToWeeks d = - let d = - d - |> ValueUnit.convertTo Units.Time.day - |> ValueUnit.getValue - |> Array.tryHead - |> Option.defaultValue 0N - - let d = d |> BigRational.ToInt32 - (d / 7) |> sprintf "%i weken" - - - /// Print an MinMax age as a string. - let printAgeMinMax (age : MinMax) = - let printAge = Limit.getValueUnit >> printAge - match age.Min, age.Max with - | Some min, Some max -> - let min = min |> printAge - let max = max |> printAge - $"leeftijd %s{min} tot %s{max}" - | Some min, None -> - let min = min |> printAge - $"leeftijd vanaf %s{min}" - | None, Some max -> - let max = max |> printAge - $"leeftijd tot %s{max}" - | _ -> "" - - - - /// Print an PatientCategory as a string. - let toString (pat : PatientCategory) = - - let gender = pat.Gender |> Gender.toString - - let age = pat.Age |> printAgeMinMax - - let neonate = - let s = - if pat.GestAge.Max.IsSome && - pat.GestAge.Max.Value - |> Limit.getValueUnit > printDaysToWeeks - - match pat.GestAge.Min, pat.GestAge.Max, pat.PMAge.Min, pat.PMAge.Max with - | _, _, Some min, Some max -> - let min = min |> printDaysToWeeks - let max = max |> printDaysToWeeks - $"{s} postconceptie leeftijd %s{min} tot %s{max}" - | _, _, Some min, None -> - let min = min |> printDaysToWeeks - $"{s} postconceptie leeftijd vanaf %s{min}" - | _, _, None, Some max -> - let max = max |> printDaysToWeeks - $"{s} postconceptie leeftijd tot %s{max}" - - | Some min, Some max, _, _ -> - let min = min |> printDaysToWeeks - let max = max |> printDaysToWeeks - $"{s} zwangerschapsduur %s{min} tot %s{max}" - | Some min, None, _, _ -> - let min = min |> printDaysToWeeks - if s = "neonaten" then s - else - $"{s} zwangerschapsduur vanaf %s{min}" - | None, Some max, _, _ -> - let max = max |> printDaysToWeeks - $"{s} zwangerschapsduur tot %s{max}" - | _ -> "" - - let weight = - let toStr lim = - lim - |> Limit.getValueUnit - |> ValueUnit.convertTo Units.Weight.kiloGram - |> Utils.ValueUnit.toString -1 - - match pat.Weight.Min, pat.Weight.Max with - | Some min, Some max -> $"gewicht %s{min |> toStr} tot %s{max |> toStr}" - | Some min, None -> $"gewicht vanaf %s{min |> toStr}" - | None, Some max -> $"gewicht tot %s{max |> toStr}" - | None, None -> "" - - [ - pat.Department |> Option.defaultValue "" - gender - neonate - age - weight - ] - |> List.filter String.notEmpty - |> List.filter (String.isNullOrWhiteSpace >> not) - |> String.concat ", " - - - -module Patient = - - open MathNet.Numerics - open Informedica.Utils.Lib.BCL - open Informedica.GenUnits.Lib - - module BSA = Informedica.GenCore.Lib.Calculations.BSA - module Conversions = Informedica.GenCore.Lib.Conversions - module Limit = Informedica.GenCore.Lib.Ranges.Limit - - /// An empty Patient. - let patient = - { - Department = None - Diagnoses = [||] - Gender = AnyGender - Age = None - Weight = None - Height = None - GestAge = None - PMAge = None - VenousAccess = [ ] - } - - - let calcPMAge (pat: Patient) = - { pat with - PMAge = - match pat.Age, pat.GestAge with - | Some ad, Some ga -> ad + ga |> Some - | _ -> None } - - - /// Calculate the BSA of a Patient. - let calcBSA (pat: Patient) = - match pat.Weight, pat.Height with - | Some w, Some h -> - Utils.Calculations.calcDuBois w h - |> Some - | _ -> None - - - /// Get the string representation of a Patient. - let rec toString (pat: Patient) = - [ - pat.Department |> Option.defaultValue "" - pat.Gender |> Gender.toString - pat.Age - |> Option.map PatientCategory.printAge - |> Option.defaultValue "" - - let printDaysToWeeks = PatientCategory.printDaysToWeeks - - let s = - if pat.GestAge.IsSome && - pat.GestAge.Value < Utils.ValueUnit.ageFullTerm then "prematuren" - else "neonaten" - - match pat.GestAge, pat.PMAge with - | _, Some a -> - let a = a |> printDaysToWeeks - $"{s} postconceptie leeftijd %s{a}" - | Some a, _ -> - let a = a |> printDaysToWeeks - $"{s} zwangerschapsduur %s{a}" - | _ -> "" - - let toStr u vu = - let v = - vu - |> ValueUnit.convertTo u - |> ValueUnit.getValue - |> Array.tryHead - |> Option.defaultValue 0N - if v.Denominator = 1I then v |> BigRational.ToInt32 |> sprintf "%i" - else - v - |> BigRational.ToDouble - |> sprintf "%A" - - pat.Weight - |> Option.map (fun w -> $"gewicht %s{w |> toStr Units.Mass.kiloGram } kg") - |> Option.defaultValue "" - - pat.Height - |> Option.map (fun h -> $"lengte {h |> toStr Units.Height.centiMeter} cm") - |> Option.defaultValue "" - - pat - |> calcBSA - |> Option.map (fun bsa -> - $"BSA {bsa |> Utils.ValueUnit.toString 3}" - ) - |> Option.defaultValue "" - ] - |> List.filter String.notEmpty - |> List.filter (String.isNullOrWhiteSpace >> not) - |> String.concat ", " - - - -module DoseType = - - - open Informedica.Utils.Lib.BCL - - - /// Get a sort order for a dose type. - let sortBy = function - | Start -> 0 - | Once -> 1 - | PRN -> 2 - | Maintenance -> 3 - | Continuous -> 4 - | StepUp n -> 50 + n - | StepDown n -> 100 + n - | AnyDoseType -> 200 - | Contraindicated -> -1 - - - /// Get a dose type from a string. - let fromString s = - let s = s |> String.toLower |> String.trim - - match s with - | "start" -> Start - | "eenmalig" -> Once - | "prn" -> PRN - | "onderhoud" -> Maintenance - | "continu" -> Continuous - | "contra" -> Contraindicated - - | _ when s |> String.startsWith "afbouw" -> - match s |> String.split(" ") with - | [_;i] -> - match i |> Int32.tryParse with - | Some i -> StepDown i - | None -> - printfn $"DoseType.fromString couldn't match {s}" - AnyDoseType - | _ -> - printfn $"DoseType.fromString couldn't match {s}" - AnyDoseType - - | _ when s |> String.startsWith "opbouw" -> - match s |> String.split(" ") with - | [_;i] -> - match i |> Int32.tryParse with - | Some i -> StepUp i - | None -> - printfn $"DoseType.fromString couldn't match {s}" - AnyDoseType - | _ -> - printfn $"DoseType.fromString couldn't match {s}" - AnyDoseType - - | _ when s |> String.isNullOrWhiteSpace -> AnyDoseType - - | _ -> - printfn $"DoseType.fromString couldn't match {s}" - AnyDoseType - - - /// Get a string representation of a dose type. - let toString = function - | Start -> "start" - | Once -> "eenmalig" - | PRN -> "prn" - | Maintenance -> "onderhoud" - | Continuous -> "continu" - | StepDown i -> $"afbouw {i}" - | StepUp i -> $"opbouw {i}" - | Contraindicated -> "contra" - | AnyDoseType -> "" - - - -module Product = - - open MathNet.Numerics - open Informedica.Utils.Lib - open Informedica.Utils.Lib.BCL - - - module GenPresProduct = Informedica.ZIndex.Lib.GenPresProduct - module ATCGroup = Informedica.ZIndex.Lib.ATCGroup - - - module Location = - - - /// Get a string representation of the VenousAccess. - let toString = function - | PVL -> "PVL" - | CVL -> "CVL" - | AnyAccess -> "" - - - /// Get a VenousAccess from a string. - let fromString s = - match s with - | _ when s |> String.equalsCapInsens "PVL" -> PVL - | _ when s |> String.equalsCapInsens "CVL" -> CVL - | _ -> AnyAccess - - - - module ShapeRoute = - - open Informedica.GenUnits.Lib - - - let private get_ () = - Web.getDataFromSheet Web.dataUrlIdGenPres "ShapeRoute" - |> fun data -> - - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - { - Shape = get "Shape" - Route = get "Route" - Unit = - get "Unit" - |> Units.fromString - |> Option.defaultValue NoUnit - DoseUnit = - get "DoseUnit" - |> Units.fromString - |> Option.defaultValue NoUnit - Timed = get "Timed" = "TRUE" - Reconstitute = get "Reconstitute" = "TRUE" - IsSolution = get "IsSolution" = "TRUE" - } - ) - - - /// - /// Get the ShapeRoute array. - /// - /// - /// This function is memoized. - /// - let get : unit -> ShapeRoute [] = - Memoization.memoize get_ - - - /// - /// Check if the given shape is a solution using - /// a ShapeRoute array. - /// - /// The ShapeRoute array - /// The Shape - let isSolution (srs : ShapeRoute []) shape = - srs - |> Array.tryFind (fun sr -> - sr.Shape |> String.equalsCapInsens shape - ) - |> Option.map (fun sr -> sr.IsSolution) - |> Option.defaultValue false - - - - module Reconstitution = - - open Utils - - // GPK - // Route - // DoseType - // Dep - // CVL - // PVL - // DiluentVol - // ExpansionVol - // Diluents - let private get_ () = - Web.getDataFromSheet Web.dataUrlIdGenPres "Reconstitution" - |> fun data -> - - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - let toBrOpt = BigRational.toBrs >> Array.tryHead - - {| - GPK = get "GPK" - Route = get "Route" - Location = - match get "CVL", get "PVL" with - | s1, _ when s1 |> String.isNullOrWhiteSpace |> not -> CVL - | _, s2 when s2 |> String.isNullOrWhiteSpace |> not -> PVL - | _ -> AnyAccess - DoseType = get "DoseType" |> DoseType.fromString - Dep = get "Dep" - DiluentVol = get "DiluentVol" |> toBrOpt - ExpansionVol = get "ExpansionVol" |> toBrOpt - Diluents = get "Diluents" - |} - ) - - - /// Get the Reconstitution array. - /// Returns an anonymous record with the following fields: - /// unit -> {| Dep: string; DiluentVol: BigRational option; Diluents: string; DoseType: DoseType; ExpansionVol: BigRational option; GPK: string; Location: VenousAccess; Route: string |} array - let get = Memoization.memoize get_ - - - /// - /// Filter the Reconstitution array to get all the reconstitution rules - /// that match the given filter. - /// - /// The Filter - /// The array of reconstitution rules - let filter (filter : Filter) (rs : Reconstitution []) = - let eqs a b = - a - |> Option.map (fun x -> x = b) - |> Option.defaultValue true - - [| - fun (r : Reconstitution) -> r.Route |> eqs filter.Route - fun (r : Reconstitution) -> - if filter.Patient.VenousAccess = [AnyAccess] || - filter.Patient.VenousAccess |> List.isEmpty then true - else - match filter.DoseType with - | AnyDoseType -> true - | _ -> filter.DoseType = r.DoseType - fun (r : Reconstitution) -> r.Department |> eqs filter.Patient.Department - fun (r : Reconstitution) -> - match r.Location, filter.Patient.VenousAccess with - | AnyAccess, _ - | _, [] - | _, [ AnyAccess ] -> true - | _ -> - filter.Patient.VenousAccess - |> List.exists ((=) r.Location) - |] - |> Array.fold (fun (acc : Reconstitution[]) pred -> - acc |> Array.filter pred - ) rs - - - - module Parenteral = - - open Informedica.GenUnits.Lib - - - let private get_ () = - Web.getDataFromSheet Web.dataUrlIdGenPres "ParentMeds" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - let toBrOpt = BigRational.toBrs >> Array.tryHead - - {| - Name = get "Name" - Substances = - [| - "volume mL", get "volume mL" |> toBrOpt - "energie kCal", get "energie kCal" |> toBrOpt - "eiwit g", get "eiwit g" |> toBrOpt - "KH g", get "KH g" |> toBrOpt - "vet g", get "vet g" |> toBrOpt - "Na mmol", get "Na mmol" |> toBrOpt - "K mmol", get "K mmol" |> toBrOpt - "Ca mmol", get "Ca mmol" |> toBrOpt - "P mmol", get "P mmol" |> toBrOpt - "Mg mmol", get "Mg mmol" |> toBrOpt - "Fe mmol", get "Fe mmol" |> toBrOpt - "VitD IE", get "VitD IE" |> toBrOpt - "Cl mmol", get "Cl mmol" |> toBrOpt - - |] - Oplosmiddel = get "volume mL" - Verdunner = get "volume mL" - |} - ) - |> Array.map (fun r -> - { - GPK = r.Name - ATC = "" - MainGroup = "" - SubGroup = "" - Generic = r.Name - TallMan = "" //r.TallMan - Synonyms = [||] - Product = r.Name - Label = r.Name - Shape = "" - Routes = [||] - ShapeQuantities = - 1N - |> ValueUnit.singleWithUnit - Units.Volume.milliLiter - ShapeUnit = - Units.Volume.milliLiter - RequiresReconstitution = false - Reconstitution = [||] - Divisible = - 10N - |> ValueUnit.singleWithUnit - Units.Count.times - Substances = - r.Substances - |> Array.map (fun (s, q) -> - let n, u = - match s |> String.split " " with - | [n; u] -> n |> String.trim, u |> String.trim - | _ -> failwith $"cannot parse substance {s}" - { - Name = n - Quantity = - q - |> Option.bind (fun q -> - u - |> Units.fromString - |> function - | None -> None - | Some u -> - q - |> ValueUnit.singleWithUnit u - |> Some - ) - MultipleQuantity = None - } - ) - } - ) - - - /// Get the Parenterals as a Product array. - let get : unit -> Product [] = - Memoization.memoize get_ - - - - open Informedica.GenUnits.Lib - - - let private get_ () = - // check if the shape is a solution - let isSol = ShapeRoute.isSolution (ShapeRoute.get ()) - - let rename (subst : Informedica.ZIndex.Lib.Types.ProductSubstance) defN = - if subst.SubstanceName |> String.startsWithCapsInsens "AMFOTERICINE B" || - subst.SubstanceName |> String.startsWithCapsInsens "COFFEINE" then - subst.GenericName - |> String.replace "0-WATER" "BASE" - else defN - |> String.toLower - - fun () -> - // first get the products from the GenPres Formulary, i.e. - // the assortment - Web.getDataFromSheet Web.dataUrlIdGenPres "Formulary" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - let formulary = - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - - {| - GPKODE = get "GPKODE" |> Int32.parse - Apotheek = get "UMCU" - ICC = get "ICC" - NEO = get "NEO" - ICK = get "ICK" - HCK = get "HCK" - tallMan = get "TallMan" - |} - ) - - formulary - // find the matching GenPresProducts - |> Array.collect (fun r -> - r.GPKODE - |> GenPresProduct.findByGPK - ) - // collect the GenericProducts - |> Array.collect (fun gpp -> - gpp.GenericProducts - |> Array.map (fun gp -> gpp, gp) - ) - // create the Product records - |> Array.map (fun (gpp, gp) -> - let atc = - gp.ATC - |> ATCGroup.findByATC5 - let su = - gp.Substances[0].ShapeUnit - |> String.toLower - |> Units.fromString - |> Option.defaultValue NoUnit - - { - GPK = $"{gp.Id}" - ATC = gp.ATC |> String.trim - MainGroup = - atc - |> Array.map (fun g -> g.AnatomicalGroup) - |> Array.tryHead - |> Option.defaultValue "" - SubGroup = - atc - |> Array.map (fun g -> g.TherapeuticSubGroup) - |> Array.tryHead - |> Option.defaultValue "" - Generic = - rename gp.Substances[0] gpp.Name - TallMan = - match formulary |> Array.tryFind(fun f -> f.GPKODE = gp.Id) with - | Some p when p.tallMan |> String.notEmpty -> p.tallMan - | _ -> "" - Synonyms = - gpp.GenericProducts - |> Array.collect (fun gp -> - gp.PrescriptionProducts - |> Array.collect (fun pp -> - pp.TradeProducts - |> Array.map (fun tp -> tp.Brand) - ) - ) - |> Array.distinct - |> Array.filter String.notEmpty - Product = - gp.PrescriptionProducts - |> Array.collect (fun pp -> - pp.TradeProducts - |> Array.map (fun tp -> tp.Label) - ) - |> Array.distinct - |> function - | [| p |] -> p - | _ -> "" - Label = gp.Label - Shape = gp.Shape |> String.toLower - Routes = gp.Route |> Array.choose Mapping.mapRoute - ShapeQuantities = - gpp.GenericProducts - |> Array.collect (fun gp -> - gp.PrescriptionProducts - |> Array.map (fun pp -> pp.Quantity) - |> Array.choose BigRational.fromFloat - - ) - |> Array.filter (fun br -> br > 0N) - |> Array.distinct - |> fun xs -> - if xs |> Array.isEmpty then [| 1N |] else xs - |> ValueUnit.withUnit su - ShapeUnit = su - RequiresReconstitution = - Mapping.requiresReconstitution (gp.Route, su, gp.Shape) - Reconstitution = - Reconstitution.get () - |> Array.filter (fun r -> - r.GPK = $"{gp.Id}" && - r.DiluentVol |> Option.isSome - ) - |> Array.map (fun r -> - { - Route = r.Route - DoseType = r.DoseType - Department = r.Dep - Location = r.Location - DiluentVolume = - r.DiluentVol.Value - |> ValueUnit.singleWithUnit Units.Volume.milliLiter - ExpansionVolume = - r.ExpansionVol - |> Option.map (fun v -> - v - |> ValueUnit.singleWithUnit Units.Volume.milliLiter - ) - Diluents = - r.Diluents - |> String.splitAt ';' - |> Array.map String.trim - } - ) - Divisible = - // TODO: need to map this to a config setting - if gp.Shape |> String.containsCapsInsens "druppel" then 20N - else - if isSol gp.Shape then 10N - else 1N - |> ValueUnit.singleWithUnit Units.Count.times - Substances = - gp.Substances - |> Array.map (fun s -> - let su = - s.SubstanceUnit - |> Units.fromString - |> Option.defaultValue NoUnit - { - Name = rename s s.SubstanceName - Quantity = - s.SubstanceQuantity - |> BigRational.fromFloat - |> Option.map (fun q -> - q - |> ValueUnit.singleWithUnit su - ) - MultipleQuantity = None - } - ) - } - ) - |> StopWatch.clockFunc "created products" - - - /// - /// Get the Product array. - /// - /// - /// This function is memoized. - /// - let get : unit -> Product [] = - Memoization.memoize get_ - - - /// - /// Reconstitute the given product according to - /// route, DoseType, department and VenousAccess location. - /// - /// The route - /// The dose type - /// The department - /// The venous access location - /// The product - /// - /// The reconstituted product or None if the product - /// does not require reconstitution. - /// - let reconstitute rte dtp dep loc (prod : Product) = - if prod.RequiresReconstitution |> not then None - else - prod.Reconstitution - |> Array.filter (fun r -> - (rte |> String.isNullOrWhiteSpace || r.Route |> String.equalsCapInsens rte) && - (r.DoseType = AnyDoseType || r.DoseType = dtp) && - (dep |> String.isNullOrWhiteSpace || r.Department |> String.equalsCapInsens dep) && - (r.Location = AnyAccess || r.Location = loc) - ) - |> Array.map (fun r -> - { prod with - ShapeUnit = - Units.Volume.milliLiter - ShapeQuantities = r.DiluentVolume - Substances = - prod.Substances - |> Array.map (fun s -> - { s with - Quantity = - s.Quantity - |> Option.map (fun q -> q / r.DiluentVolume) - } - ) - } - ) - |> function - | [| p |] -> Some p - | _ -> None - - - /// - /// Filter the Product array to get all the products - /// - /// The Filter - /// The array of Products - let filter (filter : Filter) (prods : Product []) = - let repl s = - s - |> String.replace "/" "" - |> String.replace "+" "" - - let eqs s1 s2 = - match s1, s2 with - | Some s1, s2 -> - let s1 = s1 |> repl - let s2 = s2 |> repl - s1 |> String.equalsCapInsens s2 - | _ -> false - - prods - |> Array.filter (fun p -> - p.Generic |> eqs filter.Generic && - p.Shape |> eqs filter.Shape && - p.Routes |> Array.exists (eqs filter.Route) - ) - |> Array.map (fun p -> - { p with - Reconstitution = - p.Reconstitution - |> Reconstitution.filter filter - } - ) - - - /// Get all Generics from the given Product array. - let generics (products : Product array) = - products - |> Array.map (fun p -> - p.Generic - ) - |> Array.distinct - - - /// Get all Synonyms from the given Product array. - let synonyms (products : Product array) = - products - |> Array.collect (fun p -> - p.Synonyms - ) - |> Array.append (generics products) - |> Array.distinct - - - /// Get all Shapes from the given Product array. - let shapes (products : Product array) = - products - |> Array.map (fun p -> p.Shape) - |> Array.distinct - - - -module Filter = - - - /// An empty Filter. - let filter = - { - Indication = None - Generic = None - Shape = None - Route = None - DoseType = AnyDoseType - Patient = Patient.patient - } - - - /// - /// Apply a Patient to a Filter. - /// - /// The Patient - /// The Filter - /// The Filter with the Patient applied - let setPatient (pat : Patient) (filter : Filter) = - let pat = - pat - |> Patient.calcPMAge - - { filter with - Patient = pat - } - - - let calcPMAge (filter : Filter) = - { filter with - Patient = - filter.Patient - |> Patient.calcPMAge - } - - - -module DoseRule = - - open System - open MathNet.Numerics - open Informedica.Utils.Lib - open Informedica.Utils.Lib.BCL - open Informedica.GenCore.Lib.Ranges - - module DoseLimit = - - - /// An empty DoseLimit. - let limit = - { - DoseLimitTarget = NoDoseLimitTarget - Quantity = MinMax.empty - NormQuantityAdjust = None - QuantityAdjust = MinMax.empty - PerTime = MinMax.empty - NormPerTimeAdjust = None - PerTimeAdjust = MinMax.empty - Rate = MinMax.empty - RateAdjust = MinMax.empty - } - - - /// - /// Check whether an adjust is used in - /// the DoseLimit. - /// - /// - /// If any of the adjust values is not None - /// then an adjust is used. - /// - let useAdjust (dl : DoseLimit) = - [ - dl.NormQuantityAdjust = None - dl.QuantityAdjust = MinMax.empty - dl.NormPerTimeAdjust = None - dl.PerTimeAdjust = MinMax.empty - dl.RateAdjust = MinMax.empty - ] - |> List.forall id - |> not - - - /// Get the DoseLimitTarget as a string. - let doseLimitTargetToString = function - | NoDoseLimitTarget -> "" - | ShapeDoseLimitTarget s - | SubstanceDoseLimitTarget s -> s - - - /// Get the substance from the SubstanceDoseLimitTarget. - let substanceDoseLimitTargetToString = function - | SubstanceDoseLimitTarget s -> s - | _ -> "" - - - /// Check whether the DoseLimitTarget is a SubstanceDoseLimitTarget. - let isSubstanceLimit (dl : DoseLimit) = - dl.DoseLimitTarget - |> function - | SubstanceDoseLimitTarget _ -> true - | _ -> false - - - /// Check whether the DoseLimitTarget is a SubstanceDoseLimitTarget. - let isShapeLimit (dl : DoseLimit) = - dl.DoseLimitTarget - |> function - | ShapeDoseLimitTarget _ -> true - | _ -> false - - - - module Print = - - - open Informedica.GenUnits.Lib - - let printFreqs (r : DoseRule) = - r.Frequencies - |> Option.map (fun vu -> - vu - |> Utils.ValueUnit.toString 0 - ) - |> Option.defaultValue "" - - - let printInterval (dr: DoseRule) = - if dr.IntervalTime = MinMax.empty then "" - else - dr.IntervalTime - |> MinMax.toString - "min. interval " - "min. interval " - "max. interval " - "max. interval " - - - let printTime (dr: DoseRule) = - if dr.AdministrationTime = MinMax.empty then "" - else - dr.AdministrationTime - |> MinMax.toString - "min. " - "min. " - "max. " - "max. " - - - let printDuration (dr: DoseRule) = - if dr.Duration = MinMax.empty then "" - else - dr.Duration - |> MinMax.toString - "min. duur " - "min. duur " - "max. duur " - "max. duur " - - - let printMinMaxDose (minMax : MinMax) = - if minMax = MinMax.empty then "" - else - minMax - |> MinMax.toString - "> " - "> " - "< " - "< " - - - let printNormDose vu = - match vu with - | None -> "" - | Some vu -> $"{vu |> Utils.ValueUnit.toString 3}" - - - let printDose wrap (dr : DoseRule) = - let substDls = - dr.DoseLimits - |> Array.filter DoseLimit.isSubstanceLimit - - let shapeDls = - dr.DoseLimits - |> Array.filter DoseLimit.isShapeLimit - - let useSubstDl = substDls |> Array.length > 0 - // only use shape dose limits if there are no substance dose limits - if useSubstDl then substDls - else shapeDls - |> Array.map (fun dl -> - [ - $"{dl.Rate |> printMinMaxDose}" - $"{dl.RateAdjust |> printMinMaxDose}" - - $"{dl.NormPerTimeAdjust |> printNormDose} " + - $"{dl.PerTimeAdjust |> printMinMaxDose}" - - $"{dl.PerTime |> printMinMaxDose}" - - $"{dl.NormQuantityAdjust |> printNormDose} " + - $"{dl.QuantityAdjust |> printMinMaxDose}" - - $"{dl.Quantity |> printMinMaxDose}" - ] - |> List.map String.trim - |> List.filter (String.IsNullOrEmpty >> not) - |> String.concat " " - |> fun s -> - $"%s{dl.DoseLimitTarget |> DoseLimit.substanceDoseLimitTargetToString} {wrap}{s}{wrap}" - ) - - - /// See for use of anonymous record in - /// fold: https://github.com/dotnet/fsharp/issues/6699 - let toMarkdown (rules : DoseRule array) = - let generic_md generic = - $"\n\n# {generic}\n\n---\n" - - let route_md route products = - $"\n\n### Route: {route}\n\n#### Producten\n%s{products}\n" - - let product_md product = $"* {product}" - - let indication_md indication = $"\n\n## Indicatie: %s{indication}\n\n---\n" - - let doseCapt_md = "\n\n#### Doseringen\n\n" - - let dose_md dt dose freqs intv time dur = - let dt = dt |> DoseType.toString - let freqs = - if freqs |> String.isNullOrWhiteSpace then "" - else - $" in {freqs}" - - let s = - [ - if intv |> String.isNullOrWhiteSpace |> not then - $" {intv}" - if time |> String.isNullOrWhiteSpace |> not then - $" inloop tijd {time}" - if dur |> String.isNullOrWhiteSpace |> not then - $" {dur}" - ] - |> String.concat ", " - |> fun s -> - if s |> String.isNullOrWhiteSpace then "" - else - $" ({s |> String.trim})" - - $"* *{dt}*: {dose}{freqs}{s}" - - let patient_md patient diagn = - if diagn |> String.isNullOrWhiteSpace then - $"\n\n##### Patient: **%s{patient}**\n\n" - else - $"\n\n##### Patient: **%s{patient}**\n\n%s{diagn}" - - let printDoses (rules : DoseRule array) = - ("", rules |> Array.groupBy (fun d -> d.DoseType)) - ||> Array.fold (fun acc (dt, ds) -> - let dose = - if ds |> Array.isEmpty then "" - else - ds - |> Array.collect (printDose "") - |> Array.distinct - |> String.concat " " - |> fun s -> $"{s}\n" - - let freqs = - if dose = "" then "" - else - ds - |> Array.map printFreqs - |> Array.distinct - |> function - | [| s |] -> s - | _ -> "" - - let intv = - if dose = "" then "" - else - ds - |> Array.map printInterval - |> Array.distinct - |> function - | [| s |] -> s - | _ -> "" - - let time = - if dose = "" then "" - else - ds - |> Array.map printTime - |> Array.distinct - |> function - | [| s |] -> s - | _ -> "" - - let dur = - if dose = "" then "" - else - ds - |> Array.map printDuration - |> Array.distinct - |> function - | [| s |] -> s - | _ -> "" - - if dt = Contraindicated then $"{acc}\n*gecontra-indiceerd*" - else - $"{acc}\n{dose_md dt dose freqs intv time dur}" - ) - - ({| md = ""; rules = [||] |}, - rules - |> Array.groupBy (fun d -> d.Generic) - ) - ||> Array.fold (fun acc (generic, rs) -> - {| acc with - md = generic_md generic - rules = rs - |} - |> fun r -> - if r.rules = Array.empty then r - else - (r, r.rules |> Array.groupBy (fun d -> d.Indication)) - ||> Array.fold (fun acc (indication, rs) -> - {| acc with - md = acc.md + (indication_md indication) - rules = rs - |} - |> fun r -> - if r.rules = Array.empty then r - else - (r, r.rules |> Array.groupBy (fun r -> r.Route)) - ||> Array.fold (fun acc (route, rs) -> - - let prods = - rs - |> Array.collect (fun d -> d.Products) - |> Array.sortBy (fun p -> - p.Substances - |> Array.sumBy (fun s -> - s.Quantity - |> Option.map ValueUnit.getValue - |> Option.bind Array.tryHead - |> Option.defaultValue 0N - ) - ) - |> Array.map (fun p -> product_md p.Label) - |> Array.distinct - |> String.concat "\n" - {| acc with - md = acc.md + (route_md route prods) - + doseCapt_md - rules = rs - |} - |> fun r -> - if r.rules = Array.empty then r - else - (r, r.rules - |> Array.sortBy (fun d -> d.PatientCategory |> PatientCategory.sortBy) - |> Array.groupBy (fun d -> d.PatientCategory)) - ||> Array.fold (fun acc (pat, rs) -> - let doses = - rs - |> Array.sortBy (fun r -> r.DoseType |> DoseType.sortBy) - |> printDoses - let diagn = - if pat.Diagnoses |> Array.isEmpty then "" - else - let s = pat.Diagnoses |> String.concat ", " - $"* Diagnose: **{s}**" - let pat = pat |> PatientCategory.toString - - {| acc with - rules = rs - md = acc.md + (patient_md pat diagn) + $"\n{doses}" - |} - ) - ) - ) - ) - |> fun r -> r.md - - - let printGenerics generics (doseRules : DoseRule[]) = - doseRules - |> generics - |> Array.sort - |> Array.map(fun g -> - doseRules - |> Array.filter (fun dr -> dr.Generic = g) - |> toMarkdown - ) - - - open Utils - open Informedica.GenUnits.Lib - - - /// - /// Reconstitute the products in a DoseRule that require reconstitution. - /// - /// The Department to select the reconstitution - /// The VenousAccess location to select the reconstitution - /// The DoseRule - let reconstitute dep loc (dr : DoseRule) = - { dr with - Products = - if dr.Products - |> Array.exists (fun p -> p.RequiresReconstitution) - |> not then dr.Products - else - dr.Products - |> Array.choose (Product.reconstitute dr.Route dr.DoseType dep loc) - + Weight = + { PatientCategory.patientCategory.Weight with + Max = + 37.5m + |> BigRational.fromDecimal + |> ValueUnit.singleWithUnit Units.Weight.kiloGram + |> ValueUnit.convertTo Units.Weight.gram + |> Limit.Inclusive + |> Some } - - let private get_ () = - Web.getDataFromSheet Web.dataUrlIdGenPres "DoseRules" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - let toBrOpt = BigRational.toBrs >> Array.tryHead - - {| - Indication = get "Indication" - Generic = get "Generic" - Shape = get "Shape" - Route = get "Route" - Department = get "Dep" - Diagn = get "Diagn" - Gender = get "Gender" |> Gender.fromString - MinAge = get "MinAge" |> toBrOpt - MaxAge = get "MaxAge" |> toBrOpt - MinWeight = get "MinWeight" |> toBrOpt - MaxWeight = get "MaxWeight" |> toBrOpt - MinBSA = get "MinBSA" |> toBrOpt - MaxBSA = get "MaxBSA" |> toBrOpt - MinGestAge = get "MinGestAge" |> toBrOpt - MaxGestAge = get "MaxGestAge" |> toBrOpt - MinPMAge = get "MinPMAge" |> toBrOpt - MaxPMAge = get "MaxPMAge" |> toBrOpt - DoseType = get "DoseType" |> DoseType.fromString - Frequencies = get "Freqs" |> BigRational.toBrs - DoseUnit = get "DoseUnit" - AdjustUnit = get "AdjustUnit" - FreqUnit = get "FreqUnit" - RateUnit = get "RateUnit" - MinTime = get "MinTime" |> toBrOpt - MaxTime = get "MaxTime" |> toBrOpt - TimeUnit = get "TimeUnit" - MinInterval = get "MinInt" |> toBrOpt - MaxInterval = get "MaxInt" |> toBrOpt - IntervalUnit = get "IntUnit" - MinDur = get "MinDur" |> toBrOpt - MaxDur = get "MaxDur" |> toBrOpt - DurUnit = get "DurUnit" - Substance = get "Substance" - MinQty = get "MinQty" |> toBrOpt - MaxQty = get "MaxQty" |> toBrOpt - NormQtyAdj = get "NormQtyAdj" |> toBrOpt - MinQtyAdj = get "MinQtyAdj" |> toBrOpt - MaxQtyAdj = get "MaxQtyAdj" |> toBrOpt - MinPerTime = get "MinPerTime" |> toBrOpt - MaxPerTime = get "MaxPerTime" |> toBrOpt - NormPerTimeAdj = get "NormPerTimeAdj" |> toBrOpt - MinPerTimeAdj = get "MinPerTimeAdj" |> toBrOpt - MaxPerTimeAdj = get "MaxPerTimeAdj" |> toBrOpt - MinRate = get "MinRate" |> toBrOpt - MaxRate = get "MaxRate" |> toBrOpt - MinRateAdj = get "MinRateAdj" |> toBrOpt - MaxRateAdj = get "MaxRateAdj" |> toBrOpt - |} - ) - |> Array.groupBy (fun r -> - { - Indication = r.Indication - Generic = r.Generic - Shape = r.Shape - Route = r.Route - PatientCategory = - { - Department = - if r.Department |> String.isNullOrWhiteSpace then None - else - r.Department |> Some - Diagnoses = [| r.Diagn |] |> Array.filter String.notEmpty - Gender = r.Gender - Age = - (r.MinAge, r.MaxAge) - |> MinMax.fromTuple (Some Utils.Units.day) - Weight = - (r.MinWeight, r.MaxWeight) - |> MinMax.fromTuple (Some Utils.Units.weightGram) - BSA = - (r.MinBSA, r.MaxBSA) - |> MinMax.fromTuple (Some Utils.Units.bsaM2) - GestAge = - (r.MinGestAge, r.MaxGestAge) - |> MinMax.fromTuple (Some Utils.Units.day) - PMAge = - (r.MinPMAge, r.MaxPMAge) - |> MinMax.fromTuple (Some Utils.Units.day) - Location = AnyAccess - } - DoseType = r.DoseType - Frequencies = - match r.FreqUnit |> Units.freqUnit with - | None -> None - | Some u -> - r.Frequencies - |> ValueUnit.withUnit u - |> Some - AdministrationTime = - (r.MinTime, r.MaxTime) - |> MinMax.fromTuple (r.TimeUnit |> Utils.Units.timeUnit) - IntervalTime = - (r.MinInterval, r.MaxInterval) - |> MinMax.fromTuple (r.IntervalUnit |> Utils.Units.timeUnit) - Duration = - (r.MinDur, r.MaxDur) - |> MinMax.fromTuple (r.DurUnit |> Utils.Units.timeUnit) - DoseLimits = [||] - Products = [||] - } - ) - |> Array.map (fun (dr, rs) -> - { dr with - DoseLimits = - let shapeLimits = - Mapping.filterRouteShapeUnit dr.Route dr.Shape NoUnit - |> Array.map (fun rsu -> - { DoseLimit.limit with - DoseLimitTarget = dr.Shape |> ShapeDoseLimitTarget - Quantity = - { - Min = rsu.MinDoseQty |> Option.map Limit.Inclusive - Max = rsu.MaxDoseQty |> Option.map Limit.Inclusive - } - } - ) - |> Array.distinct - - rs - |> Array.map (fun r -> - // the adjust unit - let adj = r.AdjustUnit |> Utils.Units.adjustUnit - // the dose unit - let du = r.DoseUnit |> Units.fromString - // the adjusted dose unit - let duAdj = - match adj, du with - | Some adj, Some du -> - du - |> Units.per adj - |> Some - | _ -> None - // the time unit - let tu = r.FreqUnit |> Utils.Units.timeUnit - // the dose unit per time unit - let duTime = - match du, tu with - | Some du, Some tu -> - du - |> Units.per tu - |> Some - | _ -> None - // the adjusted dose unit per time unit - let duAdjTime = - match duAdj, tu with - | Some duAdj, Some tu -> - duAdj - |> Units.per tu - |> Some - | _ -> None - // the rate unit - let ru = r.RateUnit |> Units.fromString - // the dose unit per rate unit - let duRate = - match du, ru with - | Some du, Some ru -> - du - |> Units.per ru - |> Some - | _ -> None - // the adjusted dose unit per rate unit - let duAdjRate = - match duAdj, ru with - | Some duAdj, Some ru -> - duAdj - |> Units.per ru - |> Some - | _ -> None - - { - DoseLimitTarget = r.Substance |> SubstanceDoseLimitTarget - Quantity = - (r.MinQty, r.MaxQty) - |> MinMax.fromTuple du - NormQuantityAdjust = - r.NormQtyAdj - |> ValueUnit.withOptionalUnit duAdj - QuantityAdjust = - (r.MinQtyAdj, r.MaxQtyAdj) - |> MinMax.fromTuple duAdj - PerTime = - (r.MinPerTime, r.MaxPerTime) - |> MinMax.fromTuple duTime - NormPerTimeAdjust = - r.NormPerTimeAdj - |> ValueUnit.withOptionalUnit duAdjTime - PerTimeAdjust = - (r.MinPerTimeAdj, r.MaxPerTimeAdj) - |> MinMax.fromTuple duAdjTime - Rate = - (r.MinRate, r.MaxRate) - |> MinMax.fromTuple duRate - RateAdjust = - (r.MinRateAdj, r.MaxRateAdj) - |> MinMax.fromTuple duAdjRate - } - ) - |> Array.append shapeLimits - - Products = - Product.get () - |> Product.filter - { Filter.filter with - Generic = dr.Generic |> Some - Shape = dr.Shape |> Some - Route = dr.Route |> Some - } - } - ) - - - /// - /// Get the DoseRules from the Google Sheet. - /// - /// - /// This function is memoized. - /// - let get : unit -> DoseRule [] = - Memoization.memoize get_ - - - /// - /// Filter the DoseRules according to the Filter. - /// - /// The Filter - /// The DoseRule array - let filter (filter : Filter) (drs : DoseRule array) = - let eqs a b = - a - |> Option.map (fun x -> x = b) - |> Option.defaultValue true - - [| - fun (dr : DoseRule) -> dr.Indication |> eqs filter.Indication - fun (dr : DoseRule) -> dr.Generic |> eqs filter.Generic - fun (dr : DoseRule) -> dr.Shape |> eqs filter.Shape - fun (dr : DoseRule) -> dr.Route |> eqs filter.Route - fun (dr : DoseRule) -> dr.PatientCategory |> PatientCategory.filter filter - fun (dr : DoseRule) -> - match filter.DoseType, dr.DoseType with - | AnyDoseType, _ - | _, AnyDoseType -> true - | _ -> filter.DoseType = dr.DoseType - |] - |> Array.fold (fun (acc : DoseRule[]) pred -> - acc |> Array.filter pred - ) drs - - - let private getMember getter (drs : DoseRule[]) = - drs - |> Array.map getter - |> Array.map String.trim - |> Array.distinctBy String.toLower - |> Array.sortBy String.toLower - - - - /// Extract all indications from the DoseRules. - let indications = getMember (fun dr -> dr.Indication) - - - /// Extract all the generics from the DoseRules. - let generics = getMember (fun dr -> dr.Generic) - - - /// Extract all the shapes from the DoseRules. - let shapes = getMember (fun dr -> dr.Shape) - - - /// Extract all the routes from the DoseRules. - let routes = getMember (fun dr -> dr.Route) - - - /// Extract all the departments from the DoseRules. - let departments = getMember (fun dr -> dr.PatientCategory.Department |> Option.defaultValue "") - - - /// Extract all the diagnoses from the DoseRules. - let diagnoses (drs : DoseRule []) = - drs - |> Array.collect (fun dr -> - dr.PatientCategory.Diagnoses - ) - |> Array.distinct - |> Array.sortBy String.toLower - - - /// Extract all genders from the DoseRules. - let genders = getMember (fun dr -> dr.PatientCategory.Gender |> Gender.toString) - - - /// Extract all patient categories from the DoseRules as strings. - let patients (drs : DoseRule array) = - drs - |> Array.map (fun r -> r.PatientCategory) - |> Array.sortBy PatientCategory.sortBy - |> Array.map PatientCategory.toString - |> Array.distinct - - - /// Extract all frequencies from the DoseRules as strings. - let frequencies (drs : DoseRule array) = - drs - |> Array.map Print.printFreqs - |> Array.distinct - - - let useAdjust (dr : DoseRule) = - dr.DoseLimits - |> Array.filter DoseLimit.isSubstanceLimit - |> Array.exists DoseLimit.useAdjust - - - -module SolutionRule = - - open MathNet.Numerics - open Informedica.Utils.Lib - open Informedica.Utils.Lib.BCL - - - - module SolutionLimit = - - open Informedica.GenCore.Lib.Ranges - - /// An empty SolutionLimit. - let limit = - { - Substance = "" - Quantity = MinMax.empty - Quantities = None - Concentration = MinMax.empty - } - - - open Informedica.GenUnits.Lib - - - let private get_ () = - Web.getDataFromSheet Web.dataUrlIdGenPres "SolutionRules" - |> fun data -> - let getColumn = - data - |> Array.head - |> Csv.getStringColumn - - data - |> Array.tail - |> Array.map (fun r -> - let get = getColumn r - let toBrOpt = BigRational.toBrs >> Array.tryHead - - {| - Generic = get "Generic" - Shape = get "Shape" - Route = get "Route" - Department = get "Dep" - CVL = get "CVL" - PVL = get "PVL" - MinAge = get "MinAge" |> toBrOpt - MaxAge = get "MaxAge" |> toBrOpt - MinWeight = get "MinWeight" |> toBrOpt - MaxWeight = get "MaxWeight" |> toBrOpt - MinDose = get "MinDose" |> toBrOpt - MaxDose = get "MaxDose" |> toBrOpt - DoseType = get "DoseType" - Solutions = get "Solutions" |> String.split "|" - Volumes = get "Volumes" |> BigRational.toBrs - MinVol = get "MinVol" |> toBrOpt - MaxVol = get "MaxVol" |> toBrOpt - MinPerc = get "MinPerc" |> toBrOpt - MaxPerc = get "MaxPerc" |> toBrOpt - Substance = get "Substance" - Unit = get "Unit" - Quantities = get "Quantities" |> BigRational.toBrs - MinQty = get "MinQty" |> toBrOpt - MaxQty = get "MaxQty" |> toBrOpt - MinConc = get "MinConc" |> toBrOpt - MaxConc = get "MaxConc" |> toBrOpt - |} - ) - |> Array.groupBy (fun r -> - let du = r.Unit |> Units.fromString - { - Generic = r.Generic - Shape = r.Shape - Route = r.Route - Department = r.Department - Location = - if r.CVL = "x" then CVL - else - if r.PVL = "x" then PVL - else - AnyAccess - Age = - (r.MinAge, r.MaxAge) - |> MinMax.fromTuple (Some Utils.Units.day) - Weight = - (r.MinWeight, r.MaxWeight) - |> MinMax.fromTuple (Some Utils.Units.weightGram) - Dose = - (r.MinDose, r.MaxDose) - |> MinMax.fromTuple du - DoseType = r.DoseType |> DoseType.fromString - Solutions = r.Solutions |> List.toArray - Volumes = - if r.Volumes |> Array.isEmpty then None - else - r.Volumes - |> ValueUnit.withUnit Units.mL - |> Some - Volume = - (r.MinVol, r.MaxVol) - |> MinMax.fromTuple (Some Units.mL) - DosePerc = - (r.MinPerc, r.MaxPerc) - |> MinMax.fromTuple (Some Units.Count.times) - Products = [||] - SolutionLimits = [||] - } - ) - |> Array.map (fun (sr, rs) -> - { sr with - SolutionLimits = - rs - |> Array.map (fun l -> - let u = l.Unit |> Units.fromString - { - Substance = l.Substance - Quantity = - (l.MinQty, l.MaxQty) - |> MinMax.fromTuple u - Quantities = - if l.Quantities |> Array.isEmpty then None - else - match u with - | None -> None - | Some u -> - l.Quantities - |> ValueUnit.withUnit u - |> Some - Concentration = - (l.MinConc, l.MaxConc) - |> MinMax.fromTuple (Some Units.Count.times) - } - ) - Products = - Product.get () - |> Array.filter (fun p -> - p.Generic = sr.Generic && - p.Shape = sr.Shape - ) - - } - ) - - - /// - /// Gets the SolutionRules. - /// - /// - /// This function is memoized. - /// - let get : unit -> SolutionRule [] = - Memoization.memoize get_ - - - /// - /// Get all the SolutionRules that match the given Filter. - /// - /// The Filter - /// The SolutionRules - /// The matching SolutionRules - let filter (filter : Filter) (solutionRules : SolutionRule []) = - let eqs a b = - a - |> Option.map (fun x -> x = b) - |> Option.defaultValue true - - [| - fun (sr : SolutionRule) -> sr.Generic |> eqs filter.Generic - fun (sr : SolutionRule) -> - PatientCategory.checkAgeWeightMinMax filter.Patient.Age filter.Patient.Weight sr.Age sr.Weight - fun (sr : SolutionRule) -> sr.Shape |> eqs filter.Shape - fun (sr : SolutionRule) -> sr.Route |> eqs filter.Route - fun (sr : SolutionRule) -> sr.Department |> eqs filter.Patient.Department - fun (sr : SolutionRule) -> - match filter.DoseType, sr.DoseType with - | AnyDoseType, _ - | _, AnyDoseType -> true - | _ -> filter.DoseType = sr.DoseType - fun (sr : SolutionRule) -> filter.Patient.Weight |> MinMax.inRange sr.Weight - fun (sr : SolutionRule) -> - match sr.Location with - | CVL -> filter.Patient.VenousAccess |> List.exists ((=) CVL) - | PVL //-> filter.Location = PVL - | AnyAccess -> true - |] - |> Array.fold (fun (acc : SolutionRule[]) pred -> - acc |> Array.filter pred - ) solutionRules - - - /// Helper function to get the distinct values of a member of SolutionRule. - let getMember getter (rules : SolutionRule[]) = - rules - |> Array.map getter - |> Array.distinct - |> Array.sort - - - /// Get all the distinct Generics from the given SolutionRules. - let generics = getMember (fun sr -> sr.Generic) - - - module Print = - - - module MinMax = Informedica.GenCore.Lib.Ranges.MinMax - - - /// Get the string representation of a SolutionLimit. - let printSolutionLimit (sr: SolutionRule) (limit: SolutionLimit) = - let mmToStr = MinMax.toString "van " "van " "tot " "tot" - - let loc = - match sr.Location with - | CVL -> "###### centraal: \n* " - | PVL -> "###### perifeer: \n* " - | AnyAccess -> "* " - - let qs = - limit.Quantities - |> Option.map (Utils.ValueUnit.toString -1) - |> Option.defaultValue "" - - let q = - limit.Quantity - |> mmToStr - - let vol = - if sr.Volume - |> mmToStr - |> String.isNullOrWhiteSpace then - "" - else - sr.Volume - |> mmToStr - |> fun s -> $""" in {s} ml {sr.Solutions |> String.concat "/"}""" - |> fun s -> - if s |> String.isNullOrWhiteSpace |> not then s - else - sr.Volumes - |> Option.map (Utils.ValueUnit.toString -1) - |> Option.defaultValue "" - |> fun s -> - let sols = sr.Solutions |> String.concat "/" - if s |> String.isNullOrWhiteSpace then - if sols |> String.isNullOrWhiteSpace then " puur" - else $" in {sols}" - else - $" in {s} ml {sols}" - - let conc = - if limit.Concentration - |> mmToStr - |> String.isNullOrWhiteSpace then "" - else - $"* concentratie: {limit.Concentration |> mmToStr}/ml" - - let dosePerc = - let p = - sr.DosePerc - |> mmToStr - - if p |> String.isNullOrWhiteSpace then "" - else - $"* geef {p}%% van de bereiding" - - $"\n{loc}{limit.Substance}: {q}{qs}{vol}\n{conc}\n{dosePerc}" - - - /// Get the markdown representation of the given SolutionRules. - let toMarkdown text (rules: SolutionRule []) = - let generic_md generic products = - let text = if text |> String.isNullOrWhiteSpace then generic else text - $"\n# %s{text}\n---\n#### Producten\n%s{products}\n" - - let department_md dep = - let dep = - match dep with - | _ when dep = "AICU" -> "ICC" - | _ -> dep - - $"\n### Afdeling: {dep}\n" - - let pat_md pat = - $"\n##### %s{pat}\n" - - let product_md product = - $"\n* %s{product}\n" - - - ({| md = ""; rules = [||] |}, rules |> Array.groupBy (fun d -> d.Generic)) - ||> Array.fold (fun acc (generic, rs) -> - let prods = - rs - |> Array.collect (fun d -> d.Products) - |> Array.sortBy (fun p -> - p.Substances - |> Array.sumBy (fun s -> - s.Quantity - |> Option.map ValueUnit.getValue - |> Option.bind Array.tryHead - |> Option.defaultValue 0N - ) - ) - |> Array.collect (fun p -> - if p.Reconstitution |> Array.isEmpty then - [| product_md p.Label |] - else - p.Reconstitution - |> Array.map (fun r -> - $"{p.Label} oplossen in {r.DiluentVolume |> Utils.ValueUnit.toString -1} voor {r.Route}" - |> product_md - ) - ) - |> Array.distinct - |> String.concat "\n" - - {| acc with - md = generic_md generic prods - rules = rs - |} - |> fun r -> - if r.rules = Array.empty then r - else - (r, r.rules |> Array.groupBy (fun d -> d.Department)) - ||> Array.fold (fun acc (dep, rs) -> - {| acc with - md = acc.md + (department_md dep) - rules = rs - |} - |> fun r -> - if r.rules |> Array.isEmpty then r - else - (r, - r.rules - |> Array.groupBy (fun r -> - {| - Age = r.Age - Weight = r.Weight - Dose = r.Dose - DoseType = r.DoseType - |} - ) - ) - ||> Array.fold (fun acc (sel, rs) -> - let sol = - rs - |> Array.groupBy (fun r -> r.Location) - |> Array.collect (fun (_, rs) -> - rs - |> Array.tryHead - |> function - | None -> [||] - | Some r -> - r.SolutionLimits - |> Array.map (printSolutionLimit r) - ) - |> String.concat "\n" - - let pat = - let a = sel.Age |> PatientCategory.printAgeMinMax - - let w = - let s = - sel.Weight - |> MinMax.toString - "van " - "van " - "tot " - "tot " - - if s |> String.isNullOrWhiteSpace then - "" - else - $"gewicht %s{s} kg" - - if a |> String.isNullOrWhiteSpace - && w |> String.isNullOrWhiteSpace then - "" - else - $"patient: %s{a} %s{w}" |> String.trim - - let dose = - sel.Dose - |> MinMax.toString - "van " - "van " - "tot " - "tot " - - let dt = - let s = sel.DoseType |> DoseType.toString - if s |> String.isNullOrWhiteSpace then "" - else - $"{s}" - - - {| acc with - rules = rs - md = - if pat |> String.isNullOrWhiteSpace && - dose |> String.isNullOrWhiteSpace then - acc.md + $"##### {dt}" - else - acc.md + pat_md $"{dt}, {pat}{dose}" - |> fun s -> $"{s}\n{sol}" - |} - ) - ) - - - ) - |> fun md -> md.md - - - /// Get the markdown representation of the given SolutionRules. - let printGenerics (rules: SolutionRule []) = - rules - |> generics - |> Array.map (fun generic -> - rules - |> Array.filter (fun sr -> sr.Generic = generic) - |> Array.sortBy (fun sr -> sr.Generic) - |> toMarkdown "" - ) - - (* - *) - - -open MathNet.Numerics -open Informedica.GenUnits.Lib - -module MinMax = Informedica.GenCore.Lib.Ranges.MinMax -module Limit = Informedica.GenCore.Lib.Ranges.Limit - -{ Patient.patient with - GestAge = Some (ValueUnit.singleWithUnit Units.Time.week 36N) - Age = Some (ValueUnit.singleWithUnit Units.Time.day 1N) - Weight = Some (ValueUnit.singleWithUnit Units.Mass.gram (2500N)) } -|> Patient.toString - -Product.Parenteral.get () - - -DoseRule.Print.printInterval - { - Indication = "" - Generic = "todo" - Shape = "todo" - Route = "todo" - PatientCategory = - { - Department = None - Diagnoses = [||] - Gender = AnyGender - Age = MinMax.empty - Weight = MinMax.empty - BSA = MinMax.empty - GestAge = MinMax.empty - PMAge = MinMax.empty - Location = AnyAccess - - } - DoseType = AnyDoseType - Frequencies = None - AdministrationTime = MinMax.empty - IntervalTime = - { MinMax.empty with - Min = - 1N - |> ValueUnit.singleWithUnit Units.Time.hour - |> Informedica.GenCore.Lib.Ranges.Inclusive +|> PatientCategory.filter + { Filter.filter with + Patient = + { Patient.patient with + Department = (Some "ICK") + Age = + 1460N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + Weight = + 37500N + |> ValueUnit.singleWithUnit Units.Weight.gram |> Some - Max = - 2N - |> ValueUnit.singleWithUnit Units.Time.hour - |> Informedica.GenCore.Lib.Ranges.Inclusive + Height = + 100N + |> ValueUnit.singleWithUnit Units.Height.centiMeter |> Some + VenousAccess = [CVL] } - Duration = MinMax.empty - DoseLimits = [||] - Products = [||] - } - - -DoseRule.get() -|> Array.take 200 -|> DoseRule.Print.printGenerics DoseRule.generics -|> Array.iter (printfn "%s") +(* + { Department = Some "ICK" + Diagnoses = [||] + Gender = AnyGender + Age = Some (ValueUnit ([|1460N|], Time (Day 1N))) + Weight = Some (ValueUnit ([|17000N|], Weight (WeightGram 1N))) + Height = Some (ValueUnit ([|100N|], Height (HeightCentiMeter 1N))) + GestAge = None + PMAge = None + VenousAccess = [CVL] } + *) + +PrescriptionRule.get Patient.patient +//|> Seq.length +|> Seq.item 0 \ No newline at end of file diff --git a/src/Informedica.GenForm.Lib/SolutionRule.fs b/src/Informedica.GenForm.Lib/SolutionRule.fs index c95660a..edd478b 100644 --- a/src/Informedica.GenForm.Lib/SolutionRule.fs +++ b/src/Informedica.GenForm.Lib/SolutionRule.fs @@ -8,21 +8,23 @@ module SolutionRule = open Informedica.Utils.Lib.BCL - module SolutionLimit = + open Informedica.GenCore.Lib.Ranges /// An empty SolutionLimit. let limit = { Substance = "" - Unit = "" - Quantity = MinMax.none - Quantities = [||] - Concentration = MinMax.none + Quantity = MinMax.empty + Quantities = None + Concentration = MinMax.empty } + open Informedica.GenUnits.Lib + + let private get_ () = Web.getDataFromSheet Web.dataUrlIdGenPres "SolutionRules" |> fun data -> @@ -67,6 +69,7 @@ module SolutionRule = |} ) |> Array.groupBy (fun r -> + let du = r.Unit |> Units.fromString { Generic = r.Generic Shape = r.Shape @@ -78,14 +81,29 @@ module SolutionRule = if r.PVL = "x" then PVL else AnyAccess - Age = (r.MinAge, r.MaxAge) |> MinMax.fromTuple - Weight = (r.MinWeight, r.MaxWeight) |> MinMax.fromTuple - Dose = (r.MinDose, r.MaxDose) |> MinMax.fromTuple + Age = + (r.MinAge, r.MaxAge) + |> MinMax.fromTuple (Some Utils.Units.day) + Weight = + (r.MinWeight, r.MaxWeight) + |> MinMax.fromTuple (Some Utils.Units.weightGram) + Dose = + (r.MinDose, r.MaxDose) + |> MinMax.fromTuple du DoseType = r.DoseType |> DoseType.fromString Solutions = r.Solutions |> List.toArray - Volumes = r.Volumes - Volume = (r.MinVol, r.MaxVol) |> MinMax.fromTuple - DosePerc = (r.MinPerc, r.MaxPerc) |> MinMax.fromTuple + Volumes = + if r.Volumes |> Array.isEmpty then None + else + r.Volumes + |> ValueUnit.withUnit Units.mL + |> Some + Volume = + (r.MinVol, r.MaxVol) + |> MinMax.fromTuple (Some Units.mL) + DosePerc = + (r.MinPerc, r.MaxPerc) + |> MinMax.fromTuple (Some Units.Count.times) Products = [||] SolutionLimits = [||] } @@ -95,12 +113,24 @@ module SolutionRule = SolutionLimits = rs |> Array.map (fun l -> + let u = l.Unit |> Units.fromString { Substance = l.Substance - Unit = l.Unit - Quantity = (l.MinQty, l.MaxQty) |> MinMax.fromTuple - Quantities = l.Quantities - Concentration = (l.MinConc, l.MaxConc) |> MinMax.fromTuple + Quantity = + (l.MinQty, l.MaxQty) + |> MinMax.fromTuple u + Quantities = + if l.Quantities |> Array.isEmpty then None + else + match u with + | None -> None + | Some u -> + l.Quantities + |> ValueUnit.withUnit u + |> Some + Concentration = + (l.MinConc, l.MaxConc) + |> MinMax.fromTuple (Some Units.Count.times) } ) Products = @@ -139,19 +169,19 @@ module SolutionRule = [| fun (sr : SolutionRule) -> sr.Generic |> eqs filter.Generic fun (sr : SolutionRule) -> - PatientCategory.checkAgeWeightMinMax filter.AgeInDays filter.WeightInGram sr.Age sr.Weight + PatientCategory.checkAgeWeightMinMax filter.Patient.Age filter.Patient.Weight sr.Age sr.Weight fun (sr : SolutionRule) -> sr.Shape |> eqs filter.Shape fun (sr : SolutionRule) -> sr.Route |> eqs filter.Route - fun (sr : SolutionRule) -> sr.Department |> eqs filter.Department + fun (sr : SolutionRule) -> sr.Department |> eqs filter.Patient.Department fun (sr : SolutionRule) -> match filter.DoseType, sr.DoseType with | AnyDoseType, _ | _, AnyDoseType -> true | _ -> filter.DoseType = sr.DoseType - fun (sr : SolutionRule) -> filter.WeightInGram |> MinMax.isBetween sr.Weight + fun (sr : SolutionRule) -> filter.Patient.Weight |> MinMax.inRange sr.Weight fun (sr : SolutionRule) -> match sr.Location with - | CVL -> filter.Location = CVL + | CVL -> filter.Patient.VenousAccess |> List.exists ((=) CVL) | PVL //-> filter.Location = PVL | AnyAccess -> true |] @@ -174,8 +204,14 @@ module SolutionRule = module Print = + + module MinMax = Informedica.GenCore.Lib.Ranges.MinMax + + /// Get the string representation of a SolutionLimit. let printSolutionLimit (sr: SolutionRule) (limit: SolutionLimit) = + let mmToStr = MinMax.toString "van " "van " "tot " "tot" + let loc = match sr.Location with | CVL -> "###### centraal: \n* " @@ -183,43 +219,29 @@ module SolutionRule = | AnyAccess -> "* " let qs = - if limit.Quantities |> Array.isEmpty then - "" - else - limit.Quantities - |> Array.map BigRational.toStringNl - |> String.concat ", " - |> fun s -> $" {s} {limit.Unit}" + limit.Quantities + |> Option.map (Utils.ValueUnit.toString -1) + |> Option.defaultValue "" let q = - if limit.Quantity - |> MinMax.toString - |> String.isNullOrWhiteSpace then - "" - else - limit.Quantity - |> MinMax.toString - |> fun s -> - if qs |> String.isNullOrWhiteSpace then - $" {s} {limit.Unit}" - else - $" ({s} {limit.Unit})" + limit.Quantity + |> mmToStr let vol = if sr.Volume - |> MinMax.toString + |> mmToStr |> String.isNullOrWhiteSpace then "" else sr.Volume - |> MinMax.toString + |> mmToStr |> fun s -> $""" in {s} ml {sr.Solutions |> String.concat "/"}""" |> fun s -> if s |> String.isNullOrWhiteSpace |> not then s else sr.Volumes - |> Array.map BigRational.toStringNl - |> String.concat "\n" + |> Option.map (Utils.ValueUnit.toString -1) + |> Option.defaultValue "" |> fun s -> let sols = sr.Solutions |> String.concat "/" if s |> String.isNullOrWhiteSpace then @@ -229,15 +251,16 @@ module SolutionRule = $" in {s} ml {sols}" let conc = - if limit.Concentration |> MinMax.toString |> String.isNullOrWhiteSpace then "" + if limit.Concentration + |> mmToStr + |> String.isNullOrWhiteSpace then "" else - $"* concentratie: {limit.Concentration |> MinMax.toString} {limit.Unit}/ml" + $"* concentratie: {limit.Concentration |> mmToStr}/ml" let dosePerc = let p = sr.DosePerc - |> MinMax.map (fun br -> br * 100N) (fun br -> br * 100N) - |> MinMax.toString + |> mmToStr if p |> String.isNullOrWhiteSpace then "" else @@ -274,7 +297,12 @@ module SolutionRule = |> Array.collect (fun d -> d.Products) |> Array.sortBy (fun p -> p.Substances - |> Array.sumBy (fun s -> s.Quantity |> Option.defaultValue 0N) + |> Array.sumBy (fun s -> + s.Quantity + |> Option.map ValueUnit.getValue + |> Option.bind Array.tryHead + |> Option.defaultValue 0N + ) ) |> Array.collect (fun p -> if p.Reconstitution |> Array.isEmpty then @@ -282,7 +310,7 @@ module SolutionRule = else p.Reconstitution |> Array.map (fun r -> - $"{p.Label} oplossen in {r.DiluentVolume |> BigRational.toStringNl} ml voor {r.Route}" + $"{p.Label} oplossen in {r.DiluentVolume |> Utils.ValueUnit.toString -1} voor {r.Route}" |> product_md ) ) @@ -335,7 +363,13 @@ module SolutionRule = let a = sel.Age |> PatientCategory.printAgeMinMax let w = - let s = sel.Weight |> MinMax.toString + let s = + sel.Weight + |> MinMax.toString + "van " + "van " + "tot " + "tot " if s |> String.isNullOrWhiteSpace then "" @@ -349,16 +383,12 @@ module SolutionRule = $"patient: %s{a} %s{w}" |> String.trim let dose = - let d = sel.Dose |> MinMax.toString - let u = - match rs |> Array.collect (fun r -> r.SolutionLimits) with - | [| sl |] -> sl.Unit - | _ -> "" - - if d |> String.isNullOrWhiteSpace || - u |> String.isNullOrWhiteSpace then "" - else - $"{d} {u}" + sel.Dose + |> MinMax.toString + "van " + "van " + "tot " + "tot " let dt = let s = sel.DoseType |> DoseType.toString @@ -396,3 +426,4 @@ module SolutionRule = |> toMarkdown "" ) + diff --git a/src/Informedica.GenForm.Lib/Types.fs b/src/Informedica.GenForm.Lib/Types.fs index 0630f7e..f9c3b17 100644 --- a/src/Informedica.GenForm.Lib/Types.fs +++ b/src/Informedica.GenForm.Lib/Types.fs @@ -5,7 +5,9 @@ namespace Informedica.GenForm.Lib module Types = open MathNet.Numerics + open Informedica.GenUnits.Lib + type MinMax = Informedica.GenCore.Lib.Ranges.MinMax /// Associate a Route and a Shape /// setting default values for the other fields @@ -16,13 +18,13 @@ module Types = /// The pharmacological Shape Shape : string /// The Unit of the Shape - Unit : string + Unit : Unit /// The Dose Unit to use for Dose Limits - DoseUnit : string + DoseUnit : Unit /// The minimum Dose quantity - MinDoseQty : float option + MinDoseQty : ValueUnit option /// The maximum Dose quantity - MaxDoseQty : float option + MaxDoseQty : ValueUnit option /// Whether a Dose runs over a Time Timed : bool /// Whether the Shape needs to be reconstituted @@ -68,14 +70,6 @@ module Types = | AnyDoseType - /// A MinMax type with an optional Minimum and Maximum. - type MinMax = { Minimum : BigRational option; Maximum : BigRational option } - - - /// A frequency type with a Count and a TimeUnit. - type Frequency = { Count : BigRational; TimeUnit : string } - - /// A Shape Route with associated attributes. type ShapeRoute = { @@ -84,9 +78,9 @@ module Types = /// The Route of administration Route : string /// The Unit of the Shape - Unit : string + Unit : Unit /// The Dose Unit to use for Dose Limits - DoseUnit : string + DoseUnit : Unit /// Whether a Dose runs over a Time Timed : bool /// Whether the Shape needs to be reconstituted @@ -101,14 +95,10 @@ module Types = { /// The name of the Substance Name : string - /// The Unit of the Substance - Unit : string /// The Quantity of the Substance - Quantity : BigRational option + Quantity : ValueUnit option /// The indivisible Quantity of the Substance - MultipleQuantity : BigRational option - /// The Unit of the indivisible Quantity of the Substance - MultipleUnit : string + MultipleQuantity : ValueUnit option } /// A Product type. @@ -137,9 +127,9 @@ module Types = /// The possible Routes of administration of the Product Routes : string [] /// The possible quantities of the Shape of the Product - ShapeQuantities : BigRational [] + ShapeQuantities : ValueUnit /// The uid of the Shape of the Product - ShapeUnit : string + ShapeUnit : Unit /// Whether the Shape of the Product requires reconstitution RequiresReconstitution : bool /// The possible reconstitution rules for the Product @@ -160,9 +150,9 @@ module Types = /// The location for the reconstitution Location : VenousAccess /// The volume of the reconstitution - DiluentVolume : BigRational + DiluentVolume : ValueUnit /// An optional expansion volume of the reconstitution - ExpansionVolume : BigRational option + ExpansionVolume : ValueUnit option /// The Diluents for the reconstitution Diluents : string [] } @@ -173,16 +163,14 @@ module Types = { /// The Target for the Doselimit DoseLimitTarget : DoseLimitTarget - /// The DoseUnit to use for the DoseLimit - DoseUnit : string - /// The RateUnit to use for the DoseLimit - RateUnit : string + /// The unit to dose with + DoseUnit: Unit /// A MinMax Dose Quantity for the DoseLimit Quantity : MinMax /// An optional Dose Quantity Adjust for the DoseLimit. /// Note: if this is specified a min and max QuantityAdjust /// will be assumed to be 10% minus and plus the normal value - NormQuantityAdjust : BigRational option + NormQuantityAdjust : ValueUnit option /// A MinMax Quantity Adjust for the DoseLimit QuantityAdjust : MinMax /// An optional Dose Per Time for the DoseLimit @@ -190,7 +178,7 @@ module Types = /// An optional Per Time Adjust for the DoseLimit /// Note: if this is specified a min and max NormPerTimeAdjust /// will be assumed to be 10% minus and plus the normal value - NormPerTimeAdjust : BigRational option + NormPerTimeAdjust : ValueUnit option /// A MinMax Per Time Adjust for the DoseLimit PerTimeAdjust : MinMax /// A MinMax Rate for the DoseLimit @@ -223,42 +211,41 @@ module Types = type Patient = { /// The Department of the Patient - Department : string + Department : string option /// A list of Diagnoses of the Patient Diagnoses : string [] /// The Gender of the Patient Gender : Gender /// The Age in days of the Patient - AgeInDays : BigRational option + Age : ValueUnit option /// The Weight in grams of the Patient - WeightInGram : BigRational option + Weight : ValueUnit option /// The Height in cm of the Patient - HeightInCm : BigRational option + Height : ValueUnit option /// The Gestational Age in days of the Patient - GestAgeInDays : BigRational option + GestAge : ValueUnit option /// The Post Menstrual Age in days of the Patient - PMAgeInDays : BigRational option + PMAge : ValueUnit option /// The Venous Access of the Patient - /// TODO: should be a list - VenousAccess : VenousAccess + VenousAccess : VenousAccess list } static member Gender_ = (fun (p : Patient) -> p.Gender), (fun g (p : Patient) -> { p with Gender = g}) static member Age_ = - (fun (p : Patient) -> p.AgeInDays), (fun a (p : Patient) -> { p with AgeInDays = a}) + (fun (p : Patient) -> p.Age), (fun a (p : Patient) -> { p with Age = a}) static member Weight_ = - (fun (p : Patient) -> p.WeightInGram), (fun w (p : Patient) -> { p with WeightInGram = w}) + (fun (p : Patient) -> p.Weight), (fun w (p : Patient) -> { p with Weight = w}) static member Height_ = - (fun (p : Patient) -> p.HeightInCm), (fun b (p : Patient) -> { p with HeightInCm = b}) + (fun (p : Patient) -> p.Height), (fun b (p : Patient) -> { p with Height = b}) static member GestAge_ = - (fun (p : Patient) -> p.GestAgeInDays), (fun a (p : Patient) -> { p with GestAgeInDays = a}) + (fun (p : Patient) -> p.GestAge), (fun a (p : Patient) -> { p with GestAge = a}) static member PMAge_ = - (fun (p : Patient) -> p.PMAgeInDays), (fun a (p : Patient) -> { p with PMAgeInDays = a}) + (fun (p : Patient) -> p.PMAge), (fun a (p : Patient) -> { p with PMAge = a}) static member Department_ = (fun (p : Patient) -> p.Department), (fun d (p : Patient) -> { p with Department = d}) @@ -278,26 +265,18 @@ module Types = Route : string /// The PatientCategory of the DoseRule PatientCategory : PatientCategory - /// The Adjustment Unit of the DoseRule - AdjustUnit : string /// The DoseType of the DoseRule DoseType : DoseType + /// The unit to adjust dosing with + AdjustUnit : Unit option /// The possible Frequencies of the DoseRule - Frequencies : BigRational array - /// The frequency time unit of the DoseRule - FreqTimeUnit : string + Frequencies : ValueUnit option /// The MinMax Administration Time of the DoseRule AdministrationTime : MinMax - /// The Administration Time Unit of the DoseRule - AdministrationTimeUnit : string /// The MinMax Interval Time of the DoseRule IntervalTime : MinMax - /// The Interval Time Unit of the DoseRule - IntervalTimeUnit : string /// The MinMax Duration of the DoseRule Duration : MinMax - /// The Duration Unit of the DoseRule - DurationUnit : string /// The list of associated DoseLimits of the DoseRule. /// In principle for the Shape and each Substance . DoseLimits : DoseLimit array @@ -311,12 +290,10 @@ module Types = { /// The Substance for the SolutionLimit Substance : string - /// The unit of the Substance for the SolutionLimit - Unit : string /// The MinMax Quantity of the Substance for the SolutionLimit Quantity : MinMax /// A list of possible Quantities of the Substance for the SolutionLimit - Quantities : BigRational [] + Quantities : ValueUnit option /// The Minmax Concentration of the Substance for the SolutionLimit Concentration : MinMax } @@ -349,7 +326,7 @@ module Types = /// The possible Solutions to use Solutions : string [] /// The possible Volumes to use - Volumes : BigRational [] + Volumes : ValueUnit option /// A MinMax Volume range to use Volume : MinMax /// The percentage to be use as a DoseQuantity @@ -370,26 +347,10 @@ module Types = Shape : string option /// The Route to filter on Route : string option - /// The Department to filter on - Department : string option - /// The list of Diagnoses to filter on - Diagnoses : string [] - /// The Gender to filter on - Gender : Gender - /// The Age in days to filter on - AgeInDays : BigRational option - /// The Weight in grams to filter on - WeightInGram : BigRational option - /// The Height in cm to filter on - HeightInCm : BigRational option - /// The Gestational Age in days to filter on - GestAgeInDays : BigRational option - /// The Post Menstrual Age in days to filter on - PMAgeInDays : BigRational option /// The DoseType to filter on DoseType : DoseType - /// The Venous Access Location to filter on - Location : VenousAccess + /// The patient to filter on + Patient : Patient } @@ -400,4 +361,5 @@ module Types = Patient : Patient DoseRule : DoseRule SolutionRules : SolutionRule [] - } \ No newline at end of file + } + diff --git a/src/Informedica.GenForm.Lib/Utils.fs b/src/Informedica.GenForm.Lib/Utils.fs index 88cd091..782022b 100644 --- a/src/Informedica.GenForm.Lib/Utils.fs +++ b/src/Informedica.GenForm.Lib/Utils.fs @@ -1,12 +1,11 @@ namespace Informedica.GenForm.Lib + [] module Utils = open System - open System.IO - open System.Net.Http open Informedica.Utils.Lib open Informedica.Utils.Lib.BCL @@ -80,3 +79,127 @@ module Utils = let tupleBrOpt brs1 brs2 = brs1 |> Array.tryHead, brs2 |> Array.tryHead + + + module Calculations = + + open MathNet.Numerics + open Informedica.GenUnits.Lib + + module Conversions = Informedica.GenCore.Lib.Conversions + module BSA = Informedica.GenCore.Lib.Calculations.BSA + + let calcDuBois weight height = + let w = + weight + |> ValueUnit.convertTo Units.Mass.kiloGram + |> ValueUnit.getValue + |> Array.tryHead + |> Option.defaultValue 0N + |> BigRational.toDecimal + |> Conversions.kgFromDecimal + let h = + height + |> ValueUnit.convertTo Units.Height.centiMeter + |> ValueUnit.getValue + |> Array.tryHead + |> Option.defaultValue 0N + |> BigRational.toDecimal + |> Conversions.cmFromDecimal + + BSA.calcDuBois (Some 2) w h + |> decimal + |> BigRational.fromDecimal + |> ValueUnit.singleWithUnit Units.BSA.m2 + + + module Units = + + open Informedica.GenUnits.Lib + + let week = Units.Time.week + + let day = Units.Time.day + + let weightGram = Units.Weight.gram + + let heightCm = Units.Height.centiMeter + + let bsaM2 = Units.BSA.m2 + + let timeUnit s = + if s |> String.isNullOrWhiteSpace then None + else + $"{s}[Time]" |> Units.fromString + + let freqUnit s = + if s |> String.isNullOrWhiteSpace then None + else + $"times[Count]/{s}[Time]" |> Units.fromString + + let adjustUnit s = + match s with + | _ when s |> String.equalsCapInsens "kg" -> Units.Weight.kiloGram |> Some + | _ when s |> String.equalsCapInsens "m2" -> bsaM2 |> Some + | _ -> None + + + let mL = Units.Volume.milliLiter + + + + module ValueUnit = + + open MathNet.Numerics + open Informedica.GenUnits.Lib + + + /// The full term age for a neonate + /// which is 37 weeks + let ageFullTerm = 37N |> ValueUnit.singleWithUnit Units.Time.week + + + let withOptionalUnit u v = + match v, u with + | Some v, Some u -> + v + |> ValueUnit.singleWithUnit u + |> Some + | _ -> None + + + let toString prec vu = + ValueUnit.toStringDecimalDutchShortWithPrec prec vu + |> String.replace ";" ", " + + + module MinMax = + + open Informedica.GenUnits.Lib + open Informedica.GenCore.Lib.Ranges + + let fromTuple u (min, max) = + match u with + | None -> MinMax.empty + | Some u -> + { + Min = + min + |> Option.map (ValueUnit.singleWithUnit u) + |> Option.map Inclusive + Max = + max + |> Option.map (ValueUnit.singleWithUnit u) + |> Option.map Inclusive + } + + + let inRange minMax vu = + if minMax = MinMax.empty then true + else + vu + |> Option.map (fun v -> + minMax |> MinMax.inRange v + ) + |> Option.defaultValue false + diff --git a/src/Informedica.GenForm.Lib/VenousAccess.fs b/src/Informedica.GenForm.Lib/VenousAccess.fs new file mode 100644 index 0000000..014f545 --- /dev/null +++ b/src/Informedica.GenForm.Lib/VenousAccess.fs @@ -0,0 +1,20 @@ +namespace Informedica.GenForm.Lib + + + +module VenousAccess = + + + let check location venousAccess = + match location, venousAccess with + | AnyAccess, _ -> true + | _, xs when xs |> List.isEmpty -> true + | _ -> + venousAccess + |> List.exists ((=) location) + + + + + + diff --git a/src/Informedica.GenOrder.Lib/Api.fs b/src/Informedica.GenOrder.Lib/Api.fs index 0565b05..2ec83e3 100644 --- a/src/Informedica.GenOrder.Lib/Api.fs +++ b/src/Informedica.GenOrder.Lib/Api.fs @@ -11,6 +11,7 @@ module Api = open Informedica.GenUnits.Lib open Informedica.GenForm.Lib open Informedica.GenOrder.Lib + open Informedica.GenCore.Lib.Ranges /// @@ -98,7 +99,8 @@ module Api = /// The TimeUnit for the Frequencies /// The DoseLimits for the ProductComponent /// The Products to create the ProductComponent from - let createProductComponent noSubst freqUnit (doseLimits : DoseLimit []) (ps : Product []) = + let createProductComponent noSubst (doseLimits : DoseLimit []) (ps : Product []) = + printfn $"{noSubst} with {doseLimits}" { DrugOrder.productComponent with Name = ps @@ -114,11 +116,10 @@ module Api = else s Quantities = ps - |> Array.collect (fun p -> p.ShapeQuantities) - |> Array.distinct - |> Array.toList - TimeUnit = freqUnit - RateUnit = "uur" //doseRule.RateUnit + |> Array.map (fun p -> + p.ShapeQuantities + ) + |> ValueUnit.collect Divisible = ps |> Array.choose (fun p -> p.Divisible) @@ -136,9 +137,7 @@ module Api = xs |> Array.choose (fun s -> s.Quantity) |> Array.distinct - |> Array.toList - Unit = xs |> tryHead (fun x -> x.Unit) - TimeUnit = freqUnit + |> ValueUnit.collect Dose = doseLimits |> Array.tryFind (fun l -> @@ -178,61 +177,58 @@ module Api = /// The PrescriptionRule to use let createDrugOrder (sr: SolutionRule option) (pr : PrescriptionRule) = let parenteral = Product.Parenteral.get () - // adjust unit defaults to kg + // adjust unit defaults to kg let au = - if pr.DoseRule.AdjustUnit |> String.isNullOrWhiteSpace then "kg" - else pr.DoseRule.AdjustUnit + pr.DoseRule.AdjustUnit + |> Option.defaultValue Units.Weight.kiloGram let dose = pr.DoseRule.DoseLimits - |> Array.filter (fun dl -> - // TODO make a specific match for ShapeDoseLimitTarget - match dl.DoseLimitTarget with - | ShapeDoseLimitTarget _ -> true - | _ -> false - ) - |> function - | [| dl |] -> dl |> Some - | _ -> None + |> Array.filter DoseRule.DoseLimit.isShapeLimit + |> Array.tryHead // if no subst, dose is based on shape let noSubst = - dose - |> Option.map (fun d -> d.DoseUnit = "keer") - |> Option.defaultValue false + pr.DoseRule.DoseLimits + |> Array.filter DoseRule.DoseLimit.isSubstanceLimit + |> Array.filter (fun d -> + d.DoseUnit = NoUnit || + d.DoseUnit |> ValueUnit.Group.eqsGroup Units.Count.times + ) + |> Array.isEmpty |> not + + let substLimits = + pr.DoseRule.DoseLimits + |> Array.filter DoseRule.DoseLimit.isSubstanceLimit { DrugOrder.drugOrder with Id = Guid.NewGuid().ToString() Name = pr.DoseRule.Generic Products = pr.DoseRule.Products - |> createProductComponent noSubst pr.DoseRule.FreqTimeUnit pr.DoseRule.DoseLimits + |> createProductComponent noSubst substLimits |> List.singleton - Quantities = [] - Frequencies = pr.DoseRule.Frequencies |> Array.toList - FreqUnit = pr.DoseRule.FreqTimeUnit - Unit = - pr.DoseRule.Products - |> tryHead (fun p -> p.ShapeUnit) + Quantities = None + Frequencies = pr.DoseRule.Frequencies Time = pr.DoseRule.AdministrationTime - TimeUnit = pr.DoseRule.AdministrationTimeUnit - RateUnit = "uur" Route = pr.DoseRule.Route DoseCount = - if pr.SolutionRules |> Array.isEmpty then Some 1N - else None + if pr.SolutionRules |> Array.isEmpty |> not then None + else + Units.Count.times + |> ValueUnit.withSingleValue 1N + |> Some OrderType = match pr.DoseRule.DoseType with | Informedica.GenForm.Lib.Types.Continuous -> ContinuousOrder - | _ when pr.DoseRule.AdministrationTimeUnit |> String.isNullOrWhiteSpace -> DiscontinuousOrder + | _ when pr.DoseRule.AdministrationTime = MinMax.empty -> DiscontinuousOrder | _ -> TimedOrder Dose = dose Adjust = - if au = "kg" then - pr.Patient.WeightInGram - |> Option.map (fun v -> v / 1000N) + if au |> ValueUnit.Group.eqsGroup Units.Weight.kiloGram then + pr.Patient.Weight else pr.Patient |> Patient.calcBSA - AdjustUnit = au + AdjustUnit = Some au } |> fun dro -> // add an optional solution rule @@ -243,10 +239,12 @@ module Api = Dose = { DoseRule.DoseLimit.limit with Quantity = sr.Volume - DoseUnit = "mL" + DoseUnit = Units.Volume.milliLiter } |> Some - Quantities = sr.Volumes |> Array.toList - DoseCount = sr.DosePerc.Maximum + Quantities = sr.Volumes + DoseCount = + sr.DosePerc.Max + |> Option.map Limit.getValueUnit Products = let ps = dro.Products @@ -274,7 +272,7 @@ module Api = |> function | Some p -> [|p|] - |> createProductComponent true pr.DoseRule.FreqTimeUnit [||] + |> createProductComponent true [||] |> List.singleton |> List.append ps | None -> @@ -438,8 +436,8 @@ module Api = let path = $"{__SOURCE_DIRECTORY__}/log.txt" OrderLogger.logger.Start (Some path) OrderLogger.Level.Informative - match sc.Patient.WeightInGram, sc.Patient.HeightInCm, sc.Patient.Department with - | Some w, Some h, d when d |> String.notEmpty -> + match sc.Patient.Weight, sc.Patient.Height, sc.Patient.Department with + | Some w, Some h, d when d |> Option.isSome -> let ind = if sc.Indication.IsSome then sc.Indication @@ -456,17 +454,21 @@ module Api = let filter = { Filter.filter with - Department = Some d - AgeInDays = sc.Patient.AgeInDays - GestAgeInDays = sc.Patient.GestAgeInDays - PMAgeInDays = sc.Patient.PMAgeInDays - WeightInGram = Some w - HeightInCm = Some h Indication = ind Generic = gen Route = rte Shape = shp - Location = sc.Patient.VenousAccess + Patient = { + Department = d + Age = sc.Patient.Age + GestAge = sc.Patient.GestAge + PMAge = sc.Patient.PMAge + Weight = Some w + Height = Some h + Diagnoses = [||] + Gender = sc.Patient.Gender + VenousAccess = sc.Patient.VenousAccess + } } let inds = filter |> filterIndications diff --git a/src/Informedica.GenOrder.Lib/DrugOrder.fs b/src/Informedica.GenOrder.Lib/DrugOrder.fs index 4579b19..38fe408 100644 --- a/src/Informedica.GenOrder.Lib/DrugOrder.fs +++ b/src/Informedica.GenOrder.Lib/DrugOrder.fs @@ -13,7 +13,8 @@ module DrugOrder = module DoseRule = Informedica.GenForm.Lib.DoseRule module DoseLimit = DoseRule.DoseLimit - module MinMax = Informedica.GenForm.Lib.MinMax + module MinMax = Informedica.GenCore.Lib.Ranges.MinMax + module Limit = Informedica.GenCore.Lib.Ranges.Limit /// @@ -25,19 +26,11 @@ module DrugOrder = /// If the unit is null or an empty empty string, the function returns None. /// let createValueUnitDto u brs = - if u |> String.isNullOrWhiteSpace then None + if u = NoUnit then None else - let vuDto = ValueUnit.Dto.dto() - vuDto.Value <- - brs - |> Seq.toArray - |> Array.map (fun v -> - v |> string, - v |> BigRational.toDecimal - ) - vuDto.Unit <- u - vuDto |> Some - + brs + |> ValueUnit.withUnit u + |> ValueUnit.Dto.toDto false "English" /// /// Create a single value unit dto from a string and a big rational. @@ -68,28 +61,40 @@ module DrugOrder = /// the sequence of big rationals has a single value. In that case the /// min or max value is set to the big rational minus or plus 10%. /// - let setConstraints un (brs : BigRational []) (minMax : MinMax) (dto: Informedica.GenSolver.Lib.Variable.Dto.Dto) = + let setConstraints + (norm : ValueUnit option) + (minMax : MinMax) + (dto: Informedica.GenSolver.Lib.Variable.Dto.Dto) = + + let vuToDto = Option.bind (ValueUnit.Dto.toDto false "English") + + let limToVu = Option.map Limit.getValueUnit + + let times0_95 = (95N/100N) |> ValueUnit.singleWithUnit Units.Count.times + let times1_10 = (11N/10N) |> ValueUnit.singleWithUnit Units.Count.times + let min = - match minMax.Minimum, brs with - | None, [|br|] -> br - br / 10N |> Some - | _ -> minMax.Minimum + match minMax.Min, norm with + | None, Some norm -> norm * times0_95 |> Some + | _ -> minMax.Min |> limToVu + |> vuToDto let max = - match minMax.Maximum, brs with - | None, [|br|] -> br + br / 10N |> Some - | _ -> minMax.Maximum + match minMax.Max, norm with + | None, Some norm -> norm * times1_10 |> Some + | _ -> minMax.Max |> limToVu + |> vuToDto match min with | None -> () - | Some min -> + | Some _ -> dto.MinIncl <- true - dto.MinOpt <- min |> createSingleValueUnitDto un - + dto.MinOpt <- min match max with | None -> () - | Some max -> + | Some _ -> dto.MaxIncl <- true - dto.MaxOpt <- max |> createSingleValueUnitDto un + dto.MaxOpt <- max dto @@ -100,20 +105,16 @@ module DrugOrder = Id = "" Name = "" Products = [] - Quantities = [] - Unit = "" + Quantities = None Route = "" OrderType = AnyOrder - Frequencies = [] - FreqUnit = "" - Rates = [] - RateUnit = "" - Time = MinMax.none - TimeUnit = "" + AdjustUnit = None + Frequencies = None + Rates = None + Time = MinMax.empty Dose = None DoseCount = None Adjust = None - AdjustUnit = "" } @@ -122,10 +123,8 @@ module DrugOrder = { Name = "" Shape = "" - Quantities = [] - TimeUnit = "" - RateUnit = "" - Divisible = Some 1N + Quantities = None + Divisible = None Substances = [] } @@ -134,9 +133,7 @@ module DrugOrder = let substanceItem = { Name = "" - Concentrations = [] - Unit = "" - TimeUnit = "" + Concentrations = None Dose = None //DoseLimit.limit Solution = None } @@ -156,6 +153,21 @@ module DrugOrder = let toOrderDto (d : DrugOrder) = let toArr = Option.map Array.singleton >> Option.defaultValue [||] + let vuToDto = Option.bind (ValueUnit.Dto.toDto false "English") + + let limToDto = Option.map Limit.getValueUnit >> vuToDto + + let oru = Units.Volume.milliLiter |> Units.per Units.Time.hour + // assumes the drugorder has products and these have quantities + let ou = + d.Products + |> List.map (fun p -> + p.Quantities + |> Option.map (fun q -> q |> ValueUnit.getUnit) + ) + |> List.choose id + |> List.head + let standDoseRate un (orbDto : Order.Orderable.Dto.Dto) = orbDto.Dose.Rate.Constraints.IncrOpt <- 1N/10N |> createSingleValueUnitDto un orbDto.Dose.Rate.Constraints.MinIncl <- true @@ -163,70 +175,43 @@ module DrugOrder = orbDto.Dose.Rate.Constraints.MaxIncl <- true orbDto.Dose.Rate.Constraints.MaxOpt <- 1000N |> createSingleValueUnitDto un - // create the units - let cu = "x[Count]" - let ml = "ml[Volume]" - - let ou = d.Unit |> unitGroup - let au = - match d.AdjustUnit with - | s when s = "kg" -> "kg[Weight]" - | s when s = "m2" -> "m2[BSA]" - | _ -> $"cannot parse adjust unit: {d.AdjustUnit}" |> failwith - let du = - match d.Dose with - | Some dl when dl.DoseUnit |> String.notEmpty -> dl.DoseUnit |> unitGroup - | _ -> ou - let ft = $"{d.FreqUnit}[Time]" - let ru = $"{d.RateUnit}[Time]" - let tu = $"{d.TimeUnit}[Time]" - - let ofu = $"{cu}/{ft}" - let oru = $"{ml}/{ru}" - let ora = $"{ml}/{au}/{ru}" - let oda = $"{du}/{au}" - let opt = $"{du}/{ft}" - let pta = $"{du}/{au}/{ft}" - let orbDto = Order.Orderable.Dto.dto d.Id d.Name - orbDto.DoseCount.Constraints.ValsOpt <- - d.DoseCount - |> Option.bind (createSingleValueUnitDto cu) + orbDto.DoseCount.Constraints.ValsOpt <- d.DoseCount |> vuToDto - orbDto.OrderableQuantity.Constraints.ValsOpt <- d.Quantities |> createValueUnitDto ou + orbDto.OrderableQuantity.Constraints.ValsOpt <- d.Quantities |> vuToDto let setOrbDoseRate (dl : DoseLimit) = - orbDto.Dose.Rate.Constraints.MinIncl <- dl.Rate.Minimum.IsSome - orbDto.Dose.Rate.Constraints.MinOpt <- dl.Rate.Minimum |> Option.bind (createSingleValueUnitDto oru) - orbDto.Dose.Rate.Constraints.MinIncl <- dl.Rate.Maximum.IsSome - orbDto.Dose.Rate.Constraints.MinOpt <- dl.Rate.Maximum |> Option.bind (createSingleValueUnitDto oru) + orbDto.Dose.Rate.Constraints.MinIncl <- dl.Rate.Min.IsSome + orbDto.Dose.Rate.Constraints.MinOpt <- dl.Rate.Min |> limToDto + orbDto.Dose.Rate.Constraints.MinIncl <- dl.Rate.Max.IsSome + orbDto.Dose.Rate.Constraints.MinOpt <- dl.Rate.Max |> limToDto - orbDto.Dose.RateAdjust.Constraints.MinIncl <- dl.RateAdjust.Minimum.IsSome - orbDto.Dose.RateAdjust.Constraints.MinOpt <- dl.RateAdjust.Minimum |> Option.bind (createSingleValueUnitDto ora) - orbDto.Dose.RateAdjust.Constraints.MinIncl <- dl.RateAdjust.Maximum.IsSome - orbDto.Dose.RateAdjust.Constraints.MinOpt <- dl.RateAdjust.Maximum |> Option.bind (createSingleValueUnitDto ora) + orbDto.Dose.RateAdjust.Constraints.MinIncl <- dl.RateAdjust.Min.IsSome + orbDto.Dose.RateAdjust.Constraints.MinOpt <- dl.RateAdjust.Min |> limToDto + orbDto.Dose.RateAdjust.Constraints.MinIncl <- dl.RateAdjust.Max.IsSome + orbDto.Dose.RateAdjust.Constraints.MinOpt <- dl.RateAdjust.Max |> limToDto let setOrbDoseQty (dl : DoseLimit) = - orbDto.Dose.Quantity.Constraints.MinIncl <- dl.Quantity.Minimum.IsSome - orbDto.Dose.Quantity.Constraints.MinOpt <- dl.Quantity.Minimum |> Option.bind (createSingleValueUnitDto du) - orbDto.Dose.Quantity.Constraints.MaxIncl <- dl.Quantity.Maximum.IsSome - orbDto.Dose.Quantity.Constraints.MaxOpt <- dl.Quantity.Maximum |> Option.bind (createSingleValueUnitDto du) - - orbDto.Dose.QuantityAdjust.Constraints.MinIncl <- dl.QuantityAdjust.Minimum.IsSome - orbDto.Dose.QuantityAdjust.Constraints.MinOpt <- dl.QuantityAdjust.Minimum |> Option.bind (createSingleValueUnitDto oda) - orbDto.Dose.QuantityAdjust.Constraints.MaxIncl <- dl.QuantityAdjust.Maximum.IsSome - orbDto.Dose.QuantityAdjust.Constraints.MaxOpt <- dl.QuantityAdjust.Maximum |> Option.bind (createSingleValueUnitDto oda) - - orbDto.Dose.PerTime.Constraints.MinIncl <- dl.PerTime.Minimum.IsSome - orbDto.Dose.PerTime.Constraints.MinOpt <- dl.PerTime.Minimum |> Option.bind (createSingleValueUnitDto opt) - orbDto.Dose.PerTime.Constraints.MaxIncl <- dl.PerTime.Maximum.IsSome - orbDto.Dose.PerTime.Constraints.MaxOpt <- dl.PerTime.Maximum |> Option.bind (createSingleValueUnitDto opt) - - orbDto.Dose.PerTimeAdjust.Constraints.MinIncl <- dl.PerTimeAdjust.Minimum.IsSome - orbDto.Dose.PerTimeAdjust.Constraints.MinOpt <- dl.PerTimeAdjust.Minimum |> Option.bind (createSingleValueUnitDto pta) - orbDto.Dose.PerTimeAdjust.Constraints.MaxIncl <- dl.PerTimeAdjust.Maximum.IsSome - orbDto.Dose.PerTimeAdjust.Constraints.MaxOpt <- dl.PerTimeAdjust.Maximum |> Option.bind (createSingleValueUnitDto pta) + orbDto.Dose.Quantity.Constraints.MinIncl <- dl.Quantity.Min.IsSome + orbDto.Dose.Quantity.Constraints.MinOpt <- dl.Quantity.Min |> limToDto + orbDto.Dose.Quantity.Constraints.MaxIncl <- dl.Quantity.Max.IsSome + orbDto.Dose.Quantity.Constraints.MaxOpt <- dl.Quantity.Max |> limToDto + + orbDto.Dose.QuantityAdjust.Constraints.MinIncl <- dl.QuantityAdjust.Min.IsSome + orbDto.Dose.QuantityAdjust.Constraints.MinOpt <- dl.QuantityAdjust.Min |> limToDto + orbDto.Dose.QuantityAdjust.Constraints.MaxIncl <- dl.QuantityAdjust.Max.IsSome + orbDto.Dose.QuantityAdjust.Constraints.MaxOpt <- dl.QuantityAdjust.Max |> limToDto + + orbDto.Dose.PerTime.Constraints.MinIncl <- dl.PerTime.Min.IsSome + orbDto.Dose.PerTime.Constraints.MinOpt <- dl.PerTime.Min |> limToDto + orbDto.Dose.PerTime.Constraints.MaxIncl <- dl.PerTime.Max.IsSome + orbDto.Dose.PerTime.Constraints.MaxOpt <- dl.PerTime.Max |> limToDto + + orbDto.Dose.PerTimeAdjust.Constraints.MinIncl <- dl.PerTimeAdjust.Min.IsSome + orbDto.Dose.PerTimeAdjust.Constraints.MinOpt <- dl.PerTimeAdjust.Min |> limToDto + orbDto.Dose.PerTimeAdjust.Constraints.MaxIncl <- dl.PerTimeAdjust.Max.IsSome + orbDto.Dose.PerTimeAdjust.Constraints.MaxOpt <- dl.PerTimeAdjust.Max |> limToDto match d.OrderType with | AnyOrder @@ -257,94 +242,83 @@ module DrugOrder = [ for p in d.Products do let cdto = Order.Orderable.Component.Dto.dto d.Id d.Name p.Name p.Shape + let div = + p.Divisible + |> Option.bind (fun d -> + (1N / d) + |> createSingleValueUnitDto ou + ) - cdto.ComponentQuantity.Constraints.ValsOpt <- p.Quantities |> createValueUnitDto ou + cdto.ComponentQuantity.Constraints.ValsOpt <- p.Quantities |> vuToDto + cdto.OrderableQuantity.Constraints.IncrOpt <- div - if p.Divisible.IsSome then - cdto.OrderableQuantity.Constraints.IncrOpt <- 1N / p.Divisible.Value |> createSingleValueUnitDto ou if d.Products |> List.length = 1 then // if there is only one product, the concentration of that product in the // Orderable will be by definition be 1. - cdto.OrderableConcentration.Constraints.ValsOpt <- 1N |> createSingleValueUnitDto cu + cdto.OrderableConcentration.Constraints.ValsOpt <- + 1N + |> createSingleValueUnitDto Units.Count.times - if p.Divisible.IsSome then - orbDto.Dose.Quantity.Constraints.IncrOpt <- 1N / p.Divisible.Value |> createSingleValueUnitDto ou - cdto.Dose.Quantity.Constraints.IncrOpt <- 1N / p.Divisible.Value |> createSingleValueUnitDto ou + orbDto.Dose.Quantity.Constraints.IncrOpt <- div + cdto.Dose.Quantity.Constraints.IncrOpt <- div cdto.Items <- [ for s in p.Substances do - let su = s.Unit |> unitGroup - let du = - match s.Dose with - | Some dl -> - if dl.DoseUnit |> String.isNullOrWhiteSpace then su - else - dl.DoseUnit |> unitGroup - | None -> "" - let itmDto = Order.Orderable.Item.Dto.dto d.Id d.Name p.Name s.Name - itmDto.ComponentConcentration.Constraints.ValsOpt <- s.Concentrations |> createValueUnitDto $"{su}/{ou}" + itmDto.ComponentConcentration.Constraints.ValsOpt <- s.Concentrations |> vuToDto if d.Products |> List.length = 1 then // when only one product, the orderable concentration is the same as the component concentration itmDto.OrderableConcentration.Constraints.ValsOpt <- itmDto.ComponentConcentration.Constraints.ValsOpt match s.Solution with | Some sl -> - let su = sl.Unit |> unitGroup // note the solution substance unit can differ from the component substance unit! - itmDto.OrderableQuantity.Constraints.MinIncl <- sl.Quantity.Minimum.IsSome - itmDto.OrderableQuantity.Constraints.MinOpt <- sl.Quantity.Minimum |> Option.bind (createSingleValueUnitDto su) - itmDto.OrderableQuantity.Constraints.MaxIncl <- sl.Quantity.Maximum.IsSome - itmDto.OrderableQuantity.Constraints.MaxOpt <- sl.Quantity.Maximum |> Option.bind (createSingleValueUnitDto su) - itmDto.OrderableConcentration.Constraints.MinIncl <- sl.Concentration.Minimum.IsSome - itmDto.OrderableConcentration.Constraints.MinOpt <- sl.Concentration.Minimum |> Option.bind (createSingleValueUnitDto $"{su}/{ou}") - itmDto.OrderableConcentration.Constraints.MaxIncl <- sl.Concentration.Maximum.IsSome - itmDto.OrderableConcentration.Constraints.MaxOpt <- sl.Concentration.Maximum |> Option.bind (createSingleValueUnitDto $"{su}/{ou}") + itmDto.OrderableQuantity.Constraints.MinIncl <- sl.Quantity.Min.IsSome + itmDto.OrderableQuantity.Constraints.MinOpt <- sl.Quantity.Min |> limToDto + itmDto.OrderableQuantity.Constraints.MaxIncl <- sl.Quantity.Max.IsSome + itmDto.OrderableQuantity.Constraints.MaxOpt <- sl.Quantity.Max |> limToDto + itmDto.OrderableConcentration.Constraints.MinIncl <- sl.Concentration.Min.IsSome + itmDto.OrderableConcentration.Constraints.MinOpt <- sl.Concentration.Min |> limToDto + itmDto.OrderableConcentration.Constraints.MaxIncl <- sl.Concentration.Max.IsSome + itmDto.OrderableConcentration.Constraints.MaxOpt <- sl.Concentration.Max |> limToDto | None -> () let setDoseRate (dl : DoseLimit) = - let dru = $"{du}/{dl.RateUnit}[Time]" - let dra = $"{du}/{au}/{dl.RateUnit}[Time]" itmDto.Dose.Rate.Constraints <- itmDto.Dose.Rate.Constraints |> MinMax.setConstraints - dru - [||] + None dl.Rate itmDto.Dose.RateAdjust.Constraints <- itmDto.Dose.RateAdjust.Constraints - |> MinMax.setConstraints dra [||] dl.RateAdjust + |> MinMax.setConstraints None dl.RateAdjust let setDoseQty (dl : DoseLimit) = itmDto.Dose.Quantity.Constraints <- itmDto.Dose.Quantity.Constraints |> MinMax.setConstraints - du - [||] + None dl.Quantity itmDto.Dose.QuantityAdjust.Constraints <- itmDto.Dose.QuantityAdjust.Constraints |> MinMax.setConstraints - $"{du}/{au}" - (dl.NormQuantityAdjust |> toArr) + (dl.NormQuantityAdjust) dl.QuantityAdjust itmDto.Dose.PerTime.Constraints <- itmDto.Dose.PerTime.Constraints |> MinMax.setConstraints - $"{du}/{s.TimeUnit}[Time]" - [||] + None dl.PerTime itmDto.Dose.PerTimeAdjust.Constraints <- itmDto.Dose.PerTimeAdjust.Constraints |> MinMax.setConstraints - $"{du}/{au}/{s.TimeUnit}[Time]" - (dl.NormPerTimeAdjust |> toArr) + (dl.NormPerTimeAdjust) dl.PerTimeAdjust @@ -390,23 +364,23 @@ module DrugOrder = dto.Orderable <- orbDto - dto.Prescription.Frequency.Constraints.ValsOpt <- d.Frequencies |> createValueUnitDto ofu + dto.Prescription.Frequency.Constraints.ValsOpt <- d.Frequencies |> vuToDto - dto.Prescription.Time.Constraints.MinIncl <- d.Time.Minimum.IsSome - dto.Prescription.Time.Constraints.MinOpt <- d.Time.Minimum |> Option.bind (createSingleValueUnitDto tu) - dto.Prescription.Time.Constraints.MaxIncl <- d.Time.Maximum.IsSome - dto.Prescription.Time.Constraints.MaxOpt <- d.Time.Maximum |> Option.bind (createSingleValueUnitDto tu) + dto.Prescription.Time.Constraints.MinIncl <- d.Time.Min.IsSome + dto.Prescription.Time.Constraints.MinOpt <- d.Time.Min |> limToDto + dto.Prescription.Time.Constraints.MaxIncl <- d.Time.Max.IsSome + dto.Prescription.Time.Constraints.MaxOpt <- d.Time.Max |> limToDto - if au |> String.contains "kg" then + if d.AdjustUnit + |> Option.map (ValueUnit.Group.eqsGroup Units.Weight.kiloGram) + |> Option.defaultValue false then dto.Adjust.Constraints.MinOpt <- - (200N /1000N) |> createSingleValueUnitDto au + (200N /1000N) |> createSingleValueUnitDto d.AdjustUnit.Value - if au |> String.contains "kg" then - dto.Adjust.Constraints.MaxOpt <- 150N |> createSingleValueUnitDto au + dto.Adjust.Constraints.MaxOpt <- + 150N |> createSingleValueUnitDto d.AdjustUnit.Value - dto.Adjust.Constraints.ValsOpt <- - d.Adjust - |> Option.bind (createSingleValueUnitDto au) + dto.Adjust.Constraints.ValsOpt <- d.Adjust |> vuToDto dto diff --git a/src/Informedica.GenOrder.Lib/Patient.fs b/src/Informedica.GenOrder.Lib/Patient.fs index 4a3b136..dee8cbc 100644 --- a/src/Informedica.GenOrder.Lib/Patient.fs +++ b/src/Informedica.GenOrder.Lib/Patient.fs @@ -11,6 +11,7 @@ module Patient = open Informedica.Utils.Lib.BCL open Informedica.GenForm.Lib + open Informedica.GenUnits.Lib type Patient =Types.Patient type Access = VenousAccess @@ -21,15 +22,15 @@ module Patient = /// let patient : Patient = { - Department = "" + Department = None Diagnoses = [||] Gender = AnyGender - AgeInDays = None - WeightInGram = None - HeightInCm = None - GestAgeInDays = None - PMAgeInDays = None - VenousAccess = AnyAccess + Age = None + Weight = None + Height = None + GestAge = None + PMAge = None + VenousAccess = [] } @@ -48,7 +49,7 @@ module Patient = /// Converts a list of Age values to a decimal representing the number of days /// /// The Age values - let ageToDays ags = + let ageToValueUnit ags = ags |> List.fold (fun acc a -> match a with @@ -58,17 +59,25 @@ module Patient = | Days x -> (x |> decimal) |> fun x -> acc + x ) 0m + |> BigRational.fromDecimal + |> ValueUnit.singleWithUnit Units.Time.day /// /// Converts a decimal representing the number of days to a list of Age values /// /// The age in days - let ageFromDays (d : decimal) = - let yrs = (d / 365m) |> int - let mos = ((d - (365 * yrs |> decimal)) / 30m) |> int - let wks = (d - (365 * yrs |> decimal) - (30 * mos |> decimal)) / 7m |> int - let dys = (d - (365 * yrs |> decimal) - (30 * mos |> decimal) - (7 * wks |> decimal)) |> int + let ageFromValueUnit (vu : ValueUnit) = + let vu = + vu + |> ValueUnit.convertTo Units.Time.day + |> ValueUnit.getValue + |> Array.head + |> BigRational.toDecimal + let yrs = (vu / 365m) |> int + let mos = ((vu - (365 * yrs |> decimal)) / 30m) |> int + let wks = (vu - (365 * yrs |> decimal) - (30 * mos |> decimal)) / 7m |> int + let dys = (vu - (365 * yrs |> decimal) - (30 * mos |> decimal) - (7 * wks |> decimal)) |> int [ if yrs > 0 then yrs |> Years if mos > 0 then mos |> Months @@ -78,9 +87,9 @@ module Patient = // Helper method for the Optics below let ageAgeList = - Option.map (BigRational.toDecimal >> ageFromDays) + Option.map (ageFromValueUnit) >> (Option.defaultValue []), - (ageToDays >> BigRational.fromDecimal >> Some) + (ageToValueUnit >> Some) let age_ = Patient.Age_ >-> ageAgeList @@ -90,15 +99,15 @@ module Patient = let gestPMAgeList = let ageFromDec d = d - |> ageFromDays + |> ageFromValueUnit |> List.filter (fun a -> match a with | Years _ | Months _ -> false | _ -> true ) - Option.map (BigRational.toDecimal >> ageFromDec) + Option.map (ageFromDec) >> (Option.defaultValue []), - (ageToDays >> BigRational.fromDecimal >> Some) + (ageToValueUnit >> Some) let gestAge_ = Patient.GestAge_ >-> gestPMAgeList @@ -109,57 +118,55 @@ module Patient = type Weight = | Kilogram of decimal | Gram of int - let decWeight = + let vuWeight = let get w = - if w < 300m then w |> Kilogram - else - w - |> int - |> Gram - let set = function + w + |> ValueUnit.convertTo Units.Weight.gram + |> ValueUnit.getValue + |> Array.head + |> BigRational.toDecimal + |> int + |> Gram + + let set w = + match w with | Kilogram w -> w * 1000m | Gram w -> w |> decimal - Option.map (BigRational.toDecimal >> get), - Option.map (set >> BigRational.fromDecimal) + |> BigRational.fromDecimal + |> ValueUnit.singleWithUnit Units.Weight.gram + Option.map (get), + Option.map (set) - let weight_ = Patient.Weight_ >-> decWeight + + let weight_ = Patient.Weight_ >-> vuWeight type Height = | Meter of decimal | Centimeter of int - let decHeight = + let vuHeight = let get h = - if h < 10m then h |> Meter - else - h - |> int - |> Centimeter - let set = function + h + |> ValueUnit.convertTo Units.Height.centiMeter + |> ValueUnit.getValue + |> Array.head + |> BigRational.toDecimal + |> int + |> Centimeter + + let set h = + match h with | Meter h -> h * 100m | Centimeter h -> h |> decimal - Option.map (BigRational.toDecimal >> get), - Option.map (set >> BigRational.fromDecimal) - - - let height_ = Patient.Height_ >-> decHeight - - - let bigRatDec_ = - Option.map BigRational.toDecimal, - Option.map BigRational.fromDecimal - - - let ageDec_ = Patient.Age_ >-> bigRatDec_ + |> BigRational.fromDecimal + |> ValueUnit.singleWithUnit Units.Height.centiMeter - let weightDec_ = Patient.Weight_ >-> bigRatDec_ + Option.map (get), + Option.map (set) - let heightDec_ = Patient.Height_ >-> bigRatDec_ - let gestAgeDec_ = Patient.GestAge_ >-> bigRatDec_ - - let pmAgeDec_ = Patient.PMAge_ >-> bigRatDec_ + let height_ = Patient.Height_ >-> vuHeight let getGender = Optic.get Patient.Gender_ @@ -174,60 +181,30 @@ module Patient = let setAge = Optic.set age_ - let getAgeDec = Optic.get ageDec_ - - - let setAgeDec = Optic.set ageDec_ - - let getWeight = Optic.get weight_ let setWeight = Optic.set weight_ - let getWeightDec = Optic.get weightDec_ - - - let setWeightDec = Optic.set weightDec_ - - let getHeight = Optic.get height_ let setHeight = Optic.set height_ - let getHeightDec = Optic.get heightDec_ - - - let setHeightDec = Optic.set heightDec_ - - let getGestAge = Optic.get gestAge_ let setGestAge = Optic.set gestAge_ - let getGestAgeDec = Optic.get gestAgeDec_ - - - let setGestAgeDec = Optic.set gestAgeDec_ - - let getPMAge = Optic.get pmAge_ let setPMAge = Optic.set pmAge_ - let getPMAgeDec = Optic.get pmAgeDec_ - - - let setPMAgeDec = Optic.set pmAgeDec_ - - let getDepartment = Optic.get Patient.Department_ @@ -241,7 +218,7 @@ module Patient = |> setGestAge [ 32 |> Weeks ] |> setWeight (1200 |> Gram |> Some) |> setHeight (45 |> Centimeter |> Some) - |> setDepartment "NEO" + |> setDepartment (Some "NEO") let newBorn = @@ -249,7 +226,7 @@ module Patient = |> setAge [ 1 |> Weeks] |> setWeight (3.5m |> Kilogram |> Some) |> setHeight (60 |> Centimeter |> Some) - |> setDepartment "ICK" + |> setDepartment (Some "ICK") let infant = @@ -257,7 +234,7 @@ module Patient = |> setAge [ 1 |> Years] |> setWeight (11.5m |> Kilogram |> Some) |> setHeight (70 |> Centimeter |> Some) - |> setDepartment "ICK" + |> setDepartment (Some "ICK") let toddler = @@ -265,7 +242,7 @@ module Patient = |> setAge [ 3 |> Years] |> setWeight (15m |> Kilogram |> Some) |> setHeight (90 |> Centimeter |> Some) - |> setDepartment "ICK" + |> setDepartment (Some "ICK") let child = @@ -273,8 +250,8 @@ module Patient = |> setAge [ 4 |> Years] |> setWeight (17m |> Kilogram |> Some) |> setHeight (100 |> Centimeter |> Some) - |> setDepartment "ICK" - |> fun p -> { p with VenousAccess = CVL} + |> setDepartment (Some "ICK") + |> fun p -> { p with VenousAccess = [CVL]} let teenager = @@ -282,7 +259,7 @@ module Patient = |> setAge [ 12 |> Years] |> setWeight (40m |> Kilogram |> Some) |> setHeight (150 |> Centimeter |> Some) - |> setDepartment "ICK" + |> setDepartment (Some "ICK") let adult = @@ -290,6 +267,6 @@ module Patient = |> setAge [ 18 |> Years] |> setWeight (70m |> Kilogram |> Some) |> setHeight (180 |> Centimeter |> Some) - |> setDepartment "ICK" + |> setDepartment (Some "ICK") diff --git a/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx b/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx index 797f5b4..d180eca 100644 --- a/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx +++ b/src/Informedica.GenOrder.Lib/Scripts/Api2.fsx @@ -19,7 +19,7 @@ module DoseLimit = DoseRule.DoseLimit open Informedica.ZIndex.Lib // load demo or product cache -System.Environment.SetEnvironmentVariable(FilePath.GENPRES_PROD, "1") +System.Environment.SetEnvironmentVariable(FilePath.GENPRES_PROD, "0") @@ -139,14 +139,45 @@ startLogger () stopLogger () +Informedica.GenForm.Lib.DoseRule.get () +|> DoseRule.filter +// Filter.filter + { Filter.filter with Patient = Patient.premature } +|> Array.filter (fun dr -> + dr.Generic = "amoxicilline/clavulaanzuur" && + dr.Route = "iv" +) +|> Array.take 1 +|> Array.length + + +Informedica.GenForm.Lib.DoseRule.get () +|> Array.item 2 +|> fun dr -> + dr.PatientCategory + |> PatientCategory.filter { Filter.filter with Patient = Patient.child } + + +{ Department = Some ("ICK") + Diagnoses = [||] + Gender = AnyGender + Age = Some (ValueUnit ([|0N|], Units.Time.day)) + Weight = Some (ValueUnit ([|757N/200N|], Weight (WeightKiloGram 1N))) + Height = Some (ValueUnit ([|1059N/20N|], Height (HeightCentiMeter 1N))) + GestAge = None + PMAge = None + VenousAccess = [] +} +|> Api.scenarioResult +|> Api.filter Patient.child //|> fun p -> { p with VenousAccess = CVL; AgeInDays = Some 0N } |> PrescriptionRule.get //|> Array.filter (fun pr -> pr.DoseRule.Products |> Array.isEmpty |> not) |> Array.filter (fun pr -> - pr.DoseRule.Route = "iv" && - pr.DoseRule.Indication |> String.startsWith "behandeling pneumocystis" + pr.DoseRule.Route = "rect" && + pr.DoseRule.Generic = "paracetamol" ) |> Array.item 0 //|> Api.evaluate (OrderLogger.logger.Logger) |> fun pr -> pr |> Api.createDrugOrder (pr.SolutionRules[0] |> Some) //|> printfn "%A" diff --git a/src/Informedica.GenOrder.Lib/Types.fs b/src/Informedica.GenOrder.Lib/Types.fs index a4ee834..9b748bf 100644 --- a/src/Informedica.GenOrder.Lib/Types.fs +++ b/src/Informedica.GenOrder.Lib/Types.fs @@ -115,17 +115,17 @@ module Types = /// Models an `Item` in a `Component` type Item = { - // The name of the item + /// The name of the item Name: Name - // The quantity of an `Item` in a `Component` + /// The quantity of an `Item` in a `Component` ComponentQuantity: Quantity - // The quantity of an `Item` in an `Orderable` + /// The quantity of an `Item` in an `Orderable` OrderableQuantity: Quantity - // The `Item` concentration in a `Component` + /// The `Item` concentration in a `Component` ComponentConcentration: Concentration - // The `Item` concentration in an `Orderable` + /// The `Item` concentration in an `Orderable` OrderableConcentration: Concentration - // The `Item` `Dose` of `Item` administered + /// The `Item` `Dose` of `Item` administered Dose: Dose } @@ -242,79 +242,61 @@ module Types = /// to an Orderable and a Prescription. type DrugOrder = { - // Identifies the specific drug order + /// Identifies the specific drug order Id: string - // The name of the order + /// The name of the order Name : string - // The list of drug products that can be used for the order + /// The list of drug products that can be used for the order Products : ProductComponent list - // The quantities of the drug order - Quantities : BigRational list - // The unit the `DrugOrder` is measured in, - // i.e. of the `Quantities` - Unit : string - // The route by which the order is applied + /// The quantities of the drug order + Quantities : ValueUnit option + /// The route by which the order is applied Route : string - // The type of order + /// The type of order OrderType : OrderType - // The list of possible frequency values - Frequencies : BigRational list - // The time unit to be used when using a frequency - FreqUnit : string - // The list of possible rate values - Rates : BigRational list - // The time unit to be used when using a rate - RateUnit : string - // The min and/or max time for the infusion time + /// The unit to adjust the dose with + AdjustUnit : Unit option + /// The list of possible frequency values + Frequencies : ValueUnit option + /// The list of possible rate values + Rates : ValueUnit option + /// The min and/or max time for the infusion time Time : MinMax - // The time unit for infusion time (duration) - TimeUnit : string - // The dose limits for an DrugOrder + /// The dose limits for an DrugOrder Dose : DoseLimit option - // The amount of orderable that will be given each time - DoseCount : BigRational option - // The adjust quantity for the adjusted dose calculations - Adjust : BigRational option - // The adjust unit - AdjustUnit : string + /// The amount of orderable that will be given each time + DoseCount : ValueUnit option + /// The adjust quantity for the adjusted dose calculations + Adjust : ValueUnit option } /// The product components that are used by the drug order. /// A product component maps to a Component in an Orderable. and ProductComponent = { - // The name of the product + /// The name of the product Name : string - // The shape of the product + /// The shape of the product Shape : string - // The quantities of the product - // Note: measured in the same unit as - // the `DrugOrder` unit - Quantities : BigRational list - // The "divisibility" of the products + /// The quantities of the product + /// Note: measured in the same unit as + /// the `DrugOrder` unit + Quantities : ValueUnit option + /// The "divisibility" of the products Divisible : BigRational option - // The time unit used for frequency - TimeUnit : string - // The time unit used for rate - RateUnit : string - // The list of substances contained in the product + /// The list of substances contained in the product Substances: SubstanceItem list } /// A substance in a product. A substance maps to an Item in a Component. and SubstanceItem = { - // The name of the substance + /// The name of the substance Name : string - // The possible concentrations of the substance - // in the products - Concentrations : BigRational list - // The unit by which the substance is - // measured. - Unit : string - // The time unit used for the frequency - TimeUnit : string - // The dose limits for a substance + /// The possible concentrations of the substance + /// in the products + Concentrations : ValueUnit option + /// The dose limits for a substance Dose : DoseLimit option - // The solution limits for a solution + /// The solution limits for a solution Solution : SolutionLimit option } @@ -330,27 +312,27 @@ module Types = /// type Scenario = { - // the id of the scenario + /// the id of the scenario No : int - // the indication for the the order + /// the indication for the the order Indication : string - // the dose type of the order + /// the dose type of the order DoseType : string - // the name of the order + /// the name of the order Name : string - // the shape of the order + /// the shape of the order Shape : string - // the route of the order + /// the route of the order Route : string - // the prescription of the order + /// the prescription of the order Prescription : string - // the preparation of the order + /// the preparation of the order Preparation : string - // the administration of the order + /// the administration of the order Administration : string - // the order itself + /// the order itself Order : Order option - // Whether or not to us adjust + /// Whether or not to us adjust UseAdjust : bool } @@ -362,25 +344,25 @@ module Types = /// type ScenarioResult = { - // the list of indications to select from + /// the list of indications to select from Indications: string [] - // the list of generics to select from + /// the list of generics to select from Generics: string [] - // the list of routes to select from + /// the list of routes to select from Routes: string [] - // the list of shapes to select from + /// the list of shapes to select from Shapes: string [] - // the selected indication + /// the selected indication Indication: string option - // the selected generic + /// the selected generic Generic: string option - // the selected route + /// the selected route Route: string option - // the selected shape + /// the selected shape Shape: string option - // the patient + /// the patient Patient: Patient - // the list of scenarios + /// the list of scenarios Scenarios: Scenario [] } diff --git a/src/Informedica.GenOrder.Lib/ValueUnit.fs b/src/Informedica.GenOrder.Lib/ValueUnit.fs index cb67bf0..13af51f 100644 --- a/src/Informedica.GenOrder.Lib/ValueUnit.fs +++ b/src/Informedica.GenOrder.Lib/ValueUnit.fs @@ -55,6 +55,18 @@ module ValueUnit = | _ -> vu + let collect (vus : ValueUnit[]) = + match vus |> Array.tryHead with + | None -> None + | Some vu -> + let u = vu |> getUnit + vus + |> Array.collect (toBase >> getValue) + |> withUnit u + |> toUnit + |> Some + + module Units = diff --git a/src/Informedica.GenUnits.Lib/ValueUnit.fs b/src/Informedica.GenUnits.Lib/ValueUnit.fs index f6ff756..d1e243b 100644 --- a/src/Informedica.GenUnits.Lib/ValueUnit.fs +++ b/src/Informedica.GenUnits.Lib/ValueUnit.fs @@ -1482,8 +1482,11 @@ module Units = |> List.reduce (fun u1 u2 -> u1 |> Units.per u2) |> Some else + printfn $"cannot parse {s}" None - | _ -> None + | _ -> + printfn $"cannot parse {s}" + None /// Turn a unit u to a string with diff --git a/src/Server/Formulary.fs b/src/Server/Formulary.fs index e79b6a1..f176be6 100644 --- a/src/Server/Formulary.fs +++ b/src/Server/Formulary.fs @@ -3,6 +3,7 @@ module Formulary open Informedica.Utils.Lib open Informedica.Utils.Lib.BCL +open Informedica.GenUnits.Lib open Informedica.GenForm.Lib open Informedica.GenOrder.Lib @@ -18,16 +19,22 @@ let mapFormularyToFilter (form: Formulary)= Generic = form.Generic Indication = form.Indication Route = form.Route - AgeInDays = - form.Age - |> Option.bind BigRational.fromFloat - WeightInGram = - form.Weight - |> Option.map ((*) 1000.) - |> Option.bind BigRational.fromFloat - GestAgeInDays = - form.GestAge - |> Option.map BigRational.fromInt + Patient = + { Patient.patient with + Age = + form.Age + |> Option.bind BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit Units.Time.day) + Weight = + form.Weight + |> Option.bind BigRational.fromFloat + |> Option.map (ValueUnit.singleWithUnit Units.Weight.kiloGram) + GestAge = + form.GestAge + |> Option.map BigRational.fromInt + |> Option.map (ValueUnit.singleWithUnit Units.Time.day) + + } } |> Filter.calcPMAge diff --git a/src/Server/ScenarioResult.fs b/src/Server/ScenarioResult.fs index 48811d0..f86b325 100644 --- a/src/Server/ScenarioResult.fs +++ b/src/Server/ScenarioResult.fs @@ -3,6 +3,7 @@ module ScenarioResult open Informedica.Utils.Lib open Informedica.Utils.Lib.BCL +open Informedica.GenUnits.Lib open Informedica.GenForm.Lib open Informedica.GenOrder.Lib @@ -273,25 +274,30 @@ Scenarios: {sc.Scenarios |> Array.length} Department = sc.Department |> Option.defaultValue "ICK" - AgeInDays = + |> Some + Age = sc.AgeInDays |> Option.bind BigRational.fromFloat - GestAgeInDays = + |> Option.map (ValueUnit.singleWithUnit Units.Time.day) + GestAge = sc.GestAgeInDays |> Option.map BigRational.fromInt - WeightInGram = + |> Option.map (ValueUnit.singleWithUnit Units.Time.day) + Weight = sc.WeightInKg - |> Option.map (fun w -> w * 1000.) |> Option.bind BigRational.fromFloat - HeightInCm = + |> Option.map (ValueUnit.singleWithUnit Units.Weight.kiloGram) + Height = sc.HeightInCm |> Option.bind BigRational.fromFloat - VenousAccess = if sc.CVL then VenousAccess.CVL else VenousAccess.AnyAccess + |> Option.map (ValueUnit.singleWithUnit Units.Height.centiMeter) + VenousAccess = if sc.CVL then [VenousAccess.CVL] else [] } |> Patient.calcPMAge try let newSc = + printfn $"{pat}" let r = Api.scenarioResult pat { Api.scenarioResult pat with Indications = diff --git a/tests/Informedica.GenForm.Tests/Tests.fs b/tests/Informedica.GenForm.Tests/Tests.fs index 75d4796..30f4603 100644 --- a/tests/Informedica.GenForm.Tests/Tests.fs +++ b/tests/Informedica.GenForm.Tests/Tests.fs @@ -9,6 +9,8 @@ module Tests = open Expecto open Expecto.Flip + open Informedica.GenCore.Lib.Ranges + open Informedica.GenUnits.Lib open Informedica.GenForm.Lib @@ -22,11 +24,11 @@ module Tests = Department = None Diagnoses = [||] Gender = AnyGender - Age = MinMax.none - Weight = MinMax.none - BSA = MinMax.none - GestAge = MinMax.none - PMAge = MinMax.none + Age = MinMax.empty + Weight = MinMax.empty + BSA = MinMax.empty + GestAge = MinMax.empty + PMAge = MinMax.empty Location = AnyAccess } @@ -44,63 +46,178 @@ module Tests = test "a filter with female gender and patient category with female gender" { { patCat with Gender = Female } - |> PatientCategory.filter { filter with Gender = Female } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Gender = Female + } + } |> Expect.isTrue "should return true" } test "a filter with female gender and patient category with no gender" { { patCat with Gender = AnyGender } - |> PatientCategory.filter { filter with Gender = Female } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Gender = Female + } + } |> Expect.isTrue "should return true" } test "an empty filter and a patient category with a max age of 7" { - { patCat with Age = { patCat.Age with Maximum = Some 7N } } + { patCat with + Age = + { patCat.Age with + Max = + 7N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + } + } |> PatientCategory.filter filter |> Expect.isFalse "should return false" } test "a filter with age 5 and a patient category with a max age of 7" { - { patCat with Age = { patCat.Age with Maximum = Some 7N } } - |> PatientCategory.filter { filter with AgeInDays = Some 5N } + { patCat with + Age = + { patCat.Age with + Max = + 7N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + } + } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + } + } |> Expect.isTrue "should return true" } - test "a filter with age 5 and a patient category with a min age of 7" { - { patCat with Age = { patCat.Age with Minimum = Some 7N } } - |> PatientCategory.filter { filter with AgeInDays = Some 5N } + test "a filter with age 5 and a patient category with a min age of 1 week" { + { patCat with + Age = + { patCat.Age with + Min = + 1N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + } + } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + } + } |> Expect.isFalse "should return false" } test "a filter with age 5 and a patient category with a min age of 3 and max age of 7" { - { patCat with Age = { patCat.Age with Minimum = Some 3N; Maximum = Some 7N } } - |> PatientCategory.filter { filter with AgeInDays = Some 5N } + { patCat with + Age = + { patCat.Age with + Min = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 7N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + } + } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + } + } |> Expect.isTrue "should return true" } test "a filter with age 5 with a patient category with a min age of 3 and max age of 7 and gender female" { { patCat with + Gender = Female Age = { patCat.Age with - Minimum = Some 3N - Maximum = Some 7N + Min = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 7N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } - Gender = Female } - |> PatientCategory.filter { filter with AgeInDays = Some 5N } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + } + } |> Expect.isFalse "should return false" } test "a filter with age 5, gender female with a patient category with a min age of 3 and max age of 7 and gender female" { { patCat with + Gender = Female Age = { patCat.Age with - Minimum = Some 3N - Maximum = Some 7N + Min = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 7N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } - Gender = Female } - |> PatientCategory.filter { filter with AgeInDays = Some 5N; Gender = Female } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Gender = Female + Age = + 5N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + } + } |> Expect.isTrue "should return true" } @@ -108,16 +225,45 @@ module Tests = { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } GestAge = { patCat.GestAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter { filter with AgeInDays = Some 0N; GestAgeInDays = Some (30N * 7N) } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 30N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + } |> Expect.isTrue "should return true" } @@ -125,16 +271,45 @@ module Tests = { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } GestAge = { patCat.GestAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter { filter with AgeInDays = Some 0N; GestAgeInDays = Some (37N * 7N) } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 33N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + } |> Expect.isFalse "should return false" } @@ -142,124 +317,278 @@ module Tests = { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } GestAge = { patCat.GestAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter { filter with AgeInDays = Some 8N; GestAgeInDays = Some (27N * 7N) } + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 8N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 27N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + |> Patient.calcPMAge + } |> Expect.isFalse "should return false" } test "a filter with age 0 and gestational age 30 weeks with a patient category with a min age of 0 and max age of 28 and pm age min 28 and max 32 weeks" { - let filter = - { filter with AgeInDays = Some 0N; GestAgeInDays = Some (30N * 7N) } - |> Filter.calcPMAge - { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } PMAge = { patCat.PMAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter filter + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 30N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + |> Patient.calcPMAge + } + |> Expect.isTrue "should return true" } test "a filter with age 0 and gestational age 37 weeks with a patient category with a min age of 0 and max age of 28 and pm age min 28 and max 32 weeks" { - let filter = - { filter with AgeInDays = Some 0N; GestAgeInDays = Some (37N * 7N) } - |> Filter.calcPMAge - { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } PMAge = { patCat.PMAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter filter + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 37N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + |> Patient.calcPMAge + } + |> Expect.isFalse "should return false" } test "a filter with age 8 and gestational age 27 weeks with a patient category with a min age of 0 and max age of 28 and pm age min 28 and max 32 weeks" { - let filter = - { filter with AgeInDays = Some 8N; GestAgeInDays = Some (27N * 7N) } - |> Filter.calcPMAge - { patCat with Age = { patCat.Age with - Minimum = Some 0N - Maximum = Some 28N + Min = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 28N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } PMAge = { patCat.PMAge with - Minimum = Some (28N * 7N) - Maximum = Some (32N * 7N) + Min = + 28N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some + Max = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter filter + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 8N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + GestAge = + 27N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + |> Patient.calcPMAge + } |> Expect.isTrue "should return true" } test "a filter with age 0, ga = 32 and weight 1.45 with a patient category with max age = 30 and max gest 37 and max weight 1.5" { - let filter = - { filter with - AgeInDays = Some 0N - GestAgeInDays = Some (32N * 7N) - WeightInGram = Some 1450N - } - { patCat with Age = { patCat.Age with - Maximum = Some 30N - } - GestAge = - { patCat.GestAge with - Maximum = Some (37N * 7N) + Max = + 30N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } Weight = { patCat.Weight with - Maximum = Some 1500N + Max = + 1.5m + |> BigRational.FromDecimal + |> ValueUnit.singleWithUnit Units.Weight.kiloGram + |> Limit.Inclusive + |> Some + } + GestAge = + { patCat.GestAge with + Max = + 37N + |> ValueUnit.singleWithUnit Units.Time.week + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter filter + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + Weight = + 1.45m + |> BigRational.FromDecimal + |> ValueUnit.singleWithUnit Units.Weight.kiloGram + |> Some + GestAge = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + } + |> Expect.isTrue "should return true" } test "a filter with age 0, ga = 32 and weight 1.45 with a patient category with min age = 30 and max age = 720" { - let filter = - { filter with - AgeInDays = Some 0N - GestAgeInDays = Some (32N * 7N) - WeightInGram = Some 1450N - } - { patCat with Age = { patCat.Age with - Minimum = Some 30N - Maximum = Some 720N + Min = + 30N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some + Max = + 720N + |> ValueUnit.singleWithUnit Units.Time.day + |> Limit.Inclusive + |> Some } } - |> PatientCategory.filter filter + |> PatientCategory.filter + { filter with + Patient = + { filter.Patient with + Age = + 0N + |> ValueUnit.singleWithUnit Units.Time.day + |> Some + Weight = + (145N/10N) + |> ValueUnit.singleWithUnit Units.Weight.kiloGram + |> Some + GestAge = + 32N + |> ValueUnit.singleWithUnit Units.Time.week + |> Some + } + } + |> Expect.isFalse "should return false" } ]