Skip to content

Commit

Permalink
(feat) O3-3249: Introduce helper function to get well-formatted patie…
Browse files Browse the repository at this point in the history
…nt display name. (#1003)
  • Loading branch information
xprl-gjf authored May 21, 2024
1 parent eeb1c8c commit 27ab80e
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/framework/esm-api/src/types/fhir-name-use.ts
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';
1 change: 1 addition & 0 deletions packages/framework/esm-api/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './attachments-types';
export * from './concept-resource';
export * from './fetch';
export * from './fhir-name-use';
export * from './fhir-resource';
export * from './openmrs-resource';
export * from './user-resource';
Expand Down
1 change: 1 addition & 0 deletions packages/framework/esm-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './age-helpers';
export * from './is-online';
export * from './omrs-dates';
export * from './patient-helpers';
export * from './shallowEqual';
export * from './storage';
export * from './test-helpers';
Expand Down
146 changes: 146 additions & 0 deletions packages/framework/esm-utils/src/patient-helpers.test.data.ts
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',
},
],
};
69 changes: 69 additions & 0 deletions packages/framework/esm-utils/src/patient-helpers.test.ts
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();
});
});
85 changes: 85 additions & 0 deletions packages/framework/esm-utils/src/patient-helpers.ts
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;
}

0 comments on commit 27ab80e

Please sign in to comment.