-
Notifications
You must be signed in to change notification settings - Fork 218
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) O3-3249: Introduce helper function to get well-formatted patie…
…nt display name. (#1003)
- Loading branch information
Showing
6 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/* | ||
* Supported values for FHIR HumanName.use as defined by https://build.fhir.org/valueset-name-use.html | ||
*/ | ||
export type NameUse = 'usual' | 'official' | 'temp' | 'nickname' | 'anonymous' | 'old' | 'maiden'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
packages/framework/esm-utils/src/patient-helpers.test.data.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
export const nameWithFormat = { | ||
id: 'efdb246f-4142-4c12-a27a-9be60b9592e9', | ||
family: 'Wilson', | ||
given: ['John'], | ||
text: 'Wilson, John', | ||
}; | ||
|
||
export const nameWithoutFormat = { | ||
id: 'efdb246f-4142-4c12-a27a-9be60b9592e9', | ||
family: 'family name', | ||
given: ['given', 'middle'], | ||
}; | ||
|
||
export const familyNameOnly = { | ||
id: 'efdb246f-4142-4c12-a27a-9be60b9592e9', | ||
family: 'family name', | ||
}; | ||
|
||
export const givenNameOnly = { | ||
id: 'efdb246f-4142-4c12-a27a-9be60b9592e9', | ||
given: ['given'], | ||
}; | ||
|
||
export const mockPatient = { | ||
resourceType: 'Patient', | ||
id: '8673ee4f-e2ab-4077-ba55-4980f408773e', | ||
extension: [ | ||
{ | ||
url: 'http://fhir-es.transcendinsights.com/stu3/StructureDefinition/resource-date-created', | ||
valueDateTime: '2017-01-18T09:42:40+00:00', | ||
}, | ||
{ | ||
url: 'https://purl.org/elab/fhir/StructureDefinition/Creator-crew-version1', | ||
valueString: 'daemon', | ||
}, | ||
], | ||
identifier: [ | ||
{ | ||
id: '1f0ad7a1-430f-4397-b571-59ea654a52db', | ||
use: 'secondary', | ||
system: 'Old Identification Number', | ||
value: '100732HE', | ||
}, | ||
{ | ||
id: '1f0ad7a1-430f-4397-b571-59ea654a52db', | ||
use: 'usual', | ||
system: 'OpenMRS ID', | ||
value: '100GEJ', | ||
}, | ||
], | ||
active: true, | ||
name: [nameWithFormat], | ||
gender: 'male', | ||
birthDate: '1972-04-04', | ||
deceasedBoolean: false, | ||
address: [], | ||
}; | ||
|
||
export const mockPatientWithNoName = { | ||
...mockPatient, | ||
name: [], | ||
}; | ||
|
||
export const mockPatientWithMultipleNames = { | ||
// name usage may be: usual | official | temp | nickname | anonymous | old | maiden | ||
...mockPatient, | ||
name: [ | ||
{ | ||
id: 'id-of-nickname-1', | ||
use: 'nickname', | ||
given: ['nick', 'name'], | ||
}, | ||
{ | ||
id: 'id-of-nickname-2', | ||
use: 'nickname', | ||
given: ['nick', 'name'], | ||
}, | ||
{ | ||
id: 'id-of-anonymous-name-1', | ||
use: 'anonymous', | ||
given: ['john', 'doe'], | ||
}, | ||
{ | ||
id: 'id-of-old-name-1', | ||
use: 'old', | ||
given: ['previous'], | ||
family: 'name', | ||
}, | ||
{ | ||
id: 'id-of-maiden-name-1', | ||
use: 'maiden', | ||
family: 'maiden name', | ||
}, | ||
{ | ||
// this is the actual display name | ||
id: 'id-of-usual-name-1', | ||
given: ['John', 'Murray'], | ||
family: 'Smith', | ||
text: 'Smith, John Murray', | ||
}, | ||
{ | ||
// this is usable as a display name, but the usual name will take precedence. | ||
id: 'id-of-official-name-1', | ||
use: 'official', | ||
given: ['my', 'official'], | ||
family: 'name', | ||
}, | ||
], | ||
}; | ||
|
||
export const mockPatientWithOfficialName = { | ||
...mockPatient, | ||
name: [ | ||
{ | ||
// this is usable as a display name, but the usual name should be preferred | ||
id: 'id-of-official-name-1', | ||
use: 'official', | ||
given: ['my', 'official'], | ||
family: 'name', | ||
}, | ||
{ | ||
// this is the preferred display name, even though it comes after the official name | ||
id: 'id-of-usual-name-1', | ||
use: 'usual', // explicitly marked as usual name | ||
given: ['my', 'actual'], | ||
family: 'name', | ||
}, | ||
], | ||
}; | ||
|
||
export const mockPatientWithNickAndOfficialName = { | ||
...mockPatient, | ||
name: [ | ||
{ | ||
id: 'id-of-nickname-1', | ||
use: 'nickname', | ||
given: ['nick', 'name'], | ||
}, | ||
{ | ||
id: 'id-of-official-name-1', | ||
use: 'official', | ||
given: ['my', 'official'], | ||
family: 'name', | ||
}, | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { displayName, formattedName, selectPreferredName } from './patient-helpers'; | ||
import { | ||
mockPatientWithNoName, | ||
mockPatientWithOfficialName, | ||
nameWithFormat, | ||
nameWithoutFormat, | ||
familyNameOnly, | ||
givenNameOnly, | ||
mockPatientWithMultipleNames, | ||
mockPatientWithNickAndOfficialName, | ||
} from './patient-helpers.test.data'; | ||
import { type NameUse } from '../../esm-api'; | ||
|
||
describe('Formatted display name', () => { | ||
it.each([ | ||
[nameWithFormat, 'Wilson, John'], | ||
[nameWithoutFormat, 'given middle family name'], | ||
[familyNameOnly, 'family name'], | ||
[givenNameOnly, 'given'], | ||
[mockPatientWithNoName, ''], | ||
])('Is formatted name text if present else default name format', (name, expected) => { | ||
const result = formattedName(name); | ||
expect(result).toBe(expected); | ||
}); | ||
}); | ||
|
||
describe('Patient display name', () => { | ||
it.each([ | ||
[mockPatientWithMultipleNames, 'Smith, John Murray'], | ||
[mockPatientWithOfficialName, 'my actual name'], | ||
[mockPatientWithNickAndOfficialName, 'my official name'], | ||
])('Is selected from usual name or official name', (patient, expected) => { | ||
const result = displayName(patient); | ||
expect(result).toBe(expected); | ||
}); | ||
}); | ||
|
||
const usual: NameUse = 'usual'; | ||
const official: NameUse = 'official'; | ||
const maiden: NameUse = 'maiden'; | ||
const nickname: NameUse = 'nickname'; | ||
const temp: NameUse = 'temp'; | ||
|
||
describe('Preferred patient name', () => { | ||
it.each([ | ||
[mockPatientWithMultipleNames, [], 'id-of-usual-name-1'], | ||
[mockPatientWithMultipleNames, [usual], 'id-of-usual-name-1'], | ||
[mockPatientWithMultipleNames, [usual, official], 'id-of-usual-name-1'], | ||
[mockPatientWithMultipleNames, [official], 'id-of-official-name-1'], | ||
[mockPatientWithMultipleNames, [official, usual], 'id-of-official-name-1'], | ||
[mockPatientWithMultipleNames, [maiden, usual, official], 'id-of-maiden-name-1'], | ||
[mockPatientWithOfficialName, [usual, official], 'id-of-usual-name-1'], | ||
[mockPatientWithNickAndOfficialName, [nickname, official], 'id-of-nickname-1'], | ||
])('Is selected according to preferred usage', (patient, preferredUsage, expectedNameId) => { | ||
const result = selectPreferredName(patient, ...preferredUsage); | ||
expect(result?.id).toBe(expectedNameId); | ||
}); | ||
}); | ||
|
||
describe('Preferred patient name', () => { | ||
it.each([ | ||
[mockPatientWithMultipleNames, [temp]], | ||
[mockPatientWithNoName, [usual]], | ||
[mockPatientWithNoName, []], | ||
])('Is empty if preferred name is not present.', (patient, preferredUsage) => { | ||
const result = selectPreferredName(patient, ...preferredUsage); | ||
expect(result).toBeUndefined(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/** @module @category Utility */ | ||
|
||
import type { NameUse } from '../../esm-api'; | ||
|
||
/** | ||
* Gets the formatted display name for a patient. | ||
* | ||
* The display name will be taken from the patient's 'usual' name, | ||
* or may fall back to the patient's 'official' name. | ||
* | ||
* @param patient The patient details in FHIR format. | ||
* @returns The patient's display name or an empty string if name is not present. | ||
*/ | ||
export function displayName(patient: fhir.Patient): string { | ||
const name = selectPreferredName(patient, 'usual', 'official'); | ||
return formattedName(name); | ||
} | ||
|
||
/** | ||
* Get a formatted display string for an FHIR name. | ||
* @param name The name to be formatted. | ||
* @returns The formatted display name or an empty string if name is undefined. | ||
*/ | ||
export function formattedName(name: fhir.HumanName | undefined): string { | ||
if (name) return name.text ?? defaultFormat(name); | ||
return ''; | ||
} | ||
|
||
/** | ||
* Select the preferred name from the collection of names associated with a patient. | ||
* | ||
* Names may be specified with a usage such as 'usual', 'official', 'nickname', 'maiden', etc. | ||
* A name with no usage specified is treated as the 'usual' name. | ||
* | ||
* The chosen name will be selected according to the priority order of `preferredNames`, | ||
* @example | ||
* // normal use case; prefer usual name, fallback to official name | ||
* displayNameByUsage(patient, 'usual', 'official') | ||
* @example | ||
* // prefer usual name over nickname, fallback to official name | ||
* displayNameByUsage(patient, 'usual', 'nickname', 'official') | ||
* | ||
* @param patient The patient from whom a name will be selected. | ||
* @param preferredNames Optional ordered sequence of preferred name usages; defaults to 'usual' if not specified. | ||
* @return the preferred name for the patient, or undefined if no acceptable name could be found. | ||
*/ | ||
export function selectPreferredName(patient: fhir.Patient, ...preferredNames: NameUse[]): fhir.HumanName | undefined { | ||
if (preferredNames.length == 0) { | ||
preferredNames = ['usual']; | ||
} | ||
for (const usage of preferredNames) { | ||
const name = patient.name?.find((name) => nameUsageMatches(name, usage)); | ||
if (name) { | ||
return name; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
/** | ||
* Generate a display name by concatenating forenames and surname. | ||
* @param name the person's name. | ||
* @returns the person's name as a string. | ||
*/ | ||
function defaultFormat(name: fhir.HumanName): string { | ||
const forenames: string[] = name.given ?? []; | ||
const names: string[] = name.family ? forenames.concat(name.family) : forenames; | ||
return names.join(' '); | ||
} | ||
|
||
/** | ||
* Determine whether the usage of a given name matches the given NameUse. | ||
* | ||
* A name with no usage is treated as the 'usual' name. | ||
* | ||
* @param name the name to test. | ||
* @param usage the NameUse to test for. | ||
*/ | ||
function nameUsageMatches(name: fhir.HumanName, usage: NameUse): boolean { | ||
if (!name.use) | ||
// a name with no usage is treated as 'usual' | ||
return usage === 'usual'; | ||
|
||
return name.use === usage; | ||
} |