Skip to content

Commit

Permalink
feat(calc): constant murley
Browse files Browse the repository at this point in the history
  • Loading branch information
nckhell committed Feb 8, 2025
1 parent c96ecf6 commit 17fc2bc
Show file tree
Hide file tree
Showing 18 changed files with 749 additions and 988 deletions.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import { ZodError } from 'zod'
import {
best_response,
median_response,
random_response,
worst_response,
} from './__testdata__/cms_test_responses'
import { constant_murley_score } from './constant_murley_score'
import { Score } from '../../../classes'
import { ScoreLibrary } from '../../library'

const BEST_SCORES = {
PAIN_SUBSCALE: 15,
ADL_SUBSCALE: 20,
MOBILITY_SUBSCALE: 40,
STRENGTH_SUBSCALE: 25,
TOTAL: 100,
}

const MEDIAN_SCORES = {
PAIN_SUBSCALE: 7,
ADL_SUBSCALE: 10,
MOBILITY_SUBSCALE: 20,
STRENGTH_SUBSCALE: 13,
TOTAL: 50,
}

const WORST_SCORES = {
PAIN_SUBSCALE: 0,
ADL_SUBSCALE: 0,
MOBILITY_SUBSCALE: 0,
STRENGTH_SUBSCALE: 0,
TOTAL: 0,
}

const cms_calculation = new Score(constant_murley_score)

describe('constant_murley_score_orthotoolkit', function () {
it('constant_murley_score_orthotoolkit calculation function should be available as a calculation', function () {
expect(ScoreLibrary).toHaveProperty('constant_murley_score_orthotoolkit')
})

describe('specific_steps_cms_calc', function () {
describe('the score includes the correct input fields', function () {
it('should have all the expected input ids configured', function () {
const EXPECTED_INPUT_IDS = [
'Q01_PAIN_SCORE',
'Q02_SLEEP_SCORE',
'Q03_WORK_ADL_SCORE',
'Q04_SPORTS_HOBBY_SCORE',
'Q05_ADL_FUNCTIONING_SCORE',
'Q06_FLEXION_ROM',
'Q07_ABDUCTION_ROM',
'Q08_ENDOROTATION_ROM',
'Q09_EXOROTATION_ROM',
'Q10_ATTEMPT_1',
'Q11_ATTEMPT_2',
'Q12_ATTEMPT_3',
]

const configured_input_ids = Object.keys(cms_calculation.inputSchema)
expect(EXPECTED_INPUT_IDS).toEqual(configured_input_ids)
})
})

describe('each calculated score includes the correct output result and correct score title', function () {
const outcome = cms_calculation.calculate({ payload: worst_response })

it('should return 5 calculations results', function () {
expect(Object.keys(outcome).length).toEqual(5)
})

it('should have all the correct calculation ids', function () {
const EXPECTED_CALCULATION_IDS = [
'PAIN',
'ADL',
'MOBILITY',
'STRENGTH',
'TS',
]

const extracted_calculation_ids_from_outcome = Object.keys(outcome)

expect(EXPECTED_CALCULATION_IDS).toEqual(
extracted_calculation_ids_from_outcome,
)
})
})

describe('a score is only calculated when all mandatory fields are entered', function () {
describe('when an empty response is passed', function () {
const outcome = cms_calculation.calculate({
payload: {},
opts: {
returnMissingOnZodError: true,
},
})

it('should return null for the "Pain" subscale', function () {
expect(outcome.PAIN).toEqual(null)
})

it('should return null for the "ADL" subscale', function () {
expect(outcome.ADL).toEqual(null)
})

it('should return null for the "Mobility" subscale', function () {
expect(outcome.MOBILITY).toEqual(null)
})

it('should return null for the "Strength" subscale', function () {
expect(outcome.STRENGTH).toEqual(null)
})

it('should return null for the total score', function () {
expect(outcome.TS).toEqual(null)
})
})
})

describe('each calculated score includes the correct formula and outputs the correct result', function () {
describe('when worst response is passed', function () {
const outcome = cms_calculation.calculate({ payload: worst_response })

it('should return the worst score for the "Pain" subscale', function () {
expect(outcome.PAIN).toEqual(WORST_SCORES.PAIN_SUBSCALE)
})

it('should return the worst score for the "ADL" subscale', function () {
expect(outcome.ADL).toEqual(WORST_SCORES.ADL_SUBSCALE)
})

it('should return the worst score for the "Mobility" subscale', function () {
expect(outcome.MOBILITY).toEqual(WORST_SCORES.MOBILITY_SUBSCALE)
})

it('should return the worst score for the "Strength" subscale', function () {
expect(outcome.STRENGTH).toEqual(WORST_SCORES.STRENGTH_SUBSCALE)
})

it('should return the worst total score', function () {
expect(outcome.TS).toEqual(WORST_SCORES.TOTAL)
})
})

describe('when a median response is passed', function () {
const outcome = cms_calculation.calculate({
payload: median_response,
})

it('should return the median score for the "Pain" subscale', function () {
expect(outcome.PAIN).toEqual(MEDIAN_SCORES.PAIN_SUBSCALE)
})

it('should return the median score for the "ADL" subscale', function () {
expect(outcome.ADL).toEqual(MEDIAN_SCORES.ADL_SUBSCALE)
})

it('should return the median score for the "Mobility" subscale', function () {
expect(outcome.MOBILITY).toEqual(MEDIAN_SCORES.MOBILITY_SUBSCALE)
})

it('should return the median score for the "Strength" subscale', function () {
expect(outcome.STRENGTH).toEqual(MEDIAN_SCORES.STRENGTH_SUBSCALE)
})

it('should return the median total score', function () {
expect(outcome.TS).toEqual(MEDIAN_SCORES.TOTAL)
})
})

describe('when best response is passed', function () {
const outcome = cms_calculation.calculate({ payload: best_response })

it('should return the best score for the "Pain" subscale', function () {
expect(outcome.PAIN).toEqual(BEST_SCORES.PAIN_SUBSCALE)
})

it('should return the best score for the "ADL" subscale', function () {
expect(outcome.ADL).toEqual(BEST_SCORES.ADL_SUBSCALE)
})

it('should return the best score for the "Mobility" subscale', function () {
expect(outcome.MOBILITY).toEqual(BEST_SCORES.MOBILITY_SUBSCALE)
})

it('should return the best score for the "Strength" subscale', function () {
expect(outcome.STRENGTH).toEqual(BEST_SCORES.STRENGTH_SUBSCALE)
})

it('should return the best total score', function () {
expect(outcome.TS).toEqual(BEST_SCORES.TOTAL)
})
})

describe('when a random response is passed', function () {
const outcome = cms_calculation.calculate({ payload: random_response })

it('should return the expected score for the "Pain" subscale', function () {
expect(outcome.PAIN).toEqual(11)
})

it('should return the expected score for the "ADL" subscale', function () {
expect(outcome.ADL).toEqual(8)
})

it('should return the expected score for the "Mobility" subscale', function () {
expect(outcome.MOBILITY).toEqual(12)
})

it('should return the expected score for the "Strength" subscale', function () {
expect(outcome.STRENGTH).toEqual(25)
})

it('should return the expected total score', function () {
expect(outcome.TS).toEqual(56)
})
})
})

describe('values entered by the user are checked to verify they are inside specified ranges', function () {
describe('when an answer is not a number', function () {
it('should throw an error', function () {
expect(() =>
cms_calculation.calculate({
payload: {
Q01_PAIN_SCORE: "I'm not a number",
},
}),
).toThrow(ZodError)
})
})
describe('when an answer is not allowed (e.g. is below the expected range)', function () {
it('should throw an error', function () {
expect(() =>
cms_calculation.calculate({
payload: {
Q01_PAIN_SCORE: -1,
},
}),
).toThrow(ZodError)
})
})
describe('when an answer is not allowed (e.g. is above the expected range)', function () {
it('should return throw an error', function () {
expect(() =>
cms_calculation.calculate({
payload: {
Q01_PAIN_SCORE: 16,
},
}),
).toThrow(ZodError)
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ScoreType } from '../../../types'
import { CMS_INPUTS, CMS_OUTPUT } from './definition'
import { sum, max, min } from 'lodash'

export const constant_murley_score: ScoreType<
typeof CMS_INPUTS,
typeof CMS_OUTPUT
> = {
name: 'Constant Murley Score (CMS) - OrthoToolKit version',
readmeLocation: __dirname,
inputSchema: CMS_INPUTS,
outputSchema: CMS_OUTPUT,
calculate: ({ data }) => {
/**
* Mobility
*/
const POINTS_PER_EXOROTATION_CRITERIUM = 2
const exorotation_input = data.Q09_EXOROTATION_ROM
const exorotation_score =
exorotation_input.length * POINTS_PER_EXOROTATION_CRITERIUM

const other_mobility_inputs = [
data.Q06_FLEXION_ROM,
data.Q07_ABDUCTION_ROM,
data.Q08_ENDOROTATION_ROM,
]

const mobility_score = sum(other_mobility_inputs) + exorotation_score

/** Strength */
const MAX_SCALE_SCORE = 25
const CONVERT_KG_TO_LBS = 2.2

const strength_inputs = [
data.Q10_ATTEMPT_1,
data.Q11_ATTEMPT_2,
data.Q12_ATTEMPT_3,
]

const max_strength_attempt = max(strength_inputs)
const strength_score = min([
Math.round((max_strength_attempt as number) * CONVERT_KG_TO_LBS),
MAX_SCALE_SCORE,
])

/** ADL */
const adl_score = sum([
data.Q03_WORK_ADL_SCORE,
data.Q04_SPORTS_HOBBY_SCORE,
data.Q02_SLEEP_SCORE,
data.Q05_ADL_FUNCTIONING_SCORE,
])

/** Pain */
const pain_score = data.Q01_PAIN_SCORE

return {
PAIN: pain_score,
ADL: adl_score,
MOBILITY: mobility_score,
STRENGTH: strength_score ?? null,
TS: pain_score + adl_score + mobility_score + (strength_score ?? 0),
}
},
}
Loading

0 comments on commit 17fc2bc

Please sign in to comment.