Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,6 @@ documented in the table below.
| `LowBoundaryDateTimeMillisecond1` | Specification/Test | | | Diverges from FHIRPath specification. See [Discussion](https://chat.fhir.org/#narrow/channel/179266-fhirpath/topic/lowBoundary.20and.20highBoundary.20with.20incomplete.20date.20time/with/611113639). |
| `HighBoundaryDateTimeMillisecond1` | Specification/Test | | As above. | As above. |
| `HighBoundaryDateTimeMillisecond3` | Specification/Test | | As above. | As above. |
| `Comparable*` | Implementation | | | Function `comparable` is not implemented. |
| `Precision*` | Implementation | | | Function `precision` is not implemented. |
| `testIndex` | Implementation | | | `$index` is not implemented. |
| `testPeriodInvariantOld` | Implementation | | | Function `hasValue` is not implemented. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ internal fun Collection<Any>.invoke(
"lowBoundary" -> this.lowBoundary(params, fhirPathTypeResolver)
"highBoundary" -> this.highBoundary(params, fhirPathTypeResolver)
"precision" -> this.precision(fhirPathTypeResolver)
"comparable" -> this.comparable(params, fhirPathTypeResolver)
Comment thread
jingtang10 marked this conversation as resolved.

// Defined as a boolean logic operator in the specification, but the grammar handles this as a
// function invocation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import dev.ohs.fhir.fhirpath.coerceToType
import dev.ohs.fhir.fhirpath.createSecondBigDecimal
import dev.ohs.fhir.fhirpath.decimalPlaces
import dev.ohs.fhir.fhirpath.toBigDecimalPreservingScale
import dev.ohs.fhir.fhirpath.toEqualCanonicalized
import dev.ohs.fhir.fhirpath.toFhirPathType
import dev.ohs.fhir.fhirpath.toPlainStringWithMinDecimalPlaces
import dev.ohs.fhir.fhirpath.types.FhirPathDate
Expand Down Expand Up @@ -458,3 +459,31 @@ private fun computeDecimalHighBoundary(value: BigDecimal, precision: Int?): Coll
.toBigDecimalPreservingScale()
)
}

/**
* Returns whether the two singleton quantities have comparable units, i.e. whether their units
* canonicalize to the same UCUM base unit (e.g. `cm` and `[in_i]` are both lengths, so they are
* comparable; `cm` and `s` are not). A quantity with an unknown unit is only comparable to a
* quantity with the same unit.
*
* The comparison uses equal semantics ([toEqualCanonicalized]), not equivalence: per the
* specification, returning true "indicates that a result from equality or comparison functions will
* succeed, and not return empty" (https://build.fhir.org/ig/HL7/FHIRPath/#fn-comparable). For
* example, a calendar `year` is equivalent (`~`) to `1 'a'` but not comparable to it, since `1 year
* = 1 'a'` is empty (https://hl7.org/fhirpath/N1/#time-valued-quantities).
*/
Comment thread
jingtang10 marked this conversation as resolved.
internal fun Collection<Any>.comparable(
Comment thread
jingtang10 marked this conversation as resolved.
params: List<Any>,
fhirPathTypeResolver: FhirPathTypeResolver,
): Collection<Boolean> {
check(size <= 1) { "comparable() cannot be called on a collection with more than 1 item" }
val left =
singleOrNull()?.toFhirPathType(fhirPathTypeResolver) as? FhirPathQuantity ?: return emptyList()
val right =
params.singleOrNull()?.toFhirPathType(fhirPathTypeResolver) as? FhirPathQuantity
?: return emptyList()

val leftUnit = left.toEqualCanonicalized().unit ?: return emptyList()
Comment thread
FikriMilano marked this conversation as resolved.
Outdated
val rightUnit = right.toEqualCanonicalized().unit ?: return emptyList()
return listOf(leftUnit == rightUnit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2026 Open Health Stack Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package dev.ohs.fhir.fhirpath

import dev.ohs.fhir.model.r4.Resource
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.serialization.json.Json

private val fhirPathEngine = FhirPathEngine.forR4()

private val observation: Resource =
Json { ignoreUnknownKeys = true }
.decodeFromString(
"""{"resourceType": "Observation", "status": "final", "code": {"text": "weight"},
"valueQuantity": {"value": 80, "unit": "kg", "system": "http://unitsofmeasure.org",
"code": "kg"}}"""
)

class ComparableTest {

@Test
fun `mass units are comparable`() {
assertEquals(
listOf(true),
fhirPathEngine.evaluateExpression("(1 'kg').comparable(1 '[lb_av]')", null).toList(),
)
}

@Test
fun `identical unknown units are comparable`() {
assertEquals(
listOf(true),
fhirPathEngine.evaluateExpression("(1 '[s]').comparable(1 '[s]')", null).toList(),
)
}

@Test
fun `navigated quantity element works as input`() {
assertEquals(
listOf(true),
fhirPathEngine.evaluateExpression("value.comparable(1 'g')", observation).toList(),
)
assertEquals(
listOf(false),
fhirPathEngine.evaluateExpression("value.comparable(1 's')", observation).toList(),
)
Comment thread
jingtang10 marked this conversation as resolved.
}

@Test
fun `calendar year is not comparable to the UCUM year`() {
// Matches the comparison behavior in the published spec: `1 year > 1 'a'` is empty because
// calendar durations above seconds are not comparable to definite durations
// (https://hl7.org/fhirpath/N1/#time-valued-quantities).
assertEquals(
listOf(false),
fhirPathEngine.evaluateExpression("(1 year).comparable(1 'a')", null).toList(),
)
}

@Test
fun `empty input returns empty`() {
assertEquals(
emptyList(),
fhirPathEngine.evaluateExpression("{}.comparable(1 'kg')", null).toList(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ val skippedTestGroupToReasonMap =
"testEscapeUnescape" to "Unimplemented",
"testVariables" to "Unimplemented",
"testConformsTo" to "Unimplemented",
"Comparable" to "Unimplemented",
"Precision" to "Unimplemented",
)

Expand Down
Loading