From 71fb2e62e8b78b2452c313558b05853c323be5e3 Mon Sep 17 00:00:00 2001 From: marx Date: Wed, 23 Feb 2022 11:21:59 +0800 Subject: [PATCH 1/2] add .net core version --- .../DesignTimeBuild/.dtbcache.v2 | Bin 0 -> 46982 bytes IsncsciAlgorithm.sln | 25 +- .../Rhi.Isncsci.Core/Algorithm.cs | 762 ++++++++++++++++++ .../Rhi.Isncsci.Core/BinaryObservation.cs | 13 + .../Rhi.Isncsci.Core/NeurologyForm.cs | 233 ++++++ .../Rhi.Isncsci.Core/NeurologyFormLevel.cs | 72 ++ .../Rhi.Isncsci.Core/NeurologyFormTotals.cs | 317 ++++++++ .../NeurologyFormTotalsSummary.cs | 31 + .../Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj | 11 + .../Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml | 120 +++ 10 files changed, 1570 insertions(+), 14 deletions(-) create mode 100644 .vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Algorithm.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/BinaryObservation.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyForm.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormLevel.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotals.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotalsSummary.cs create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj create mode 100644 Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml diff --git a/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 b/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000000000000000000000000000000000000..cb59f8347266ff1c245adce0dc0c7f0ec90be970 GIT binary patch literal 46982 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 + /// Produces a summarized version of a NeurologyFormTotals object which can be used to be displayed in an interface or be stored in a database. + /// The values in the summary return will have ranges instead of lists of values. + /// + /// Neurology form that has been populated with the values to be used in the algorithm calculations. + /// + /// Summarized version of the totals which presents the results as ranges, rather than lists containing every possible value for every field. + /// + public static NeurologyFormTotalsSummary GetTotalsSummaryFor(NeurologyForm neurologyForm) + { + return GetTotalsSummaryFor(GetTotalsFor(neurologyForm)); + } + + /// + /// Produces a summarized version of a NeurologyFormTotals object which can be used to be displayed in an interface or be stored in a database. + /// The values in the summary return will have ranges instead of lists of values. + /// + /// The form totals object to be used to generate the summary. + /// + /// Summarized version of the totals where the enumerations get replaced by ranges. + /// + public static NeurologyFormTotalsSummary GetTotalsSummaryFor(NeurologyFormTotals totals) + { + var ais = totals.GetAsiaImpairmentScaleValues(); + var isAsiaA = ais.Contains("A"); + var couldBeOtherThanAsiaA = !isAsiaA || ais.Length > 1; + + var summary = new NeurologyFormTotalsSummary + { + AsiaImpairmentScale = ais, + Completeness = isAsiaA ? (couldBeOtherThanAsiaA ? "C,I" : "C") : "I", + LeftLowerMotorTotal = GetSummaryStringFor(totals.LeftLowerMotorTotal, totals.LeftLowerMotorTotalHasImpairmentNotDueToSci, totals.LeftLowerMotorContainsNt), + LeftMotor = GetLevelsRange(totals.GetLeftMotorValues(), false), + LeftMotorZpp = isAsiaA ? GetLevelsRange(totals.GetLeftMotorZppValues(), couldBeOtherThanAsiaA) : NotApplicable, + LeftMotorTotal = GetSummaryStringFor(totals.LeftUpperMotorTotal + totals.LeftLowerMotorTotal, + totals.LeftUpperMotorTotalHasImpairmentNotDueToSci || totals.LeftLowerMotorTotalHasImpairmentNotDueToSci, + totals.LeftUpperMotorContainsNt || totals.LeftLowerMotorContainsNt), + LeftPrickTotal = GetSummaryStringFor(totals.LeftPrickTotal, totals.LeftPrickTotalHasImpairmentNotDueToSci, totals.LeftPrickContainsNt), + LeftSensory = GetLevelsRange(totals.GetLeftSensoryValues(), false), + LeftSensoryZpp = isAsiaA ? GetLevelsRange(totals.GetLeftSensoryZppValues(), couldBeOtherThanAsiaA) : NotApplicable, + LeftTouchTotal = GetSummaryStringFor(totals.LeftTouchTotal, totals.LeftTouchTotalHasImpairmentNotDueToSci, totals.LeftTouchContainsNt), + LeftUpperMotorTotal = GetSummaryStringFor(totals.LeftUpperMotorTotal, totals.LeftUpperMotorTotalHasImpairmentNotDueToSci, totals.LeftUpperMotorContainsNt), + LowerMotorTotal = GetSummaryStringFor(totals.LowerMotorTotal, + totals.RightLowerMotorTotalHasImpairmentNotDueToSci || totals.LeftLowerMotorTotalHasImpairmentNotDueToSci, + totals.RightLowerMotorContainsNt || totals.LeftLowerMotorContainsNt), + NeurologicalLevelOfInjury = GetLevelsRange(totals.GetNeurologicalLevelsOfInjury(), false), + PrickTotal = GetSummaryStringFor(totals.RightPrickTotal + totals.LeftPrickTotal, + totals.RightPrickTotalHasImpairmentNotDueToSci || totals.LeftPrickTotalHasImpairmentNotDueToSci, + totals.RightPrickContainsNt || totals.LeftPrickContainsNt), + RightLowerMotorTotal = GetSummaryStringFor(totals.RightLowerMotorTotal, totals.RightLowerMotorTotalHasImpairmentNotDueToSci, totals.RightLowerMotorContainsNt), + RightMotor = GetLevelsRange(totals.GetRightMotorValues(), false), + RightMotorZpp = isAsiaA ? GetLevelsRange(totals.GetRightMotorZppValues(), couldBeOtherThanAsiaA) : NotApplicable, + RightMotorTotal = GetSummaryStringFor(totals.RightUpperMotorTotal + totals.RightLowerMotorTotal, + totals.RightUpperMotorTotalHasImpairmentNotDueToSci || totals.RightLowerMotorTotalHasImpairmentNotDueToSci, + totals.RightUpperMotorContainsNt || totals.RightLowerMotorContainsNt), + RightPrickTotal = GetSummaryStringFor(totals.RightPrickTotal, totals.RightPrickTotalHasImpairmentNotDueToSci, totals.RightPrickContainsNt), + RightSensory = GetLevelsRange(totals.GetRightSensoryValues(), false), + RightSensoryZpp = isAsiaA ? GetLevelsRange(totals.GetRightSensoryZppValues(), couldBeOtherThanAsiaA) : NotApplicable, + RightTouchTotal = GetSummaryStringFor(totals.RightTouchTotal, totals.RightTouchTotalHasImpairmentNotDueToSci, totals.RightTouchContainsNt), + RightUpperMotorTotal = GetSummaryStringFor(totals.RightUpperMotorTotal, totals.RightUpperMotorTotalHasImpairmentNotDueToSci, totals.RightUpperMotorContainsNt), + TouchTotal = GetSummaryStringFor(totals.RightTouchTotal + totals.LeftTouchTotal, + totals.RightTouchTotalHasImpairmentNotDueToSci || totals.LeftTouchTotalHasImpairmentNotDueToSci, + totals.RightTouchContainsNt || totals.LeftTouchContainsNt), + UpperMotorTotal = GetSummaryStringFor(totals.UpperMotorTotal, + totals.RightUpperMotorTotalHasImpairmentNotDueToSci || totals.LeftUpperMotorTotalHasImpairmentNotDueToSci, + totals.RightUpperMotorContainsNt || totals.LeftUpperMotorContainsNt) + }; + + return summary; + } + + /// + /// Returns the results produced by the ISNCSCI Algorithm ir a raw values format. + /// + /// Neurology form that has been populated with the values to be used in the algorithm calculations. + /// + /// Totals in raw values format. The results contain lists with every prossible value for each field. + /// You can use the resulting object to obtained a summarized version, which uses ranges, by passing the result to the method GetTotalsSummaryFor + /// + public static NeurologyFormTotals GetTotalsFor(NeurologyForm neurologyForm) + { + var totals = new NeurologyFormTotals(); + + UpdateTotalsWithLevelAt(neurologyForm, totals, neurologyForm.GetLevelWithName("C2"), false, false); + + totals.UpperMotorTotal = totals.RightUpperMotorTotal + totals.LeftUpperMotorTotal; + totals.LowerMotorTotal = totals.RightLowerMotorTotal + totals.LeftLowerMotorTotal; + totals.TouchTotal = totals.RightTouchTotal + totals.LeftTouchTotal; + totals.PrickTotal = totals.RightPrickTotal + totals.LeftPrickTotal; + + var s4_5 = neurologyForm.GetLevelWithName("S4_5"); + + var c1 = neurologyForm.GetLevelWithName("C2").Previous; + + if (totals.RightSensoryZppHasOnlySoftValues) + totals.AddRightSensoryZppValue(c1); + + if (totals.LeftSensoryZppHasOnlySoftValues) + totals.AddLeftSensoryZppValue(c1); + + if (totals.RightMotorZppHasOnlySoftValues) + totals.AddRightMotorZppValue(c1); + + if (totals.LeftMotorZppHasOnlySoftValues) + totals.AddLeftMotorZppValue(c1); + + if (totals.MostRostralRightLevelWithMotorFunction == null) + totals.MostRostralRightLevelWithMotorFunction = c1; + + if (totals.MostRostralLeftLevelWithMotorFunction == null) + totals.MostRostralLeftLevelWithMotorFunction = c1; + + if (totals.MostCaudalRightLevelWithMotorFunction == null) + totals.MostCaudalRightLevelWithMotorFunction = c1; + + if (totals.MostCaudalLeftLevelWithMotorFunction == null) + totals.MostCaudalLeftLevelWithMotorFunction = c1; + + // [ASIA learning center 2012-11-14] Sensory incomplete: Sacral sparing of sensory function + var isSensoryIncomplete = neurologyForm.AnalSensation == BinaryObservation.Yes || neurologyForm.AnalSensation == BinaryObservation.NT + || !"0".Equals(s4_5.RightTouchName) || !"0".Equals(s4_5.LeftTouchName) + || !"0".Equals(s4_5.RightPrickName) || !"0".Equals(s4_5.LeftPrickName); + + var couldNotHaveMotorFunctionMoreThan3LevelsBelowMotorLevel = CouldNotHaveMotorFunctionMoreThan3LevelsBelowMotorLevel(neurologyForm, totals); + + var couldNotBeMotorIncomplete = (neurologyForm.AnalContraction == BinaryObservation.No || neurologyForm.AnalContraction == BinaryObservation.NT) + && isSensoryIncomplete && couldNotHaveMotorFunctionMoreThan3LevelsBelowMotorLevel; + + if ((neurologyForm.AnalContraction == BinaryObservation.No || neurologyForm.AnalContraction == BinaryObservation.NT) + && (neurologyForm.AnalSensation == BinaryObservation.No || neurologyForm.AnalSensation == BinaryObservation.NT) + && s4_5.RightTouchValue == 0 && !s4_5.RightTouchImpairmentNotDueToSci + && s4_5.RightPrickValue == 0 && !s4_5.RightPrickImpairmentNotDueToSci + && s4_5.LeftTouchValue == 0 && !s4_5.LeftTouchImpairmentNotDueToSci + && s4_5.LeftPrickValue == 0 && !s4_5.LeftPrickImpairmentNotDueToSci) + totals.AddAsiaImpairmentScaleValue("A"); + + if (couldNotBeMotorIncomplete && totals.MostRostralNeurologicalLevelOfInjury.Name != "S4_5") + totals.AddAsiaImpairmentScaleValue("B"); + + + //// Not an ASIA E only + //// AND not an ASIA A only + if (totals.MostRostralNeurologicalLevelOfInjury.Name != "S4_5" + && (isSensoryIncomplete || neurologyForm.AnalContraction == BinaryObservation.Yes || neurologyForm.AnalContraction == BinaryObservation.NT)) + { + CouldBeAsiaCorD(neurologyForm, totals, out bool couldBeAsiaC, out bool couldBeAsiaD); + + if (couldBeAsiaC) + totals.AddAsiaImpairmentScaleValue("C"); + + if (couldBeAsiaD) + totals.AddAsiaImpairmentScaleValue("D"); + } + + if (totals.RightSensoryContains("S4_5") && totals.LeftSensoryContains("S4_5") + && totals.RightMotorContains("S4_5") && totals.LeftMotorContains("S4_5")) + totals.AddAsiaImpairmentScaleValue("E"); + + return totals; + } + + /// + /// Formats a total value depending on the specified flags. + /// + /// Raw total value. + /// Flag indicating if any value used in the calculation of this total presents impairment not due to a spinal cord injury. + /// Flag indicating if any value used in the calculation of this total is set to Not Testable. + /// + /// The value, followed by an exclamation mark if the hasImpairmentNotDueToSci is set to true + /// or + /// UTD (Unable to determine) if the containsNt flag is set to true. + /// + private static string GetSummaryStringFor(int total, bool hasImpairmentNotDueToSci, bool containsNt) + { + if (containsNt) + return NotDeterminable; + + return string.Format("{0}{1}", total, hasImpairmentNotDueToSci ? "!" : string.Empty); + } + + /// + /// Returns a range string based on the specified list. + /// + /// Neurological levels returned in a calculation. + /// Used in the Zone of Partial Preservation to indicate that one of the possible Asia Impairment Scale values is "A" + /// A representation of the list as a range string. E.g. NA,C4-C6,T1,T2 + private static string GetLevelsRange(List levels, bool addNa) + { + if (levels.Count == 0) + return addNa ? NotApplicable : string.Empty; + + levels.Sort(CompareNeurologyFormTotalsLevelByOrdinal); + NeurologyFormLevel previous = null; + var text = string.Empty; + var isRange = false; + + foreach (var level in levels) + { + if (previous == null || previous.Ordinal < level.Ordinal) + { + var name = level.Ordinal == 28 ? Intact : level.Name; + + if (previous == null) + { + text = name; + } + else if (level.Ordinal == previous.Ordinal + 1) + { + isRange = true; + } + else + { + text += (isRange ? "-" + previous.Name + "," : ",") + name; + isRange = false; + } + + previous = level; + } + } + + if (isRange) + text += "-" + (previous.Ordinal == 28 ? Intact : previous.Name); + + return addNa ? string.IsNullOrEmpty(text) ? NotApplicable : string.Format("{0},{1}", NotApplicable, text) : text; + } + + /// + /// Compares two neurology levels based on property "Ordinal". + /// + /// Neurology form level A + /// Neurology form level B + /// + /// Returns + /// – 1 if a is more caudal (the ordinal of a is greater than the one of b) + /// – 0 if both ordinal values are the same + /// – -1 if b is more caudal (the ordinal of b is greater than the one of a). + /// + private static int CompareNeurologyFormTotalsLevelByOrdinal(NeurologyFormLevel a, + NeurologyFormLevel b) + { + // Both are null so they are equal + // OR + // a is null and b is not so b is greater + if (a == null) + return b == null ? 0 : -1; + + // b is null and a is not so a is greater + if (b == null) + return 1; + + return a.Ordinal > b.Ordinal ? 1 : a.Ordinal == b.Ordinal ? 0 : -1; + } + + /// + /// Recursive method which iterates through the values in a nuerology form while it updates the totals generating the results produced by the algorithm. + /// + /// Form being evaluated. + /// Brand new totals object where the results are to be stored. + /// Current neurology level being evaluated + /// Flag used to evaluate the Kathy Collins condition on the right motor results. + /// Flag used to evaluate the Kathy Collins condition on the left motor results. + private static void UpdateTotalsWithLevelAt(NeurologyForm neurologyForm, NeurologyFormTotals totals, NeurologyFormLevel level, bool nextNonKeyMuscleShouldBeRightMotor, bool nextNonKeyMuscleShouldBeLeftMotor) + { + totals.RightTouchTotal += level.RightTouchValue; + totals.LeftTouchTotal += level.LeftTouchValue; + totals.RightPrickTotal += level.RightPrickValue; + totals.LeftPrickTotal += level.LeftPrickValue; + + if (level.IsKeyMuscle) + { + if (level.IsLowerMuscle) + { + if (level.RightMotorImpairmentNotDueToSci && !totals.RightLowerMotorTotalHasImpairmentNotDueToSci) + totals.RightLowerMotorTotalHasImpairmentNotDueToSci = true; + + if (level.LeftMotorImpairmentNotDueToSci && !totals.LeftLowerMotorTotalHasImpairmentNotDueToSci) + totals.LeftLowerMotorTotalHasImpairmentNotDueToSci = true; + + if (NtRegex.IsMatch(level.RightMotorName) && !level.RightMotorImpairmentNotDueToSci) + totals.RightLowerMotorContainsNt = true; + + if (NtRegex.IsMatch(level.LeftMotorName) && !level.LeftMotorImpairmentNotDueToSci) + totals.LeftLowerMotorContainsNt = true; + } + else + { + if (level.RightMotorImpairmentNotDueToSci && !totals.RightUpperMotorTotalHasImpairmentNotDueToSci) + totals.RightUpperMotorTotalHasImpairmentNotDueToSci = true; + + if (level.LeftMotorImpairmentNotDueToSci && !totals.LeftUpperMotorTotalHasImpairmentNotDueToSci) + totals.LeftUpperMotorTotalHasImpairmentNotDueToSci = true; + + if (NtRegex.IsMatch(level.RightMotorName) && !level.RightMotorImpairmentNotDueToSci) + totals.RightUpperMotorContainsNt = true; + + if (NtRegex.IsMatch(level.LeftMotorName) && !level.LeftMotorImpairmentNotDueToSci) + totals.LeftUpperMotorContainsNt = true; + } + } + else + { + if (nextNonKeyMuscleShouldBeRightMotor) + { + nextNonKeyMuscleShouldBeRightMotor = false; + totals.AddRightMotorValue(level.Previous); + + if (!totals.RightSensoryHasOnlySoftValues) + totals.RightMotorHasOnlySoftValues = false; + } + + if (nextNonKeyMuscleShouldBeLeftMotor) + { + nextNonKeyMuscleShouldBeLeftMotor = false; + totals.AddLeftMotorValue(level.Previous); + + if (!totals.LeftSensoryHasOnlySoftValues) + totals.LeftMotorHasOnlySoftValues = false; + } + } + + if (level.RightTouchImpairmentNotDueToSci && !totals.RightTouchTotalHasImpairmentNotDueToSci) + totals.RightTouchTotalHasImpairmentNotDueToSci = true; + + if (level.LeftTouchImpairmentNotDueToSci && !totals.LeftTouchTotalHasImpairmentNotDueToSci) + totals.LeftTouchTotalHasImpairmentNotDueToSci = true; + + if (level.RightPrickImpairmentNotDueToSci && !totals.RightPrickTotalHasImpairmentNotDueToSci) + totals.RightPrickTotalHasImpairmentNotDueToSci = true; + + if (level.LeftPrickImpairmentNotDueToSci && !totals.LeftPrickTotalHasImpairmentNotDueToSci) + totals.LeftPrickTotalHasImpairmentNotDueToSci = true; + + // Check if a column contains any NT value so we can properly format the total presented to the user + if (NtRegex.IsMatch(level.RightTouchName) && !level.RightTouchImpairmentNotDueToSci && !totals.RightTouchContainsNt) + totals.RightTouchContainsNt = true; + + if (NtRegex.IsMatch(level.LeftTouchName) && !level.LeftTouchImpairmentNotDueToSci && !totals.LeftTouchContainsNt) + totals.LeftTouchContainsNt = true; + + if (NtRegex.IsMatch(level.RightPrickName) && !level.RightPrickImpairmentNotDueToSci && !totals.RightPrickContainsNt) + totals.RightPrickContainsNt = true; + + if (NtRegex.IsMatch(level.LeftPrickName) && !level.LeftPrickImpairmentNotDueToSci && !totals.LeftPrickContainsNt) + totals.LeftPrickContainsNt = true; + + + + + if (totals.RightSensoryHasOnlySoftValues && + ((level.RightTouchValue != 2 && !level.RightTouchImpairmentNotDueToSci) || + (level.RightPrickValue != 2 && !level.RightPrickImpairmentNotDueToSci))) + { + totals.AddRightSensoryValue(level.Previous); + + if ("S4_5".Equals(level.Name) + && (level.RightTouchValue == 2 || NtRegex.IsMatch(level.RightTouchName)) + && (level.RightPrickValue == 2 || NtRegex.IsMatch(level.RightPrickName))) + { + totals.AddRightSensoryValue(level); + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level); + } + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level.Previous); + + if ((level.RightTouchValue != 2 && !NtRegex.IsMatch(level.RightTouchName)) + || (level.RightPrickValue != 2 && !NtRegex.IsMatch(level.RightPrickName))) + { + totals.RightSensoryHasOnlySoftValues = false; + totals.NeurologicalLevelOfInjuryHasOnlySoftValues = false; + } + + if (level.IsKeyMuscle) + { + nextNonKeyMuscleShouldBeRightMotor = true; + totals.HasRightCollins = true; + } + } + + if (totals.LeftSensoryHasOnlySoftValues && + ((level.LeftTouchValue != 2 && !level.LeftTouchImpairmentNotDueToSci) || + (level.LeftPrickValue != 2 && !level.LeftPrickImpairmentNotDueToSci))) + { + totals.AddLeftSensoryValue(level.Previous); + + if ("S4_5".Equals(level.Name) + && (level.LeftTouchValue == 2 || NtRegex.IsMatch(level.LeftTouchName)) + && (level.LeftPrickValue == 2 || NtRegex.IsMatch(level.LeftPrickName))) + { + totals.AddLeftSensoryValue(level); + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level); + } + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level.Previous); + + if ((level.LeftTouchValue != 2 && !NtRegex.IsMatch(level.LeftTouchName)) + || (level.LeftPrickValue != 2 && !NtRegex.IsMatch(level.LeftPrickName))) + { + totals.LeftSensoryHasOnlySoftValues = false; + totals.NeurologicalLevelOfInjuryHasOnlySoftValues = false; + } + + if (level.IsKeyMuscle) + { + nextNonKeyMuscleShouldBeLeftMotor = true; + totals.HasLeftCollins = true; + } + } + + if (totals.RightMotorHasOnlySoftValues && level.RightMotorValue != 5 && !level.RightMotorImpairmentNotDueToSci) + { + if (level.IsKeyMuscle && (level.RightMotorValue >= 3 || NtRegex.IsMatch(level.RightMotorName))) + { + totals.AddRightMotorValue(level); + + // Check if left won't make the NLI have to be the previous level. + // Let the logic for left motor handle the SNL instead + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues + && (level.LeftMotorValue > 2 || level.LeftMotorImpairmentNotDueToSci || NtRegex.IsMatch(level.LeftMotorName))) + { + totals.AddNeurologicalLevelOfInjury(level); + + if (!NtRegex.IsMatch(level.RightMotorName)) + totals.NeurologicalLevelOfInjuryHasOnlySoftValues = false; + } + } + + if (level.RightMotorValue < 3 || NtRegex.IsMatch(level.RightMotorName)) + { + totals.AddRightMotorValue(level.Previous); + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + { + totals.AddNeurologicalLevelOfInjury(level.Previous); + + if (!NtRegex.IsMatch(level.RightMotorName)) + totals.NeurologicalLevelOfInjuryHasOnlySoftValues = false; + } + } + + if (!NtRegex.IsMatch(level.RightMotorName)) + totals.RightMotorHasOnlySoftValues = false; + } + + if (totals.LeftMotorHasOnlySoftValues && level.LeftMotorValue != 5 && !level.LeftMotorImpairmentNotDueToSci) + { + if (level.IsKeyMuscle && (level.LeftMotorValue >= 3 || NtRegex.IsMatch(level.LeftMotorName))) + { + totals.AddLeftMotorValue(level); + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level); + } + + if (level.LeftMotorValue < 3 || NtRegex.IsMatch(level.LeftMotorName)) + { + totals.AddLeftMotorValue(level.Previous); + + if (totals.NeurologicalLevelOfInjuryHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level.Previous); + } + + if (!NtRegex.IsMatch(level.LeftMotorName)) + { + totals.LeftMotorHasOnlySoftValues = false; + totals.NeurologicalLevelOfInjuryHasOnlySoftValues = false; + } + } + + /* -- RECURSIVE CALL --------------- */ + if (level.Next != null) + UpdateTotalsWithLevelAt(neurologyForm, totals, level.Next, + nextNonKeyMuscleShouldBeRightMotor && totals.RightMotorHasOnlySoftValues, + nextNonKeyMuscleShouldBeLeftMotor && totals.LeftMotorHasOnlySoftValues); + + #region This happens when there are INTACT values ------------------------------------------- + if ("S4_5".Equals(level.Name)) + { + if (totals.RightSensoryHasOnlySoftValues && totals.LeftSensoryHasOnlySoftValues + && totals.RightMotorHasOnlySoftValues && totals.LeftMotorHasOnlySoftValues) + totals.AddNeurologicalLevelOfInjury(level); + + if (totals.RightSensoryHasOnlySoftValues) + { + totals.AddRightSensoryValue(level); + totals.RightSensoryHasOnlySoftValues = false; + } + + if (totals.LeftSensoryHasOnlySoftValues) + { + totals.AddLeftSensoryValue(level); + totals.LeftSensoryHasOnlySoftValues = false; + } + + if (totals.RightMotorHasOnlySoftValues) + { + totals.AddRightMotorValue(level); + totals.RightMotorHasOnlySoftValues = false; + } + + if (totals.LeftMotorHasOnlySoftValues) + { + totals.AddLeftMotorValue(level); + totals.LeftMotorHasOnlySoftValues = false; + } + } + #endregion + + if (totals.RightSensoryZppHasOnlySoftValues && (!"0".Equals(level.RightTouchName) || !"0".Equals(level.RightPrickName))) + { + if ((level.RightTouchValue > 0 || level.RightTouchImpairmentNotDueToSci) + || (level.RightPrickValue > 0 || level.RightPrickImpairmentNotDueToSci)) + totals.RightSensoryZppHasOnlySoftValues = false; + + totals.AddRightSensoryZppValue(level); + } + + if (totals.LeftSensoryZppHasOnlySoftValues && (!"0".Equals(level.LeftTouchName) || !"0".Equals(level.LeftPrickName))) + { + if ((level.LeftTouchValue > 0 || level.LeftTouchImpairmentNotDueToSci) + || (level.LeftPrickValue > 0 || level.LeftPrickImpairmentNotDueToSci)) + totals.LeftSensoryZppHasOnlySoftValues = false; + + totals.AddLeftSensoryZppValue(level); + } + + if (totals.RightMotorZppHasOnlySoftValues + && (level.HasOtherRightMotorFunction + || (!"0".Equals(level.RightMotorName) && (level.IsKeyMuscle || totals.RightMotorContains(level.Name))))) + { + if ((level.RightMotorImpairmentNotDueToSci || level.HasOtherRightMotorFunction || !NtRegex.IsMatch(level.RightMotorName)) && + (level.IsKeyMuscle + || level.Ordinal < 4 + || (level.Ordinal > 25 && !totals.RightUpperMotorContainsNt && !totals.RightLowerMotorContainsNt && !totals.HasRightCollins) + || (level.Ordinal > 8 && level.Ordinal < 21 && !totals.RightUpperMotorContainsNt))) + totals.RightMotorZppHasOnlySoftValues = false; + + totals.AddRightMotorZppValue(level); + } + + if (totals.LeftMotorZppHasOnlySoftValues + && (level.HasOtherLeftMotorFunction + || (!"0".Equals(level.LeftMotorName) && (level.IsKeyMuscle || totals.LeftMotorContains(level.Name))))) + { + if ((level.LeftMotorImpairmentNotDueToSci || level.HasOtherLeftMotorFunction || !NtRegex.IsMatch(level.LeftMotorName)) && + (level.IsKeyMuscle + || level.Ordinal < 4 + || (level.Ordinal > 25 && !totals.LeftUpperMotorContainsNt && !totals.LeftLowerMotorContainsNt && !totals.HasLeftCollins) + || (level.Ordinal > 8 && level.Ordinal < 21 && !totals.LeftUpperMotorContainsNt))) + totals.LeftMotorZppHasOnlySoftValues = false; + + totals.AddLeftMotorZppValue(level); + } + + + // Update most Rostral levels with motor function + if ((level.IsKeyMuscle || level.HasOtherRightMotorFunction) + && totals.MostRostralRightLevelWithMotorFunction == null + && (level.RightMotorImpairmentNotDueToSci || level.HasOtherRightMotorFunction || (level.RightMotorValue != 0 && level.IsKeyMuscle))) + totals.MostRostralRightLevelWithMotorFunction = level; + + if ((level.IsKeyMuscle || level.HasOtherLeftMotorFunction) + && totals.MostRostralLeftLevelWithMotorFunction == null + && (level.LeftMotorImpairmentNotDueToSci || level.HasOtherLeftMotorFunction || (level.LeftMotorValue != 0 && level.IsKeyMuscle))) + totals.MostRostralLeftLevelWithMotorFunction = level; + + // Update most Caudal levels with motor function + if ((level.IsKeyMuscle || level.HasOtherRightMotorFunction) + && totals.MostCaudalRightLevelWithMotorFunction == null + && (!"0".Equals(level.RightMotorName) || level.HasOtherRightMotorFunction)) + totals.MostCaudalRightLevelWithMotorFunction = level; + + if ((level.IsKeyMuscle || level.HasOtherLeftMotorFunction) + && totals.MostCaudalLeftLevelWithMotorFunction == null + && (!"0".Equals(level.LeftMotorName) || level.HasOtherLeftMotorFunction)) + totals.MostCaudalLeftLevelWithMotorFunction = level; + + + if (!level.IsKeyMuscle) + return; + + if (level.IsLowerMuscle) + { + totals.RightLowerMotorTotal += level.RightMotorValue; + totals.LeftLowerMotorTotal += level.LeftMotorValue; + } + else + { + totals.RightUpperMotorTotal += level.RightMotorValue; + totals.LeftUpperMotorTotal += level.LeftMotorValue; + } + } + + /// + /// Evaluates the specified form and totals to determine if any of the different + /// return conditions could produce a case where there could be motor function more + /// than 3 levels below the injury level. + /// + /// Form that was used to produce the totals. + /// Totals retunred by the algorithm. + /// Flag indicating if any combination in the totals could have a case with motor function more than 3 levels below the injury level. + private static bool CouldNotHaveMotorFunctionMoreThan3LevelsBelowMotorLevel(NeurologyForm neurologyForm, NeurologyFormTotals totals) + { + NeurologyFormLevel mostRostralRightLevelWithMotor = null; + NeurologyFormLevel mostRostralLeftLevelWithMotor = null; + + var currentLevel = neurologyForm.GetLevelWithName("S1"); + + while ((mostRostralRightLevelWithMotor == null || mostRostralLeftLevelWithMotor == null) + && currentLevel != null // Could happen if SNL is C1 + && currentLevel.Ordinal >= totals.MostCaudalNeurologicalLevelOfInjury.Ordinal) + { + if (mostRostralRightLevelWithMotor == null && + (currentLevel.RightMotorImpairmentNotDueToSci || currentLevel.HasOtherRightMotorFunction || (currentLevel.RightMotorValue != 0 && currentLevel.IsKeyMuscle))) + mostRostralRightLevelWithMotor = currentLevel; + + if (mostRostralLeftLevelWithMotor == null && + (currentLevel.LeftMotorImpairmentNotDueToSci || currentLevel.HasOtherLeftMotorFunction || (currentLevel.LeftMotorValue != 0 && currentLevel.IsKeyMuscle))) + mostRostralLeftLevelWithMotor = currentLevel; + + currentLevel = currentLevel.Previous; + } + + return (mostRostralRightLevelWithMotor == null || mostRostralRightLevelWithMotor.Ordinal - totals.MostCaudalRightMotor.Ordinal <= 3) + && (mostRostralLeftLevelWithMotor == null || mostRostralLeftLevelWithMotor.Ordinal - totals.MostCaudalLeftMotor.Ordinal <= 3); + } + + /// + /// Evaluates the specified form and totals to determine if any of the different + /// return conditions could produce a case where the Asia Impairment Scale is C o D. + /// + /// Form that was used to produce the totals. + /// Totals retunred by the algorithm. + /// out variable use as flag indicating if this case is a possible ASIA C. + /// out variable use as flag indicating if this case is a possible ASIA D. + private static void CouldBeAsiaCorD(NeurologyForm neurologyForm, NeurologyFormTotals totals, out bool couldBeAsiaC, out bool couldBeAsiaD) + { + couldBeAsiaC = false; + couldBeAsiaD = false; + NeurologyFormLevel rightMotor = null; + NeurologyFormLevel leftMotor = null; + + // Check if the patient could be motor incoplete via sphincter contraction + // Otherwise we need to check for motor function more than 3 levels below motor level. + var couldHaveAnalContraction = neurologyForm.AnalContraction == BinaryObservation.Yes || neurologyForm.AnalContraction == BinaryObservation.NT; + //var couldNotHaveAnalContraction = neurologyForm.AnalContraction == BinaryObservation.No || neurologyForm.AnalContraction == BinaryObservation.NT; + + var nliEnum = totals.GetNeurologicalLevelsOfInjury().GetEnumerator(); + + while (nliEnum.MoveNext() && (!couldBeAsiaC || !couldBeAsiaD)) + { + //var otherMotorIsMoreThanThreeLevelsBelowNli = (neurologyForm.) + + // RIGHT MOTOR + // If not motor incomplete already, find the right motor level that correspond to this particular neurological level + if (!couldHaveAnalContraction && (rightMotor == null || rightMotor.Ordinal < nliEnum.Current.Ordinal)) + { + var rightMotorValues = totals.GetRightMotorValues().GetEnumerator(); + rightMotor = null; + + while (rightMotor == null && rightMotorValues.MoveNext()) + { + if (rightMotorValues.Current.Ordinal >= nliEnum.Current.Ordinal) + rightMotor = rightMotorValues.Current; + } + + rightMotorValues.Dispose(); + } + + // LEFT MOTOR + // If not motor incomplete already, find the left motor level that correspond to this particular neurological level + if (!couldHaveAnalContraction && (leftMotor == null || leftMotor.Ordinal < nliEnum.Current.Ordinal)) + { + var leftMotorValues = totals.GetLeftMotorValues().GetEnumerator(); + leftMotor = null; + + while (leftMotor == null && leftMotorValues.MoveNext()) + { + if (leftMotorValues.Current.Ordinal >= nliEnum.Current.Ordinal) + leftMotor = leftMotorValues.Current; + } + + leftMotorValues.Dispose(); + } + + //if (couldNotHaveAnalContraction + // && (rightMotor == null || totals.MostCaudalRightLevelWithMotorFunction.Ordinal - rightMotor.Ordinal <= 3) + // && (leftMotor == null || totals.MostCaudalLeftLevelWithMotorFunction.Ordinal - leftMotor.Ordinal <= 3)) + // couldBeAsiaB = true; + + // If the test is not motor incomplete at this level, do not continue to count motor levels, move to the next nli available. + if (!couldHaveAnalContraction + && totals.MostCaudalRightLevelWithMotorFunction.Ordinal - rightMotor.Ordinal <= 3 + && totals.MostCaudalLeftLevelWithMotorFunction.Ordinal - leftMotor.Ordinal <= 3) + continue; + + // When motor incomplete and the nli is S1 or more caudal, it is automatically D since there are no myotomes to count from. + // We can break the loop. + if (nliEnum.Current.Ordinal > 24) // Greater than L5 (24) + { + couldBeAsiaD = true; + break; + } + + // If motor incomplete, count the motor levels with muscle grade greater than two + var levelsWithMuscleGradeGreaterThanTwo = 0; + var levelsWithMuscleGradeLessThanThree = 0; + var eligibleLevelCount = 0; + var currentLevel = nliEnum.Current.Next; + + while (currentLevel != null) + { + if (currentLevel.IsKeyMuscle) + { + eligibleLevelCount += 2; + + if (currentLevel.RightMotorValue > 2 || currentLevel.RightMotorImpairmentNotDueToSci || NtRegex.IsMatch(currentLevel.RightMotorName)) + levelsWithMuscleGradeGreaterThanTwo++; + + if ((currentLevel.RightMotorValue < 3 || NtRegex.IsMatch(currentLevel.RightMotorName)) && !currentLevel.RightMotorImpairmentNotDueToSci) + levelsWithMuscleGradeLessThanThree++; + + if (currentLevel.LeftMotorValue > 2 || currentLevel.LeftMotorImpairmentNotDueToSci || NtRegex.IsMatch(currentLevel.LeftMotorName)) + levelsWithMuscleGradeGreaterThanTwo++; + + if ((currentLevel.LeftMotorValue < 3 || NtRegex.IsMatch(currentLevel.LeftMotorName)) && !currentLevel.LeftMotorImpairmentNotDueToSci) + levelsWithMuscleGradeLessThanThree++; + } + + currentLevel = currentLevel.Next; + } + + // If not more than half the myotomes contain values less to 3, this is an Asia C + if (levelsWithMuscleGradeLessThanThree > eligibleLevelCount / 2) + couldBeAsiaC = true; + + // If at least half the myotomes below the current NLI containing values greater or equal to 3, hooray! it is ASIA D. + if (levelsWithMuscleGradeGreaterThanTwo >= eligibleLevelCount / 2) + couldBeAsiaD = true; + } + + nliEnum.Dispose(); + } + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/BinaryObservation.cs b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/BinaryObservation.cs new file mode 100644 index 0000000..775b615 --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/BinaryObservation.cs @@ -0,0 +1,13 @@ +using System; + +namespace Rhi.Isncsci.Core +{ + [Flags] + public enum BinaryObservation + { + None = 0x0, + Yes = 0x1, + No = 0x2, + NT = 0x4 + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyForm.cs b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyForm.cs new file mode 100644 index 0000000..15af54e --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyForm.cs @@ -0,0 +1,233 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Rhi.Isncsci.Core +{ + public class NeurologyForm + { + private static readonly string[] LevelNames = new[] + { + "C2", "C3", "C4", "C5", "C6", "C7", "C8", "T1", "T2", "T3" + , "T4", "T5", "T6", "T7", "T8", "T9", "T10", "T11", "T12", + "L1", "L2", "L3", "L4", "L5", "S1", "S2", "S3", "S4_5" + }; + + private readonly static string[] KeyMuscles = new[] { "C5", "C6", "C7", "C8", "T1", "L2", "L3", "L4", "L5", "S1" }; + private readonly Dictionary _levels; + private static readonly Regex NtRegex = new Regex(@"\bNT\b", RegexOptions.IgnoreCase); + private static readonly Regex NonSciImpairmentRegex = new Regex(".+[!]"); + private static readonly Regex NonSciImpairmentFlagsRegex = new Regex("[\\*!]"); + + private const int NormalMotorValue = 5; + private const int NormalSensoryValue = 2; + + public NeurologyFormLevel C1 { get; protected set; } + + public BinaryObservation AnalContraction { get; set; } + public BinaryObservation AnalSensation { get; set; } + public NeurologyFormLevel RightLowestNonKeyMuscleWithMotorFunction { get; protected set; } + public NeurologyFormLevel LeftLowestNonKeyMuscleWithMotorFunction { get; protected set; } + + public NeurologyForm() + { + C1 = new NeurologyFormLevel + { + Ordinal = 0, + IsKeyMuscle = false, + IsLowerMuscle = false, + Name = "C1", + Previous = null, + RightMotorName = "5", + RightMotorValue = 5, + LeftMotorName = "5", + LeftMotorValue = 5, + RightTouchName = "2", + RightTouchValue = 2, + LeftTouchName = "2", + LeftTouchValue = 2, + RightPrickName = "2", + RightPrickValue = 2, + LeftPrickName = "2", + LeftPrickValue = 2 + }; + + _levels = new Dictionary(); + + var previousLevel = C1; + + for (var i = 0; i < LevelNames.Length; i++) + { + var name = LevelNames[i]; + + var currentLevel = new NeurologyFormLevel + { + IsKeyMuscle = KeyMuscles.Contains(name), + IsLowerMuscle = (i >= 20 && i <= 24), + Name = name, + Ordinal = i + 1, + Previous = previousLevel + }; + + previousLevel.Next = currentLevel; + previousLevel = currentLevel; + _levels.Add(name, currentLevel); + } + } + + public void SetRightLowestNonKeyMuscleWithMotorFunction(string levelName) + { + if (string.IsNullOrEmpty(levelName)) + return; + + var key = levelName.ToUpper(); + + if (!_levels.ContainsKey(key)) + return; + + if (RightLowestNonKeyMuscleWithMotorFunction != null) + RightLowestNonKeyMuscleWithMotorFunction.HasOtherRightMotorFunction = false; + + RightLowestNonKeyMuscleWithMotorFunction = _levels[key]; + RightLowestNonKeyMuscleWithMotorFunction.HasOtherRightMotorFunction = true; + } + + public void SetLeftLowestNonKeyMuscleWithMotorFunction(string levelName) + { + if (string.IsNullOrEmpty(levelName)) + return; + + var key = levelName.ToUpper(); + + if (!_levels.ContainsKey(key)) + return; + + if (LeftLowestNonKeyMuscleWithMotorFunction != null) + LeftLowestNonKeyMuscleWithMotorFunction.HasOtherLeftMotorFunction = false; + + LeftLowestNonKeyMuscleWithMotorFunction = _levels[key]; + LeftLowestNonKeyMuscleWithMotorFunction.HasOtherLeftMotorFunction = true; + } + + /// + /// + /// + /// + /// Neurology level at the specified index. + public NeurologyFormLevel GetLevelAt(int index) + { + return index >= 0 && index < _levels.Count ? _levels.ElementAt(index).Value : null; + } + + /// + /// Returns the neurology level based on a level name. E.g. C2,C3,C4...S1,S2,S3,S4_5 + /// + /// Lavel name being searched. + /// Neurology level that matches the specified level name. + public NeurologyFormLevel GetLevelWithName(string levelName) + { + var key = levelName.ToUpper(); + + return _levels.ContainsKey(key) ? _levels[key] : null; + } + + /// + /// Updates the values of the neurology level with the specified name. + /// You can pass strings containing values between 0-2 for touch and prick and 0-5 for motor. + /// You can also use the exclamation mark and asterisk at the end of the string to indicate impairment not due to a spinal cord injury. + /// Finally, you can also pass NT to indicate that a value was not testable. + /// + /// Name of the respective neurology level. + /// Right touch + /// Left touch + /// Right Prick + /// Left prick + /// Right motor + /// Left motor + /// + public NeurologyForm UpdateLevelAt(string levelName, string rightTouchName, string leftTouchName, + string rightPrickName, string leftPrickName, string rightMotorName, string leftMotorName) + { + var level = GetLevelWithName(levelName); + + if (level != null) + UpdateLevel(level, rightTouchName, leftTouchName, rightPrickName, leftPrickName, rightMotorName, leftMotorName); + + return this; + } + + private static int GetDermatomeValue(string text, int normalValue) + { + if (text.ToUpper() == "NT*") + return normalValue; + + int value; + int.TryParse(NonSciImpairmentFlagsRegex.Replace(text, string.Empty), out value); + + return value; + } + + private static void UpdateLevel(NeurologyFormLevel level, string rightTouchName, string leftTouchName, + string rightPrickName, string leftPrickName, string rightMotorName, string leftMotorName) + { + // Right Touch + level.RightTouchName = rightTouchName; + level.RightTouchValue = GetDermatomeValue(rightTouchName, NormalSensoryValue); + level.RightTouchImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(rightTouchName); + + // Left Touch + level.LeftTouchName = leftTouchName; + level.LeftTouchValue = GetDermatomeValue(leftTouchName, NormalSensoryValue); + level.LeftTouchImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(leftTouchName); + + // Right Prick + level.RightPrickName = rightPrickName; + level.RightPrickValue = GetDermatomeValue(rightPrickName, NormalSensoryValue); + level.RightPrickImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(rightPrickName); + + // Left Prick + level.LeftPrickName = leftPrickName; + level.LeftPrickValue = GetDermatomeValue(leftPrickName, NormalSensoryValue); + level.LeftPrickImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(leftPrickName); + + // Right Motor + level.RightMotorName = rightMotorName; + level.RightMotorValue = GetDermatomeValue(rightMotorName, NormalMotorValue); + level.RightMotorImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(rightMotorName); + + // Left Motor + level.LeftMotorName = leftMotorName; + level.LeftMotorValue = GetDermatomeValue(leftMotorName, NormalMotorValue); + level.LeftMotorImpairmentNotDueToSci = NonSciImpairmentRegex.IsMatch(leftMotorName); + + if (!level.IsKeyMuscle) + { + if ((level.RightTouchValue == 2 || level.RightTouchImpairmentNotDueToSci) + && (level.RightPrickValue == 2 || level.RightPrickImpairmentNotDueToSci)) + { + level.RightMotorName = "5"; + level.RightMotorValue = 5; + } + else + { + level.RightMotorName = (NtRegex.IsMatch(level.RightTouchName) || level.RightTouchValue == 2) + && (NtRegex.IsMatch(level.RightPrickName) || level.RightPrickValue == 2) ? "NT" : "0"; + level.RightMotorValue = 0; + } + + if ((level.LeftTouchValue == 2 || level.LeftTouchImpairmentNotDueToSci) + && (level.LeftPrickValue == 2 || level.LeftPrickImpairmentNotDueToSci)) + { + level.LeftMotorName = "5"; + level.LeftMotorValue = 5; + } + else + { + level.LeftMotorName = (NtRegex.IsMatch(level.LeftTouchName) || level.LeftTouchValue == 2) + && (NtRegex.IsMatch(level.LeftPrickName) || level.LeftPrickValue == 2) ? "NT" : "0"; + level.LeftMotorValue = 0; + } + } + } + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormLevel.cs b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormLevel.cs new file mode 100644 index 0000000..f93b2c0 --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormLevel.cs @@ -0,0 +1,72 @@ +namespace Rhi.Isncsci.Core +{ + public class NeurologyFormLevel + { + public virtual bool IsKeyMuscle { get; set; } + public virtual bool IsLowerMuscle { get; set; } + public virtual bool LeftTouchImpairmentNotDueToSci { get; set; } + public virtual string LeftTouchName { get; set; } + public virtual int LeftTouchValue { get; set; } + public virtual bool LeftPrickImpairmentNotDueToSci { get; set; } + public virtual string LeftPrickName { get; set; } + public virtual int LeftPrickValue { get; set; } + public virtual bool LeftMotorImpairmentNotDueToSci { get; set; } + public virtual bool HasOtherLeftMotorFunction { get; set; } + public virtual string LeftMotorName { get; set; } + public virtual int LeftMotorValue { get; set; } + public virtual string Name { get; set; } + public virtual NeurologyFormLevel Next { get; set; } + //public virtual NeurologyTest NeurologyTest { get; set; } + public virtual int Ordinal { get; set; } + public virtual NeurologyFormLevel Previous { get; set; } + public virtual bool RightTouchImpairmentNotDueToSci { get; set; } + public virtual string RightTouchName { get; set; } + public virtual int RightTouchValue { get; set; } + public virtual bool RightPrickImpairmentNotDueToSci { get; set; } + public virtual string RightPrickName { get; set; } + public virtual int RightPrickValue { get; set; } + public virtual bool RightMotorImpairmentNotDueToSci { get; set; } + public bool HasOtherRightMotorFunction { get; set; } + public virtual string RightMotorName { get; set; } + public virtual int RightMotorValue { get; set; } + + public virtual NeurologyFormLevel SetValues(int ordinal, bool isKeyMuscle, bool isLowerMuscle, + string rightTouchName, int rightTouchValue, bool rightTouchImpairmentNotDueToSci, + string leftTouchName, int leftTouchValue, bool leftTouchImpairmentNotDueToSci, + string rightPrickName, int rightPrickValue, bool rightPrickImpairmentNotDueToSci, + string leftPrickName, int leftPrickValue, bool leftPrickImpairmentNotDueToSci, + string rightMotorName, int rightMotorValue, bool rightMotorImpairmentNotDueToSci, + string leftMotorName, int leftMotorValue, bool leftMotorImpairmentNotDueToSci) + { + IsKeyMuscle = isKeyMuscle; + IsLowerMuscle = isLowerMuscle; + Ordinal = ordinal; + + RightTouchName = rightPrickName; + RightTouchValue = rightPrickValue; + RightTouchImpairmentNotDueToSci = rightTouchImpairmentNotDueToSci; + + LeftTouchName = leftTouchName; + LeftTouchValue = leftTouchValue; + LeftTouchImpairmentNotDueToSci = leftTouchImpairmentNotDueToSci; + + RightPrickName = rightPrickName; + RightPrickValue = rightPrickValue; + RightPrickImpairmentNotDueToSci = rightPrickImpairmentNotDueToSci; + + LeftPrickName = leftPrickName; + LeftPrickValue = leftPrickValue; + LeftPrickImpairmentNotDueToSci = leftPrickImpairmentNotDueToSci; + + RightMotorName = rightMotorName; + RightMotorValue = rightMotorValue; + RightMotorImpairmentNotDueToSci = rightMotorImpairmentNotDueToSci; + + LeftMotorName = leftMotorName; + LeftMotorValue = leftMotorValue; + LeftMotorImpairmentNotDueToSci = leftMotorImpairmentNotDueToSci; + + return this; + } + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotals.cs b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotals.cs new file mode 100644 index 0000000..12c4564 --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotals.cs @@ -0,0 +1,317 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Rhi.Isncsci.Core +{ + public class NeurologyFormTotals + { + public int RightUpperMotorTotal; + public bool RightUpperMotorTotalHasImpairmentNotDueToSci; + public bool RightUpperMotorContainsNt; + public int LeftUpperMotorTotal; + public bool LeftUpperMotorTotalHasImpairmentNotDueToSci; + public bool LeftUpperMotorContainsNt; + public int RightLowerMotorTotal; + public bool RightLowerMotorTotalHasImpairmentNotDueToSci; + public bool RightLowerMotorContainsNt; + public int LeftLowerMotorTotal; + public bool LeftLowerMotorTotalHasImpairmentNotDueToSci; + public bool LeftLowerMotorContainsNt; + public int RightTouchTotal; + public bool RightTouchTotalHasImpairmentNotDueToSci; + public bool RightTouchContainsNt; + public int LeftTouchTotal; + public bool LeftTouchTotalHasImpairmentNotDueToSci; + public bool LeftTouchContainsNt; + public int RightPrickTotal; + public bool RightPrickTotalHasImpairmentNotDueToSci; + public bool RightPrickContainsNt; + public int LeftPrickTotal; + public bool LeftPrickTotalHasImpairmentNotDueToSci; + public bool LeftPrickContainsNt; + + public int UpperMotorTotal; + public int LowerMotorTotal; + public int TouchTotal; + public int PrickTotal; + + #region Right Sensory + private readonly List _rightSensoryValues = new List(); + public bool RightSensoryHasOnlySoftValues = true; + + public bool RightSensoryIsEmpty() + { + return !_rightSensoryValues.Any(); + } + + public void AddRightSensoryValue(NeurologyFormLevel level) + { + if (_rightSensoryValues.Any(l => l.Name.ToUpper() == level.Name)) + return; + + _rightSensoryValues.Add(level); + } + + public List GetRightSensoryValues() + { + return new List(_rightSensoryValues); + } + + public bool RightSensoryContains(string levelName) + { + return _rightSensoryValues.Any(l => l.Name.ToUpper() == levelName.ToUpper()); + } + #endregion + + #region Left Sensory + private readonly List _leftSensoryValues = new List(); + public bool LeftSensoryHasOnlySoftValues = true; + + public bool LeftSensoryIsEmpty() + { + return !_leftSensoryValues.Any(); + } + + public void AddLeftSensoryValue(NeurologyFormLevel level) + { + if (_leftSensoryValues.Any(l => l.Name.ToUpper() == level.Name)) + return; + + _leftSensoryValues.Add(level); + } + + public List GetLeftSensoryValues() + { + return new List(_leftSensoryValues); + } + + public bool LeftSensoryContains(string levelName) + { + return _leftSensoryValues.Any(l => l.Name.ToUpper() == levelName.ToUpper()); + } + #endregion + + #region Right Motor + private readonly List _rightMotorValues = new List(); + public bool RightMotorHasOnlySoftValues = true; + public bool HasRightCollins = false; + public NeurologyFormLevel MostRostralRightMotor { get; protected set; } + public NeurologyFormLevel MostCaudalRightMotor { get; protected set; } + + public bool RightMotorIsEmpty() + { + return !_rightMotorValues.Any(); + } + + public void AddRightMotorValue(NeurologyFormLevel level) + { + if (_rightMotorValues.Any(l => l.Name.ToUpper() == level.Name)) + return; + + if (MostRostralRightMotor == null || level.Ordinal < MostRostralRightMotor.Ordinal) + MostRostralRightMotor = level; + + if (MostCaudalRightMotor == null || level.Ordinal > MostCaudalRightMotor.Ordinal) + MostCaudalRightMotor = level; + + _rightMotorValues.Add(level); + } + + public List GetRightMotorValues() + { + return new List(_rightMotorValues); + } + + public bool RightMotorContains(string levelName) + { + return _rightMotorValues.Any(l => l.Name.ToUpper() == levelName.ToUpper()); + } + #endregion + + #region Left Motor + private readonly List _leftMotorValues = new List(); + public bool LeftMotorHasOnlySoftValues = true; + public bool HasLeftCollins = false; + + public bool LeftMotorIsEmpty() + { + return !_leftMotorValues.Any(); + } + + public void AddLeftMotorValue(NeurologyFormLevel level) + { + if (_leftMotorValues.Any(l => l.Name.ToUpper() == level.Name)) + return; + + if (MostRostralLeftMotor == null || level.Ordinal < MostRostralLeftMotor.Ordinal) + MostRostralLeftMotor = level; + + if (MostCaudalLeftMotor == null || level.Ordinal > MostCaudalLeftMotor.Ordinal) + MostCaudalLeftMotor = level; + + _leftMotorValues.Add(level); + } + + public List GetLeftMotorValues() + { + return new List(_leftMotorValues); + } + + public bool LeftMotorContains(string levelName) + { + return _leftMotorValues.Any(l => l.Name.ToUpper() == levelName.ToUpper()); + } + #endregion + + #region Neurological Level of Injury + private readonly List _neurologicalLevelsOfInjury = new List(); + public bool NeurologicalLevelOfInjuryHasOnlySoftValues = true; + + public void AddNeurologicalLevelOfInjury(NeurologyFormLevel level) + { + if (_neurologicalLevelsOfInjury.Any(l => l.Name.ToLower() == level.Name.ToLower())) + return; + + if (MostRostralNeurologicalLevelOfInjury == null || level.Ordinal < MostRostralNeurologicalLevelOfInjury.Ordinal) + MostRostralNeurologicalLevelOfInjury = level; + + if (MostCaudalNeurologicalLevelOfInjury == null || level.Ordinal > MostCaudalNeurologicalLevelOfInjury.Ordinal) + MostCaudalNeurologicalLevelOfInjury = level; + + _neurologicalLevelsOfInjury.Add(level); + } + + public List GetNeurologicalLevelsOfInjury() + { + return new List(_neurologicalLevelsOfInjury); + } + #endregion + + #region Right Sensory ZPP + private readonly List _rightSensoryZppValues = new List(); + public bool RightSensoryZppHasOnlySoftValues = true; + + public bool RightSensoryZppIsEmpty() + { + return !_rightSensoryZppValues.Any(); + } + + public void AddRightSensoryZppValue(NeurologyFormLevel level) + { + if (_rightSensoryZppValues.Any(l => l.Name.ToUpper() == level.Name) + || "S4_5".Equals(level.Name)) + return; + + _rightSensoryZppValues.Add(level); + } + + public List GetRightSensoryZppValues() + { + return new List(_rightSensoryZppValues); + } + #endregion + + #region Left Sensory ZPP + private readonly List _leftSensoryZppValues = new List(); + public bool LeftSensoryZppHasOnlySoftValues = true; + public bool LeftSensoryZppIsEmpty() + { + return !_leftSensoryZppValues.Any(); + } + + public void AddLeftSensoryZppValue(NeurologyFormLevel level) + { + if (_leftSensoryZppValues.Any(l => l.Name.ToUpper() == level.Name) + || "S4_5".Equals(level.Name)) + return; + + _leftSensoryZppValues.Add(level); + } + + public List GetLeftSensoryZppValues() + { + return new List(_leftSensoryZppValues); + } + #endregion + + #region Right Motor ZPP + private readonly List _rightMotorZppValues = new List(); + public bool RightMotorZppHasOnlySoftValues = true; + + public bool RightMotorZppIsEmpty() + { + return !_rightMotorZppValues.Any(); + } + + public void AddRightMotorZppValue(NeurologyFormLevel level) + { + if (_rightMotorZppValues.Any(l => l.Name.ToUpper() == level.Name) + || "S4_5".Equals(level.Name)) + return; + + _rightMotorZppValues.Add(level); + } + + public List GetRightMotorZppValues() + { + return new List(_rightMotorZppValues); + } + #endregion + + #region Left Motor ZPP + private readonly List _leftMotorZppValues = new List(); + public bool LeftMotorZppHasOnlySoftValues = true; + + public bool LeftMotorZppIsEmpty() + { + return !_leftMotorZppValues.Any(); + } + + public void AddLeftMotorZppValue(NeurologyFormLevel level) + { + if (_leftMotorZppValues.Any(l => l.Name.ToUpper() == level.Name) + || "S4_5".Equals(level.Name)) + return; + + _leftMotorZppValues.Add(level); + } + + public List GetLeftMotorZppValues() + { + return new List(_leftMotorZppValues); + } + #endregion + + #region Asia Impairment Scale + //public string Complete; + //public bool IsSensoryIncomplete; + //public bool IsMotorIncomplete; + private readonly List _asiaImpairmentScaleValues = new List(); + + public void AddAsiaImpairmentScaleValue(string value) + { + if (string.IsNullOrEmpty(value) || _asiaImpairmentScaleValues.Contains(value.ToUpper())) + return; + + _asiaImpairmentScaleValues.Add(value.ToUpper()); + } + + public string GetAsiaImpairmentScaleValues() + { + return string.Join(",", _asiaImpairmentScaleValues.OrderBy(v => v)); + } + #endregion + + + public NeurologyFormLevel MostRostralLeftMotor { get; protected set; } + public NeurologyFormLevel MostCaudalLeftMotor { get; protected set; } + + public NeurologyFormLevel MostRostralNeurologicalLevelOfInjury { get; protected set; } + public NeurologyFormLevel MostCaudalNeurologicalLevelOfInjury { get; protected set; } + + public NeurologyFormLevel MostRostralRightLevelWithMotorFunction { get; set; } + public NeurologyFormLevel MostCaudalRightLevelWithMotorFunction { get; set; } + public NeurologyFormLevel MostRostralLeftLevelWithMotorFunction { get; set; } + public NeurologyFormLevel MostCaudalLeftLevelWithMotorFunction { get; set; } + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotalsSummary.cs b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotalsSummary.cs new file mode 100644 index 0000000..a5a7814 --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/NeurologyFormTotalsSummary.cs @@ -0,0 +1,31 @@ +namespace Rhi.Isncsci.Core +{ + public class NeurologyFormTotalsSummary + { + public string AsiaImpairmentScale; + public string Completeness; + public string LeftLowerMotorTotal; + public string LeftMotor; + public string LeftMotorZpp; + public string LeftMotorTotal; + public string LeftPrickTotal; + public string LeftSensory; + public string LeftSensoryZpp; + public string LeftTouchTotal; + public string LeftUpperMotorTotal; + public string LowerMotorTotal; + public string NeurologicalLevelOfInjury; + public string PrickTotal; + public string RightLowerMotorTotal; + public string RightMotor; + public string RightMotorZpp; + public string RightMotorTotal; + public string RightPrickTotal; + public string RightSensory; + public string RightSensoryZpp; + public string RightTouchTotal; + public string RightUpperMotorTotal; + public string TouchTotal; + public string UpperMotorTotal; + } +} diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj new file mode 100644 index 0000000..d126cee --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + E:\MarxGit\IsncsciAlgorithm\Rhi.Isncsci.Core\Rhi.Isncsci.Core.xml + + + diff --git a/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml new file mode 100644 index 0000000..b3d645f --- /dev/null +++ b/Rhi.Isncsci.Tests/Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml @@ -0,0 +1,120 @@ + + + + Rhi.Isncsci.Core + + + + + Produces a summarized version of a NeurologyFormTotals object which can be used to be displayed in an interface or be stored in a database. + The values in the summary return will have ranges instead of lists of values. + + Neurology form that has been populated with the values to be used in the algorithm calculations. + + Summarized version of the totals which presents the results as ranges, rather than lists containing every possible value for every field. + + + + + Produces a summarized version of a NeurologyFormTotals object which can be used to be displayed in an interface or be stored in a database. + The values in the summary return will have ranges instead of lists of values. + + The form totals object to be used to generate the summary. + + Summarized version of the totals where the enumerations get replaced by ranges. + + + + + Returns the results produced by the ISNCSCI Algorithm ir a raw values format. + + Neurology form that has been populated with the values to be used in the algorithm calculations. + + Totals in raw values format. The results contain lists with every prossible value for each field. + You can use the resulting object to obtained a summarized version, which uses ranges, by passing the result to the method GetTotalsSummaryFor + + + + + Formats a total value depending on the specified flags. + + Raw total value. + Flag indicating if any value used in the calculation of this total presents impairment not due to a spinal cord injury. + Flag indicating if any value used in the calculation of this total is set to Not Testable. + + The value, followed by an exclamation mark if the hasImpairmentNotDueToSci is set to true + or + UTD (Unable to determine) if the containsNt flag is set to true. + + + + + Returns a range string based on the specified list. + + Neurological levels returned in a calculation. + Used in the Zone of Partial Preservation to indicate that one of the possible Asia Impairment Scale values is "A" + A representation of the list as a range string. E.g. NA,C4-C6,T1,T2 + + + + + Recursive method which iterates through the values in a nuerology form while it updates the totals generating the results produced by the algorithm. + + Form being evaluated. + Brand new totals object where the results are to be stored. + Current neurology level being evaluated + Flag used to evaluate the Kathy Collins condition on the right motor results. + Flag used to evaluate the Kathy Collins condition on the left motor results. + + + + Evaluates the specified form and totals to determine if any of the different + return conditions could produce a case where there could be motor function more + than 3 levels below the injury level. + + Form that was used to produce the totals. + Totals retunred by the algorithm. + Flag indicating if any combination in the totals could have a case with motor function more than 3 levels below the injury level. + + + + Evaluates the specified form and totals to determine if any of the different + return conditions could produce a case where the Asia Impairment Scale is C o D. + + Form that was used to produce the totals. + Totals retunred by the algorithm. + out variable use as flag indicating if this case is a possible ASIA C. + out variable use as flag indicating if this case is a possible ASIA D. + + + + + + + Neurology level at the specified index. + + + + Returns the neurology level based on a level name. E.g. C2,C3,C4...S1,S2,S3,S4_5 + + Lavel name being searched. + Neurology level that matches the specified level name. + + + + Updates the values of the neurology level with the specified name. + You can pass strings containing values between 0-2 for touch and prick and 0-5 for motor. + You can also use the exclamation mark and asterisk at the end of the string to indicate impairment not due to a spinal cord injury. + Finally, you can also pass NT to indicate that a value was not testable. + + Name of the respective neurology level. + Right touch + Left touch + Right Prick + Left prick + Right motor + Left motor + + + + From 0c965d159b1ba5057d5746c8997f8bf0108cda86 Mon Sep 17 00:00:00 2001 From: marx Date: Wed, 23 Feb 2022 11:29:54 +0800 Subject: [PATCH 2/2] add .net core version --- .../DesignTimeBuild/.dtbcache.v2 | Bin 46982 -> 47105 bytes .../Algorithm.cs | 0 .../BinaryObservation.cs | 0 .../NeurologyForm.cs | 0 .../NeurologyFormLevel.cs | 0 .../NeurologyFormTotals.cs | 0 .../NeurologyFormTotalsSummary.cs | 0 .../Rhi.Isncsci.Core.csproj | 0 .../Rhi.Isncsci.Core.xml | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/Algorithm.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/BinaryObservation.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/NeurologyForm.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/NeurologyFormLevel.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/NeurologyFormTotals.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/NeurologyFormTotalsSummary.cs (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/Rhi.Isncsci.Core.csproj (100%) rename {Rhi.Isncsci.Tests/Rhi.Isncsci.Core => Rhi.Isncsci.Core}/Rhi.Isncsci.Core.xml (100%) diff --git a/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 b/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 index cb59f8347266ff1c245adce0dc0c7f0ec90be970..b4a98f43d54a6df4073007c20dbc896d2327e801 100644 GIT binary patch literal 47105 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 literal 46982 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