From 40e2978032c9ef9d38401ecd7aa526f2af3d4d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Perpignane?= Date: Mon, 16 Oct 2023 23:28:39 +0200 Subject: [PATCH] test improvements, longterm quick insulin service, react native paper --- .vscode/launch.json | 24 +++ package-lock.json | 184 +++++++++++++++- package.json | 3 +- src/components/DbButtonComponent.tsx | 6 +- .../DbNumericTextInputComponent.tsx | 2 +- src/components/DbTextInputComponent.tsx | 3 +- .../__tests__/DbButtonComponent.test.tsx | 25 ++- .../__tests__/DbTextInputComponent.test.tsx | 49 +++++ src/core/Acetone.ts | 4 + src/core/BasalInsulin.ts | 42 +++- src/core/QuickInsulin.ts | 66 +++++- src/core/TrendService.ts | 25 +++ src/core/__tests__/BasalInsulin.test.ts | 43 +++- src/core/__tests__/QuickInsulin.test.ts | 197 ++++++++++++------ src/core/__tests__/TrendService.test.ts | 27 +++ src/core/basal_insulin_adaptation_config.json | 14 ++ src/core/quick_insulin_conditions.json | 34 --- ..._insulin_longterm_adaptation_criteria.json | 14 ++ ..._insulin_punctual_adaptation_criteria.json | 47 +++++ src/screens/BasalAdaptationScreen.tsx | 68 ++++-- src/screens/PunctualGlycemiaScreen.tsx | 30 ++- src/screens/QuickInsulinAdaptationScreen.tsx | 0 .../__tests__/PunctualGlycemiaScreen.test.tsx | 10 +- 23 files changed, 770 insertions(+), 147 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/components/__tests__/DbTextInputComponent.test.tsx create mode 100644 src/core/TrendService.ts create mode 100644 src/core/__tests__/TrendService.test.ts create mode 100644 src/core/basal_insulin_adaptation_config.json delete mode 100644 src/core/quick_insulin_conditions.json create mode 100644 src/core/quick_insulin_longterm_adaptation_criteria.json create mode 100644 src/core/quick_insulin_punctual_adaptation_criteria.json create mode 100644 src/screens/QuickInsulinAdaptationScreen.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..dc95908 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug tests", + "skipFiles": [ + "/**" + ], + "env": {"CI": "true"}, + //"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", + "args": ["--verbose", "-i", "--no-cache", "${workspaceFolder}/src/components/__tests__/DbTextInputComponent.test.tsx"], + "console": "integratedTerminal", + "program": "${workspaceRoot}/node_modules/.bin/jest", + // "outFiles": [ + // "${workspaceFolder}/**/*.js" + // ] + } + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8836bef..3fae4e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "DiabetorRnGui", + "name": "diabetor-gui", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "DiabetorRnGui", + "name": "diabetor-gui", "version": "0.0.1", "dependencies": { "@react-navigation/native": "^6.1.8", @@ -13,8 +13,11 @@ "@types/jest": "^29.5.5", "react": "18.2.0", "react-native": "0.72.5", + "react-native-paper": "^5.10.6", + "react-native-picker-select": "^8.1.0", "react-native-safe-area-context": "^4.7.2", - "react-native-screens": "^3.25.0" + "react-native-screens": "^3.25.0", + "react-native-select-dropdown": "^3.4.0" }, "devDependencies": { "@babel/core": "^7.20.0", @@ -2008,6 +2011,26 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@callstack/react-theme-provider": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", + "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==", + "dependencies": { + "deepmerge": "^3.2.0", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@callstack/react-theme-provider/node_modules/deepmerge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", + "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -5571,6 +5594,15 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -5584,6 +5616,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", @@ -7510,6 +7551,19 @@ "node": ">= 8" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -10032,6 +10086,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -11895,6 +11954,53 @@ "react": "18.2.0" } }, + "node_modules/react-native-paper": { + "version": "5.10.6", + "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.10.6.tgz", + "integrity": "sha512-n5r2S53/pGpHRyQV/n75W8ibU+VyamMGSqKWGSti9ZmChcgEWC0s4o/TnpUvmAKNZCQwcYwKSLrmMq5wR5nUNA==", + "dependencies": { + "@callstack/react-theme-provider": "^3.0.9", + "color": "^3.1.2", + "use-latest-callback": "^0.1.5" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-safe-area-context": "*", + "react-native-vector-icons": "*" + } + }, + "node_modules/react-native-picker-select": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-8.1.0.tgz", + "integrity": "sha512-iLsLv2OEWpXnQMDYJS6du5Cl1HTHy887n60Yp5OOiMny0TDB9w5CfxTUYWtpsvJJrUa/Yrv+1NMQiJy7IA4ETw==", + "dependencies": { + "@react-native-picker/picker": "^1.8.3", + "lodash.isequal": "^4.5.0" + } + }, + "node_modules/react-native-picker-select/node_modules/@react-native-picker/picker": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-1.16.8.tgz", + "integrity": "sha512-pacdQDX6V6EmjF+HoiIh6u++qx4mTK0WnhgUHRc01B+Qt5eoeUwseBqmqfTSXTx/aHDEd6PiIw7UGvKgFoqgFQ==", + "peerDependencies": { + "react": "16 || 17", + "react-native": ">=0.57" + } + }, + "node_modules/react-native-picker-select/node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-native-safe-area-context": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.7.2.tgz", @@ -11917,6 +12023,65 @@ "react-native": "*" } }, + "node_modules/react-native-select-dropdown": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-native-select-dropdown/-/react-native-select-dropdown-3.4.0.tgz", + "integrity": "sha512-9xK1z4tYpwMxp3hsM0y1+xfqq/JoMh+l7sBsJ81b2eikFwT6ZbndFBqupQXrsQ1akce9hgC/10Nkzj0LjVjeXQ==" + }, + "node_modules/react-native-vector-icons": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.0.0.tgz", + "integrity": "sha512-efMOVbZIebY8RszZPzPBoXi9pvD/NFYmjIDYxRoc9LYSzV8rMJtT8FfcO2hPu85Rn2x9xktha0+qn0B7EqMAcQ==", + "peer": true, + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "peer": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "peer": true, + "engines": { + "node": ">=10" + } + }, "node_modules/react-native/node_modules/@jest/types": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", @@ -12570,6 +12735,19 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", diff --git a/package.json b/package.json index 7d2f6f6..a30ef63 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "lint": "eslint .", "lint-fix": "eslint --fix .", "start": "react-native start", - "test": "jest --coverage" + "test": "jest --coverage --verbose" }, "dependencies": { "@react-navigation/native": "^6.1.8", @@ -16,6 +16,7 @@ "@types/jest": "^29.5.5", "react": "18.2.0", "react-native": "0.72.5", + "react-native-paper": "^5.10.6", "react-native-safe-area-context": "^4.7.2", "react-native-screens": "^3.25.0" }, diff --git a/src/components/DbButtonComponent.tsx b/src/components/DbButtonComponent.tsx index 0944b1c..7ed51df 100644 --- a/src/components/DbButtonComponent.tsx +++ b/src/components/DbButtonComponent.tsx @@ -1,6 +1,7 @@ import React from 'react' -import {Pressable, PressableProps, StyleSheet, Text} from 'react-native' +import {Pressable, PressableProps, StyleSheet} from 'react-native' +import {Text} from 'react-native-paper' interface DbButtonProps extends PressableProps { title: string @@ -9,14 +10,13 @@ interface DbButtonProps extends PressableProps { export function DbButton(props: DbButtonProps): JSX.Element { return ( [ { backgroundColor: pressed ? 'darkturquoise' : 'blue', }, styles.dbButton, ]} - testID={props.testID}> + {...props}> {props.title} ) diff --git a/src/components/DbNumericTextInputComponent.tsx b/src/components/DbNumericTextInputComponent.tsx index 4dab9e6..01fada1 100644 --- a/src/components/DbNumericTextInputComponent.tsx +++ b/src/components/DbNumericTextInputComponent.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {TextInputProps} from 'react-native' +import {TextInputProps} from 'react-native-paper' import {DbTextInput} from './DbTextInputComponent' export function DbNumericTextInput(props: TextInputProps): JSX.Element { diff --git a/src/components/DbTextInputComponent.tsx b/src/components/DbTextInputComponent.tsx index b36d3e1..742d1fb 100644 --- a/src/components/DbTextInputComponent.tsx +++ b/src/components/DbTextInputComponent.tsx @@ -1,5 +1,6 @@ import React from 'react' -import {StyleSheet, TextInput, TextInputProps} from 'react-native' +import {TextInput, TextInputProps} from 'react-native-paper' +import {StyleSheet} from 'react-native' export function DbTextInput(props: TextInputProps): JSX.Element { let localProps diff --git a/src/components/__tests__/DbButtonComponent.test.tsx b/src/components/__tests__/DbButtonComponent.test.tsx index 33349db..3eccdf8 100644 --- a/src/components/__tests__/DbButtonComponent.test.tsx +++ b/src/components/__tests__/DbButtonComponent.test.tsx @@ -12,10 +12,33 @@ import {it, expect} from '@jest/globals' import {fireEvent, render, screen} from '@testing-library/react-native' it('renders correctly', async () => { - const Sample = () => + let i = 0 + + const Sample = () => ( + i++} /> + ) render() + let punctualButton = screen.getByTestId('coucou') expect(punctualButton).toBeDefined() + + fireEvent(punctualButton, 'onPress') + expect(i).toEqual(1) +}) + +it('renders correctly when pressed', async () => { + let i = 0 + + const Sample = () => ( + i++} /> + ) + + render() + + let punctualButton = screen.getByTestId('coucou') + expect(punctualButton).toBeDefined() + fireEvent(punctualButton, 'onPress') + expect(i).toEqual(1) }) diff --git a/src/components/__tests__/DbTextInputComponent.test.tsx b/src/components/__tests__/DbTextInputComponent.test.tsx new file mode 100644 index 0000000..18ce638 --- /dev/null +++ b/src/components/__tests__/DbTextInputComponent.test.tsx @@ -0,0 +1,49 @@ +/** + * @format + */ +import 'react-native' +import React from 'react' + +// Note: import explicitly to use the types shiped with jest. +import {it, expect} from '@jest/globals' + +// Note: test renderer must be required after react-native. +import {fireEvent, render, screen} from '@testing-library/react-native' +import {DbTextInput} from '../DbTextInputComponent' + +it('renders correctly', async () => { + const Sample = () => + + render() + + let dbTextInput = screen.getByTestId('coucou') + expect(dbTextInput).toBeDefined() +}) + +it('changes text as expected', async () => { + let inputText = '' + + const Sample = () => ( + (inputText = nextText)} + /> + ) + + render() + + let dbTextInput = screen.getByTestId('coucou') + fireEvent.changeText(dbTextInput, 'hello world') + expect(inputText).toEqual('hello world') +}) + +it('takes custom styles', async () => { + const Sample = () => ( + + ) + + render() + + let dbTextInput = screen.getByTestId('coucou') + expect(dbTextInput).toBeDefined() +}) diff --git a/src/core/Acetone.ts b/src/core/Acetone.ts index 66c5a72..6898521 100644 --- a/src/core/Acetone.ts +++ b/src/core/Acetone.ts @@ -20,4 +20,8 @@ export class Acetone { return this.acetoneAdaptationByLevel.get(acetoneLevel) } + + getAcetoneLevels(): number[] { + return acetoneCriteria.map(ac => ac.level) + } } diff --git a/src/core/BasalInsulin.ts b/src/core/BasalInsulin.ts index cca73cb..ba2238a 100644 --- a/src/core/BasalInsulin.ts +++ b/src/core/BasalInsulin.ts @@ -1,6 +1,44 @@ +import {Trend, TrendService} from './TrendService' +import basalAdaptationCriteria from './basal_insulin_adaptation_config.json' + +export class NightGlycemiaInterval { + private _beforeSleepingGlycemia: number + private _wakeUpGlycemia: number + private _trend: Trend + + constructor(beforeSleepingGlycemia: number, wakeUpGlycemia: number) { + this._beforeSleepingGlycemia = beforeSleepingGlycemia + this._wakeUpGlycemia = wakeUpGlycemia + + let delta = wakeUpGlycemia - beforeSleepingGlycemia + + if (delta > 0.3) { + this._trend = Trend.UP + } else if (delta < -0.3) { + this._trend = Trend.DOWN + } else { + this._trend = Trend.STABLE + } + } + + public get trend() { + return this._trend + } + + toString() { + return `{before=${this._beforeSleepingGlycemia},after=${this._wakeUpGlycemia},trend=${this._trend}` + } +} + class BasalInsulin { - computeAdaptation(): number { - return 0 + computeAdaptation(glycemiaIntervals: NightGlycemiaInterval[]): number { + let trendService = new TrendService() + let trend = trendService.findTrend(glycemiaIntervals) + let result = basalAdaptationCriteria.find(c => c.trend === trend) + if (!result) { + throw new Error(`No result found for trend ${trend}`) + } + return result.adaptation } } diff --git a/src/core/QuickInsulin.ts b/src/core/QuickInsulin.ts index 87ac512..f16c927 100644 --- a/src/core/QuickInsulin.ts +++ b/src/core/QuickInsulin.ts @@ -1,9 +1,46 @@ import {Acetone} from './Acetone' -import conditions from './quick_insulin_conditions.json' +import punctualAdaptationCriteria from './quick_insulin_punctual_adaptation_criteria.json' +import longtermAdaptationCriteria from './quick_insulin_longterm_adaptation_criteria.json' +import {Trend, TrendService} from './TrendService' + +export interface GlycemiaObjective { + min: number, + max: number +} + +export class MealGlycemiaMeasure { + private _afterMealGlycemia: number + private _trend: Trend + + constructor(afterMealGlycemia: number, objective: GlycemiaObjective) { + this._afterMealGlycemia = afterMealGlycemia + let {min, max} = objective + this._trend = this.computeTrend({min, max}) + } + + private computeTrend(glycemiaObjective: GlycemiaObjective) { + + if (this._afterMealGlycemia > glycemiaObjective.max) { + return Trend.UP + } + else if (this._afterMealGlycemia < glycemiaObjective.min) { + return Trend.DOWN + } + else { + return Trend.STABLE + } + } + + public get trend() { + return this._trend + } + +} export class PuntualAdaptationResult { private _glycemiaAdaptation: number private _acetoneAdaptation: number + private _totalAdaptation: number private _checkAcetone: boolean constructor( @@ -14,6 +51,7 @@ export class PuntualAdaptationResult { this._glycemiaAdaptation = glycemiaAdapation this._acetoneAdaptation = acetoneAdaptation this._checkAcetone = checkAcetone + this._totalAdaptation = acetoneAdaptation + glycemiaAdapation } public get glycemiaAdaptation() { @@ -25,7 +63,7 @@ export class PuntualAdaptationResult { } public get totalAdaptation() { - return this.acetoneAdaptation + this.glycemiaAdaptation + return this._totalAdaptation } public get checkAcetone() { @@ -34,11 +72,12 @@ export class PuntualAdaptationResult { } export class QuickInsulin { - computePunctualAdaptation( + + public computePunctualAdaptation( glycemiaLevel: number, acetoneLevel?: number, ): PuntualAdaptationResult { - let glycemiaCondition = conditions.find(element => { + let glycemiaCondition = punctualAdaptationCriteria.find(element => { let min = element.min == null ? Number.MIN_VALUE : element.min let max = element.max == null ? Number.MAX_VALUE : element.max @@ -57,7 +96,7 @@ export class QuickInsulin { let acetoneAdaptation = 0 - if (acetoneLevel != null) { + if (acetoneLevel !== undefined) { let acetone = new Acetone() acetoneAdaptation = acetone.computeAdaptation(acetoneLevel) } @@ -70,6 +109,23 @@ export class QuickInsulin { : glycemiaCondition.checkAcetone, ) } + + public computeLongtermAdaptation = ( + glycemiaIntervals: MealGlycemiaMeasure[], + ): number => { + let trendService = new TrendService() + let trend = trendService.findTrend(glycemiaIntervals) + let result = longtermAdaptationCriteria.find(c => c.trend === trend) + if (!result) { + throw new Error(`No result found for trend ${trend}`) + } + return result.adaptation + } + + findObjectiveCriterion = () => { + return punctualAdaptationCriteria.find(c => c.objective) + } + } export class AcetoneNeededError extends Error { diff --git a/src/core/TrendService.ts b/src/core/TrendService.ts new file mode 100644 index 0000000..6ec939d --- /dev/null +++ b/src/core/TrendService.ts @@ -0,0 +1,25 @@ +export enum Trend { + UP = 'UP', + DOWN = 'DOWN', + STABLE = 'STABLE', +} + +interface TrendInterval { + trend: Trend +} + +export class TrendService { + findTrend(intervals: TrendInterval[]): Trend { + if (intervals.length < 3) { + throw new Error('Please provide at least 3 intervals') + } + + let trends = new Set(intervals.map(gi => gi.trend)) + + if (trends.size === 1) { + return trends.values().next().value + } + + return Trend.STABLE + } +} diff --git a/src/core/__tests__/BasalInsulin.test.ts b/src/core/__tests__/BasalInsulin.test.ts index 3718ab1..65bd5c1 100644 --- a/src/core/__tests__/BasalInsulin.test.ts +++ b/src/core/__tests__/BasalInsulin.test.ts @@ -1,8 +1,39 @@ -import {test, expect} from '@jest/globals' -import BasalInsulin from '../BasalInsulin' +import {expect} from '@jest/globals' +import BasalInsulin, {NightGlycemiaInterval} from '../BasalInsulin' -test('basal insulin adaptation is zero if no trend found', () => { - let basalInsulin = new BasalInsulin() - - expect(basalInsulin.computeAdaptation()).toEqual(0) +describe('basal glycemia adaptation', () => { + it.each([ + [ + [ + new NightGlycemiaInterval(1, 1.2), + new NightGlycemiaInterval(1.1, 1.3), + new NightGlycemiaInterval(2, 2.1), + ], + 0, + ], + [ + [ + new NightGlycemiaInterval(1, 1.4), + new NightGlycemiaInterval(1.1, 1.6), + new NightGlycemiaInterval(2, 2.5), + ], + +2, + ], + [ + [ + new NightGlycemiaInterval(1.4, 1.0), + new NightGlycemiaInterval(1.6, 1.1), + new NightGlycemiaInterval(2.5, 2), + ], + -2, + ], + ])( + 'when glycemia inputs are %j, adaptation is %i', + (glycemias, expectedAdaptation) => { + let basalInsulin = new BasalInsulin() + expect(basalInsulin.computeAdaptation(glycemias)).toEqual( + expectedAdaptation, + ) + }, + ) }) diff --git a/src/core/__tests__/QuickInsulin.test.ts b/src/core/__tests__/QuickInsulin.test.ts index f1fc738..3a92809 100644 --- a/src/core/__tests__/QuickInsulin.test.ts +++ b/src/core/__tests__/QuickInsulin.test.ts @@ -1,8 +1,8 @@ import {test, expect} from '@jest/globals' -import {AcetoneNeededError, QuickInsulin} from '../QuickInsulin' +import {AcetoneNeededError, MealGlycemiaMeasure, QuickInsulin} from '../QuickInsulin' jest.mock( - '../quick_insulin_conditions.json', + '../quick_insulin_punctual_adaptation_criteria.json', () => [ { max: 0.7, @@ -41,66 +41,139 @@ jest.mock( {virtual: true}, ) -test('adaptation for glycemia measure 0.8 is 0', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 0.8 - - expect( - quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, - ).toEqual(0) -}) - -test('adaptation for glycemia measure 2.3 is +2', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 2.3 - - expect( - quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, - ).toEqual(2) -}) - -test('adaptation for glycemia measure 1.8 is +1', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 1.8 - - expect( - quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, - ).toEqual(1) -}) - -test('exception when condition not found', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 2.1 - - const t = () => quickInsulin.computePunctualAdaptation(glycemiaLevel) - - expect(t).toThrowError() -}) - -test('adaptation for glycemia measure 2.6 and acetone level = 0 is +3', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 2.6 - let acetoneLevel = 0 +jest.mock( + '../quick_insulin_longterm_adaptation_criteria', + () => [ + { + trend: 'UP', + adaptation: +2, + }, + { + trend: 'DOWN', + adaptation: -2, + }, + { + trend: 'STABLE', + adaptation: 0, + }, + ], + {virtual: true}, +) - expect( - quickInsulin.computePunctualAdaptation(glycemiaLevel, acetoneLevel) - .totalAdaptation, - ).toEqual(3) +describe('punctual adaptation', () => { + test('adaptation for glycemia measure 0.8 is 0', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 0.8 + + expect( + quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, + ).toEqual(0) + }) + + test('adaptation for glycemia measure 2.3 is +2', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 2.3 + + expect( + quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, + ).toEqual(2) + }) + + test('adaptation for glycemia measure 1.8 is +1', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 1.8 + + expect( + quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation, + ).toEqual(1) + }) + + test('exception when condition not found', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 2.1 + + const t = () => quickInsulin.computePunctualAdaptation(glycemiaLevel) + + expect(t).toThrowError() + }) + + test('adaptation for glycemia measure 2.6 and acetone level = 0 is +3', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 2.6 + let acetoneLevel = 0 + + expect( + quickInsulin.computePunctualAdaptation(glycemiaLevel, acetoneLevel) + .totalAdaptation, + ).toEqual(3) + }) + + test('acetone level required error when glycemia >= 2.5', () => { + let quickInsulin = new QuickInsulin() + + let glycemiaLevel = 2.5 + + const t = () => { + quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation + } + + expect(t).toThrow(AcetoneNeededError) + }) + }) -test('acetone level required error when glycemia >= 2.5', () => { - let quickInsulin = new QuickInsulin() - - let glycemiaLevel = 2.5 - - const t = () => { - quickInsulin.computePunctualAdaptation(glycemiaLevel).totalAdaptation - } - - expect(t).toThrow(AcetoneNeededError) -}) +describe('long term adaptaion', () => { + + let objective = {min: 0.7, max: 1.4} + + it.each( + [ + [ + 'Adaptation is +2 when trend is UP', + [ + new MealGlycemiaMeasure(1.6, objective), + new MealGlycemiaMeasure(1.7, objective), + new MealGlycemiaMeasure(1.5, objective) + ], + +2 + ], + [ + 'Adaptation is -2 when trend is DOWN', + [ + new MealGlycemiaMeasure(0.6, objective), + new MealGlycemiaMeasure(0.55, objective), + new MealGlycemiaMeasure(0.65, objective) + ], + -2 + ], + [ + 'Adaptation is 0 when no trend', + [ + new MealGlycemiaMeasure(0.6, objective), + new MealGlycemiaMeasure(0.55, objective), + new MealGlycemiaMeasure(0.65, objective) + ], + -2 + ], + [ + 'Adaptation is 0 when trend is STABLE', + [ + new MealGlycemiaMeasure(1.3, objective), + new MealGlycemiaMeasure(0.8, objective), + new MealGlycemiaMeasure(0.99, objective) + ], + 0 + ], + ] + ) ('%s', (_label, measures, expectedAdaptation) => { + let quickInsulin = new QuickInsulin() + let adaptation = quickInsulin.computeLongtermAdaptation(measures) + expect(adaptation).toEqual(expectedAdaptation) + }) + +}) \ No newline at end of file diff --git a/src/core/__tests__/TrendService.test.ts b/src/core/__tests__/TrendService.test.ts new file mode 100644 index 0000000..96bb308 --- /dev/null +++ b/src/core/__tests__/TrendService.test.ts @@ -0,0 +1,27 @@ +import {test, expect} from '@jest/globals' +import { Trend, TrendService } from '../TrendService' + +describe('trend service', () => { + + test.each([ + [{trend: Trend.DOWN}, {trend:Trend.UP} , {trend: Trend.STABLE} , Trend.STABLE], + [{trend: Trend.DOWN}, {trend:Trend.DOWN} , {trend: Trend.DOWN} , Trend.DOWN], + [{trend: Trend.UP}, {trend:Trend.UP} , {trend: Trend.UP} , Trend.UP], + [{trend: Trend.STABLE}, {trend:Trend.STABLE} , {trend: Trend.STABLE} , Trend.STABLE], + ]) + ( + ' when trend1 = %j and trend2 = %j and trend3 = %j, computed trend must give %s', + (trend1: {trend: Trend}, trend2: {trend: Trend}, trend3: {trend: Trend}, expected: Trend) => { + let trendService = new TrendService() + expect(trendService.findTrend([trend1, trend2, trend3])).toEqual(expected) + } + ) + }) + + it('throws error when not enough measures are provided', () => { + let trendService = new TrendService() + let t = () => {trendService.findTrend([{trend: Trend.DOWN}, {trend: Trend.DOWN}])} + + expect(t).toThrowError() + + }) diff --git a/src/core/basal_insulin_adaptation_config.json b/src/core/basal_insulin_adaptation_config.json new file mode 100644 index 0000000..09a5d88 --- /dev/null +++ b/src/core/basal_insulin_adaptation_config.json @@ -0,0 +1,14 @@ +[ + { + "trend": "UP", + "adaptation": 2 + }, + { + "trend": "DOWN", + "adaptation": -2 + }, + { + "trend": "STABLE", + "adaptation": 0 + } +] \ No newline at end of file diff --git a/src/core/quick_insulin_conditions.json b/src/core/quick_insulin_conditions.json deleted file mode 100644 index a5607b5..0000000 --- a/src/core/quick_insulin_conditions.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "max": 0.7, - "adaptation": -1, - "endOfMeal": true - }, - { - "min": 0.7, - "max": 1.4, - "objective": true, - "adaptation": 0 - }, - { - "min": 1.4, - "max": 2.0, - "adaptation": 1 - }, - { - "min": 2.0, - "max": 2.5, - "adaptation": 2 - }, - { - "min": 2.5, - "max": 3.0, - "adaptation": 3, - "checkAcetone": true - }, - { - "min": 3.0, - "adaptation": 4, - "checkAcetone": true - } -] diff --git a/src/core/quick_insulin_longterm_adaptation_criteria.json b/src/core/quick_insulin_longterm_adaptation_criteria.json new file mode 100644 index 0000000..09a5d88 --- /dev/null +++ b/src/core/quick_insulin_longterm_adaptation_criteria.json @@ -0,0 +1,14 @@ +[ + { + "trend": "UP", + "adaptation": 2 + }, + { + "trend": "DOWN", + "adaptation": -2 + }, + { + "trend": "STABLE", + "adaptation": 0 + } +] \ No newline at end of file diff --git a/src/core/quick_insulin_punctual_adaptation_criteria.json b/src/core/quick_insulin_punctual_adaptation_criteria.json new file mode 100644 index 0000000..824716b --- /dev/null +++ b/src/core/quick_insulin_punctual_adaptation_criteria.json @@ -0,0 +1,47 @@ +[ + { + "max": 0.7, + "objective": false, + "adaptation": -1, + "endOfMeal": true, + "checkAcetone": false + }, + { + "min": 0.7, + "max": 1.4, + "objective": true, + "adaptation": 0, + "endOfMeal": false, + "checkAcetone": false + }, + { + "min": 1.4, + "max": 2.0, + "objective": false, + "adaptation": 1, + "endOfMeal": false, + "checkAcetone": false + }, + { + "min": 2.0, + "max": 2.5, + "objective": false, + "adaptation": 2, + "endOfMeal": false, + "checkAcetone": false + }, + { + "min": 2.5, + "max": 3.0, + "objective": false, + "adaptation": 3, + "endOfMeal": false, + "checkAcetone": true + }, + { + "min": 3.0, + "objective": false, + "adaptation": 4, + "checkAcetone": true + } +] diff --git a/src/screens/BasalAdaptationScreen.tsx b/src/screens/BasalAdaptationScreen.tsx index 296dd0e..2aac129 100644 --- a/src/screens/BasalAdaptationScreen.tsx +++ b/src/screens/BasalAdaptationScreen.tsx @@ -2,39 +2,82 @@ import React from 'react' import {SafeAreaView, Text, View} from 'react-native' import {DbNumericTextInput} from '../components/DbNumericTextInputComponent' import {screenStyles} from './styles' -//import {ScreenComponentType} from '@react-navigation/core/src/types' +import BasalInsulin, {NightGlycemiaInterval} from '../core/BasalInsulin' +import {componentStyles} from '../components/styles' interface BasalState { glycemiasBefore: number[] glycemiasAfter: number[] + adaptation: number | undefined } export class BasalAdaptationScreen extends React.Component<{}, BasalState> { constructor(props: {}) { super(props) this.state = { - glycemiasBefore: [], - glycemiasAfter: [], + glycemiasBefore: new Array(3), + glycemiasAfter: new Array(3), + adaptation: undefined, } } + manageComputation = () => { + const containsInvalidElement = (numbers: number[]): boolean => { + for (let n of numbers) { + if (n === undefined || isNaN(n)) { + return true + } + } + return false + } + + if ( + containsInvalidElement(this.state.glycemiasBefore) || + containsInvalidElement(this.state.glycemiasAfter) + ) { + this.setState({ + adaptation: undefined, + }) + return + } + + let intervals: NightGlycemiaInterval[] = [] + for (let index = 0; index < this.state.glycemiasBefore.length; index++) { + const before = this.state.glycemiasBefore[index] + const after = this.state.glycemiasAfter[index] + + intervals.push(new NightGlycemiaInterval(before, after)) + } + + let basalInsulin = new BasalInsulin() + this.setState({ + adaptation: basalInsulin.computeAdaptation(intervals), + }) + } + render() { const manageGlycemiaBefore = (i: number, glycemiaStr: string) => { let newBefore = Array.from(this.state.glycemiasBefore) newBefore[i] = parseFloat(glycemiaStr) - this.setState({ - glycemiasBefore: newBefore, - }) + this.setState( + { + glycemiasBefore: newBefore, + }, + this.manageComputation, + ) } const manageGlycemiaAfter = (i: number, glycemiaStr: string) => { let newAfter = Array.from(this.state.glycemiasAfter) newAfter[i] = parseFloat(glycemiaStr) - this.setState({ - glycemiasAfter: newAfter, - }) + this.setState( + { + glycemiasAfter: newAfter, + }, + this.manageComputation, + ) } const glycemiaIntervalInputs = [] @@ -63,11 +106,8 @@ export class BasalAdaptationScreen extends React.Component<{}, BasalState> { {glycemiaIntervalInputs} - - {this.state.glycemiasBefore.join(', ')} - - - {this.state.glycemiasAfter.join(', ')} + + {this.state.adaptation} diff --git a/src/screens/PunctualGlycemiaScreen.tsx b/src/screens/PunctualGlycemiaScreen.tsx index e2e64cb..b08c793 100644 --- a/src/screens/PunctualGlycemiaScreen.tsx +++ b/src/screens/PunctualGlycemiaScreen.tsx @@ -16,6 +16,8 @@ import { } from '../core/QuickInsulin' import {DbNumericTextInput} from '../components/DbNumericTextInputComponent' import {screenStyles} from './styles' +import {Acetone} from '../core/Acetone' +import {SegmentedButtons} from 'react-native-paper' interface PunctualGlycemiaState { glycemiaLevel: number | undefined @@ -58,7 +60,6 @@ export class PunctualGlycemiaScreen extends React.Component< } private setGlycemiaLevel = (glycemiaLevel: number | undefined): void => { - console.log('setting glycemia level') this.setState({ glycemiaLevel: glycemiaLevel, }) @@ -119,7 +120,7 @@ export class PunctualGlycemiaScreen extends React.Component< } private manageNumberInput = (strValue: string): number | undefined => { - if (!strValue) { + if (strValue === '') { return undefined } let numberValue: number = Number(strValue) @@ -156,6 +157,16 @@ export class PunctualGlycemiaScreen extends React.Component< backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, } + const acetoneLevels = new Acetone().getAcetoneLevels().map(i => { + return { + label: i === 0 ? '0' : '+'.repeat(i), + value: i.toString(), + style: {color: 'white', backgroundColor: 'gray'}, + checkedColor: 'blue', + testID: `acetoneLevel${i}`, + } + }) + return ( Acetone level: - this.manageAcetoneLevelInput(newText)} - testID="acetoneInput" + this.manageAcetoneLevelInput(nexText)} /> )} @@ -194,7 +208,7 @@ export class PunctualGlycemiaScreen extends React.Component< )} - {this.state.punctualAdaptationResult != null && ( + {this.state.punctualAdaptationResult && ( {this.formatResult(this.state.punctualAdaptationResult)} diff --git a/src/screens/QuickInsulinAdaptationScreen.tsx b/src/screens/QuickInsulinAdaptationScreen.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/screens/__tests__/PunctualGlycemiaScreen.test.tsx b/src/screens/__tests__/PunctualGlycemiaScreen.test.tsx index 29a25a8..e6080cb 100644 --- a/src/screens/__tests__/PunctualGlycemiaScreen.test.tsx +++ b/src/screens/__tests__/PunctualGlycemiaScreen.test.tsx @@ -15,7 +15,7 @@ it('renders correctly', () => { render() expect(screen.getByTestId('glycemiaInput')).toBeDefined() - expect(() => screen.getByTestId('acetoneInput')).toThrowError() + expect(() => screen.getByTestId('acetoneLevel1')).toThrowError() expect(() => screen.getByTestId('resultDetails')).toThrowError() }) @@ -48,7 +48,7 @@ it('displays acetone input when needed', () => { fireEvent.changeText(glycemiaInput, '2.6') - let acetoneInput = screen.getByTestId('acetoneInput') + let acetoneInput = screen.getByTestId('acetoneLevel1') expect(acetoneInput).toBeDefined() }) @@ -63,10 +63,10 @@ it('displays results when glycemia + acetone inputs are needed', () => { fireEvent.changeText(glycemiaInput, '2.6') - let acetoneInput = screen.getByTestId('acetoneInput') + let acetoneInput = screen.getByTestId('acetoneLevel1') expect(acetoneInput).toBeDefined() - fireEvent.changeText(acetoneInput, '1') + fireEvent(acetoneInput, 'onPress') let adaptationText = screen.getByTestId('adaptationText') expect(adaptationText.props.children).toBeTruthy() @@ -88,8 +88,6 @@ it('displays error message when entered glycemia is not valid', () => { let errorMessage = screen.getByTestId('errorMessage') - console.log(errorMessage.props.children) - expect(errorMessage.props.children).toEqual('Invalid glycemia level') })