use ifc_model::{EntityId, Model};
use crate::quantity::{Quantity, QuantityKind};
use crate::unit::{unit, UnitKind};
#[derive(Debug, Clone, PartialEq)]
pub enum Comparison {
Agrees {
authored: f64,
computed: f64,
relative_difference: f64,
},
Disagrees {
authored: f64,
computed: f64,
relative_difference: f64,
},
UnitMismatch {
authored_unit: String,
computed_unit: String,
},
KindMismatch {
authored: QuantityKind,
computed: QuantityKind,
},
NotComparable,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ComputedQuantity {
pub kind: QuantityKind,
pub value: f64,
pub unit: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Tolerance {
pub relative: f64,
pub absolute: f64,
}
impl Default for Tolerance {
fn default() -> Self {
Self {
relative: 1e-6,
absolute: 1e-9,
}
}
}
pub fn compare(
model: &Model,
quantity: &Quantity,
computed: &ComputedQuantity,
tolerance: Tolerance,
) -> Comparison {
let Quantity::Simple {
kind,
value,
unit: unit_id,
..
} = quantity
else {
return Comparison::NotComparable;
};
if *kind != computed.kind {
return Comparison::KindMismatch {
authored: *kind,
computed: computed.kind,
};
}
if let Some(authored_unit) = unit_id.and_then(|id| unit_name(model, id)) {
if !authored_unit.eq_ignore_ascii_case(&computed.unit) {
return Comparison::UnitMismatch {
authored_unit,
computed_unit: computed.unit.clone(),
};
}
}
let difference = (value - computed.value).abs();
let magnitude = value.abs().max(computed.value.abs());
let relative_difference = if magnitude > 0.0 {
difference / magnitude
} else {
0.0
};
let agrees = difference <= tolerance.absolute || relative_difference <= tolerance.relative;
if agrees {
Comparison::Agrees {
authored: *value,
computed: computed.value,
relative_difference,
}
} else {
Comparison::Disagrees {
authored: *value,
computed: computed.value,
relative_difference,
}
}
}
fn unit_name(model: &Model, id: EntityId) -> Option<String> {
match unit(model, id)? {
UnitKind::Si { name, prefix, .. } => Some(match prefix {
Some(p) => format!("{p}{name}"),
None => name.to_string(),
}),
UnitKind::Conversion { name, .. } => name.map(|n| n.to_string()),
UnitKind::Monetary { currency } => currency.map(|c| c.to_string()),
UnitKind::Derived { unit_type, .. } | UnitKind::ContextDependent { unit_type } => {
Some(unit_type.to_string())
}
}
}