diff --git a/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 b/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..b4a98f4 Binary files /dev/null and b/.vs/IsncsciAlgorithm/DesignTimeBuild/.dtbcache.v2 differ diff --git a/IsncsciAlgorithm.sln b/IsncsciAlgorithm.sln index 3512b11..d481236 100644 --- a/IsncsciAlgorithm.sln +++ b/IsncsciAlgorithm.sln @@ -1,25 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30110.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rhi.Isncsci", "Rhi.Isncsci\Rhi.Isncsci.csproj", "{CFD110C1-CE77-40FD-932B-3EF70F8F56EE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rhi.Isncsci.Tests", "Rhi.Isncsci.Tests\Rhi.Isncsci.Tests.csproj", "{A354D889-1BE4-4439-9FD9-E4DB9E470B7D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rhi.Isncsci.Core", "Rhi.Isncsci.Core\Rhi.Isncsci.Core.csproj", "{98729432-48E7-424C-9A0D-3487A4F16B90}" +EndProject Global - GlobalSection(TeamFoundationVersionControl) = preSolution - SccNumberOfProjects = 3 - SccEnterpriseProvider = {4CA58AB2-18FA-4F8D-95D4-32DDF27D184C} - SccTeamFoundationServer = https://eddiemachete.visualstudio.com/defaultcollection - SccLocalPath0 = . - SccProjectUniqueName1 = Rhi.Isncsci\\Rhi.Isncsci.csproj - SccProjectName1 = Rhi.Isncsci - SccLocalPath1 = Rhi.Isncsci - SccProjectUniqueName2 = Rhi.Isncsci.Tests\\Rhi.Isncsci.Tests.csproj - SccProjectName2 = Rhi.Isncsci.Tests - SccLocalPath2 = Rhi.Isncsci.Tests - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -33,8 +23,15 @@ Global {A354D889-1BE4-4439-9FD9-E4DB9E470B7D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A354D889-1BE4-4439-9FD9-E4DB9E470B7D}.Release|Any CPU.ActiveCfg = Release|Any CPU {A354D889-1BE4-4439-9FD9-E4DB9E470B7D}.Release|Any CPU.Build.0 = Release|Any CPU + {98729432-48E7-424C-9A0D-3487A4F16B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98729432-48E7-424C-9A0D-3487A4F16B90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98729432-48E7-424C-9A0D-3487A4F16B90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98729432-48E7-424C-9A0D-3487A4F16B90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {80A3FAA5-1BCF-4618-9B21-FFE1B06C7D7D} + EndGlobalSection EndGlobal diff --git a/Rhi.Isncsci.Core/Algorithm.cs b/Rhi.Isncsci.Core/Algorithm.cs new file mode 100644 index 0000000..7205db7 --- /dev/null +++ b/Rhi.Isncsci.Core/Algorithm.cs @@ -0,0 +1,762 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Rhi.Isncsci.Core +{ + public sealed class Algorithm + { + private static readonly Regex NtRegex = new Regex(@"\bNT\b", RegexOptions.IgnoreCase); + private const string NotDeterminable = "UTD"; + private const string NotApplicable = "NA"; + private const string Intact = "INT"; + + /// + /// 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.Core/BinaryObservation.cs b/Rhi.Isncsci.Core/BinaryObservation.cs new file mode 100644 index 0000000..775b615 --- /dev/null +++ b/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.Core/NeurologyForm.cs b/Rhi.Isncsci.Core/NeurologyForm.cs new file mode 100644 index 0000000..15af54e --- /dev/null +++ b/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.Core/NeurologyFormLevel.cs b/Rhi.Isncsci.Core/NeurologyFormLevel.cs new file mode 100644 index 0000000..f93b2c0 --- /dev/null +++ b/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.Core/NeurologyFormTotals.cs b/Rhi.Isncsci.Core/NeurologyFormTotals.cs new file mode 100644 index 0000000..12c4564 --- /dev/null +++ b/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.Core/NeurologyFormTotalsSummary.cs b/Rhi.Isncsci.Core/NeurologyFormTotalsSummary.cs new file mode 100644 index 0000000..a5a7814 --- /dev/null +++ b/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.Core/Rhi.Isncsci.Core.csproj b/Rhi.Isncsci.Core/Rhi.Isncsci.Core.csproj new file mode 100644 index 0000000..d126cee --- /dev/null +++ b/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.Core/Rhi.Isncsci.Core.xml b/Rhi.Isncsci.Core/Rhi.Isncsci.Core.xml new file mode 100644 index 0000000..b3d645f --- /dev/null +++ b/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 + + + +