Skip to content

Commit

Permalink
fix: Add lookup of rxnorm codes to names, return all codes provided (#…
Browse files Browse the repository at this point in the history
…2816)

* fix: Add lookup of rxnorm codes to names, return all codes provided

* fix: tell build about rxnorm.csv

* build: try using a user to get curl to work

* build: a different incantation

* build: install jq in the docker container

* fix: handle multiple medication codes in the front end

* [pre-commit.ci] auto fixes from pre-commit hooks

* test: add unit tests for getMedicationDisplayName

* fix: export getMedicationDisplayName for testing

* [pre-commit.ci] auto fixes from pre-commit hooks

* test: test medication conversion

* fix: handle observation data change

* [pre-commit.ci] auto fixes from pre-commit hooks

* test: more lab results

* fix: pr feedback

* fix: handle null codes

* fix: change unknown to unknown medication name

* docs: more debugging hints

* fix: handle null pointer

* fix: more robust coding name check

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
mcmcgrath13 and pre-commit-ci[bot] authored Oct 29, 2024
1 parent e3abfe5 commit 98a4eed
Show file tree
Hide file tree
Showing 21 changed files with 123,624 additions and 92 deletions.
6 changes: 3 additions & 3 deletions containers/ecr-viewer/src/app/api/fhirPath.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ specimenCollectionTime: "Observation.extension[0].extension.where(url = 'specime
specimenReceivedTime: "Observation.extension[0].extension.where(url = 'specimen receive time').valueDateTime"
specimenSource: "Observation.extension[0].extension.where(url = 'specimen source').valueString"
observationReferenceValue: "Observation.extension[0].extension.where(url = 'observation entry reference value').valueString"
observationComponent: "code.coding.display"
observationComponent: "code.coding.display.first()"
observationValue: (valueQuantity.value.toString() | valueString | iif(interpretation.coding.display.exists(), ' (' | interpretation.coding.display | ')', '')).join('')
observationReferenceRange: "referenceRange.text"
observationDeviceReference: "device.reference"
observationNote: "note.text"
observationMethod: "method"
observationOrganism: "code.coding.display"
observationAntibiotic: "code.coding.display"
observationOrganism: "code.coding.display.first()"
observationAntibiotic: "code.coding.display.first()"
observationOrganismMethod: "extension.where(url = 'methodCode originalText').valueString"
observationSusceptibility: "valueString"

Expand Down
7 changes: 4 additions & 3 deletions containers/ecr-viewer/src/app/services/labsService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
formatPhoneNumber,
TableJson,
} from "@/app/services/formatService";
import { ObservationComponent } from "fhir/r4b";
import { Coding, ObservationComponent } from "fhir/r4b";
import EvaluateTable, {
ColumnInfoInput,
} from "@/app/view-data/components/EvaluateTable";
Expand Down Expand Up @@ -284,7 +284,8 @@ export function evaluateObservationTable(
) ?? []
).filter(
(observation) =>
!observation.component && observation.code?.coding[0]?.display,
!observation.component &&
observation.code?.coding.some((c: Coding) => c?.display),
);

let obsTable;
Expand Down Expand Up @@ -521,7 +522,7 @@ export const evaluateLabInfoData = (
const element = (
<AccordionLabResults
key={report.id}
title={report.code.coding[0].display}
title={report.code.coding.find((c: Coding) => c.display).display}
abnormalTag={checkAbnormalTag(labReportJson)}
content={content}
organizationId={organizationId}
Expand Down
13 changes: 13 additions & 0 deletions containers/ecr-viewer/src/app/tests/assets/BundleLab.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,19 @@
"status": "final",
"code": {
"coding": [
{
"code": "somelocalcode",
"system": "some.thing"
},
{
"code": "LAB10082",
"display": "STOOL PATHOGENS, NAAT, 12 TO 25 TARGETS",
"system": "http://www.ama-assn.org/go/cpt"
},
{
"code": "a loinc code",
"display": "Stool Pathogens 12 To 25 Targets",
"system": "http://loinc.org"
}
]
},
Expand Down Expand Up @@ -412,6 +421,10 @@
"status": "final",
"code": {
"coding": [
{
"code": "somelocalcode",
"system": "1.2.3"
},
{
"code": "82196-7",
"display": "Campylobacter, NAAT",
Expand Down
60 changes: 60 additions & 0 deletions containers/ecr-viewer/src/app/tests/view-data.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react";
import { render } from "@testing-library/react";
import ECRViewerPage from "../view-data/page";
import { getMedicationDisplayName } from "../view-data/components/common";

jest.mock("../view-data/component-utils", () => ({
metrics: jest.fn(),
Expand Down Expand Up @@ -34,3 +35,62 @@ describe("ECRViewerPage", () => {
unmount(); // Clean up
});
});

describe("getMedicationDisplayName", () => {
it("handles undefined case", () => {
expect(getMedicationDisplayName(undefined)).toBe(undefined);
});

it("handles empty case", () => {
expect(getMedicationDisplayName({ coding: [] })).toBe(undefined);
});

it("handles single named case", () => {
expect(
getMedicationDisplayName({
coding: [{ code: "123", display: "medication", system: "ABC" }],
}),
).toBe("medication");
});

it("handles single un-named case", () => {
expect(
getMedicationDisplayName({
coding: [{ code: "123", system: "ABC" }],
}),
).toBe("Unknown medication name - ABC code 123");
});

it("handles multiple named case", () => {
expect(
getMedicationDisplayName({
coding: [
{ code: "123", display: "first", system: "ABC" },
{ code: "456", display: "second", system: "DEF" },
],
}),
).toBe("first");
});

it("handles multiple mixed named case", () => {
expect(
getMedicationDisplayName({
coding: [
{ code: "123", system: "ABC" },
{ code: "456", display: "second", system: "DEF" },
],
}),
).toBe("second");
});

it("handles multiple un-named case", () => {
expect(
getMedicationDisplayName({
coding: [
{ code: "123", system: "ABC" },
{ code: "456", system: "DEF" },
],
}),
).toBe("Unknown medication name - ABC code 123");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type AdministeredMedicationProps = {
medicationData: AdministeredMedicationTableData[];
};
export type AdministeredMedicationTableData = {
name: string;
name?: string;
date?: string;
};

Expand Down
43 changes: 35 additions & 8 deletions containers/ecr-viewer/src/app/view-data/components/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Bundle,
CarePlanActivity,
CareTeamParticipant,
CodeableConcept,
Condition,
FhirResource,
Immunization,
Expand Down Expand Up @@ -661,16 +662,42 @@ const evaluateAdministeredMedication = (
);
}

if (medication?.code?.coding?.[0]?.display) {
data.push({
date:
medicationAdministration.effectiveDateTime ??
medicationAdministration.effectivePeriod?.start,
name: medication?.code?.coding?.[0]?.display,
});
}
data.push({
date:
medicationAdministration.effectiveDateTime ??
medicationAdministration.effectivePeriod?.start,
name: getMedicationDisplayName(medication?.code),
});
return data;
},
[],
);
};

/**
* Given a CodeableConcept, find an appropriate display name
* @param code - The codable concept if available.
* @returns - String with a name to display (can be "Unknown")
*/
export function getMedicationDisplayName(
code: CodeableConcept | undefined,
): string | undefined {
const codings = code?.coding ?? [];
let name;
// Pull out the first name we find,
for (const coding of codings) {
if (coding.display) {
name = coding.display;
break;
}
}

// There is a code, but no names, pull out the first code to give the user
// something to go off of
if (name === undefined && codings.length > 0) {
const { system, code } = codings[0];
name = `Unknown medication name - ${system} code ${code}`;
}

return name;
}
8 changes: 7 additions & 1 deletion containers/fhir-converter/CustomFhir/CustomFhir.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@
</Content>
</ItemGroup>

</Project>
<ItemGroup>
<Content Include="rxnorm.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
40 changes: 27 additions & 13 deletions containers/fhir-converter/CustomFhir/CustomFilters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Newtonsoft.Json.Linq;
using Microsoft.VisualBasic.FileIO;
using DotLiquid.Util;
using System.IO.Enumeration;

namespace Microsoft.Health.Fhir.Liquid.Converter
{
Expand All @@ -22,6 +23,7 @@ public partial class Filters
{"paragraph", "p"}
};
private static Dictionary<string, string>? loincDict;
private static Dictionary<string, string>? rxnormDict;

// Items from the filter could be arrays or objects, process them to be the same
private static List<Dictionary<string, object>> ProcessItem(object item)
Expand Down Expand Up @@ -306,9 +308,9 @@ private static string CleanStringFromTabs(string value)
// Overloaded method with default level value
public static void PrintObject(object obj)
{
var devMode = Environment.GetEnvironmentVariable("DEV_MODE");
var debugLog = Environment.GetEnvironmentVariable("DEBUG_LOG");
if (devMode != "true" || debugLog != "true")
var devMode = Environment.GetEnvironmentVariable("DEV_MODE") ?? "false";
var debugLog = Environment.GetEnvironmentVariable("DEBUG_LOG") ?? "false";
if (devMode.Trim() != "true" || debugLog.Trim() != "true")
{
return;
}
Expand All @@ -318,9 +320,9 @@ public static void PrintObject(object obj)

private static void PrintObject(object obj, int level)
{
var devMode = Environment.GetEnvironmentVariable("DEV_MODE");
var debugLog = Environment.GetEnvironmentVariable("DEBUG_LOG");
if (devMode != "true" || debugLog != "true")
var devMode = Environment.GetEnvironmentVariable("DEV_MODE") ?? "false";
var debugLog = Environment.GetEnvironmentVariable("DEBUG_LOG") ?? "false";
if (devMode.Trim() != "true" || debugLog.Trim() != "true")
{
return;
}
Expand Down Expand Up @@ -455,12 +457,12 @@ public static string ToHtmlStringJoinBr(object data)
}

/// <summary>
/// Parses a CSV file containing LOINC codes and Long Common Names and returns a dictionary where the LOINC codes are keys and the LCN are values.
/// Parses a CSV file containing keys and values in the first and second columns and returns a dictionary.
/// </summary>
/// <returns>A dictionary where the keys are LOINC codes and the values are descriptions.</returns>
private static Dictionary<string, string> LoincDictionary()
/// <returns>A dictionary where the keys are codes and the values are descriptions.</returns>
private static Dictionary<string, string> CSVMapDictionary(string filename)
{
TextFieldParser parser = new TextFieldParser("Loinc.csv");
TextFieldParser parser = new TextFieldParser(filename);
Dictionary<string, string> csvData = new Dictionary<string, string>();

parser.HasFieldsEnclosedInQuotes = true;
Expand Down Expand Up @@ -489,8 +491,20 @@ private static Dictionary<string, string> LoincDictionary()
/// <returns>The name associated with the specified LOINC code, or null if the code is not found in the dictionary.</returns>
public static string? GetLoincName(string loinc)
{
loincDict ??= LoincDictionary();
loincDict.TryGetValue(loinc, out string? element);
loincDict ??= CSVMapDictionary("Loinc.csv");
loincDict.TryGetValue(loinc ?? string.Empty, out string? element);
return element;
}

/// <summary>
/// Retrieves the name associated with the specified RxNorm code from the RxNorm dictionary.
/// </summary>
/// <param name="rxnorm">The RxNorm code for which to retrieve the name.</param>
/// <returns>The name associated with the specified LOINC code, or null if the code is not found in the dictionary.</returns>
public static string? GetRxnormName(string rxnorm)
{
rxnormDict ??= CSVMapDictionary("rxnorm.csv");
rxnormDict.TryGetValue(rxnorm ?? string.Empty, out string? element);
return element;
}

Expand Down Expand Up @@ -714,4 +728,4 @@ public static IDictionary<string, bool> GetDiagnosisDictionary(IList<object> ent
return result;
}
}
}
}
32 changes: 31 additions & 1 deletion containers/fhir-converter/CustomFhir/CustomFiltersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,36 @@ public void GetLoincName_InvalidLOINC_ReturnsNull()
var actual = Filters.GetLoincName(loinc);
Assert.Null(actual);
}

[Fact]
public void GetLoincName_Null_ReturnsNull()
{
var actual = Filters.GetLoincName(null);
Assert.Null(actual);
}

[Fact]
public void GetRxnormName_ValidRxnorm_ReturnsName()
{
var rxnorm = "1044916";
var actual = Filters.GetRxnormName(rxnorm);
Assert.Equal("VioNex", actual);
}

[Fact]
public void GetRxnormName_InvalidRxnorm_ReturnsNull()
{
var rxnorm = "ABC";
var actual = Filters.GetRxnormName(rxnorm);
Assert.Null(actual);
}

[Fact]
public void GetRxnormName_Null_ReturnsNull()
{
var actual = Filters.GetRxnormName(null);
Assert.Null(actual);
}

[Fact]
public void FindObjectByIdRecursive_ValidId_ReturnsObject()
Expand Down Expand Up @@ -296,4 +326,4 @@ public void GetDiagnosisDictionary_ReturnsDiagnosisDictionary()
var actual = Filters.GetDiagnosisDictionary(CustomFilterTestFixtures.EncounterDiagnoses);
Assert.Equal(new Dictionary<string, bool>() {{ "B05.9", true }, { "B06.0", true }, { "B06.1", true }}, actual);
}
}
}
Loading

0 comments on commit 98a4eed

Please sign in to comment.