Add simple local implementation of UCUM temperature canonicalization … (#3872)

* Add simple local implementation of UCUM temperature canonicalization for Celsius and Fahrenheit

* Adjust conversion to not increase precision, as it matter when finding ranges

* Simplify test variables

Co-authored-by: juan.marchionatto <juan.marchionatto@smilecdr.com>
This commit is contained in:
jmarchionatto 2022-08-05 10:48:37 -04:00 committed by GitHub
parent 2e1f4c25f5
commit ebac65cb31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 19 deletions

View File

@ -0,0 +1,5 @@
---
type: fix
issue: 3865
title: "Previously, Celsius and Fahrenheit temperature quantities were not normalized. This is now fixed.
This change requires reindexing of resources containing Celsius or Fahrenheit temperature quantities."

View File

@ -1548,25 +1548,24 @@ public class FhirResourceDaoR4SearchWithElasticSearchIT extends BaseJpaTest impl
}
@Nested
@Disabled // These conversions are not supported by the library we use
public class TemperatureUnitConversions {
@Test
public void celsiusToFahrenheit() {
public void storeCelsiusSearchFahrenheit() {
withObservationWithQuantity(37.5, UCUM_CODESYSTEM_URL, "Cel" );
assertFind( "when eq UCUM 99.5 degF", "/Observation?value-quantity=99.5|" + UCUM_CODESYSTEM_URL + "|degF");
assertNotFind( "when eq UCUM 101.1 degF", "/Observation?value-quantity=101.1|" + UCUM_CODESYSTEM_URL + "|degF");
assertNotFind( "when eq UCUM 97.8 degF", "/Observation?value-quantity=97.8|" + UCUM_CODESYSTEM_URL + "|degF");
assertFind( "when eq UCUM 99.5 degF", "/Observation?value-quantity=99.5|" + UCUM_CODESYSTEM_URL + "|[degF]");
assertNotFind( "when eq UCUM 101.1 degF", "/Observation?value-quantity=101.1|" + UCUM_CODESYSTEM_URL + "|[degF]");
assertNotFind( "when eq UCUM 97.8 degF", "/Observation?value-quantity=97.8|" + UCUM_CODESYSTEM_URL + "|[degF]");
}
// @Test
public void fahrenheitToCelsius() {
withObservationWithQuantity(99.5, UCUM_CODESYSTEM_URL, "degF" );
@Test
public void storeFahrenheitSearchCelsius() {
withObservationWithQuantity(99.5, UCUM_CODESYSTEM_URL, "[degF]" );
assertFind( "when eq UCUM 37.5 Cel", "/Observation?value-quantity=99.5|" + UCUM_CODESYSTEM_URL + "|Cel");
assertNotFind( "when eq UCUM 38.1 Cel", "/Observation?value-quantity=101.1|" + UCUM_CODESYSTEM_URL + "|Cel");
assertNotFind( "when eq UCUM 36.9 Cel", "/Observation?value-quantity=97.8|" + UCUM_CODESYSTEM_URL + "|Cel");
assertFind( "when eq UCUM 37.5 Cel", "/Observation?value-quantity=37.5|" + UCUM_CODESYSTEM_URL + "|Cel");
assertNotFind( "when eq UCUM 37.3 Cel", "/Observation?value-quantity=37.3|" + UCUM_CODESYSTEM_URL + "|Cel");
assertNotFind( "when eq UCUM 37.7 Cel", "/Observation?value-quantity=37.7|" + UCUM_CODESYSTEM_URL + "|Cel");
}
}

View File

@ -22,6 +22,8 @@ package ca.uhn.fhir.jpa.model.util;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import ca.uhn.fhir.rest.param.QuantityParam;
import org.fhir.ucum.Decimal;
@ -43,6 +45,10 @@ public class UcumServiceUtil {
private static final Logger ourLog = LoggerFactory.getLogger(UcumServiceUtil.class);
public static final String CELSIUS_CODE = "Cel";
public static final String FAHRENHEIT_CODE = "[degF]";
public static final float CELSIUS_KELVIN_DIFF = 273.15f;
public static final String UCUM_CODESYSTEM_URL = "http://unitsofmeasure.org";
private static final String UCUM_SOURCE = "/ucum-essence.xml";
@ -63,7 +69,7 @@ public class UcumServiceUtil {
myUcumEssenceService = new UcumEssenceService(input);
} catch (UcumException e) {
ourLog.warn("Failed to load ucum code from ", UCUM_SOURCE, e);
ourLog.warn("Failed to load ucum code from {}: {}", UCUM_SOURCE, e);
} finally {
ClasspathUtil.close(input);
}
@ -87,6 +93,15 @@ public class UcumServiceUtil {
if (!UCUM_CODESYSTEM_URL.equals(theSystem) || theValue == null || theCode == null)
return null;
if ( isCelsiusOrFahrenheit(theCode) ) {
try {
return getCanonicalFormForCelsiusOrFahrenheit(theValue, theCode);
} catch (UcumException theE) {
ourLog.error("Exception when trying to obtain canonical form for value {} and code {}: {}", theValue, theCode, theE.getMessage());
return null;
}
}
init();
Pair theCanonicalPair;
@ -103,6 +118,46 @@ public class UcumServiceUtil {
return theCanonicalPair;
}
private static Pair getCanonicalFormForCelsiusOrFahrenheit(BigDecimal theValue, String theCode) throws UcumException {
return theCode.equals(CELSIUS_CODE)
? canonicalizeCelsius(theValue)
: canonicalizeFahrenheit(theValue);
}
/**
* Returns the received Fahrenheit value converted to Kelvin units and code
* Formula is K = (x°F 32) × 5/9 + 273.15
*/
private static Pair canonicalizeFahrenheit(BigDecimal theValue) throws UcumException {
BigDecimal converted = theValue
.subtract( BigDecimal.valueOf(32) )
.multiply( BigDecimal.valueOf(5f / 9f) )
.add( BigDecimal.valueOf(CELSIUS_KELVIN_DIFF) );
// disallow precision larger than input, as it matters when defining ranges
BigDecimal adjusted = converted.setScale(theValue.precision(), RoundingMode.HALF_UP);
Decimal newValue = new Decimal(adjusted.toPlainString());
return new Pair(newValue, "K");
}
/**
* Returns the received Celsius value converted to Kelvin units and code
*/
private static Pair canonicalizeCelsius(BigDecimal theValue) throws UcumException {
Decimal valueDec = new Decimal(theValue.toPlainString(), theValue.precision());
Decimal converted = valueDec
.add(new Decimal(Float.toString(CELSIUS_KELVIN_DIFF)));
return new Pair(converted, "K");
}
private static boolean isCelsiusOrFahrenheit(String theCode) {
return theCode.equals(CELSIUS_CODE) || theCode.equals(FAHRENHEIT_CODE);
}
@Nullable
public static QuantityParam toCanonicalQuantityOrNull(QuantityParam theQuantityParam) {
Pair canonicalForm = getCanonicalForm(theQuantityParam.getSystem(), theQuantityParam.getValue(), theQuantityParam.getUnits());

View File

@ -1,10 +1,16 @@
package ca.uhn.fhir.jpa.model.util;
import static ca.uhn.fhir.jpa.model.util.UcumServiceUtil.CELSIUS_KELVIN_DIFF;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import org.fhir.ucum.Decimal;
import org.fhir.ucum.Pair;
import org.fhir.ucum.UcumException;
import org.junit.jupiter.api.Test;
public class UcumServiceUtilTest {
@ -54,9 +60,31 @@ public class UcumServiceUtilTest {
}
@Test
public void testUcumDegreeFahrenheit() {
public void testUcumDegreeFahrenheit() throws UcumException {
Pair canonicalPair = UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal("99.82"), "[degF]");
Decimal converted = canonicalPair.getValue();
Decimal expected = new Decimal("310.8278");
// System.out.println("expected: " + expected);
// System.out.println("converted: " + converted);
assertTrue( converted.equals(expected));
assertEquals("K", canonicalPair.getCode());
assertEquals(null, UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal(99.82), "[degF]"));
}
@Test
public void testUcumDegreeCelsius() throws UcumException {
// round it up to set expected decimal precision
BigDecimal roundedCelsiusKelvinDiff = new BigDecimal(CELSIUS_KELVIN_DIFF)
.round(new MathContext(5, RoundingMode.HALF_UP));
Pair canonicalPair = UcumServiceUtil.getCanonicalForm(UcumServiceUtil.UCUM_CODESYSTEM_URL, new BigDecimal("73.54"), "Cel");
Decimal converted = canonicalPair.getValue();
Decimal expected = new Decimal("73.54").add(new Decimal(roundedCelsiusKelvinDiff.toString()));
// System.out.println("expected: " + expected);
// System.out.println("converted: " + converted);
// System.out.println("diff: " + converted.subtract(expectedApprox));
assertTrue( converted.equals(expected));
assertEquals("K", canonicalPair.getCode());
}

View File

@ -4839,10 +4839,10 @@ public class ResourceProviderR4Test extends BaseResourceProviderR4Test {
assertEquals(1, returnedBundle.getEntry().size());
//-- check only use original quantity table to search
//-- check use normalized quantity table to search
String searchSql = myCaptureQueriesListener.getSelectQueries().get(0).getSql(true, true);
assertThat(searchSql, containsString("HFJ_SPIDX_QUANTITY t0"));
assertThat(searchSql, not(containsString("HFJ_SPIDX_QUANTITY_NRML")));
assertThat(searchSql, not (containsString("HFJ_SPIDX_QUANTITY t0")));
assertThat(searchSql, (containsString("HFJ_SPIDX_QUANTITY_NRML")));
}
@Test