Skip to content

Commit bf11871

Browse files
authored
Merge pull request #88 from mcode/case-insensitive-lookups
Implements the case insensitive CancerDiseaseStatus lookups
2 parents 959622f + fd758c9 commit bf11871

File tree

6 files changed

+204
-52
lines changed

6 files changed

+204
-52
lines changed

src/helpers/diseaseStatusUtils.js

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,20 @@
11
const moment = require('moment');
2-
const { invertObject } = require('./helperUtils');
2+
const { lowercaseLookupQuery } = require('./lookupUtils');
3+
const {
4+
mcodeDiseaseStatusTextToCodeLookup,
5+
icareDiseaseStatusTextToCodeLookup,
6+
mcodeDiseaseStatusCodeToTextLookup,
7+
icareDiseaseStatusCodeToTextLookup,
8+
evidenceTextToCodeLookup,
9+
evidenceCodeToTextLookup,
10+
} = require('./lookups/diseaseStatusLookup');
311

412
// Translate an M-language epoch date to an appropriate moment date
513
function mEpochToDate(date) {
614
const epochDate = moment('1840-12-31');
715
return epochDate.add(date, 'days');
816
}
917

10-
// Code mapping is based on current values at http://standardhealthrecord.org/guides/icare/mapping_guidance.html
11-
const currentDiseaseStatusTextToCodeLookup = {
12-
'Not detected (qualifier)': '260415000',
13-
'Patient condition improved (finding)': '268910001',
14-
'Patient\'s condition stable (finding)': '359746009',
15-
'Patient\'s condition worsened (finding)': '271299001',
16-
'Patient condition undetermined (finding)': '709137006',
17-
};
18-
const currentDiseaseStatusCodeToTextLookup = invertObject(currentDiseaseStatusTextToCodeLookup);
19-
20-
// Code mapping is based on initial values still in use by icare implementors
21-
// specifically using lowercase versions of the text specified by ICARE for status
22-
const icareDiseaseStatusTextToCodeLookup = {
23-
'no evidence of disease': '260415000',
24-
responding: '268910001',
25-
stable: '359746009',
26-
progressing: '271299001',
27-
'not evaluated': '709137006',
28-
};
29-
const icareDiseaseStatusCodeToTextLookup = invertObject(icareDiseaseStatusTextToCodeLookup);
30-
31-
// Code mapping is based on http://standardhealthrecord.org/guides/icare/mapping_guidance.html
32-
// specifically using lowercase versions of the text specified by ICARE for Reason
33-
const evidenceTextToCodeLookup = {
34-
imaging: '363679005',
35-
pathology: '252416005',
36-
symptoms: '711015009',
37-
'physical exam': '5880005',
38-
'lab results': '386344002',
39-
};
40-
const evidenceCodeToTextLookup = invertObject(evidenceTextToCodeLookup);
41-
4218
/**
4319
* Converts Text Value to code in mCODE's ConditionStatusTrendVS
4420
* @param {string} text, limited to 'no evidence of disease', Responding, Stable, Progressing, or 'not evaluated'
@@ -47,9 +23,9 @@ const evidenceCodeToTextLookup = invertObject(evidenceTextToCodeLookup);
4723
function getDiseaseStatusCode(text, implementation) {
4824
switch (implementation) {
4925
case 'icare':
50-
return icareDiseaseStatusTextToCodeLookup[text];
26+
return lowercaseLookupQuery(text, icareDiseaseStatusTextToCodeLookup);
5127
default:
52-
return currentDiseaseStatusTextToCodeLookup[text];
28+
return lowercaseLookupQuery(text, mcodeDiseaseStatusTextToCodeLookup);
5329
}
5430
}
5531

@@ -61,9 +37,9 @@ function getDiseaseStatusCode(text, implementation) {
6137
function getDiseaseStatusDisplay(code, implementation) {
6238
switch (implementation) {
6339
case 'icare':
64-
return icareDiseaseStatusCodeToTextLookup[code];
40+
return lowercaseLookupQuery(code, icareDiseaseStatusCodeToTextLookup);
6541
default:
66-
return currentDiseaseStatusCodeToTextLookup[code];
42+
return lowercaseLookupQuery(code, mcodeDiseaseStatusCodeToTextLookup);
6743
}
6844
}
6945

@@ -73,7 +49,7 @@ function getDiseaseStatusDisplay(code, implementation) {
7349
* @return {string} corresponding Evidence code
7450
*/
7551
function getDiseaseStatusEvidenceCode(text) {
76-
return evidenceTextToCodeLookup[text];
52+
return lowercaseLookupQuery(text, evidenceTextToCodeLookup);
7753
}
7854

7955
/**
@@ -82,7 +58,7 @@ function getDiseaseStatusEvidenceCode(text) {
8258
* @return {string} corresponding Evidence display text
8359
*/
8460
function getDiseaseStatusEvidenceDisplay(code) {
85-
return evidenceCodeToTextLookup[code];
61+
return lowercaseLookupQuery(code, evidenceCodeToTextLookup);
8662
}
8763

8864
module.exports = {

src/helpers/helperUtils.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/helpers/lookupUtils.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Create a lookup table that swaps keys and values s.t. (k->v) becomes (v->k)
3+
* @param {Object} lookup - a lookup table, defined by key-value pairs
4+
* @return {Object} the lookup table, with all keys and values inverted
5+
*/
6+
function createInvertedLookup(lookup) {
7+
// NOTE: This will produce collisions if values aren't unique
8+
// and unspecified behavior if values are non-strings
9+
return Object.entries(lookup).reduce((ret, entry) => {
10+
const [key, value] = entry;
11+
// eslint-disable-next-line no-param-reassign
12+
ret[value] = key;
13+
return ret;
14+
}, {});
15+
}
16+
17+
/**
18+
* Create a lookup table where all the keys are lowercased
19+
* @param {Object} lookup - a lookup table, defined by key-value pairs
20+
* @return {Object} the lookup table, with all keys lowercased
21+
*/
22+
function createLowercaseLookup(lookup) {
23+
// NOTE: This will produce collisions if keys aren't unique w/r/t case
24+
return Object.entries(lookup).reduce((ret, entry) => {
25+
const [k, v] = entry;
26+
// eslint-disable-next-line no-param-reassign
27+
ret[k.toLowerCase()] = v;
28+
return ret;
29+
}, {});
30+
}
31+
32+
/**
33+
* Performs a lookup using the original key and a lowercase key
34+
* @param {String} key - key being queried for in the lookup
35+
* @param {Object} lookup - lookup table
36+
* @return {any} the value associated with that key
37+
*/
38+
function lowercaseLookupQuery(key, lookup) {
39+
const lowerKey = key.toLowerCase();
40+
return lookup[key] || lookup[lowerKey];
41+
}
42+
43+
module.exports = {
44+
lowercaseLookupQuery,
45+
createLowercaseLookup,
46+
createInvertedLookup,
47+
};
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { createInvertedLookup, createLowercaseLookup } = require('../lookupUtils');
2+
3+
// Code mapping is based on current values at http://standardhealthrecord.org/guides/icare/mapping_guidance.html
4+
const mcodeDiseaseStatusTextToCodeLookup = {
5+
'Not detected (qualifier)': '260415000',
6+
'Patient condition improved (finding)': '268910001',
7+
'Patient\'s condition stable (finding)': '359746009',
8+
'Patient\'s condition worsened (finding)': '271299001',
9+
'Patient condition undetermined (finding)': '709137006',
10+
};
11+
const mcodeDiseaseStatusCodeToTextLookup = createInvertedLookup(mcodeDiseaseStatusTextToCodeLookup);
12+
13+
// Code mapping is based on initial values still in use by icare implementors
14+
// specifically using lowercase versions of the text specified by ICARE for status
15+
const icareDiseaseStatusTextToCodeLookup = {
16+
'no evidence of disease': '260415000',
17+
responding: '268910001',
18+
stable: '359746009',
19+
progressing: '271299001',
20+
'not evaluated': '709137006',
21+
};
22+
const icareDiseaseStatusCodeToTextLookup = createInvertedLookup(icareDiseaseStatusTextToCodeLookup);
23+
24+
// Code mapping is based on http://standardhealthrecord.org/guides/icare/mapping_guidance.html
25+
// specifically using lowercase versions of the text specified by ICARE for Reason
26+
const evidenceTextToCodeLookup = {
27+
imaging: '363679005',
28+
pathology: '252416005',
29+
symptoms: '711015009',
30+
'physical exam': '5880005',
31+
'lab results': '386344002',
32+
};
33+
const evidenceCodeToTextLookup = createInvertedLookup(evidenceTextToCodeLookup);
34+
35+
module.exports = {
36+
mcodeDiseaseStatusTextToCodeLookup: createLowercaseLookup(mcodeDiseaseStatusTextToCodeLookup),
37+
mcodeDiseaseStatusCodeToTextLookup: createLowercaseLookup(mcodeDiseaseStatusCodeToTextLookup),
38+
icareDiseaseStatusTextToCodeLookup: createLowercaseLookup(icareDiseaseStatusTextToCodeLookup),
39+
icareDiseaseStatusCodeToTextLookup: createLowercaseLookup(icareDiseaseStatusCodeToTextLookup),
40+
evidenceTextToCodeLookup: createLowercaseLookup(evidenceTextToCodeLookup),
41+
evidenceCodeToTextLookup: createLowercaseLookup(evidenceCodeToTextLookup),
42+
};

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const {
6464
} = require('./helpers/conditionUtils');
6565
const { getDiseaseStatusCode, getDiseaseStatusEvidenceCode, mEpochToDate } = require('./helpers/diseaseStatusUtils');
6666
const { formatDate, formatDateTime } = require('./helpers/dateUtils');
67+
const { lowercaseLookupQuery, createLowercaseLookup, createInvertedLookup } = require('./helpers/lookupUtils');
6768
const { getConditionEntriesFromContext, getConditionsFromContext, getEncountersFromContext, getPatientFromContext } = require('./helpers/contextUtils');
6869

6970
module.exports = {
@@ -106,6 +107,8 @@ module.exports = {
106107
MCODEClient,
107108
// FHIR and resource helpers
108109
allResourcesInBundle,
110+
createLowercaseLookup,
111+
createInvertedLookup,
109112
firstEntryInBundle,
110113
firstIdentifierEntry,
111114
firstResourceInBundle,
@@ -131,6 +134,7 @@ module.exports = {
131134
isConditionCodeCancer,
132135
isConditionCancer,
133136
logOperationOutcomeInfo,
137+
lowercaseLookupQuery,
134138
mEpochToDate,
135139
// Context operations
136140
getConditionEntriesFromContext,

test/helpers/lookupUtils.test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const { createLowercaseLookup, createInvertedLookup, lowercaseLookupQuery } = require('../../src/helpers/lookupUtils');
2+
3+
describe('lookupUtils', () => {
4+
describe('createLowercaseLookup', () => {
5+
const exampleLookup = {
6+
foo: 'bar',
7+
'apples AND': 'oranges',
8+
AI: 'ethics',
9+
};
10+
const lowercaseLookup = createLowercaseLookup(exampleLookup);
11+
12+
test('should fail if supplied an undefined or null value', () => {
13+
expect(() => createInvertedLookup()).toThrow();
14+
expect(() => createInvertedLookup(undefined)).toThrow();
15+
expect(() => createInvertedLookup(null)).toThrow();
16+
});
17+
18+
test('all new keys should match the lowercased original keys', () => {
19+
const lowercasedExampleKeys = Object.keys(exampleLookup).map((k) => k.toLowerCase());
20+
expect(Object.keys(lowercaseLookup)).toEqual(lowercasedExampleKeys);
21+
});
22+
23+
test('given a lookup with unique keys, values should remain unchanged after lowercasing', () => {
24+
const originalValues = Object.values(exampleLookup);
25+
expect(Object.values(lowercaseLookup)).toEqual(originalValues);
26+
});
27+
28+
test('given a lookup with unique keys, # of keys should stay the same', () => {
29+
expect(Object.keys(lowercaseLookup).length).toEqual(Object.keys(exampleLookup).length);
30+
});
31+
});
32+
33+
describe('createInvertedLookup', () => {
34+
const exampleLookup = {
35+
foo: 'bar',
36+
'apples AND': 'oranges',
37+
AI: 'ethics',
38+
};
39+
const invertedLookup = createInvertedLookup(exampleLookup);
40+
41+
test('should fail if supplied a non-object', () => {
42+
expect(() => createInvertedLookup()).toThrow();
43+
expect(() => createInvertedLookup(undefined)).toThrow();
44+
expect(() => createInvertedLookup(null)).toThrow();
45+
});
46+
47+
test('all old values should be keys after inversion', () => {
48+
expect(Object.keys(invertedLookup)).toEqual(expect.arrayContaining(Object.values(exampleLookup)));
49+
});
50+
51+
test('all old keys should be values after inversion', () => {
52+
expect(Object.values(invertedLookup)).toEqual(expect.arrayContaining(Object.keys(exampleLookup)));
53+
});
54+
55+
test('given a lookup with unique values, # of new keys should match the # of values in the old object', () => {
56+
expect(Object.keys(invertedLookup).length).toEqual(Object.values(exampleLookup).length);
57+
});
58+
59+
test('given a lookup with unique values, # of new values should match the # of keys in the old object', () => {
60+
expect(Object.values(invertedLookup).length).toEqual(Object.keys(exampleLookup).length);
61+
});
62+
});
63+
64+
describe('lowercaseLookupQuery', () => {
65+
const exampleLookup = {
66+
foo: 'bar',
67+
'apples AND': 'oranges',
68+
AI: 'ethics',
69+
};
70+
// Create a lowercased lookup as this is
71+
const lowercaseLookup = createLowercaseLookup(exampleLookup);
72+
const originalCaseKeys = Object.keys(exampleLookup);
73+
const lowercaseKeys = Object.keys(exampleLookup).map((k) => k.toLowerCase());
74+
const uppercaseKeys = Object.keys(exampleLookup).map((k) => k.toUpperCase());
75+
76+
test('lookup should return undefined when the value provided is not defined in the lookup', () => {
77+
expect(lowercaseLookupQuery('', lowercaseLookup)).toEqual(undefined);
78+
});
79+
80+
test('lookup should work for the lowercased keys', () => {
81+
lowercaseKeys.forEach((lowercaseKey) => {
82+
expect(lowercaseLookupQuery(lowercaseKey, lowercaseLookup)).toEqual(lowercaseLookup[lowercaseKey]);
83+
});
84+
});
85+
test('lookup should work for the originalcase keys', () => {
86+
originalCaseKeys.forEach((originalCaseKey) => {
87+
expect(lowercaseLookupQuery(originalCaseKey, lowercaseLookup)).toEqual(lowercaseLookup[originalCaseKey.toLowerCase()]);
88+
});
89+
});
90+
test('lookup should work for the uppercased keys', () => {
91+
uppercaseKeys.forEach((uppercaseKey) => {
92+
expect(lowercaseLookupQuery(uppercaseKey, lowercaseLookup)).toEqual(lowercaseLookup[uppercaseKey.toLowerCase()]);
93+
});
94+
});
95+
});
96+
});

0 commit comments

Comments
 (0)