diff --git a/src/main/java/io/github/linuxforhealth/core/terminology/SimpleCode.java b/src/main/java/io/github/linuxforhealth/core/terminology/SimpleCode.java index 0346bc26..a2e2dac6 100644 --- a/src/main/java/io/github/linuxforhealth/core/terminology/SimpleCode.java +++ b/src/main/java/io/github/linuxforhealth/core/terminology/SimpleCode.java @@ -13,6 +13,7 @@ public class SimpleCode { private String system; private String code; private String display; + private String version; /** * Returns simple representation of Code. @@ -20,12 +21,21 @@ public class SimpleCode { * @param code * @param system * @param display + * @param version */ public SimpleCode(String code, String system, String display) { this.code = code; this.system = system; this.display = display; + this.version = null; + } + + public SimpleCode(String code, String system, String display, String version) { + this.code = code; + this.system = system; + this.display = display; + this.version = version; } public String getSystem() { @@ -36,12 +46,18 @@ public String getCode() { return code; } - - public String getDisplay() { return display; } + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + public String toString() { return ReflectionToStringBuilder.toString(this); } diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java index d772943d..b0e6db6e 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/Hl7DataHandlerUtil.java @@ -95,6 +95,22 @@ public static String getOriginalDisplayText(Object obj) { return null; } + public static String getVersion(Object obj) { + if (obj instanceof CWE) { + CWE id = (CWE) obj; + ST st = id.getCodingSystemVersionID(); + if (st != null) { + String str = st.getValue(); + if (str != null) { + return str.trim(); + } + return null; + } + return null; + } + return null; + } + private static String getAssociatedtable(CWE id) { ID val = id.getCwe3_NameOfCodingSystem(); if (val != null && StringUtils.startsWith(val.getValue(), "HL7")) { diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java index 07bd0565..eed56d7b 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataTypeMapper.java @@ -33,6 +33,7 @@ public enum SimpleDataTypeMapper { NAMED_UUID(SimpleDataValueResolver.NAMED_UUID), OBJECT(SimpleDataValueResolver.OBJECT), CODING_SYSTEM_V2(SimpleDataValueResolver.CODING_SYSTEM_V2), + CODING_SYSTEM_V2_ALTERNATE(SimpleDataValueResolver.CODING_SYSTEM_V2_ALTERNATE), SYSTEM_URL(SimpleDataValueResolver.SYSTEM_URL), SYSTEM_ID(SimpleDataValueResolver.SYSTEM_ID), diff --git a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java index a76c1905..55c19a78 100644 --- a/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java +++ b/src/main/java/io/github/linuxforhealth/hl7/data/SimpleDataValueResolver.java @@ -251,10 +251,28 @@ public class SimpleDataValueResolver { return value; }; + public static final ValueExtractor CODING_SYSTEM_V2_ALTERNATE = (Object value) -> { + // ensure we have a CWE + if (value instanceof CWE) { + CWE cwe = (CWE) value; + String table = Hl7DataHandlerUtil.getStringValue(cwe.getCwe6_NameOfAlternateCodingSystem()); + String code = Hl7DataHandlerUtil.getStringValue(cwe.getCwe4_AlternateIdentifier()); + String text = Hl7DataHandlerUtil.getStringValue(cwe.getCwe5_AlternateText()); + String version = Hl7DataHandlerUtil.getStringValue(cwe.getCwe8_AlternateCodingSystemVersionID()); + return commonCodingSystemV2(table, code, text, version); + } + return null; + }; + public static final ValueExtractor CODING_SYSTEM_V2 = (Object value) -> { String table = Hl7DataHandlerUtil.getTableNumber(value); String code = Hl7DataHandlerUtil.getStringValue(value); String text = Hl7DataHandlerUtil.getOriginalDisplayText(value); + String version = Hl7DataHandlerUtil.getVersion(value); + return commonCodingSystemV2(table, code, text, version); + }; + + private static final SimpleCode commonCodingSystemV2 (String table, String code, String text, String version) { if (table != null && code != null) { // Found table and a code. Try looking it up. SimpleCode coding = TerminologyLookup.lookup(table, code); @@ -265,8 +283,9 @@ public class SimpleDataValueResolver { if (display.isEmpty()) { // We have a table, code, but unknown display, so we can't tell if it's good, use the original display text - coding = new SimpleCode(coding.getCode(), coding.getSystem(), text); + coding = new SimpleCode(coding.getCode(), coding.getSystem(), text, version); } + coding.setVersion(version); // We have a table, code, and display, so code was valid return coding; } else { @@ -276,7 +295,7 @@ public class SimpleDataValueResolver { } } else { // No success looking up the code, build our own fall-back system using table name - return new SimpleCode(code, "urn:id:"+table, text) ; + return new SimpleCode(code, "urn:id:"+table, text, version) ; } } else if (code != null) { // A code but no system: build a simple systemless code @@ -284,7 +303,7 @@ public class SimpleDataValueResolver { } else { return null; } - }; + } public static final ValueExtractor BUILD_IDENTIFIER_FROM_CWE = (Object value) -> { CWE newValue = ((CWE) value); diff --git a/src/main/resources/hl7/datatype/CodeableConcept.yml b/src/main/resources/hl7/datatype/CodeableConcept.yml index 07d545c6..9736d056 100644 --- a/src/main/resources/hl7/datatype/CodeableConcept.yml +++ b/src/main/resources/hl7/datatype/CodeableConcept.yml @@ -26,17 +26,14 @@ coding_2: vars: code: ID |IS |TX +# Generalized CWE for alternate codes handles mismatched systems and codes coding_3: - valueOf: datatype/Coding + valueOf: $coding generateList: true - expressionType: resource - condition: $code NOT_NULL && $coding NULL + condition: $coding NOT_NULL specs: CWE vars: - code: CWE.4 - system: CWE.6 - display: CWE.5 - version: CWE.8 + coding: CODING_SYSTEM_V2_ALTERNATE, CWE # Generalized CWE through CODING_SYSTEM_V2 handles mismatched systems and codes coding_4: @@ -52,6 +49,8 @@ coding_5: condition: $coding NOT_NULL generateList: true +# If hideText is not passed by parent, the value is NULL, making it backward compatible with all other uses of CodeableConcept +# Any value in hideText will cause the text to be hidden. Used by Identifier_SystemID. text: condition: $hideText NULL && $displayText NOT_NULL type: STRING @@ -62,5 +61,3 @@ text: # ---------------- # Orphan comments: # ---------------- -# Was at begin of line:56 :# If hideText is not passed by parent, the value is NULL, making it backward compatible with all other uses of CodeableConcept -# Was at begin of line:57 :# Any value in hideText will cause the text to be hidden. Used by Identifier_SystemID. \ No newline at end of file diff --git a/src/main/resources/hl7/resource/Patient.yml b/src/main/resources/hl7/resource/Patient.yml index 009a65d8..ed761161 100644 --- a/src/main/resources/hl7/resource/Patient.yml +++ b/src/main/resources/hl7/resource/Patient.yml @@ -177,9 +177,7 @@ extension: valueCodeableConcept: valueOf: datatype/CodeableConcept expressionType: resource - condition: $coding NOT_NULL - vars: - coding: CODING_SYSTEM_V2, CWE + specs: CWE communication: condition: $language NOT_NULL diff --git a/src/test/java/io/github/linuxforhealth/hl7/segments/CodeableConceptTest.java b/src/test/java/io/github/linuxforhealth/hl7/segments/CodeableConceptTest.java index d30e91af..2b348f58 100644 --- a/src/test/java/io/github/linuxforhealth/hl7/segments/CodeableConceptTest.java +++ b/src/test/java/io/github/linuxforhealth/hl7/segments/CodeableConceptTest.java @@ -42,7 +42,7 @@ void testCodeableConceptNoSystem() { String patientWithCodeableConcept = "MSH|^~\\&|MIICEHRApplication|MIIC|MIIC|MIIC|201705130822||VXU^V04^VXU_V04|test1100|P|2.5.1|||AL|AL|||||Z22^CDCPHINVS|^^^^^MIIC^SR^^^MIIC|MIIC\n" // Test text only race - + "PID|1||12345678^^^^MR||Jane^TestPatientLastName|||||W\n"; + + "PID|1||12345678^^^^MR||TestPatientLastName^Jane^|||||W\n"; Patient patient = PatientUtils.createPatientFromHl7Segment(patientWithCodeableConcept); assertThat(patient.hasExtension()).isTrue(); @@ -53,6 +53,7 @@ void testCodeableConceptNoSystem() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); assertThat(ccW.hasText()).isFalse(); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isFalse(); @@ -77,6 +78,7 @@ void testCodeableConceptWithCDCRECSystem() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); assertThat(ccW.hasText()).isTrue(); assertThat(ccW.getText()).hasToString("White"); Coding coding = ccW.getCodingFirstRep(); @@ -115,6 +117,7 @@ void testCodeableConceptWithCustomText() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isTrue(); assertThat(coding.hasCode()).isTrue(); @@ -151,6 +154,7 @@ void testCodeableConceptWithUnknownEncoding() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isTrue(); assertThat(coding.hasCode()).isTrue(); @@ -170,9 +174,8 @@ void testCodeableConceptWithMissingDisplayText() { // "system": , // "display": ; // "code": - // } ], - // "text": ; - // }, + // } ] + // }, << No TEXT because there is no CWE.2 or CWE.9 String patientWithCodeableConcept = "MSH|^~\\&|MIICEHRApplication|MIIC|MIIC|MIIC|201705130822||VXU^V04^VXU_V04|test1100|P|2.5.1|||AL|AL|||||Z22^CDCPHINVS|^^^^^MIIC^SR^^^MIIC|MIIC\n" // Race with good code in known system but no display text @@ -187,7 +190,8 @@ void testCodeableConceptWithMissingDisplayText() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); - assertThat(ccW.hasText()).isTrue(); + assertThat(ccW.hasText()).isFalse(); + assertThat(ccW.getCoding().size()).isEqualTo(1); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isTrue(); assertThat(coding.hasCode()).isTrue(); @@ -222,6 +226,7 @@ void testCodeableConceptWithBadCodeKnownSystem() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isTrue(); assertThat(coding.hasCode()).isFalse(); @@ -242,7 +247,6 @@ void testCodeableConceptWithBadCodeUnknownTextKnownSystem() { // "text": // }, - String patientWithCodeableConcept = "MSH|^~\\&|MIICEHRApplication|MIIC|MIIC|MIIC|201705130822||VXU^V04^VXU_V04|test1100|P|2.5.1|||AL|AL|||||Z22^CDCPHINVS|^^^^^MIIC^SR^^^MIIC|MIIC\n" // This code does not exist in the CDCREC system, and will fail the lookup, resulting in an error message in display. + "PID|1||12345678^^^^MR||Jane^TestPatientLastName|||||2186-5^hispan^CDCREC\n"; @@ -256,6 +260,7 @@ void testCodeableConceptWithBadCodeUnknownTextKnownSystem() { assertThat(extensions.get(0).hasValue()).isTrue(); CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.getCoding().size()).isEqualTo(1); Coding coding = ccW.getCodingFirstRep(); assertThat(coding.hasDisplay()).isTrue(); assertThat(coding.hasCode()).isFalse(); @@ -303,37 +308,119 @@ void testCodeableConceptForNDC() { } @Test - public void testCodeableConceptForI9() { + public void testCodeableConceptForLoincAltenativeI9WithVersions() { - // With a valid (to us) but unregistered (to FHIR) system, we should produce: + // With a valid (to us) but unregistered (to FHIR) system, and with a version, we should produce: // "code": { // "coding": [ { - // "system": , + // "system": , << FIRST CODING + // "display": + // "code": + // "version": + // }, + // { + // "system": , << SECOND (ALTERNATE CODING) // "display": // "code": + // "version": // } ], // "text": // }, - String conditionWithCodeableConcept = "MSH|^~\\&|||||20040629164652|1|PPR^PC1|331|P|2.3.1||\n" + String conditionWithVersionedAlternateCodeableConcept = "MSH|^~\\&|||||20040629164652|1|PPR^PC1|331|P|2.3.1||\n" + "PID|||1234^^^^MR||DOE^JANE^|||F||||||||||||||||||||||\n" - + "PRB|AD|2004062916460000|596.5^BLADDER DYSFUNCTION^I9||||20040629||||||ACTIVE|||20040629"; + + "PRB|AD|2004062916460000|2148-5^CREATININE^LN^F-11380^CREATININE^I9^474747^22222||||20040629||||||ACTIVE|||20040629"; - Condition condition = ResourceUtils.getCondition(conditionWithCodeableConcept); + Condition condition = ResourceUtils.getCondition(conditionWithVersionedAlternateCodeableConcept); assertThat(condition.hasCode()).isTrue(); CodeableConcept condCC = condition.getCode(); assertThat(condCC.hasText()).isTrue(); - assertThat(condCC.getText()).isEqualTo("BLADDER DYSFUNCTION"); + assertThat(condCC.getText()).isEqualTo("CREATININE"); assertThat(condCC.hasCoding()).isTrue(); + assertThat(condCC.getCoding().size()).isEqualTo(2); Coding condCoding = condCC.getCoding().get(0); assertThat(condCoding.hasSystem()).isTrue(); + assertThat(condCoding.getSystem()).isEqualTo("http://loinc.org"); + assertThat(condCoding.hasCode()).isTrue(); + assertThat(condCoding.getCode()).isEqualTo("2148-5"); + assertThat(condCoding.hasDisplay()).isTrue(); + assertThat(condCoding.getDisplay()).isEqualTo("CREATININE"); + assertThat(condCoding.hasVersion()).isTrue(); + assertThat(condCoding.getVersion()).isEqualTo("474747"); + + condCoding = condCC.getCoding().get(1); + assertThat(condCoding.hasSystem()).isTrue(); assertThat(condCoding.getSystem()).isEqualTo("http://terminology.hl7.org/CodeSystem/icd9"); assertThat(condCoding.hasCode()).isTrue(); - assertThat(condCoding.getCode()).isEqualTo("596.5"); + assertThat(condCoding.getCode()).isEqualTo("F-11380"); assertThat(condCoding.hasDisplay()).isTrue(); - assertThat(condCoding.getDisplay()).isEqualTo("BLADDER DYSFUNCTION"); + assertThat(condCoding.getDisplay()).isEqualTo("CREATININE"); + assertThat(condCoding.hasVersion()).isTrue(); + assertThat(condCoding.getVersion()).isEqualTo("22222"); + } + + @Test + public void testCodeableConceptDoubleRaceWithVersionAndAlternate() { + + // This has both a known and an unknown system. + // "valueCodeableConcept": { + // "coding": [ { + // "system": , << FIRST CODING + // "display": + // "code": + // "version": + // }, + // { + // "system": , << SECOND (ALTERNATE CODING) + // "display": + // "code": + // "version": + // } ], + // "text": + // }, + + String patientWithDoubleRaceWithVersionAndAlternate = "MSH|^~\\&|MIICEHRApplication|MIIC|MIIC|MIIC|201705130822||VXU^V04^VXU_V04|test1100|P|2.5.1|||AL|AL|||||Z22^CDCPHINVS|^^^^^MIIC^SR^^^MIIC|MIIC\n" + // Test double race in the SAME CWE (not a second CWE) and versions. Use made up Cauc to ensure test doesn't mix up whites. + + "PID|1||12345678^^^^MR||TestPatientLastName^Jane|||||2106-3^White^CDCREC^CA^Caucasian^L^1.1^4|\n"; + + Patient patient = PatientUtils.createPatientFromHl7Segment(patientWithDoubleRaceWithVersionAndAlternate); + assertThat(patient.hasExtension()).isTrue(); + + List extensions = patient.getExtensionsByUrl(UrlLookup.getExtensionUrl("race")); + assertThat(extensions).isNotNull(); + assertThat(extensions.size()).isEqualTo(1); + + assertThat(extensions.get(0).hasValue()).isTrue(); + CodeableConcept ccW = (CodeableConcept) extensions.get(0).getValue(); + assertThat(ccW.hasCoding()).isTrue(); + assertThat(ccW.hasText()).isTrue(); + assertThat(ccW.getText()).hasToString("White"); + + List codings = ccW.getCoding(); + assertThat(codings.size()).isEqualTo(2); + + Coding coding = codings.get(0); + assertThat(coding.hasDisplay()).isTrue(); + assertThat(coding.getDisplay()).hasToString("White"); + assertThat(coding.hasCode()).isTrue(); + assertThat(coding.getCode()).hasToString("2106-3"); + assertThat(coding.hasSystem()).isTrue(); + assertThat(coding.getSystem()).hasToString("http://terminology.hl7.org/CodeSystem/v3-Race"); + assertThat(coding.hasVersion()).isTrue(); + assertThat(coding.getVersion()).hasToString("1.1"); + + coding = codings.get(1); + assertThat(coding.hasDisplay()).isTrue(); + assertThat(coding.getDisplay()).hasToString("Caucasian"); + assertThat(coding.hasCode()).isTrue(); + assertThat(coding.getCode()).hasToString("CA"); + assertThat(coding.hasSystem()).isTrue(); + assertThat(coding.getSystem()).hasToString("urn:id:L"); + assertThat(coding.hasVersion()).isTrue(); + assertThat(coding.getVersion()).hasToString("4"); + } }