ferrobridge-fhirpath 0.1.127

A FHIRPath Normative Release 2.0.0 evaluator with the FHIR R4 additions, over the fhir-types element table
Documentation
// SPDX-FileCopyrightText: Cadasto B.V.
// SPDX-License-Identifier: BUSL-1.1

//! Equality (`=`, N1 §6.1.1) and equivalence (`~`, N1 §6.1.2 with the FHIR
//! R4 changes of §2.9.1.6) of items and collections.

use std::cmp::Ordering;
use std::collections::BTreeMap;

use rust_decimal::Decimal;

use super::convert::system;
use super::error::EvalError;
use crate::model::Model;
use crate::quantity::{Quantity, Unit, equivalent_decimals};
use crate::value::node::Node;
use crate::value::{Collection, Item};

/// Whether two items are equal, `None` when unknown.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn equal(
    model: &dyn Model,
    left: &Item<'_>,
    right: &Item<'_>,
) -> Result<Option<bool>, EvalError> {
    let (Some(left), Some(right)) = (system(model, left)?, system(model, right)?) else {
        return Ok(None);
    };
    Ok(match (&left, &right) {
        (Item::Boolean(a), Item::Boolean(b)) => Some(a == b),
        (Item::String(a), Item::String(b)) => Some(a == b),
        (Item::Integer(a), Item::Integer(b)) => Some(a == b),
        (Item::Date(a), Item::Date(b)) => a.compare(*b).map(|order| order == Ordering::Equal),
        (Item::Time(a), Item::Time(b)) => a.compare(b).map(|order| order == Ordering::Equal),
        (Item::Quantity(a), Item::Quantity(b)) => a.equals(b),
        (Item::Node(a), Item::Node(b)) => nodes_equal(model, a, b)?,
        (Item::Type(a), Item::Type(b)) => Some(a == b),
        _ => {
            if let (Some(a), Some(b)) = (number(&left), number(&right)) {
                Some(a == b)
            } else if let (Some(a), Some(b)) = (date_time(&left), date_time(&right)) {
                a.compare(&b).map(|order| order == Ordering::Equal)
            } else if let (Some(a), Some(b)) = (quantity(&left), quantity(&right)) {
                a.equals(&b)
            } else {
                Some(false)
            }
        }
    })
}

/// Whether two items are equivalent.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn equivalent(model: &dyn Model, left: &Item<'_>, right: &Item<'_>) -> Result<bool, EvalError> {
    let (left, right) = match (system(model, left)?, system(model, right)?) {
        (Some(left), Some(right)) => (left, right),
        (None, None) => return Ok(true),
        _ => return Ok(false),
    };
    Ok(match (&left, &right) {
        (Item::Boolean(a), Item::Boolean(b)) => a == b,
        (Item::String(a), Item::String(b)) => fold(a) == fold(b),
        (Item::Integer(a), Item::Integer(b)) => a == b,
        (Item::Date(a), Item::Date(b)) => a.equivalent(*b),
        (Item::Time(a), Item::Time(b)) => a.equivalent(b),
        (Item::Quantity(a), Item::Quantity(b)) => a.equivalent(b) == Some(true),
        (Item::Node(a), Item::Node(b)) => nodes_equivalent(model, a, b)?,
        (Item::Type(a), Item::Type(b)) => a == b,
        _ => {
            if let (Some(a), Some(b)) = (number(&left), number(&right)) {
                equivalent_decimals(a, b)
            } else if let (Some(a), Some(b)) = (date_time(&left), date_time(&right)) {
                a.equivalent(&b)
            } else if let (Some(a), Some(b)) = (quantity(&left), quantity(&right)) {
                a.equivalent(&b) == Some(true)
            } else {
                false
            }
        }
    })
}

/// A string as string equivalence compares it: case ignored, every
/// whitespace character one (N1 §6.1.2).
fn fold(text: &str) -> String {
    text.chars()
        .map(|c| if c.is_whitespace() { ' ' } else { c })
        .flat_map(char::to_lowercase)
        .collect()
}

/// An Integer or Decimal as a decimal, the implicit conversion of N1 §5.5.
#[must_use]
pub fn number(item: &Item<'_>) -> Option<Decimal> {
    match item {
        Item::Integer(value) => Some(Decimal::from(*value)),
        Item::Decimal(value) => Some(*value),
        _ => None,
    }
}

/// A `Date` or `DateTime` as a `DateTime`, the implicit conversion of N1 §5.5.
#[must_use]
pub fn date_time(item: &Item<'_>) -> Option<crate::temporal::DateTime> {
    match item {
        Item::Date(value) => Some(value.to_date_time()),
        Item::DateTime(value) => Some(*value),
        _ => None,
    }
}

/// A number or a quantity as a quantity, a number taking the unit `'1'` (N1
/// §5.5).
#[must_use]
pub fn quantity(item: &Item<'_>) -> Option<Quantity> {
    match item {
        Item::Quantity(value) => Some(value.clone()),
        other => number(other).map(|value| Quantity::new(value, Unit::one())),
    }
}

/// A node's children grouped by member name.
fn grouped<'a>(
    model: &dyn Model,
    node: &Node<'a>,
    skip_id: bool,
) -> Result<BTreeMap<String, Collection<'a>>, EvalError> {
    let mut groups: BTreeMap<String, Collection<'a>> = BTreeMap::new();
    for (name, item) in node.named_children(model)? {
        if skip_id && name == "id" {
            continue;
        }
        groups.entry(name).or_default().push(item);
    }
    Ok(groups)
}

/// The member `name` of a node, empty when its type does not define it.
fn members<'n>(
    model: &dyn Model,
    node: &Node<'n>,
    name: &str,
) -> Result<Collection<'n>, EvalError> {
    Ok(node.member(model, name)?.unwrap_or_default())
}

/// Equality of two complex nodes: every child property equal, recursively
/// (N1 §6.1.1).
fn nodes_equal(
    model: &dyn Model,
    left: &Node<'_>,
    right: &Node<'_>,
) -> Result<Option<bool>, EvalError> {
    if let (Some(a), Some(b)) = (left.fhir_type(), right.fhir_type())
        && a.name != b.name
    {
        return Ok(Some(false));
    }
    let (left, right) = (grouped(model, left, false)?, grouped(model, right, false)?);
    if !left.keys().eq(right.keys()) {
        return Ok(Some(false));
    }
    let mut unknown = false;
    for (name, items) in &left {
        let others = right.get(name).map_or(&[][..], Vec::as_slice);
        match collections_equal(model, items, others)? {
            Some(false) => return Ok(Some(false)),
            None => unknown = true,
            Some(true) => {}
        }
    }
    Ok(if unknown { None } else { Some(true) })
}

/// Equivalence of two complex nodes, FHIR R4 §2.9.1.6: ids ignored, a
/// `Coding` by its system and code, a `CodeableConcept` by any shared
/// coding.
fn nodes_equivalent(
    model: &dyn Model,
    left: &Node<'_>,
    right: &Node<'_>,
) -> Result<bool, EvalError> {
    let (a, b) = (
        left.fhir_type().map(|ty| ty.name),
        right.fhir_type().map(|ty| ty.name),
    );
    if let (Some(a), Some(b)) = (a, b)
        && a != b
    {
        return Ok(false);
    }
    match a.or(b) {
        Some("Coding") => {
            for name in ["system", "code"] {
                let (mine, theirs) = (members(model, left, name)?, members(model, right, name)?);
                if !collections_equivalent(model, &mine, &theirs)? {
                    return Ok(false);
                }
            }
            Ok(true)
        }
        Some("CodeableConcept") => {
            let (codings, others) = (
                members(model, left, "coding")?,
                members(model, right, "coding")?,
            );
            for coding in &codings {
                for other in &others {
                    if equivalent(model, coding, other)? {
                        return Ok(true);
                    }
                }
            }
            Ok(false)
        }
        _ => {
            let (left, right) = (grouped(model, left, true)?, grouped(model, right, true)?);
            if !left.keys().eq(right.keys()) {
                return Ok(false);
            }
            for (name, items) in &left {
                let others = right.get(name).map_or(&[][..], Vec::as_slice);
                if !collections_equivalent(model, items, others)? {
                    return Ok(false);
                }
            }
            Ok(true)
        }
    }
}

/// `=` over collections (N1 §6.1.1): empty when either is empty, `false` for
/// different lengths, else every item equal in order.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn collections_equal(
    model: &dyn Model,
    left: &[Item<'_>],
    right: &[Item<'_>],
) -> Result<Option<bool>, EvalError> {
    if left.is_empty() || right.is_empty() {
        return Ok(None);
    }
    if left.len() != right.len() {
        return Ok(Some(false));
    }
    let mut unknown = false;
    for (a, b) in left.iter().zip(right) {
        match equal(model, a, b)? {
            Some(false) => return Ok(Some(false)),
            None => unknown = true,
            Some(true) => {}
        }
    }
    Ok(if unknown { None } else { Some(true) })
}

/// `~` over collections (N1 §6.1.2): two empty collections are equivalent,
/// otherwise every item has an equivalent counterpart, in any order.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn collections_equivalent(
    model: &dyn Model,
    left: &[Item<'_>],
    right: &[Item<'_>],
) -> Result<bool, EvalError> {
    if left.len() != right.len() {
        return Ok(false);
    }
    let mut used = vec![false; right.len()];
    for item in left {
        let mut matched = false;
        for (index, other) in right.iter().enumerate() {
            if used.get(index).copied().unwrap_or(true) {
                continue;
            }
            if equivalent(model, item, other)? {
                if let Some(slot) = used.get_mut(index) {
                    *slot = true;
                }
                matched = true;
                break;
            }
        }
        if !matched {
            return Ok(false);
        }
    }
    Ok(true)
}

/// Whether `collection` holds an item equal to `item`.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn contains(
    model: &dyn Model,
    collection: &[Item<'_>],
    item: &Item<'_>,
) -> Result<bool, EvalError> {
    for candidate in collection {
        if equal(model, candidate, item)? == Some(true) {
            return Ok(true);
        }
    }
    Ok(false)
}

/// The bucket an item falls in for [`distinct`]: two items `=` calls equal
/// always share one, so only the items of a bucket need comparing.
///
/// Strings, Booleans and numbers are equal only to a value of their own kind
/// with the same value (N1 §6.1.1), and a quantity that converts to the unit
/// `'1'` is equal to the number it converts to (N1 §5.5), so those key by
/// value. Every other kind shares one bucket per kind, compared item by item.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Bucket {
    /// A Boolean.
    Boolean(bool),
    /// A String.
    String(String),
    /// An Integer, a Decimal, or a quantity in the unit `'1'`, by value.
    Number(Decimal),
    /// A `Date` or a `DateTime`, which compare across the two types.
    DateTime,
    /// A Time.
    Time,
    /// A quantity that does not convert to `'1'`.
    Quantity,
    /// A complex element or resource.
    Node,
    /// A type description.
    Type,
}

/// The bucket of a system value.
fn bucket(item: &Item<'_>) -> Bucket {
    match item {
        Item::Boolean(value) => Bucket::Boolean(*value),
        Item::String(value) => Bucket::String(value.clone()),
        Item::Integer(value) => Bucket::Number(Decimal::from(*value).normalize()),
        Item::Decimal(value) => Bucket::Number(value.normalize()),
        Item::Date(_) | Item::DateTime(_) => Bucket::DateTime,
        Item::Time(_) => Bucket::Time,
        Item::Quantity(value) => match value.convert(&Unit::one()) {
            Some(number) => Bucket::Number(number.value.normalize()),
            None => Bucket::Quantity,
        },
        Item::Node(_) => Bucket::Node,
        Item::Type(_) => Bucket::Type,
    }
}

/// The distinct items of `items`, the first of each kept in order.
///
/// Each item is compared with `=` (N1 §5.1.4) against the kept items of its
/// bucket only, so a collection of distinct strings or numbers costs a map
/// lookup per item.
///
/// # Errors
///
/// Returns [`EvalError::Document`] when a node contradicts the model.
pub fn distinct<'a>(model: &dyn Model, items: Collection<'a>) -> Result<Collection<'a>, EvalError> {
    let mut kept: Collection<'a> = Vec::with_capacity(items.len());
    let mut buckets: BTreeMap<Bucket, Vec<usize>> = BTreeMap::new();
    for item in items {
        // NOTE: N1 §6.1.1 answers empty for an item with no value, so it equals nothing and stays.
        let Some(key) = system(model, &item)?.as_ref().map(bucket) else {
            kept.push(item);
            continue;
        };
        let members = buckets.entry(key).or_default();
        let mut seen = false;
        for index in members.iter() {
            if let Some(candidate) = kept.get(*index)
                && equal(model, candidate, &item)? == Some(true)
            {
                seen = true;
                break;
            }
        }
        if !seen {
            members.push(kept.len());
            kept.push(item);
        }
    }
    Ok(kept)
}