diff --git a/src/scores/library.ts b/src/scores/library.ts index 60bd7de..3378d53 100644 --- a/src/scores/library.ts +++ b/src/scores/library.ts @@ -112,11 +112,11 @@ import { sccai } from './sccai/sccai' import { sf12 } from './sf12/sf12' // import { spadi } from './spadi/spadi' import { simple_shoulder_test } from './sst/simple_shoulder_test' -// import { start_back_screening_tool } from './start_back_screening_tool/start_back_screening_tool' +import { start_back_screening_tool } from './start_back_screening_tool/start_back_screening_tool' import { tampa } from './tampa/tampa' -// import { visa_a, visa_g, visa_p } from './visa' +import { visa_a, visa_g, visa_p } from './visa' import { yp_core } from './yp_core/yp_core' -// import { zarit_12 } from './zarit_12/zarit_12' +import { zarit_12 } from './zarit_12/zarit_12' import { acro } from './acro/acro' import { Score } from '../classes' import { ScoreType } from '../types' @@ -260,13 +260,13 @@ export const ScoreLibrary = createScoreLibrary({ // short_fes_i, // spadi, simple_shoulder_test, - // start_back_screening_tool, + start_back_screening_tool, stop_bang, tampa, ten_meter_walk_test, - // visa_a, - // visa_g, - // visa_p, + visa_a, + visa_g, + visa_p, yp_core, - // zarit_12, + zarit_12, }) diff --git a/todo/start_back_screening_tool/README.md b/src/scores/start_back_screening_tool/README.md similarity index 100% rename from todo/start_back_screening_tool/README.md rename to src/scores/start_back_screening_tool/README.md diff --git a/todo/start_back_screening_tool/__testdata__/start_back_test_responses.ts b/src/scores/start_back_screening_tool/__testdata__/start_back_test_responses.ts similarity index 100% rename from todo/start_back_screening_tool/__testdata__/start_back_test_responses.ts rename to src/scores/start_back_screening_tool/__testdata__/start_back_test_responses.ts diff --git a/todo/start_back_screening_tool/definition/index.ts b/src/scores/start_back_screening_tool/definition/index.ts similarity index 100% rename from todo/start_back_screening_tool/definition/index.ts rename to src/scores/start_back_screening_tool/definition/index.ts diff --git a/todo/start_back_screening_tool/definition/start_back_inputs.ts b/src/scores/start_back_screening_tool/definition/start_back_inputs.ts similarity index 71% rename from todo/start_back_screening_tool/definition/start_back_inputs.ts rename to src/scores/start_back_screening_tool/definition/start_back_inputs.ts index 5c4d012..7bce9db 100644 --- a/todo/start_back_screening_tool/definition/start_back_inputs.ts +++ b/src/scores/start_back_screening_tool/definition/start_back_inputs.ts @@ -1,92 +1,77 @@ -import type { InputType } from '../../../src/types/calculations.types' -import { NumberInputType } from '../../../src/types/calculations/inputs/calculation-inputs.types' +import { z } from 'zod' +import { EnumNumberInputType, ScoreInputSchemaType } from '../../../types' const DISAGREE_ANSWER = 0 const AGREE_ANSWER = 1 -const type: NumberInputType = { - type: 'number', - allowed_answers: [ - { value: DISAGREE_ANSWER, label: { en: 'Disagree', nl: 'Oneens' } }, - { value: AGREE_ANSWER, label: { en: 'Agree', nl: 'Eens' } }, - ], -} +const type = { + type: z.union([z.literal(DISAGREE_ANSWER), z.literal(AGREE_ANSWER)]), + uiOptions: { + options: [ + { value: DISAGREE_ANSWER, label: { en: 'Disagree', nl: 'Oneens' } }, + { value: AGREE_ANSWER, label: { en: 'Agree', nl: 'Eens' } }, + ], + }, +} satisfies EnumNumberInputType -export const START_BACK_INPUTS: Array = [ - { - input_id: 'Q01', +export const START_BACK_INPUTS = { + Q01: { label: { en: 'My back pain has spread down my leg(s) at some time in the last 2 weeks', nl: 'In de laatste 2 weken straalde mijn rugpijn wel eens uit naar één of beide benen.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q02', + Q02: { label: { en: 'I have had pain in the shoulder or neck at some time in the last 2 weeks', nl: 'In de laatste 2 weken heb ik wel eens pijn in mijn schouder of nek gehad.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q03', + Q03: { label: { en: 'I have only walked short distances because of my back pain', nl: 'Vanwege mijn rugpijn liep ik alleen korte afstanden.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q04', + Q04: { label: { en: 'In the last 2 weeks, I have dressed more slowly than usual because of back pain', nl: 'In de laatste 2 weken kleedde ik me trager dan gewoonlijk aan vanwege mijn rugpijn.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q05', + Q05: { label: { en: `It's not really safe for a person with a condition like mine to be physically active`, nl: 'Voor iemand in mijn toestand is het echt niet veilig om lichamelijk actief te zijn.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q06', + Q06: { label: { en: 'Worrying thoughts have been going through my mind a lot of the time', nl: 'Ongeruste gedachten gingen vaak door mijn hoofd.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q07', + Q07: { label: { en: `I feel that my back pain is terrible and it's never going to get any better`, nl: 'Ik vind dat mijn rugpijn verschrikkelijk is en ik geloof dat het nooit meer beter zal worden.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q08', + Q08: { label: { en: 'In general I have not enjoyed all the things I used to enjoy', nl: 'Over het geheel genomen heb ik niet genoten van alle dingen waar ik vroeger wel van genoot.', }, - input_type, - required: true, + ...type, }, - { - input_id: 'Q09', + Q09: { label: { en: 'Overall, how bothersome has your back pain been in th last 2 weeks?', nl: 'Over het geheel genomen, hoe hinderlijk was uw rugpijn in de laatste 2 weken ?', @@ -95,9 +80,15 @@ export const START_BACK_INPUTS: Array = [ * Answer values 1 and 2 will be recoded to 0 * Answer values 3 and 4 will be recoded to 1 */ - type: { - type: 'number', - allowed_answers: [ + type: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + ]), + uiOptions: { + options: [ { value: 0, label: { en: 'Not at all', nl: 'In het geheel niet' } }, { value: 1, label: { en: 'Slightly', nl: 'Een beetje' } }, { value: 2, label: { en: 'Moderately', nl: 'Matig' } }, @@ -109,6 +100,5 @@ export const START_BACK_INPUTS: Array = [ en: '"Slightly" (1) and "Moderately" (2) will be recoded to 0; "Very much" (3) and "Extremely" (4) will be recoded to 1;', nl: '"Een beetje" (1) en "matig" (2) worden gehercodeerd naar 0; "Erg" en "Extreem" worden gehercodeerd naar 1; ', }, - required: true, }, -] +} satisfies ScoreInputSchemaType diff --git a/src/scores/start_back_screening_tool/definition/start_back_output.ts b/src/scores/start_back_screening_tool/definition/start_back_output.ts new file mode 100644 index 0000000..f84be5a --- /dev/null +++ b/src/scores/start_back_screening_tool/definition/start_back_output.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' +import { ScoreOutputSchemaType } from '../../../types' + +export const START_BACK_OUTPUT = { + START_BACK_TOTAL: { + label: { en: 'Total score', nl: 'Totale score' }, + type: z.number(), + }, + START_BACK_SUBSCALE: { + label: { en: 'Subscale score', nl: 'Sub uitslag (Q5-9)' }, + type: z.number(), + }, + START_BACK_RISK_CLASSIFICATION: { + label: { en: 'Risk classification', nl: 'Risicoprofiel' }, + type: z.string(), + }, +} satisfies ScoreOutputSchemaType diff --git a/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.test.ts b/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.test.ts new file mode 100644 index 0000000..6f65d59 --- /dev/null +++ b/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.test.ts @@ -0,0 +1,27 @@ +import { classifyRisk, RiskClassification } from './classifyRisk' +describe('Classify risk', function () { + describe('when total score and subscale score are not missing', function () { + describe('when total score <= 3', function () { + it('should return low risk', function () { + const outcome = classifyRisk({ totalScore: 0, subscaleScore: 0 }) + expect(outcome).toEqual(RiskClassification.LOW_RISK) + }) + }) + + describe('when total score >= 4', function () { + describe('and when subscale score <= 3', function () { + it('should return medium risk', function () { + const outcome = classifyRisk({ totalScore: 4, subscaleScore: 0 }) + expect(outcome).toEqual(RiskClassification.MEDIUM_RISK) + }) + }) + + describe('and when subscale score >= 4', function () { + it('should return medium risk', function () { + const outcome = classifyRisk({ totalScore: 4, subscaleScore: 4 }) + expect(outcome).toEqual(RiskClassification.HIGH_RISK) + }) + }) + }) + }) +}) diff --git a/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.ts b/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.ts new file mode 100644 index 0000000..23f08e5 --- /dev/null +++ b/src/scores/start_back_screening_tool/helpers/classifyRisk/classifyRisk.ts @@ -0,0 +1,29 @@ +export enum RiskClassification { + LOW_RISK = 'Low risk', + MEDIUM_RISK = 'Medium risk', + HIGH_RISK = 'High risk', +} + +export const classifyRisk = ({ + totalScore, + subscaleScore, +}: { + totalScore: number + subscaleScore: number +}): RiskClassification => { + const TOTAL_SCORE_CUT_OFF = 3 + const SUBSCALE_SCORE_CUT_OFF = 3 + + if (totalScore <= TOTAL_SCORE_CUT_OFF) { + return RiskClassification.LOW_RISK + } + + if ( + totalScore > TOTAL_SCORE_CUT_OFF && + subscaleScore <= SUBSCALE_SCORE_CUT_OFF + ) { + return RiskClassification.MEDIUM_RISK + } + + return RiskClassification.HIGH_RISK +} diff --git a/src/scores/start_back_screening_tool/start_back_screening_tool.test.ts b/src/scores/start_back_screening_tool/start_back_screening_tool.test.ts new file mode 100644 index 0000000..8f186d6 --- /dev/null +++ b/src/scores/start_back_screening_tool/start_back_screening_tool.test.ts @@ -0,0 +1,209 @@ +import { + max_response, + median_response, + min_response, + random_response, +} from './__testdata__/start_back_test_responses' +import { start_back_screening_tool } from './start_back_screening_tool' +import { RiskClassification } from './helpers/classifyRisk/classifyRisk' +import { Score } from '../../classes' +import { ScoreLibrary } from '../library' +import { ZodError } from 'zod' + +const START_BACK_TOTAL_MIN_SCORE = 0 +const START_BACK_TOTAL_MEDIAN_SCORE = 4 +const START_BACK_TOTAL_MAX_SCORE = 9 + +const START_BACK_SUBSCALE_MIN_SCORE = 0 +const START_BACK_SUBSCALE_MEDIAN_SCORE = 0 +const START_BACK_SUBSCALE_MAX_SCORE = 5 + +const start_back_calculation = new Score(start_back_screening_tool) + +describe('start_back_screening_tool', function () { + it('start_back_screening_tool calculation function should be available as a calculation', function () { + expect(ScoreLibrary).toHaveProperty('start_back_screening_tool') + }) + + describe('the score includes the correct input fields', function () { + it('should use the correct input fields', function () { + const EXPECTED_CALCULATION_INPUT_IDS = [ + 'Q01', + 'Q02', + 'Q03', + 'Q04', + 'Q05', + 'Q06', + 'Q07', + 'Q08', + 'Q09', + ] + + const configured_calculation_input_ids = Object.keys( + start_back_calculation.inputSchema, + ) + + expect(configured_calculation_input_ids).toEqual( + EXPECTED_CALCULATION_INPUT_IDS, + ) + }) + }) + + describe('each calculated score includes the correct output result and correct score title', function () { + const outcome = start_back_calculation.calculate({ payload: min_response }) + + it('should return 3 scores', function () { + expect(Object.keys(outcome).length).toEqual(3) + }) + + it('should have the correct calculation ids', function () { + const EXPECTED_CALCULATION_IDS = [ + 'START_BACK_TOTAL', + 'START_BACK_SUBSCALE', + 'START_BACK_RISK_CLASSIFICATION', + ] + + const extracted_calculation_ids_from_outcome = Object.keys(outcome) + + expect(EXPECTED_CALCULATION_IDS).toEqual( + extracted_calculation_ids_from_outcome, + ) + }) + }) + + describe('each calculated score includes the correct formula and outputs the correct result', function () { + describe('when a minimum response is passed', function () { + const outcome = start_back_calculation.calculate({ + payload: min_response, + }) + + it('should return the minimum total score', function () { + expect(outcome.START_BACK_TOTAL).toEqual(START_BACK_TOTAL_MIN_SCORE) + }) + + it('should return the minimum subscale score', function () { + expect(outcome.START_BACK_SUBSCALE).toEqual( + START_BACK_SUBSCALE_MIN_SCORE, + ) + }) + + it('should return low risk as the START_BACK_RISK_CLASSIFICATION', function () { + expect(outcome.START_BACK_RISK_CLASSIFICATION).toEqual( + RiskClassification.LOW_RISK, + ) + }) + }) + + describe('when a median response is passed', function () { + const outcome = start_back_calculation.calculate({ + payload: median_response, + }) + + it('should return the median total score', function () { + expect(outcome.START_BACK_TOTAL).toEqual(START_BACK_TOTAL_MEDIAN_SCORE) + }) + + it('should return the median subscale score', function () { + expect(outcome.START_BACK_SUBSCALE).toEqual( + START_BACK_SUBSCALE_MEDIAN_SCORE, + ) + }) + + it('should return medium risk as the START_BACK_RISK_CLASSIFICATION', function () { + expect(outcome.START_BACK_RISK_CLASSIFICATION).toEqual( + RiskClassification.MEDIUM_RISK, + ) + }) + }) + + describe('when a maximum response is passed', function () { + const outcome = start_back_calculation.calculate({ + payload: max_response, + }) + + it('should return the maximum total score', function () { + expect(outcome.START_BACK_TOTAL).toEqual(START_BACK_TOTAL_MAX_SCORE) + }) + + it('should return the maximum subscale score', function () { + expect(outcome.START_BACK_SUBSCALE).toEqual( + START_BACK_SUBSCALE_MAX_SCORE, + ) + }) + + it('should return high risk as the START_BACK_RISK_CLASSIFICATION', function () { + expect(outcome.START_BACK_RISK_CLASSIFICATION).toEqual( + RiskClassification.HIGH_RISK, + ) + }) + }) + + describe('when a random response is passed', function () { + const outcome = start_back_calculation.calculate({ + payload: random_response, + }) + + it('should return the expected total score', function () { + expect(outcome.START_BACK_TOTAL).toEqual(4) + }) + + it('should return the expected subscale score', function () { + expect(outcome.START_BACK_SUBSCALE).toEqual(2) + }) + + it('should return expected START_BACK_RISK_CLASSIFICATION', function () { + expect(outcome.START_BACK_RISK_CLASSIFICATION).toEqual( + RiskClassification.MEDIUM_RISK, + ) + }) + }) + }) + + describe('a score is only calculated when all mandatory fields are entered', function () { + describe('when an empty response is passed', function () { + it('should throw a ZodError', function () { + expect(() => + start_back_calculation.calculate({ + payload: {}, + }), + ).toThrow(ZodError) + }) + }) + }) + + 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 ZodError', function () { + expect(() => + start_back_calculation.calculate({ + payload: { + Q01: "I'm not a number", + }, + }), + ).toThrow(ZodError) + }) + }) + describe('when an answer is below the allowed answers', function () { + it('should throw an ZodError', function () { + expect(() => + start_back_calculation.calculate({ + payload: { + Q01: -1, + }, + }), + ).toThrow(ZodError) + }) + }) + describe('when an answer is above the allowed answers', function () { + it('should throw an ZodError', function () { + expect(() => + start_back_calculation.calculate({ + payload: { + Q01: 2, + }, + }), + ).toThrow(ZodError) + }) + }) + }) +}) diff --git a/src/scores/start_back_screening_tool/start_back_screening_tool.ts b/src/scores/start_back_screening_tool/start_back_screening_tool.ts new file mode 100644 index 0000000..bd2aa24 --- /dev/null +++ b/src/scores/start_back_screening_tool/start_back_screening_tool.ts @@ -0,0 +1,39 @@ +import { ScoreType } from '../../types' +import { START_BACK_OUTPUT, START_BACK_INPUTS } from './definition' +import { sum } from 'lodash' +import { classifyRisk } from './helpers/classifyRisk/classifyRisk' + +export const start_back_screening_tool: ScoreType< + typeof START_BACK_INPUTS, + typeof START_BACK_OUTPUT +> = { + name: 'Start Back Screening Tool', + readmeLocation: __dirname, + inputSchema: START_BACK_INPUTS, + outputSchema: START_BACK_OUTPUT, + calculate: ({ data }) => { + const q9Recoding = { + '0': 0, + '1': 0, + '2': 0, + '3': 1, + '4': 1, + } + + const q9Recorded = q9Recoding[data.Q09] + const subscaleItems = [data.Q05, data.Q06, data.Q07, data.Q08, q9Recorded] + const allItems = [data.Q01, data.Q02, data.Q03, data.Q04, ...subscaleItems] + + const subscaleScore = sum(subscaleItems) + const totalScore = sum(allItems) + + return { + START_BACK_TOTAL: totalScore, + START_BACK_SUBSCALE: subscaleScore, + START_BACK_RISK_CLASSIFICATION: classifyRisk({ + totalScore, + subscaleScore, + }), + } + }, +} diff --git a/todo/visa/index.ts b/src/scores/visa/index.ts similarity index 100% rename from todo/visa/index.ts rename to src/scores/visa/index.ts diff --git a/todo/visa/visa_a/README.md b/src/scores/visa/visa_a/README.md similarity index 100% rename from todo/visa/visa_a/README.md rename to src/scores/visa/visa_a/README.md diff --git a/todo/visa/visa_a/__testdata__/visa_a_test_responses.ts b/src/scores/visa/visa_a/__testdata__/visa_a_test_responses.ts similarity index 100% rename from todo/visa/visa_a/__testdata__/visa_a_test_responses.ts rename to src/scores/visa/visa_a/__testdata__/visa_a_test_responses.ts diff --git a/todo/visa/visa_a/definition/index.ts b/src/scores/visa/visa_a/definition/index.ts similarity index 100% rename from todo/visa/visa_a/definition/index.ts rename to src/scores/visa/visa_a/definition/index.ts diff --git a/src/scores/visa/visa_a/definition/visa_a_inputs.ts b/src/scores/visa/visa_a/definition/visa_a_inputs.ts new file mode 100644 index 0000000..11137d0 --- /dev/null +++ b/src/scores/visa/visa_a/definition/visa_a_inputs.ts @@ -0,0 +1,113 @@ +import { z } from 'zod' +import { EnumNumberInputType, ScoreInputSchemaType } from '../../../../types' + +const type = { + type: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + z.literal(5), + z.literal(6), + z.literal(7), + z.literal(8), + z.literal(9), + z.literal(10), + ]), +} satisfies EnumNumberInputType + +export const VISA_A_INPUTS = { + VISA_A_Q1: { + ...type, + }, + VISA_A_Q02: { + ...type, + }, + VISA_A_Q03: { + ...type, + }, + VISA_A_Q04: { + ...type, + }, + VISA_A_Q05: { + ...type, + }, + VISA_A_Q06: { + ...type, + }, + VISA_A_Q07: { + type: z.union([z.literal(0), z.literal(4), z.literal(7), z.literal(10)]), + }, + VISA_A_Q08: { + label: { + en: "Select what's applicable", + }, + type: z.union([z.literal('A'), z.literal('B'), z.literal('C')]).optional(), + uiOptions: { + options: [ + { + value: 'A', + label: { + en: 'I have no pain while undertaking Achilles tendon loading sports', + }, + }, + { + value: 'B', + label: { + en: 'I have pain while undertaking Achilles tendon loading sports but it does not stop me from completing the activity', + }, + }, + { + value: 'C', + label: { + en: 'I have pain that stops me from completing Achilles tendon loading sports', + }, + }, + ], + }, + info: { + en: 'This question determines whether "VISA_A_Q08_A", "VISA_A_Q08_B" or "VISA_A_Q08_C" should be answered.', + }, + }, + VISA_A_Q08_A: { + type: z + .union([ + z.literal(0), + z.literal(7), + z.literal(14), + z.literal(21), + z.literal(30), + ]) + .optional(), + info: { en: 'Should only be scored when there is no pain during sports' }, + }, + VISA_A_Q08_B: { + type: z + .union([ + z.literal(0), + z.literal(4), + z.literal(10), + z.literal(14), + z.literal(20), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', + }, + }, + VISA_A_Q08_C: { + type: z + .union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', + }, + }, +} satisfies ScoreInputSchemaType diff --git a/src/scores/visa/visa_a/definition/visa_a_output.ts b/src/scores/visa/visa_a/definition/visa_a_output.ts new file mode 100644 index 0000000..62c4309 --- /dev/null +++ b/src/scores/visa/visa_a/definition/visa_a_output.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' +import { ScoreOutputSchemaType } from '../../../../types' + +export const VISA_A_OUTPUT = { + VISA_A: { + label: { en: 'VISA-A Score' }, + type: z.number(), + }, +} satisfies ScoreOutputSchemaType diff --git a/todo/visa/visa_a/visa_a.test.ts b/src/scores/visa/visa_a/visa_a.test.ts similarity index 54% rename from todo/visa/visa_a/visa_a.test.ts rename to src/scores/visa/visa_a/visa_a.test.ts index beead2b..57c2afd 100644 --- a/todo/visa/visa_a/visa_a.test.ts +++ b/src/scores/visa/visa_a/visa_a.test.ts @@ -1,30 +1,23 @@ -import { expect } from 'chai' -import { compose } from 'ramda' - -import { ZodError } from '../../../errors' -import { execute_test_calculation } from '../../../lib/execute_test_calculation' -import { get_result_ids_from_calculation_output } from '../../../lib/get_result_ids_from_calculation_output' -import { view_result } from '../../../lib/view_result' -import { CALCULATIONS } from '../../calculation_library' -import { get_input_ids_from_calculation_blueprint } from '../../shared_functions' +import { ZodError } from 'zod' +import { Score } from '../../../classes' +import { ScoreLibrary } from '../../library' import { max_response, median_response, min_response, random_response, } from './__testdata__/visa_a_test_responses' -import { VISA_A_INPUTS } from './definition/visa_a_inputs' import { visa_a } from './visa_a' const VISA_A_MIN_SCORE = 0 const VISA_A_MEDIAN_SCORE = 50 const VISA_A_MAX_SCORE = 100 -const visa_a_calculation = execute_test_calculation(visa_a) +const visa_a_calculation = new Score(visa_a) describe('visa_a', function () { it('visa_a calculation function should be available as a calculation', function () { - expect(CALCULATIONS).toHaveProperty('visa_a') + expect(ScoreLibrary).toHaveProperty('visa_a') }) describe('the score includes the correct input fields', function () { @@ -43,25 +36,25 @@ describe('visa_a', function () { 'VISA_A_Q08_C', ] - const configured_calculation_input_ids = - get_input_ids_from_calculation_blueprint(VISA_A_INPUTS) + const configured_calculation_input_ids = Object.keys( + visa_a_calculation.inputSchema, + ) - expect(configured_calculation_input_ids).to.have.members( + expect(configured_calculation_input_ids).toEqual( EXPECTED_CALCULATION_INPUT_IDS, ) }) }) describe('each calculated score includes the correct output result and correct score title', function () { - const outcome = visa_a_calculation(min_response) + const outcome = visa_a_calculation.calculate({ payload: min_response }) it('should calculate a single score', function () { - expect(outcome).toHaveLength(1) + expect(Object.keys(outcome).length).toEqual(1) }) it('should have the correct calculation id', function () { - const configured_calculation_id = - get_result_ids_from_calculation_output(outcome) + const configured_calculation_id = Object.keys(outcome) expect(configured_calculation_id).toEqual(['VISA_A']) }) @@ -70,57 +63,69 @@ describe('visa_a', function () { describe('each calculated score includes the correct formula and outputs the correct result', function () { describe('when a minimum response is passed', function () { it('should return the minimum score', function () { - const score = compose( - view_result('VISA-A'), - visa_a_calculation, - )(min_response) + const score = visa_a_calculation.calculate({ payload: min_response }) - expect(score).toEqual(VISA_A_MIN_SCORE) + expect(score.VISA_A).toEqual(VISA_A_MIN_SCORE) }) }) describe('when a median response is passed', function () { it('should return the median score', function () { - const score = compose( - view_result('VISA-A'), - visa_a_calculation, - )(median_response) + const score = visa_a_calculation.calculate({ + payload: median_response, + }) - expect(score).toEqual(VISA_A_MEDIAN_SCORE) + expect(score.VISA_A).toEqual(VISA_A_MEDIAN_SCORE) }) }) describe('when a maximum response is passed', function () { it('should return the maximum score', function () { - const score = compose( - view_result('VISA-A'), - visa_a_calculation, - )(max_response) + const score = visa_a_calculation.calculate({ payload: max_response }) - expect(score).toEqual(VISA_A_MAX_SCORE) + expect(score.VISA_A).toEqual(VISA_A_MAX_SCORE) }) }) describe('when a random response is passed', function () { it('should return the expected score', function () { - const score = compose( - view_result('VISA-A'), - visa_a_calculation, - )(random_response) + const score = visa_a_calculation.calculate({ payload: random_response }) const EXPECTED_SCORE = 51 - expect(score).toEqual(EXPECTED_SCORE) + expect(score.VISA_A).toEqual(EXPECTED_SCORE) }) }) }) describe('a score is only calculated when all mandatory fields are entered', function () { describe('when an empty response is passed', function () { - it('should return undefined as the result', function () { - const score = compose(view_result('VISA-A'), visa_a_calculation)({}) + it('should throw a ZodError', function () { + expect(() => visa_a_calculation.calculate({ payload: {} })).toThrow( + ZodError, + ) + }) + }) - expect(score).toEqual(undefined) + describe('when an more than one subquestion of Q08 is passed', function () { + it('should throw an error', function () { + expect(() => + visa_a_calculation.calculate({ + payload: { + VISA_A_Q01: 0, + VISA_A_Q02: 0, + VISA_A_Q03: 0, + VISA_A_Q04: 0, + VISA_A_Q05: 0, + VISA_A_Q06: 0, + VISA_A_Q07: 0, + VISA_A_Q08: 'A', + VISA_A_Q08_A: 1, + VISA_A_Q08_B: 2, + VISA_A_Q08_C: 3, + }, + }), + ).toThrow() }) }) }) @@ -129,8 +134,10 @@ describe('visa_a', function () { describe('when an answer is not a number', function () { it('should throw an ZodError', function () { expect(() => - visa_a_calculation({ - VISA_A_Q1: "I'm not a number", + visa_a_calculation.calculate({ + payload: { + VISA_A_Q1: "I'm not a number", + }, }), ).toThrow(ZodError) }) @@ -138,8 +145,10 @@ describe('visa_a', function () { describe('when an answer is not one of the allowed answers', function () { it('should throw an ZodError', function () { expect(() => - visa_a_calculation({ - VISA_A_Q1: 11, + visa_a_calculation.calculate({ + payload: { + VISA_A_Q1: 11, + }, }), ).toThrow(ZodError) }) diff --git a/src/scores/visa/visa_a/visa_a.ts b/src/scores/visa/visa_a/visa_a.ts new file mode 100644 index 0000000..998ca3c --- /dev/null +++ b/src/scores/visa/visa_a/visa_a.ts @@ -0,0 +1,30 @@ +import { ScoreType } from '../../../types' +import { VISA_A_OUTPUT, VISA_A_INPUTS } from './definition' +import { sum, omit } from 'lodash' + +export const visa_a: ScoreType = { + name: 'Victorian Institute of Sports Assessment – Achilles score (VISA-A)', + readmeLocation: __dirname, + inputSchema: VISA_A_INPUTS, + outputSchema: VISA_A_OUTPUT, + calculate: ({ data }) => { + const optionalInputs = [ + data.VISA_A_Q08_A, + data.VISA_A_Q08_B, + data.VISA_A_Q08_C, + ] + + /** + * Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored + */ + if (optionalInputs.filter(v => v !== undefined).length > 1) { + throw new Error( + 'Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored', + ) + } + + return { + VISA_A: sum(Object.values(omit(data, ['VISA_A_Q08']))), + } + }, +} diff --git a/todo/visa/visa_g/README.md b/src/scores/visa/visa_g/README.md similarity index 100% rename from todo/visa/visa_g/README.md rename to src/scores/visa/visa_g/README.md diff --git a/todo/visa/visa_g/__testdata__/visa_g_test_responses.ts b/src/scores/visa/visa_g/__testdata__/visa_g_test_responses.ts similarity index 100% rename from todo/visa/visa_g/__testdata__/visa_g_test_responses.ts rename to src/scores/visa/visa_g/__testdata__/visa_g_test_responses.ts diff --git a/todo/visa/visa_g/definition/index.ts b/src/scores/visa/visa_g/definition/index.ts similarity index 100% rename from todo/visa/visa_g/definition/index.ts rename to src/scores/visa/visa_g/definition/index.ts diff --git a/src/scores/visa/visa_g/definition/visa_g_inputs.ts b/src/scores/visa/visa_g/definition/visa_g_inputs.ts new file mode 100644 index 0000000..d776d87 --- /dev/null +++ b/src/scores/visa/visa_g/definition/visa_g_inputs.ts @@ -0,0 +1,139 @@ +import { z } from 'zod' +import { ScoreInputSchemaType } from '../../../../types' + +export const VISA_G_INPUTS = { + VISA_G_Q01: { + type: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + z.literal(5), + z.literal(6), + z.literal(7), + z.literal(8), + z.literal(9), + z.literal(10), + ]), + }, + VISA_G_Q02: { + type: z.union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]), + }, + VISA_G_Q03: { + type: z.union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]), + }, + VISA_G_Q04: { + type: z.union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]), + }, + VISA_G_Q05: { + type: z.union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]), + }, + VISA_G_Q06: { + type: z.union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]), + }, + VISA_G_Q07: { + type: z.union([z.literal(0), z.literal(4), z.literal(7), z.literal(10)]), + }, + VISA_G_Q08: { + label: { + en: "Select what's applicable", + }, + type: z.union([z.literal('A'), z.literal('B'), z.literal('C')]).optional(), + uiOptions: { + options: [ + { + value: 'A', + label: { + en: 'My hip pain is so severe that it will stop me from walking, shopping, running or other weight bearing exercise.', + }, + }, + { + value: 'B', + label: { + en: 'My hip pain is present with exercise, but it does not stop me from walking, shopping, running or other weight bearing type exercise.', + }, + }, + { + value: 'C', + label: { + en: 'If you have no pain while you undertake walking, shopping, running or other weight bearing type exercise.', + }, + }, + ], + }, + info: { + en: 'This question determines whether "VISA_G_Q08_A", "VISA_G_Q08_B" or "VISA_G_Q08_C" should be answered.', + }, + }, + VISA_G_Q08_A: { + type: z + .union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]) + .optional(), + info: { en: 'Should only be scored when there is no pain during sports' }, + }, + VISA_G_Q08_B: { + type: z + .union([ + z.literal(0), + z.literal(5), + z.literal(10), + z.literal(15), + z.literal(20), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', + }, + }, + VISA_G_Q08_C: { + type: z + .union([ + z.literal(6), + z.literal(12), + z.literal(18), + z.literal(24), + z.literal(30), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', + }, + }, +} satisfies ScoreInputSchemaType diff --git a/src/scores/visa/visa_g/definition/visa_g_output.ts b/src/scores/visa/visa_g/definition/visa_g_output.ts new file mode 100644 index 0000000..438cb7c --- /dev/null +++ b/src/scores/visa/visa_g/definition/visa_g_output.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' +import { ScoreOutputSchemaType } from '../../../../types' + +export const VISA_G_OUTPUT = { + VISA_G: { + label: { en: 'VISA-G Score' }, + type: z.number(), + }, +} satisfies ScoreOutputSchemaType diff --git a/todo/visa/visa_g/visa_g.test.ts b/src/scores/visa/visa_g/visa_g.test.ts similarity index 53% rename from todo/visa/visa_g/visa_g.test.ts rename to src/scores/visa/visa_g/visa_g.test.ts index ac993a1..9fbdb8e 100644 --- a/todo/visa/visa_g/visa_g.test.ts +++ b/src/scores/visa/visa_g/visa_g.test.ts @@ -1,30 +1,23 @@ -import { expect } from 'chai' -import { compose } from 'ramda' - -import { ZodError } from '../../../errors' -import { execute_test_calculation } from '../../../lib/execute_test_calculation' -import { get_result_ids_from_calculation_output } from '../../../lib/get_result_ids_from_calculation_output' -import { view_result } from '../../../lib/view_result' -import { CALCULATIONS } from '../../calculation_library' -import { get_input_ids_from_calculation_blueprint } from '../../shared_functions' +import { ZodError } from 'zod' +import { Score } from '../../../classes' +import { ScoreLibrary } from '../../library' import { max_response, median_response, min_response, random_response, } from './__testdata__/visa_g_test_responses' -import { VISA_G_INPUTS } from './definition/visa_g_inputs' import { visa_g } from './visa_g' const VISA_G_MIN_SCORE = 0 const VISA_G_MEDIAN_SCORE = 50 const VISA_G_MAX_SCORE = 100 -const visa_g_calculation = execute_test_calculation(visa_g) +const visa_g_calculation = new Score(visa_g) describe('visa_g', function () { it('visa_g calculation function should be available as a calculation', function () { - expect(CALCULATIONS).toHaveProperty('visa_g') + expect(ScoreLibrary).toHaveProperty('visa_g') }) describe('the score includes the correct input fields', function () { @@ -43,25 +36,27 @@ describe('visa_g', function () { 'VISA_G_Q08_C', ] - const configured_calculation_input_ids = - get_input_ids_from_calculation_blueprint(VISA_G_INPUTS) + const configured_calculation_input_ids = Object.keys( + visa_g_calculation.inputSchema, + ) - expect(configured_calculation_input_ids).to.have.members( + expect(configured_calculation_input_ids).toEqual( EXPECTED_CALCULATION_INPUT_IDS, ) }) }) describe('each calculated score includes the correct output result and correct score title', function () { - const outcome = visa_g_calculation(min_response) + const outcome = visa_g_calculation.calculate({ + payload: min_response, + }) it('should calculate a single score', function () { - expect(outcome).toHaveLength(1) + expect(Object.keys(outcome).length).toEqual(1) }) it('should have the correct calculation id', function () { - const configured_calculation_id = - get_result_ids_from_calculation_output(outcome) + const configured_calculation_id = Object.keys(outcome) expect(configured_calculation_id).toEqual(['VISA_G']) }) @@ -70,57 +65,77 @@ describe('visa_g', function () { describe('each calculated score includes the correct formula and outputs the correct result', function () { describe('when a minimum response is passed', function () { it('should return the minimum score', function () { - const score = compose( - view_result('VISA-G'), - visa_g_calculation, - )(min_response) + const score = visa_g_calculation.calculate({ + payload: min_response, + }) - expect(score).toEqual(VISA_G_MIN_SCORE) + expect(score.VISA_G).toEqual(VISA_G_MIN_SCORE) }) }) describe('when a median response is passed', function () { it('should return the median score', function () { - const score = compose( - view_result('VISA-G'), - visa_g_calculation, - )(median_response) + const score = visa_g_calculation.calculate({ + payload: median_response, + }) - expect(score).toEqual(VISA_G_MEDIAN_SCORE) + expect(score.VISA_G).toEqual(VISA_G_MEDIAN_SCORE) }) }) describe('when a maximum response is passed', function () { it('should return the maximum score', function () { - const score = compose( - view_result('VISA-G'), - visa_g_calculation, - )(max_response) + const score = visa_g_calculation.calculate({ + payload: max_response, + }) - expect(score).toEqual(VISA_G_MAX_SCORE) + expect(score.VISA_G).toEqual(VISA_G_MAX_SCORE) }) }) describe('when a random response is passed', function () { it('should return the expected score', function () { - const score = compose( - view_result('VISA-G'), - visa_g_calculation, - )(random_response) + const score = visa_g_calculation.calculate({ + payload: random_response, + }) const EXPECTED_SCORE = 56 - expect(score).toEqual(EXPECTED_SCORE) + expect(score.VISA_G).toEqual(EXPECTED_SCORE) }) }) }) describe('a score is only calculated when all mandatory fields are entered', function () { describe('when an empty response is passed', function () { - it('should return undefined as the result', function () { - const score = compose(view_result('VISA-G'), visa_g_calculation)({}) + it('should throw a ZodError', function () { + expect(() => + visa_g_calculation.calculate({ + payload: {}, + }), + ).toThrow(ZodError) + }) + }) - expect(score).toEqual(undefined) + describe('when an more than one subquestion of Q08 is passed', function () { + it('should throw an error', function () { + expect(() => + visa_g_calculation.calculate({ + payload: { + VISA_G_Q01: 0, + VISA_G_Q02: 0, + VISA_G_Q03: 0, + VISA_G_Q04: 0, + VISA_G_Q05: 0, + VISA_G_Q06: 0, + VISA_G_Q07: 0, + VISA_G_Q08: 'A', + VISA_G_Q08_A: 1, + VISA_G_Q08_B: 2, + VISA_G_Q08_C: 3, + }, + }), + ).toThrow() }) }) }) @@ -129,8 +144,10 @@ describe('visa_g', function () { describe('when an answer is not a number', function () { it('should throw an ZodError', function () { expect(() => - visa_g_calculation({ - VISA_G_Q01: "I'm not a number", + visa_g_calculation.calculate({ + payload: { + VISA_G_Q01: "I'm not a number", + }, }), ).toThrow(ZodError) }) @@ -138,8 +155,10 @@ describe('visa_g', function () { describe('when an answer is not one of the allowed answers', function () { it('should throw an ZodError', function () { expect(() => - visa_g_calculation({ - VISA_G_Q01: 11, + visa_g_calculation.calculate({ + payload: { + VISA_G_Q01: 11, + }, }), ).toThrow(ZodError) }) diff --git a/src/scores/visa/visa_g/visa_g.ts b/src/scores/visa/visa_g/visa_g.ts new file mode 100644 index 0000000..b14e687 --- /dev/null +++ b/src/scores/visa/visa_g/visa_g.ts @@ -0,0 +1,30 @@ +import { ScoreType } from '../../../types' +import { VISA_G_OUTPUT, VISA_G_INPUTS } from './definition' +import { sum, omit } from 'lodash' + +export const visa_g: ScoreType = { + name: 'Victorian Institute of Sports Assessment – Gluteal Tendinopathy score (VISA-G)', + readmeLocation: __dirname, + inputSchema: VISA_G_INPUTS, + outputSchema: VISA_G_OUTPUT, + calculate: ({ data }) => { + const optionalInputs = [ + data.VISA_G_Q08_A, + data.VISA_G_Q08_B, + data.VISA_G_Q08_C, + ] + + /** + * Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored + */ + if (optionalInputs.filter(v => v !== undefined).length > 1) { + throw new Error( + 'Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored', + ) + } + + return { + VISA_G: sum(Object.values(omit(data, ['VISA_G_Q08']))), + } + }, +} diff --git a/todo/visa/visa_p/README.md b/src/scores/visa/visa_p/README.md similarity index 100% rename from todo/visa/visa_p/README.md rename to src/scores/visa/visa_p/README.md diff --git a/todo/visa/visa_p/__testdata__/visa_p_test_responses.ts b/src/scores/visa/visa_p/__testdata__/visa_p_test_responses.ts similarity index 100% rename from todo/visa/visa_p/__testdata__/visa_p_test_responses.ts rename to src/scores/visa/visa_p/__testdata__/visa_p_test_responses.ts diff --git a/todo/visa/visa_p/definition/index.ts b/src/scores/visa/visa_p/definition/index.ts similarity index 100% rename from todo/visa/visa_p/definition/index.ts rename to src/scores/visa/visa_p/definition/index.ts diff --git a/src/scores/visa/visa_p/definition/visa_p_inputs.ts b/src/scores/visa/visa_p/definition/visa_p_inputs.ts new file mode 100644 index 0000000..8f85095 --- /dev/null +++ b/src/scores/visa/visa_p/definition/visa_p_inputs.ts @@ -0,0 +1,113 @@ +import { z } from 'zod' +import { EnumNumberInputType, ScoreInputSchemaType } from '../../../../types' + +const type = { + type: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + z.literal(5), + z.literal(6), + z.literal(7), + z.literal(8), + z.literal(9), + z.literal(10), + ]), +} satisfies EnumNumberInputType + +export const VISA_P_INPUTS = { + VISA_P_Q01: { + ...type, + }, + VISA_P_Q02: { + ...type, + }, + VISA_P_Q03: { + ...type, + }, + VISA_P_Q04: { + ...type, + }, + VISA_P_Q05: { + ...type, + }, + VISA_P_Q06: { + ...type, + }, + VISA_P_Q07: { + type: z.union([z.literal(0), z.literal(4), z.literal(7), z.literal(10)]), + }, + VISA_P_Q08: { + label: { + en: "Select what's applicable", + }, + type: z.union([z.literal('A'), z.literal('B'), z.literal('C')]).optional(), + uiOptions: { + options: [ + { + value: 'A', + label: { + en: 'I have no pain while undertaking sport', + }, + }, + { + value: 'B', + label: { + en: 'I have pain while undertaking sport but it does not stop me from completing the activity,', + }, + }, + { + value: 'C', + label: { + en: 'I have pain that stops me from completing sporting activities,', + }, + }, + ], + }, + info: { + en: 'This question determines whether "VISA_P_Q08_A", "VISA_P_Q08_B" or "VISA_P_Q08_C" should be answered.', + }, + }, + VISA_P_Q08_A: { + type: z + .union([ + z.literal(0), + z.literal(7), + z.literal(14), + z.literal(21), + z.literal(30), + ]) + .optional(), + info: { en: 'Should only be scored when there is pain during sports' }, + }, + VISA_P_Q08_B: { + type: z + .union([ + z.literal(0), + z.literal(4), + z.literal(10), + z.literal(14), + z.literal(20), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', + }, + }, + VISA_P_Q08_C: { + type: z + .union([ + z.literal(0), + z.literal(2), + z.literal(5), + z.literal(7), + z.literal(10), + ]) + .optional(), + info: { + en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', + }, + }, +} satisfies ScoreInputSchemaType diff --git a/src/scores/visa/visa_p/definition/visa_p_output.ts b/src/scores/visa/visa_p/definition/visa_p_output.ts new file mode 100644 index 0000000..46b1fd3 --- /dev/null +++ b/src/scores/visa/visa_p/definition/visa_p_output.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' +import { ScoreOutputSchemaType } from '../../../../types' + +export const VISA_P_OUTPUT = { + VISA_P: { + label: { en: 'VISA-P Score' }, + type: z.number(), + }, +} satisfies ScoreOutputSchemaType diff --git a/todo/visa/visa_p/visa_p.test.ts b/src/scores/visa/visa_p/visa_p.test.ts similarity index 54% rename from todo/visa/visa_p/visa_p.test.ts rename to src/scores/visa/visa_p/visa_p.test.ts index 8c9bcda..0044dca 100644 --- a/todo/visa/visa_p/visa_p.test.ts +++ b/src/scores/visa/visa_p/visa_p.test.ts @@ -1,30 +1,23 @@ -import { expect } from 'chai' -import { compose } from 'ramda' - -import { ZodError } from '../../../errors' -import { execute_test_calculation } from '../../../lib/execute_test_calculation' -import { get_result_ids_from_calculation_output } from '../../../lib/get_result_ids_from_calculation_output' -import { view_result } from '../../../lib/view_result' -import { CALCULATIONS } from '../../calculation_library' -import { get_input_ids_from_calculation_blueprint } from '../../shared_functions' +import { ZodError } from 'zod' +import { Score } from '../../../classes' +import { ScoreLibrary } from '../../library' import { max_response, median_response, min_response, random_response, } from './__testdata__/visa_p_test_responses' -import { VISA_P_INPUTS } from './definition/visa_p_inputs' import { visa_p } from './visa_p' const VISA_P_MIN_SCORE = 0 const VISA_P_MEDIAN_SCORE = 50 const VISA_P_MAX_SCORE = 100 -const visa_p_calculation = execute_test_calculation(visa_p) +const visa_p_calculation = new Score(visa_p) describe('visa_p', function () { it('visa_p calculation function should be available as a calculation', function () { - expect(CALCULATIONS).toHaveProperty('visa_p') + expect(ScoreLibrary).toHaveProperty('visa_p') }) describe('the score includes the correct input fields', function () { @@ -43,25 +36,25 @@ describe('visa_p', function () { 'VISA_P_Q08_C', ] - const configured_calculation_input_ids = - get_input_ids_from_calculation_blueprint(VISA_P_INPUTS) + const configured_calculation_input_ids = Object.keys( + visa_p_calculation.inputSchema, + ) - expect(configured_calculation_input_ids).to.have.members( + expect(configured_calculation_input_ids).toEqual( EXPECTED_CALCULATION_INPUT_IDS, ) }) }) describe('each calculated score includes the correct output result and correct score title', function () { - const outcome = visa_p_calculation(min_response) + const outcome = visa_p_calculation.calculate({ payload: min_response }) it('should calculate a single score', function () { - expect(outcome).toHaveLength(1) + expect(Object.keys(outcome).length).toEqual(1) }) it('should have the correct calculation id', function () { - const configured_calculation_id = - get_result_ids_from_calculation_output(outcome) + const configured_calculation_id = Object.keys(outcome) expect(configured_calculation_id).toEqual(['VISA_P']) }) @@ -70,57 +63,73 @@ describe('visa_p', function () { describe('each calculated score includes the correct formula and outputs the correct result', function () { describe('when a minimum response is passed', function () { it('should return the minimum score', function () { - const score = compose( - view_result('VISA-P'), - visa_p_calculation, - )(min_response) + const score = visa_p_calculation.calculate({ payload: min_response }) - expect(score).toEqual(VISA_P_MIN_SCORE) + expect(score.VISA_P).toEqual(VISA_P_MIN_SCORE) }) }) describe('when a median response is passed', function () { it('should return the median score', function () { - const score = compose( - view_result('VISA-P'), - visa_p_calculation, - )(median_response) + const score = visa_p_calculation.calculate({ + payload: median_response, + }) - expect(score).toEqual(VISA_P_MEDIAN_SCORE) + expect(score.VISA_P).toEqual(VISA_P_MEDIAN_SCORE) }) }) describe('when a maximum response is passed', function () { it('should return the maximum score', function () { - const score = compose( - view_result('VISA-P'), - visa_p_calculation, - )(max_response) + const score = visa_p_calculation.calculate({ + payload: max_response, + }) - expect(score).toEqual(VISA_P_MAX_SCORE) + expect(score.VISA_P).toEqual(VISA_P_MAX_SCORE) }) }) describe('when a random response is passed', function () { it('should return the expected score', function () { - const score = compose( - view_result('VISA-P'), - visa_p_calculation, - )(random_response) + const score = visa_p_calculation.calculate({ + payload: random_response, + }) const EXPECTED_SCORE = 47 - expect(score).toEqual(EXPECTED_SCORE) + expect(score.VISA_P).toEqual(EXPECTED_SCORE) }) }) }) describe('a score is only calculated when all mandatory fields are entered', function () { describe('when an empty response is passed', function () { - it('should return undefined as the result', function () { - const score = compose(view_result('VISA-P'), visa_p_calculation)({}) + it('should throw a ZodError', function () { + expect(() => visa_p_calculation.calculate({ payload: {} })).toThrow( + ZodError, + ) + }) + }) - expect(score).toEqual(undefined) + describe('when an more than one subquestion of Q08 is passed', function () { + it('should throw an error', function () { + expect(() => + visa_p_calculation.calculate({ + payload: { + VISA_P_Q01: 0, + VISA_P_Q02: 0, + VISA_P_Q03: 0, + VISA_P_Q04: 0, + VISA_P_Q05: 0, + VISA_P_Q06: 0, + VISA_P_Q07: 0, + VISA_P_Q08: 'B', + VISA_P_Q08_A: 1, + VISA_P_Q08_B: 2, + VISA_P_Q08_C: 3, + }, + }), + ).toThrow() }) }) }) @@ -129,8 +138,10 @@ describe('visa_p', function () { describe('when an answer is not a number', function () { it('should throw an ZodError', function () { expect(() => - visa_p_calculation({ - VISA_P_Q01: "I'm not a number", + visa_p_calculation.calculate({ + payload: { + VISA_P_Q01: "I'm not a number", + }, }), ).toThrow(ZodError) }) @@ -138,8 +149,10 @@ describe('visa_p', function () { describe('when an answer is not one of the allowed answers', function () { it('should throw an ZodError', function () { expect(() => - visa_p_calculation({ - VISA_P_Q01: 11, + visa_p_calculation.calculate({ + payload: { + VISA_P_Q01: 11, + }, }), ).toThrow(ZodError) }) diff --git a/src/scores/visa/visa_p/visa_p.ts b/src/scores/visa/visa_p/visa_p.ts new file mode 100644 index 0000000..d28e457 --- /dev/null +++ b/src/scores/visa/visa_p/visa_p.ts @@ -0,0 +1,30 @@ +import { ScoreType } from '../../../types' +import { VISA_P_OUTPUT, VISA_P_INPUTS } from './definition' +import { sum, omit } from 'lodash' + +export const visa_p: ScoreType = { + name: 'Victorian Institute of Sports Assessment – Patella score (VISA-P)', + readmeLocation: __dirname, + inputSchema: VISA_P_INPUTS, + outputSchema: VISA_P_OUTPUT, + calculate: ({ data }) => { + const optionalInputs = [ + data.VISA_P_Q08_A, + data.VISA_P_Q08_B, + data.VISA_P_Q08_C, + ] + + /** + * Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored + */ + if (optionalInputs.filter(v => v !== undefined).length > 1) { + throw new Error( + 'Only one of the optional inputs (Q08A, Q08B, Q08C) should be scored', + ) + } + + return { + VISA_P: sum(Object.values(omit(data, ['VISA_P_Q08']))), + } + }, +} diff --git a/todo/zarit_12/README.md b/src/scores/zarit_12/README.md similarity index 100% rename from todo/zarit_12/README.md rename to src/scores/zarit_12/README.md diff --git a/todo/zarit_12/__testdata__/zarit_12_test_responses.ts b/src/scores/zarit_12/__testdata__/zarit_12_test_responses.ts similarity index 100% rename from todo/zarit_12/__testdata__/zarit_12_test_responses.ts rename to src/scores/zarit_12/__testdata__/zarit_12_test_responses.ts diff --git a/todo/zarit_12/definition/index.ts b/src/scores/zarit_12/definition/index.ts similarity index 100% rename from todo/zarit_12/definition/index.ts rename to src/scores/zarit_12/definition/index.ts diff --git a/src/scores/zarit_12/definition/zarit_12_inputs.ts b/src/scores/zarit_12/definition/zarit_12_inputs.ts new file mode 100644 index 0000000..8a397b3 --- /dev/null +++ b/src/scores/zarit_12/definition/zarit_12_inputs.ts @@ -0,0 +1,60 @@ +import { z } from 'zod' +import { EnumNumberInputType, ScoreInputSchemaType } from '../../../types' + +const type = { + type: z.union([ + z.literal(0), + z.literal(1), + z.literal(2), + z.literal(3), + z.literal(4), + ]), + uiOptions: { + options: [ + { value: 0 }, + { value: 1 }, + { value: 2 }, + { value: 3 }, + { value: 4 }, + ], + }, +} satisfies EnumNumberInputType + +export const ZARRIT_INPUTS = { + ZARIT_12_Q01: { + ...type, + }, + ZARIT_12_Q02: { + ...type, + }, + ZARIT_12_Q03: { + ...type, + }, + ZARIT_12_Q04: { + ...type, + }, + ZARIT_12_Q05: { + ...type, + }, + ZARIT_12_Q06: { + ...type, + }, + ZARIT_12_Q07: { + ...type, + }, + ZARIT_12_Q08: { + ...type, + }, + ZARIT_12_Q09: { + ...type, + }, + ZARIT_12_Q10: { + ...type, + }, + ZARIT_12_Q11: { + ...type, + }, + ZARIT_12_Q12: { + ...type, + }, +} satisfies ScoreInputSchemaType diff --git a/src/scores/zarit_12/definition/zarit_12_output.ts b/src/scores/zarit_12/definition/zarit_12_output.ts new file mode 100644 index 0000000..b24cb48 --- /dev/null +++ b/src/scores/zarit_12/definition/zarit_12_output.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' +import { ScoreOutputSchemaType } from '../../../types' +export const ZARIT_12_OUTPUT = { + ZARIT_12: { + label: { en: 'Total score' }, + type: z.number(), + }, +} satisfies ScoreOutputSchemaType diff --git a/todo/zarit_12/zarit_12.test.ts b/src/scores/zarit_12/zarit_12.test.ts similarity index 58% rename from todo/zarit_12/zarit_12.test.ts rename to src/scores/zarit_12/zarit_12.test.ts index 12e98cd..819c72c 100644 --- a/todo/zarit_12/zarit_12.test.ts +++ b/src/scores/zarit_12/zarit_12.test.ts @@ -1,30 +1,23 @@ -import { expect } from 'chai' -import R from 'ramda' - -import { ZodError } from '../../errors' -import { execute_test_calculation } from '../../lib/execute_test_calculation' -import { get_result_ids_from_calculation_output } from '../../lib/get_result_ids_from_calculation_output' -import { view_result } from '../../lib/view_result' -import { CALCULATIONS } from '../calculation_library' -import { get_input_ids_from_calculation_blueprint } from '../shared_functions' +import { ZodError } from 'zod' +import { Score } from '../../classes' +import { ScoreLibrary } from '../library' import { max_response, median_response, min_response, random_response, } from './__testdata__/zarit_12_test_responses' -import { ZARRIT_INPUTS } from './definition/zarit_12_inputs' import { zarit_12 } from './zarit_12' const ZARIT_12_MIN_SCORE = 0 const ZARIT_12_MEDIAN_SCORE = 24 const ZARIT_12_MAX_SCORE = 48 -const zarit_12_calculation = execute_test_calculation(zarit_12) +const zarit_12_calculation = new Score(zarit_12) describe('zarit_12', function () { it('zarit_12 calculation function should be available as a calculation', function () { - expect(CALCULATIONS).toHaveProperty('zarit_12') + expect(ScoreLibrary).toHaveProperty('zarit_12') }) describe('the score includes the correct input fields', function () { @@ -44,25 +37,25 @@ describe('zarit_12', function () { 'ZARIT_12_Q12', ] - const configured_calculation_input_ids = - get_input_ids_from_calculation_blueprint(ZARRIT_INPUTS) + const configured_calculation_input_ids = Object.keys( + zarit_12_calculation.inputSchema, + ) - expect(configured_calculation_input_ids).to.have.members( + expect(configured_calculation_input_ids).toEqual( EXPECTED_CALCULATION_INPUT_IDS, ) }) }) describe('each calculated score includes the correct output result and correct score title', function () { - const outcome = zarit_12_calculation(min_response) + const outcome = zarit_12_calculation.calculate({ payload: min_response }) it('should calculate a single score', function () { - expect(outcome).toHaveLength(1) + expect(Object.keys(outcome)).toHaveLength(1) }) it('should have the correct calculation id', function () { - const configured_calculation_id = - get_result_ids_from_calculation_output(outcome) + const configured_calculation_id = Object.keys(outcome) expect(configured_calculation_id).toEqual(['ZARIT_12']) }) @@ -71,57 +64,55 @@ describe('zarit_12', function () { describe('each calculated score includes the correct formula and outputs the correct result', function () { describe('when a minimum response is passed', function () { it('should return the minimum score', function () { - const outcome = R.compose( - view_result(), - zarit_12_calculation, - )(min_response) + const outcome = zarit_12_calculation.calculate({ + payload: min_response, + }) - expect(outcome).toEqual(ZARIT_12_MIN_SCORE) + expect(outcome.ZARIT_12).toEqual(ZARIT_12_MIN_SCORE) }) }) describe('when a median response is passed', function () { it('should return the median score', function () { - const outcome = R.compose( - view_result(), - zarit_12_calculation, - )(median_response) + const outcome = zarit_12_calculation.calculate({ + payload: median_response, + }) - expect(outcome).toEqual(ZARIT_12_MEDIAN_SCORE) + expect(outcome.ZARIT_12).toEqual(ZARIT_12_MEDIAN_SCORE) }) }) describe('when a maximum response is passed', function () { it('should return the maximum score', function () { - const outcome = R.compose( - view_result(), - zarit_12_calculation, - )(max_response) + const outcome = zarit_12_calculation.calculate({ + payload: max_response, + }) - expect(outcome).toEqual(ZARIT_12_MAX_SCORE) + expect(outcome.ZARIT_12).toEqual(ZARIT_12_MAX_SCORE) }) }) describe('when a random response is passed', function () { it('should return the expected score', function () { - const outcome = R.compose( - view_result(), - zarit_12_calculation, - )(random_response) + const outcome = zarit_12_calculation.calculate({ + payload: random_response, + }) const EXPECTED_SCORE = 20 - expect(outcome).toEqual(EXPECTED_SCORE) + expect(outcome.ZARIT_12).toEqual(EXPECTED_SCORE) }) }) }) describe('a score is only calculated when all mandatory fields are entered', function () { describe('when an empty response is passed', function () { - it('should return undefined as the result', function () { - const outcome = R.compose(view_result(), zarit_12_calculation)({}) - - expect(outcome).toEqual(undefined) + it('should throw a ZodError', function () { + expect(() => + zarit_12_calculation.calculate({ + payload: {}, + }), + ).toThrow(ZodError) }) }) }) @@ -130,8 +121,10 @@ describe('zarit_12', function () { describe('when an answer is not a number', function () { it('should throw an ZodError', function () { expect(() => - zarit_12_calculation({ - ZARIT_12_Q01: "I'm not a number", + zarit_12_calculation.calculate({ + payload: { + ZARIT_12_Q01: "I'm not a number", + }, }), ).toThrow(ZodError) }) @@ -139,8 +132,10 @@ describe('zarit_12', function () { describe('when an answer is below one of the expected answers', function () { it('should throw an ZodError', function () { expect(() => - zarit_12_calculation({ - ZARIT_12_Q01: -1, + zarit_12_calculation.calculate({ + payload: { + ZARIT_12_Q01: -1, + }, }), ).toThrow(ZodError) }) @@ -148,8 +143,10 @@ describe('zarit_12', function () { describe('when an answer is above one of the expected answers', function () { it('should throw an ZodError', function () { expect(() => - zarit_12_calculation({ - ZARIT_12_Q01: 5, + zarit_12_calculation.calculate({ + payload: { + ZARIT_12_Q01: 5, + }, }), ).toThrow(ZodError) }) diff --git a/src/scores/zarit_12/zarit_12.ts b/src/scores/zarit_12/zarit_12.ts new file mode 100644 index 0000000..210379f --- /dev/null +++ b/src/scores/zarit_12/zarit_12.ts @@ -0,0 +1,17 @@ +import { ScoreType } from '../../types' +import { ZARIT_12_OUTPUT, ZARRIT_INPUTS } from './definition' +import { sum } from 'lodash' +export const zarit_12: ScoreType = + { + name: 'Zarit Caregiver Burden Interview - Short Form (ZBI-12 / ZARIT-12)', + readmeLocation: __dirname, + inputSchema: ZARRIT_INPUTS, + outputSchema: ZARIT_12_OUTPUT, + calculate: ({ data }) => { + const score = sum(Object.values(data)) + + return { + ZARIT_12: score, + } + }, + } diff --git a/todo/start_back_screening_tool/definition/start_back_output.ts b/todo/start_back_screening_tool/definition/start_back_output.ts deleted file mode 100644 index 8c91a61..0000000 --- a/todo/start_back_screening_tool/definition/start_back_output.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { CalculationOutputDefinition } from '../../../src/types/calculations.types' - -export const START_BACK_OUTPUT: CalculationOutputDefinition[] = [ - { - subresult_id: 'START_BACK_TOTAL', - label: { en: 'Total score', nl: 'Totale score' }, - type: 'number', - }, - { - subresult_id: 'START_BACK_SUBSCALE', - label: { en: 'Subscale score', nl: 'Sub uitslag (Q5-9)' }, - type: 'number', - }, - { - subresult_id: 'START_BACK_RISK_CLASSIFICATION', - label: { en: 'Risk classification', nl: 'Risicoprofiel' }, - type: 'number', - }, -] diff --git a/todo/start_back_screening_tool/helpers/calculate_total_and_subscale_score.ts b/todo/start_back_screening_tool/helpers/calculate_total_and_subscale_score.ts deleted file mode 100644 index ef16109..0000000 --- a/todo/start_back_screening_tool/helpers/calculate_total_and_subscale_score.ts +++ /dev/null @@ -1,110 +0,0 @@ -import R from 'ramda' - -import type { InputType } from '../../../src/types/calculations.types' -import { - inputIdLens, - rawInputValueLens, -} from '../../../lib/calculation_variants/api/input/lenses' -import { - do_all_required_inputs_have_a_valid_value, - get_valid_values, -} from '../../../lib/calculation_variants/simple_calculation' -import { MISSING_MESSAGE } from '../../../PARAMETERS' -import { is_numeric } from '../../../src/calculation_suite/calculations/shared_functions' - -//@ts-expect-error to do -const recode_input_9 = answer => { - const numeric_answer = Number(answer) - - if (is_numeric(numeric_answer)) { - const old_value_to_new_value_dict = { - '0': 0, - '1': 0, - '2': 0, - '3': 1, - '4': 1, - } - - //@ts-expect-error to do - return old_value_to_new_value_dict[numeric_answer] - } - - return MISSING_MESSAGE -} - -export const calculate_total_and_subscale_score = ( - inputs_with_answers: Array, -) => { - /** - * Recode question 9 - */ - const Q09_index = R.findIndex(input => R.view(inputIdLens, input) === 'Q09')( - inputs_with_answers, - ) - - const recoded_value = recode_input_9( - R.view(rawInputValueLens, inputs_with_answers[Q09_index]), - ) - - const start_back_inputs_with_q9_recoded = R.set( - R.compose(R.lensIndex(Q09_index), rawInputValueLens), - recoded_value, - inputs_with_answers, - ) - - const START_BACK_SUBSCALE_QUESTIONS = ['Q05', 'Q06', 'Q07', 'Q08', 'Q09'] - - const calclation_input_start_back_total_score = - start_back_inputs_with_q9_recoded - const calclation_input_start_back_psychosocial_subscale_score = R.filter( - input => START_BACK_SUBSCALE_QUESTIONS.includes(R.view(inputIdLens, input)), - start_back_inputs_with_q9_recoded, - ) - - let START_BACK_TOTAL_SCORE = MISSING_MESSAGE - let START_BACK_SUBSCALE_SCORE = MISSING_MESSAGE - - if ( - do_all_required_inputs_have_a_valid_value( - calclation_input_start_back_total_score, - ) - ) { - //@ts-expect-error to do - START_BACK_TOTAL_SCORE = R.sum( - get_valid_values(calclation_input_start_back_total_score), - ) - } - - if ( - do_all_required_inputs_have_a_valid_value( - calclation_input_start_back_psychosocial_subscale_score, - ) - ) { - //@ts-expect-error to do - START_BACK_SUBSCALE_SCORE = R.sum( - get_valid_values(calclation_input_start_back_psychosocial_subscale_score), - ) - } - - const SBT_MAX_TOTAL_SCORE = 9 - const SBT_MAX_SUBSCALE_SCORE = 5 - - return [ - { - id: 'START_BACK_TOTAL', - score: START_BACK_TOTAL_SCORE, - interpretation: { - en: `Maximum total score is ${SBT_MAX_TOTAL_SCORE}`, - nl: `Maximale score is ${SBT_MAX_TOTAL_SCORE}`, - }, - }, - { - id: 'START_BACK_SUBSCALE', - score: START_BACK_SUBSCALE_SCORE, - interpretation: { - en: `Maximum subscale score is ${SBT_MAX_SUBSCALE_SCORE}`, - nl: `Maximale sub score is ${SBT_MAX_SUBSCALE_SCORE}`, - }, - }, - ] -} diff --git a/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.test.ts b/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.test.ts deleted file mode 100644 index 8734eeb..0000000 --- a/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { expect } from 'chai' - -import { MISSING_MESSAGE } from '../../../../PARAMETERS' -import { classify_risk } from './classify_risk' - -describe('classify_risk', function () { - describe('when total score and subscale score are not missing', function () { - describe('when total score <= 3', function () { - it('should return low risk', function () { - const outcome = classify_risk({ total_score: 0, subscale_score: 0 }) - expect(outcome.interpretation?.en).toEqual('Low risk') - }) - }) - - describe('when total score >= 4', function () { - describe('and when subscale score <= 3', function () { - it('should return medium risk', function () { - const outcome = classify_risk({ total_score: 4, subscale_score: 0 }) - expect(outcome.interpretation?.en).toEqual('Medium risk') - }) - }) - - describe('and when subscale score >= 4', function () { - it('should return medium risk', function () { - const outcome = classify_risk({ total_score: 4, subscale_score: 4 }) - expect(outcome.interpretation?.en).toEqual('High risk') - }) - }) - }) - }) - describe('when total score or subscale score are missing', function () { - it('should return "Missing"', function () { - const outcome = classify_risk({}) - expect(outcome.score).toEqual(MISSING_MESSAGE) - }) - }) -}) diff --git a/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.ts b/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.ts deleted file mode 100644 index 64b1734..0000000 --- a/todo/start_back_screening_tool/helpers/classify_risk/classify_risk.ts +++ /dev/null @@ -1,85 +0,0 @@ -import R from 'ramda' - -import type { LabelType } from '../../../../src/types/localization.types' -import { MISSING_MESSAGE } from '../../../../PARAMETERS' - -export class CannotDetermineRiskClassification extends Error { - constructor({ - total_score, - subscale_score, - }: { - total_score: number - subscale_score: number - }) { - super( - `Cannot determine risk classification with a total score of ${total_score} and a subscale score of ${subscale_score}).`, - ) - } -} - -const classifications = { - LOW_RISK: { - score: 1, - interpretation: { en: 'Low risk', nl: 'Laag risico' }, - }, - MEDIUM_RISK: { - score: 2, - interpretation: { en: 'Medium risk', nl: 'Matig risico' }, - }, - HIGH_RISK: { - score: 3, - interpretation: { en: 'High risk', nl: 'Hoog risico' }, - }, - MISSING: { - score: MISSING_MESSAGE, - }, -} - -export const classify_risk = ({ - total_score, - subscale_score, -}: { - total_score?: number | string - subscale_score?: number | string -}): { - score: number | string - interpretation?: LabelType -} => { - if ( - R.isNil(total_score) || - R.isNil(subscale_score) || - total_score === MISSING_MESSAGE || - subscale_score === MISSING_MESSAGE - ) { - return classifications.MISSING - } - - const numeric_total_score = Number(total_score) - const numeric_subscale_score = Number(subscale_score) - - const TOTAL_SCORE_CUT_OFF = 3 - const SUBSCALE_SCORE_CUT_OFF = 3 - - if (numeric_total_score <= TOTAL_SCORE_CUT_OFF) { - return classifications.LOW_RISK - } - - if ( - numeric_total_score > TOTAL_SCORE_CUT_OFF && - numeric_subscale_score <= SUBSCALE_SCORE_CUT_OFF - ) { - return classifications.MEDIUM_RISK - } - - if ( - numeric_total_score > TOTAL_SCORE_CUT_OFF && - numeric_subscale_score > SUBSCALE_SCORE_CUT_OFF - ) { - return classifications.HIGH_RISK - } - - throw new CannotDetermineRiskClassification({ - total_score: numeric_total_score, - subscale_score: numeric_subscale_score, - }) -} diff --git a/todo/start_back_screening_tool/start_back_screening_tool.test.ts b/todo/start_back_screening_tool/start_back_screening_tool.test.ts deleted file mode 100644 index 236f4f1..0000000 --- a/todo/start_back_screening_tool/start_back_screening_tool.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { expect } from 'chai' - -import { ZodError } from '../../errors' -import { execute_test_calculation } from '../../lib/execute_test_calculation' -import { get_result_ids_from_calculation_output } from '../../lib/get_result_ids_from_calculation_output' -import { view_result } from '../../lib/view_result' -import { CALCULATIONS } from '../calculation_library' -import { get_input_ids_from_calculation_blueprint } from '../shared_functions' -import { - max_response, - median_response, - min_response, - random_response, -} from './__testdata__/start_back_test_responses' -import { START_BACK_INPUTS } from './definition/start_back_inputs' -import { start_back_screening_tool } from './start_back_screening_tool' - -const START_BACK_TOTAL_MIN_SCORE = 0 -const START_BACK_TOTAL_MEDIAN_SCORE = 4 -const START_BACK_TOTAL_MAX_SCORE = 9 - -const START_BACK_SUBSCALE_MIN_SCORE = 0 -const START_BACK_SUBSCALE_MEDIAN_SCORE = 0 -const START_BACK_SUBSCALE_MAX_SCORE = 5 - -const LOW_RISK = 1 -const MEDIUM_RISK = 2 -const HIGH_RISK = 3 - -const start_back_calculation = execute_test_calculation( - start_back_screening_tool, -) - -describe('start_back_screening_tool', function () { - it('start_back_screening_tool calculation function should be available as a calculation', function () { - expect(CALCULATIONS).toHaveProperty('start_back_screening_tool') - }) - - describe('the score includes the correct input fields', function () { - it('should use the correct input fields', function () { - const EXPECTED_CALCULATION_INPUT_IDS = [ - 'Q01', - 'Q02', - 'Q03', - 'Q04', - 'Q05', - 'Q06', - 'Q07', - 'Q08', - 'Q09', - ] - - const configured_calculation_input_ids = - get_input_ids_from_calculation_blueprint(START_BACK_INPUTS) - - expect(configured_calculation_input_ids).to.have.members( - EXPECTED_CALCULATION_INPUT_IDS, - ) - }) - }) - - describe('each calculated score includes the correct output result and correct score title', function () { - const outcome = start_back_calculation(min_response) - - it('should return 3 scores', function () { - expect(outcome).toHaveLength(3) - }) - - it('should have the correct calculation ids', function () { - const EXPECTED_CALCULATION_IDS = [ - 'START_BACK_TOTAL', - 'START_BACK_SUBSCALE', - 'START_BACK_RISK_CLASSIFICATION', - ] - - const extracted_calculation_ids_from_outcome = - get_result_ids_from_calculation_output(outcome) - - expect(EXPECTED_CALCULATION_IDS).toEqual( - extracted_calculation_ids_from_outcome, - ) - }) - }) - - describe('each calculated score includes the correct formula and outputs the correct result', function () { - describe('when a minimum response is passed', function () { - const outcome = start_back_calculation(min_response) - - it('should return the minimum total score', function () { - const TOTAL_SCORE = view_result('START_BACK_TOTAL')(outcome) - expect(TOTAL_SCORE).toEqual(START_BACK_TOTAL_MIN_SCORE) - }) - - it('should return the minimum subscale score', function () { - const SUBSCALE_SCORE = view_result('START_BACK_SUBSCALE')(outcome) - expect(SUBSCALE_SCORE).toEqual(START_BACK_SUBSCALE_MIN_SCORE) - }) - - it('should return low risk as the START_BACK_RISK_CLASSIFICATION', function () { - const RISK_SCORE = view_result('START_BACK_RISK_CLASSIFICATION')( - outcome, - ) - expect(RISK_SCORE).toEqual(LOW_RISK) - }) - }) - - describe('when a median response is passed', function () { - const outcome = start_back_calculation(median_response) - - it('should return the median total score', function () { - const TOTAL_SCORE = view_result('START_BACK_TOTAL')(outcome) - expect(TOTAL_SCORE).toEqual(START_BACK_TOTAL_MEDIAN_SCORE) - }) - - it('should return the median subscale score', function () { - const SUBSCALE_SCORE = view_result('START_BACK_SUBSCALE')(outcome) - expect(SUBSCALE_SCORE).toEqual(START_BACK_SUBSCALE_MEDIAN_SCORE) - }) - - it('should return medium risk as the START_BACK_RISK_CLASSIFICATION', function () { - const RISK_SCORE = view_result('START_BACK_RISK_CLASSIFICATION')( - outcome, - ) - expect(RISK_SCORE).toEqual(MEDIUM_RISK) - }) - }) - - describe('when a maximum response is passed', function () { - const outcome = start_back_calculation(max_response) - - it('should return the maximum total score', function () { - const TOTAL_SCORE = view_result('START_BACK_TOTAL')(outcome) - expect(TOTAL_SCORE).toEqual(START_BACK_TOTAL_MAX_SCORE) - }) - - it('should return the maximum subscale score', function () { - const SUBSCALE_SCORE = view_result('START_BACK_SUBSCALE')(outcome) - expect(SUBSCALE_SCORE).toEqual(START_BACK_SUBSCALE_MAX_SCORE) - }) - - it('should return high risk as the START_BACK_RISK_CLASSIFICATION', function () { - const RISK_SCORE = view_result('START_BACK_RISK_CLASSIFICATION')( - outcome, - ) - expect(RISK_SCORE).toEqual(HIGH_RISK) - }) - }) - - describe('when a random response is passed', function () { - const outcome = start_back_calculation(random_response) - - it('should return the expected total score', function () { - const TOTAL_SCORE = view_result('START_BACK_TOTAL')(outcome) - const EXPECTED_TOTAL_SCORE = 4 - expect(TOTAL_SCORE).toEqual(EXPECTED_TOTAL_SCORE) - }) - - it('should return the expected subscale score', function () { - const SUBSCALE_SCORE = view_result('START_BACK_SUBSCALE')(outcome) - const EXPECTED_SUBSCALE_SCORE = 2 - expect(SUBSCALE_SCORE).toEqual(EXPECTED_SUBSCALE_SCORE) - }) - - it('should return expected START_BACK_RISK_CLASSIFICATION', function () { - const RISK_SCORE = view_result('START_BACK_RISK_CLASSIFICATION')( - outcome, - ) - const EXPECTED_RISK_CLASSIFICATION = MEDIUM_RISK - expect(RISK_SCORE).toEqual(EXPECTED_RISK_CLASSIFICATION) - }) - }) - }) - - describe('a score is only calculated when all mandatory fields are entered', function () { - describe('when an empty response is passed', function () { - const outcome = start_back_calculation({}) - - it('should return MISSING_MESSAGE as the total score', function () { - const TOTAL_SCORE = view_result('START_BACK_TOTAL')(outcome) - expect(TOTAL_SCORE).toEqual(undefined) - }) - - it('should return MISSING_MESSAGE as the subscale score', function () { - const SUBSCALE_SCORE = view_result('START_BACK_SUBSCALE')(outcome) - expect(SUBSCALE_SCORE).toEqual(undefined) - }) - - it('should return MISSING_MESSAGE as the START_BACK_RISK_CLASSIFICATION', function () { - const RISK_SCORE = view_result('START_BACK_RISK_CLASSIFICATION')( - outcome, - ) - expect(RISK_SCORE).toEqual(undefined) - }) - }) - }) - - 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 ZodError', function () { - expect(() => - start_back_calculation({ - Q01: "I'm not a number", - }), - ).toThrow(ZodError) - }) - }) - describe('when an answer is below the allowed answers', function () { - it('should throw an ZodError', function () { - expect(() => - start_back_calculation({ - Q01: -1, - }), - ).toThrow(ZodError) - }) - }) - describe('when an answer is above the allowed answers', function () { - it('should throw an ZodError', function () { - expect(() => - start_back_calculation({ - Q01: 2, - }), - ).toThrow(ZodError) - }) - }) - }) -}) diff --git a/todo/start_back_screening_tool/start_back_screening_tool.ts b/todo/start_back_screening_tool/start_back_screening_tool.ts deleted file mode 100644 index e0b3f0d..0000000 --- a/todo/start_back_screening_tool/start_back_screening_tool.ts +++ /dev/null @@ -1,62 +0,0 @@ -import R from 'ramda' - -import type { - CalculationType, - InputType, - WIPCalculationResultType, -} from '../../src/types/calculations.types' -import { - scoreLens, - subscaleIdLens, -} from '../../lib/calculation_variants/api/subscale/lenses' -import { add_raw_values_to_inputs } from '../../lib/calculation_variants/simple_calculation' -import { create_calculation } from '../../lib/create_calculation' -import { START_BACK_INPUTS, START_BACK_OUTPUT } from './definition' -import { calculate_total_and_subscale_score } from './helpers/calculate_total_and_subscale_score' -import { classify_risk } from './helpers/classify_risk/classify_risk' - -const calculate_start_back_screening_tool_scores = ( - calculation_input: Array, -): WIPCalculationResultType => { - const calculated_scores = - calculate_total_and_subscale_score(calculation_input) - - const start_back_total_score = R.compose( - R.view(scoreLens), - R.find(score => R.view(subscaleIdLens, score) === 'START_BACK_TOTAL'), - )(calculated_scores) - - const start_back_subscale_score = R.compose( - R.view(scoreLens), - R.find(score => R.view(subscaleIdLens, score) === 'START_BACK_SUBSCALE'), - )(calculated_scores) - - const risk_classification = classify_risk({ - total_score: start_back_total_score, - subscale_score: start_back_subscale_score, - }) - - return [ - ...calculated_scores, - { - id: 'START_BACK_RISK_CLASSIFICATION', - score: risk_classification.score, - interpretation: risk_classification.interpretation, - }, - ] -} - -export const specific_steps_start_back_calc = [ - calculate_start_back_screening_tool_scores, - add_raw_values_to_inputs(START_BACK_INPUTS), -] - -export const start_back_screening_tool: CalculationType = create_calculation({ - calculation_name: 'Start Back Screening Tool', - readme_location: __dirname, - calculation_steps: specific_steps_start_back_calc, - calculation_definition: { - input_definition: START_BACK_INPUTS, - output_definition: START_BACK_OUTPUT, - }, -}) diff --git a/todo/visa/common/calculate_visa_score.ts b/todo/visa/common/calculate_visa_score.ts deleted file mode 100644 index e82ce86..0000000 --- a/todo/visa/common/calculate_visa_score.ts +++ /dev/null @@ -1,50 +0,0 @@ -import R from 'ramda' - -import type { - InputType, - WIPCalculationResultType, -} from '../../../src/types/calculations.types' -import { - inputRequiredLens, - rawInputValueLens, -} from '../../../lib/calculation_variants/api/input/lenses' -import { MISSING_MESSAGE } from '../../../PARAMETERS' -import { is_numeric } from '../../../src/calculation_suite/calculations/shared_functions' - -export const calculate_visa_score = - (calculation_name: string) => - (visa_inputs: Array): WIPCalculationResultType => { - //@ts-expect-error to do - const nbr_of_required_inputs = R.compose( - R.length, - R.filter(required => required === true), - R.map(input => R.view(inputRequiredLens, input)), - )(visa_inputs) - - const valid_answers = R.compose( - R.filter(is_numeric), - R.map(answer => Number(answer)), - R.map(input => R.view(rawInputValueLens, input)), - )(visa_inputs) - - //@ts-expect-error to do - if (valid_answers.length > nbr_of_required_inputs) - throw new Error( - `${calculation_name} could not be calculated because for question 8 there are too many answers passed. Question 8 is conditional, meaning that patient only needs to answer question 8A, 8B or 8C.`, - ) - - if (valid_answers.length !== nbr_of_required_inputs) - return [ - { - id: calculation_name, - score: MISSING_MESSAGE, - }, - ] - - return [ - { - id: calculation_name, - score: R.sum(valid_answers), - }, - ] - } diff --git a/todo/visa/visa_a/definition/visa_a_inputs.ts b/todo/visa/visa_a/definition/visa_a_inputs.ts deleted file mode 100644 index d6043ba..0000000 --- a/todo/visa/visa_a/definition/visa_a_inputs.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { InputType } from '../../../../src/types/calculations.types' -import { NumberInputType } from '../../../../src/types/calculations/inputs/calculation-inputs.types' - -const DEFAULT_type: NumberInputType = { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - { value: 5 }, - { value: 6 }, - { value: 7 }, - { value: 8 }, - { value: 9 }, - { value: 10 }, - ], -} - -export const VISA_A_INPUTS: Array = [ - { - input_id: 'VISA_A_Q1', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q02', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q03', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q04', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q05', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q06', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_A_Q07', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 4 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_A_Q08', - label: { - en: "Select what's applicable", - }, - type: { - type: 'string', - allowed_answers: [ - { - value: 'A', - label: { - en: 'I have no pain while undertaking Achilles tendon loading sports', - }, - }, - { - value: 'B', - label: { - en: 'I have pain while undertaking Achilles tendon loading sports but it does not stop me from completing the activity', - }, - }, - { - value: 'C', - label: { - en: 'I have pain that stops me from completing Achilles tendon loading sports', - }, - }, - ], - }, - required: true, - info: { - en: 'This question determines whether "VISA_A_Q08_A", "VISA_A_Q08_B" or "VISA_A_Q08_C" should be answered.', - }, - }, - { - input_id: 'VISA_A_Q08_A', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 7 }, - { value: 14 }, - { value: 21 }, - { value: 30 }, - ], - }, - required: false, - info: { en: 'Should only be scored when there is no pain during sports' }, - not_applicable_if: { - input_id: 'VISA_A_Q08', - value_is_one_of: ['B', 'C'], - }, - }, - { - input_id: 'VISA_A_Q08_B', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 4 }, - { value: 10 }, - { value: 14 }, - { value: 20 }, - ], - }, - required: false, // Should only be answered when there's pain during sports but does not need to quit sports activities because of it - info: { - en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', - }, - not_applicable_if: { - input_id: 'VISA_A_Q08', - value_is_one_of: ['A', 'C'], - }, - }, - { - input_id: 'VISA_A_Q08_C', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: false, // Should only be answered when there's pain during sports resulting in canceling the sports activities - info: { - en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', - }, - not_applicable_if: { - input_id: 'VISA_A_Q08', - value_is_one_of: ['A', 'B'], - }, - }, -] diff --git a/todo/visa/visa_a/definition/visa_a_output.ts b/todo/visa/visa_a/definition/visa_a_output.ts deleted file mode 100644 index 9cec77c..0000000 --- a/todo/visa/visa_a/definition/visa_a_output.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CalculationOutputDefinition } from '../../../../src/types/calculations.types' - -export const VISA_A_OUTPUT: CalculationOutputDefinition[] = [ - { - subresult_id: 'VISA_A', - label: { en: 'VISA-A Score' }, - type: 'number', - }, -] diff --git a/todo/visa/visa_a/visa_a.ts b/todo/visa/visa_a/visa_a.ts deleted file mode 100644 index a37406a..0000000 --- a/todo/visa/visa_a/visa_a.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { CalculationType } from '../../../src/types/calculations.types' -import { add_raw_values_to_inputs } from '../../../lib/calculation_variants/simple_calculation' -import { create_calculation } from '../../../lib/create_calculation' -import { calculate_visa_score } from '../common/calculate_visa_score' -import { VISA_A_INPUTS, VISA_A_OUTPUT } from './definition' - -const CALCULATION_NAME = 'VISA_A' - -export const specific_steps_visa_a_calc = [ - calculate_visa_score(CALCULATION_NAME), - add_raw_values_to_inputs(VISA_A_INPUTS), -] - -export const visa_a: CalculationType = create_calculation({ - calculation_name: - 'Victorian Institute of Sports Assessment – Achilles score (VISA-A)', - readme_location: __dirname, - calculation_steps: specific_steps_visa_a_calc, - calculation_definition: { - input_definition: VISA_A_INPUTS, - output_definition: VISA_A_OUTPUT, - }, -}) diff --git a/todo/visa/visa_g/definition/visa_g_inputs.ts b/todo/visa/visa_g/definition/visa_g_inputs.ts deleted file mode 100644 index ac64f08..0000000 --- a/todo/visa/visa_g/definition/visa_g_inputs.ts +++ /dev/null @@ -1,201 +0,0 @@ -import type { InputType } from '../../../../src/types/calculations.types' - -export const VISA_G_INPUTS: Array = [ - { - input_id: 'VISA_G_Q01', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - { value: 5 }, - { value: 6 }, - { value: 7 }, - { value: 8 }, - { value: 9 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q02', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q03', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q04', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q05', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q06', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q07', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 4 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_G_Q08', - label: { - en: "Select what's applicable", - }, - type: { - type: 'string', - allowed_answers: [ - { - value: 'A', - label: { - en: 'My hip pain is so severe that it will stop me from walking, shopping, running or other weight bearing exercise.', - }, - }, - { - value: 'B', - label: { - en: 'My hip pain is present with exercise, but it does not stop me from walking, shopping, running or other weight bearing type exercise.', - }, - }, - { - value: 'C', - label: { - en: 'If you have no pain while you undertake walking, shopping, running or other weight bearing type exercise.', - }, - }, - ], - }, - required: true, - info: { - en: 'This question determines whether "VISA_G_Q08_A", "VISA_G_Q08_B" or "VISA_G_Q08_C" should be answered.', - }, - }, - { - input_id: 'VISA_G_Q08_A', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: false, // Should only be answered when there is no pain during sports - info: { en: 'Should only be scored when there is no pain during sports' }, - not_applicable_if: { - input_id: 'VISA_G_Q08', - value_is_one_of: ['B', 'C'], - }, - }, - { - input_id: 'VISA_G_Q08_B', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 5 }, - { value: 10 }, - { value: 15 }, - { value: 20 }, - ], - }, - required: false, // Should only be answered when there's pain during sports but does not need to quit sports activities because of it - info: { - en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', - }, - not_applicable_if: { - input_id: 'VISA_G_Q08', - value_is_one_of: ['A', 'C'], - }, - }, - { - input_id: 'VISA_G_Q08_C', - type: { - type: 'number', - allowed_answers: [ - { value: 6 }, - { value: 12 }, - { value: 18 }, - { value: 24 }, - { value: 30 }, - ], - }, - required: false, // Should only be answered when there's pain during sports resulting in canceling the sports activities - info: { - en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', - }, - not_applicable_if: { - input_id: 'VISA_G_Q08', - value_is_one_of: ['A', 'B'], - }, - }, -] diff --git a/todo/visa/visa_g/definition/visa_g_output.ts b/todo/visa/visa_g/definition/visa_g_output.ts deleted file mode 100644 index bf1be61..0000000 --- a/todo/visa/visa_g/definition/visa_g_output.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CalculationOutputDefinition } from '../../../../src/types/calculations.types' - -export const VISA_G_OUTPUT: CalculationOutputDefinition[] = [ - { - subresult_id: 'VISA_G', - label: { en: 'VISA-G Score' }, - type: 'number', - }, -] diff --git a/todo/visa/visa_g/visa_g.ts b/todo/visa/visa_g/visa_g.ts deleted file mode 100644 index 68e10ee..0000000 --- a/todo/visa/visa_g/visa_g.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CalculationType } from '../../../src/types/calculations.types' -import { add_raw_values_to_inputs } from '../../../lib/calculation_variants/simple_calculation' -import { create_calculation } from '../../../lib/create_calculation' -import { calculate_visa_score } from '../common/calculate_visa_score' -import { VISA_G_INPUTS, VISA_G_OUTPUT } from './definition' - -const CALCULATION_NAME = 'VISA_G' - -export const specific_steps_visa_g_calc = [ - calculate_visa_score(CALCULATION_NAME), - add_raw_values_to_inputs(VISA_G_INPUTS), -] - -export const visa_g: CalculationType = create_calculation({ - calculation_name: - 'Victorian Institute of Sports Assessment - Gluteal Tendinopathy score (VISA-G)', - - readme_location: __dirname, - calculation_steps: specific_steps_visa_g_calc, - calculation_definition: { - input_definition: VISA_G_INPUTS, - output_definition: VISA_G_OUTPUT, - }, -}) diff --git a/todo/visa/visa_p/definition/visa_p_inputs.ts b/todo/visa/visa_p/definition/visa_p_inputs.ts deleted file mode 100644 index 886f5a3..0000000 --- a/todo/visa/visa_p/definition/visa_p_inputs.ts +++ /dev/null @@ -1,159 +0,0 @@ -import type { InputType } from '../../../../src/types/calculations.types' -import { NumberInputType } from '../../../../src/types/calculations/inputs/calculation-inputs.types' - -const DEFAULT_type: NumberInputType = { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - { value: 5 }, - { value: 6 }, - { value: 7 }, - { value: 8 }, - { value: 9 }, - { value: 10 }, - ], -} - -export const VISA_P_INPUTS: Array = [ - { - input_id: 'VISA_P_Q01', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q02', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q03', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q04', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q05', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q06', - type: DEFAULT_INPUT_TYPE, - required: true, - }, - { - input_id: 'VISA_P_Q07', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 4 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: true, - }, - { - input_id: 'VISA_P_Q08', - label: { - en: "Select what's applicable", - }, - type: { - type: 'string', - allowed_answers: [ - { - value: 'A', - label: { - en: 'I have no pain while undertaking sport', - }, - }, - { - value: 'B', - label: { - en: 'I have pain while undertaking sport but it does not stop me from completing the activity,', - }, - }, - { - value: 'C', - label: { - en: 'I have pain that stops me from completing sporting activities,', - }, - }, - ], - }, - required: true, - info: { - en: 'This question determines whether "VISA_P_Q08_A", "VISA_P_Q08_B" or "VISA_P_Q08_C" should be answered.', - }, - }, - { - input_id: 'VISA_P_Q08_A', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 7 }, - { value: 14 }, - { value: 21 }, - { value: 30 }, - ], - }, - required: false, // Should only be answered when there is no pain during sports - info: { en: 'Should only be scored when there is pain during sports' }, - not_applicable_if: { - input_id: 'VISA_P_Q08', - value_is_one_of: ['B', 'C'], - }, - }, - { - input_id: 'VISA_P_Q08_B', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 4 }, - { value: 10 }, - { value: 14 }, - { value: 20 }, - ], - }, - required: false, // Should only be answered when there's pain during sports but does not need to quit sports activities because of it - info: { - en: 'Should only be scored when there is pain during sports but does not need to quit sports activities because of it', - }, - not_applicable_if: { - input_id: 'VISA_P_Q08', - value_is_one_of: ['A', 'C'], - }, - }, - { - input_id: 'VISA_P_Q08_C', - type: { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 2 }, - { value: 5 }, - { value: 7 }, - { value: 10 }, - ], - }, - required: false, // Should only be answered when there's pain during sports resulting in canceling the sports activities - info: { - en: 'Should only be scored when there is pain during sports resulting in canceling the sports activities', - }, - not_applicable_if: { - input_id: 'VISA_P_Q08', - value_is_one_of: ['A', 'B'], - }, - }, -] diff --git a/todo/visa/visa_p/definition/visa_p_output.ts b/todo/visa/visa_p/definition/visa_p_output.ts deleted file mode 100644 index a5e47df..0000000 --- a/todo/visa/visa_p/definition/visa_p_output.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CalculationOutputDefinition } from '../../../../src/types/calculations.types' - -export const VISA_P_OUTPUT: CalculationOutputDefinition[] = [ - { - subresult_id: 'VISA_P', - label: { en: 'VISA-P Score' }, - type: 'number', - }, -] diff --git a/todo/visa/visa_p/visa_p.ts b/todo/visa/visa_p/visa_p.ts deleted file mode 100644 index 5d66100..0000000 --- a/todo/visa/visa_p/visa_p.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CalculationType } from '../../../src/types/calculations.types' -import { add_raw_values_to_inputs } from '../../../lib/calculation_variants/simple_calculation' -import { create_calculation } from '../../../lib/create_calculation' -import { calculate_visa_score } from '../common/calculate_visa_score' -import { VISA_P_INPUTS, VISA_P_OUTPUT } from './definition' - -export const CALCULATION_NAME = 'VISA_P' - -export const specific_steps_visa_p_calc = [ - calculate_visa_score(CALCULATION_NAME), - add_raw_values_to_inputs(VISA_P_INPUTS), -] - -export const visa_p: CalculationType = create_calculation({ - calculation_name: - 'Victorian Institute of Sport Assessment – Patella score (VISA-P)', - - readme_location: __dirname, - calculation_steps: specific_steps_visa_p_calc, - calculation_definition: { - input_definition: VISA_P_INPUTS, - output_definition: VISA_P_OUTPUT, - }, -}) diff --git a/todo/zarit_12/definition/zarit_12_inputs.ts b/todo/zarit_12/definition/zarit_12_inputs.ts deleted file mode 100644 index 5548b85..0000000 --- a/todo/zarit_12/definition/zarit_12_inputs.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { InputType } from '../../../src/types/calculations.types' -import { NumberInputType } from '../../../src/types/calculations/inputs/calculation-inputs.types' - -const type: NumberInputType = { - type: 'number', - allowed_answers: [ - { value: 0 }, - { value: 1 }, - { value: 2 }, - { value: 3 }, - { value: 4 }, - ], -} - -export const ZARRIT_INPUTS: Array = [ - { - input_id: 'ZARIT_12_Q01', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q02', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q03', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q04', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q05', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q06', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q07', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q08', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q09', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q10', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q11', - input_type, - required: true, - }, - { - input_id: 'ZARIT_12_Q12', - input_type, - required: true, - }, -] diff --git a/todo/zarit_12/definition/zarit_12_output.ts b/todo/zarit_12/definition/zarit_12_output.ts deleted file mode 100644 index 51d8438..0000000 --- a/todo/zarit_12/definition/zarit_12_output.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CalculationOutputDefinition } from '../../../src/types/calculations.types' - -export const ZARIT_12_OUTPUT: CalculationOutputDefinition[] = [ - { - subresult_id: 'ZARIT_12', - label: { en: 'Total score' }, - type: 'number', - }, -] diff --git a/todo/zarit_12/zarit_12.ts b/todo/zarit_12/zarit_12.ts deleted file mode 100644 index 7f7501e..0000000 --- a/todo/zarit_12/zarit_12.ts +++ /dev/null @@ -1,51 +0,0 @@ -import R from 'ramda' - -import type { - CalculationType, - InputType, - WIPCalculationResultType, -} from '../../src/types/calculations.types' -import { - add_raw_values_to_inputs, - do_all_required_inputs_have_a_valid_value, - get_valid_values, -} from '../../lib/calculation_variants/simple_calculation' -import { create_calculation } from '../../lib/create_calculation' -import { MISSING_MESSAGE } from '../../PARAMETERS' -import { ZARIT_12_OUTPUT, ZARRIT_INPUTS } from './definition' - -const calculate_zarit_12_score = ( - calculation_input: Array, -): WIPCalculationResultType => { - if (do_all_required_inputs_have_a_valid_value(calculation_input)) - return [ - { - id: 'ZARIT_12', - score: R.sum(get_valid_values(calculation_input)), - }, - ] - - return [ - { - id: 'ZARIT_12', - score: MISSING_MESSAGE, - }, - ] -} - -export const specific_steps_ZARIT_12_calc = [ - calculate_zarit_12_score, - add_raw_values_to_inputs(ZARRIT_INPUTS), -] - -export const zarit_12: CalculationType = create_calculation({ - calculation_name: - 'Zarit Caregiver Burden Interview - Short Form (ZBI-12 / ZARIT-12)', - - readme_location: __dirname, - calculation_steps: specific_steps_ZARIT_12_calc, - calculation_definition: { - input_definition: ZARRIT_INPUTS, - output_definition: ZARIT_12_OUTPUT, - }, -})