Skip to content

Commit

Permalink
feat(calc): oswestry
Browse files Browse the repository at this point in the history
  • Loading branch information
nckhell committed Jan 9, 2025
1 parent 432debb commit 15e0e20
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 237 deletions.
File renamed without changes.
File renamed without changes.
98 changes: 98 additions & 0 deletions src/scores/oswestry/definition/oswestry_inputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { z } from 'zod'
import { EnumNumberInputType, ScoreInputSchemaType } from '../../../types'

export const type = {
type: z
.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4),
z.literal(5),
])
.optional(),
uiOptions: {
options: [
{ value: 0 },
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 },
{ value: 5 },
],
},
} satisfies EnumNumberInputType

export const OSWESTRY_INPUTS = {
'1_pain': {
label: {
en: 'Pain intensity',
nl: '',
},
...type,
},
'2_personal_care': {
label: {
en: 'Personal care (washing, dressing etc)',
nl: '',
},
...type,
},
'3_lifting': {
label: {
en: 'Lifting',
nl: '',
},
...type,
},
'4_running': {
label: {
en: 'Walking',
nl: '',
},
...type,
},
'5_sitting': {
label: {
en: 'Sitting',
nl: '',
},
...type,
},
'6_standing': {
label: {
en: 'Standing',
nl: '',
},
...type,
},
'7_sleep': {
label: {
en: 'Sleeping',
nl: '',
},
...type,
},
'8_sex_life': {
label: {
en: 'Sex life (if applicable)',
nl: '',
},
...type,
},
'9_social_life': {
label: {
en: 'Social life',
nl: '',
},
...type,
},
'10_travel': {
label: {
en: 'Travelling',
nl: '',
},
...type,
},
} satisfies ScoreInputSchemaType
9 changes: 9 additions & 0 deletions src/scores/oswestry/definition/oswestry_output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod'
import { ScoreOutputSchemaType } from '../../../types'

export const OSWESTRY_OUTPUT = {
OSWESTRY: {
label: { en: 'Oswestry score' },
type: z.number(),
},
} satisfies ScoreOutputSchemaType
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { expect } from 'chai'
import R from 'ramda'

import { ZodError } from 'zod'
import { Score } from '../../classes'
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 { ScoreLibrary } from '../library'
import { get_input_ids_from_calculation_blueprint } from '../../src/calculation_suite/calculations/shared_functions'
import {
max_response,
median_response,
min_response,
random_response,
response_with_one_missing_item,
} from './__testdata__/oswestry_form_responses'
import { OSWESTRY_INPUTS } from './definition/oswestry_inputs'
// eslint-disable-next-line sort-imports
import { oswestry, OSWESTRY_CALCULATION_ID } from './oswestry'
import { oswestry } from './oswestry'

const OSWESTRY_MIN_SCORE = 0
const OSWESTRY_MEDIAN_SCORE = 50
const OSWESTRY_MAX_SCORE = 100

const oswestry_calculation = execute_test_calculation(oswestry)
const oswestry_calculation = new Score(oswestry)

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

describe('the score includes the correct input fields', function () {
Expand All @@ -44,25 +36,25 @@ describe('oswestry', function () {
'10_travel',
]

const configured_calculation_input_ids =
get_input_ids_from_calculation_blueprint(OSWESTRY_INPUTS)
const configured_calculation_input_ids = Object.keys(
oswestry_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 = oswestry_calculation(min_response)
const outcome = oswestry_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(['OSWESTRY'])
})
Expand All @@ -71,70 +63,60 @@ describe('oswestry', function () {
describe('each calculated score includes the correct formula and outputs the correct result', function () {
describe('when called with a minimum response', function () {
it('should return the minimum score', function () {
const score = R.compose(
view_result(OSWESTRY_CALCULATION_ID),
oswestry_calculation,
)(min_response)

expect(score).toEqual(OSWESTRY_MIN_SCORE)
const score = oswestry_calculation.calculate({ payload: min_response })
expect(score.OSWESTRY).toEqual(OSWESTRY_MIN_SCORE)
})
})

describe('when called with a median response', function () {
it('should return the median score', function () {
const score = R.compose(
view_result(OSWESTRY_CALCULATION_ID),
oswestry_calculation,
)(median_response)

expect(score).toEqual(OSWESTRY_MEDIAN_SCORE)
const score = oswestry_calculation.calculate({
payload: median_response,
})
expect(score.OSWESTRY).toEqual(OSWESTRY_MEDIAN_SCORE)
})
})

describe('when called with a maximum response', function () {
it('should return the maximum score', function () {
const score = R.compose(
view_result(OSWESTRY_CALCULATION_ID),
oswestry_calculation,
)(max_response)
const score = oswestry_calculation.calculate({
payload: max_response,
})

expect(score).toEqual(OSWESTRY_MAX_SCORE)
expect(score.OSWESTRY).toEqual(OSWESTRY_MAX_SCORE)
})
})

describe('when called with a random response', function () {
it('should return the expected score', function () {
const score = R.compose(
view_result(OSWESTRY_CALCULATION_ID),
oswestry_calculation,
)(random_response)

const score = oswestry_calculation.calculate({
payload: random_response,
})
const EXPECTED_SCORE = 60

expect(score).toEqual(EXPECTED_SCORE)
expect(score.OSWESTRY).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 result = R.compose(view_result(), oswestry_calculation)({})
it('should return null as the result', function () {
const result = oswestry_calculation.calculate({ payload: {} })

expect(result).toEqual(undefined)
expect(result.OSWESTRY).toEqual(null)
})
})

describe('when called with a response with one missing item', function () {
it('should return the expected score', function () {
const score = R.compose(
view_result(OSWESTRY_CALCULATION_ID),
oswestry_calculation,
)(response_with_one_missing_item)
const score = oswestry_calculation.calculate({
payload: response_with_one_missing_item,
})

const EXPECTED_SCORE = 36 // actual score is 35.5 but number is rounded up

expect(score).toEqual(EXPECTED_SCORE)
expect(score.OSWESTRY).toEqual(EXPECTED_SCORE)
})
})
})
Expand All @@ -143,26 +125,32 @@ describe('oswestry', function () {
describe('when an answer is not a number', function () {
it('should throw an ZodError', function () {
expect(() =>
oswestry_calculation({
'1_pain': "I'm not a number",
oswestry_calculation.calculate({
payload: {
'1_pain': "I'm not a number",
},
}),
).toThrow(ZodError)
})
})
describe('when an answer is below one of the expected answers', function () {
it('should throw an ZodError', function () {
expect(() =>
oswestry_calculation({
'1_pain': -1,
oswestry_calculation.calculate({
payload: {
'1_pain': -1,
},
}),
).toThrow(ZodError)
})
})
describe('when an answer is above one of the expected answers', function () {
it('should throw an ZodError', function () {
expect(() =>
oswestry_calculation({
'1_pain': 6,
oswestry_calculation.calculate({
payload: {
'1_pain': 6,
},
}),
).toThrow(ZodError)
})
Expand Down
33 changes: 33 additions & 0 deletions src/scores/oswestry/oswestry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ScoreType } from '../../types'
import { OSWESTRY_OUTPUT, OSWESTRY_INPUTS } from './definition'
import { sum } from 'lodash'

export const oswestry: ScoreType<
typeof OSWESTRY_INPUTS,
typeof OSWESTRY_OUTPUT
> = {
name: 'Oswestry',
readmeLocation: __dirname,
inputSchema: OSWESTRY_INPUTS,
outputSchema: OSWESTRY_OUTPUT,
calculate: ({ data }) => {
const MIN_ANSWERS_NEEDED_TO_CALCULATE_SCORE =
Object.keys(OSWESTRY_INPUTS).length - 1

const validInputs = Object.values(data).filter(v => v !== undefined)

if (validInputs.length < MIN_ANSWERS_NEEDED_TO_CALCULATE_SCORE) {
return {
OSWESTRY: null,
}
}

const MAX_ANSWER = 5
const MAX_SCORE = validInputs.length * MAX_ANSWER
const score = (sum(validInputs) / MAX_SCORE) * 100

return {
OSWESTRY: Math.round(score),
}
},
}
Loading

0 comments on commit 15e0e20

Please sign in to comment.