use ifc_model::{Entity, EntityId, Model, Transaction, Value};
use super::validate::invalid;
use super::CostAuthoringResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantityKind {
Length,
Area,
Volume,
Count,
Weight,
Time,
Number,
}
impl QuantityKind {
pub fn entity_name(self) -> &'static str {
match self {
Self::Length => "IFCQUANTITYLENGTH",
Self::Area => "IFCQUANTITYAREA",
Self::Volume => "IFCQUANTITYVOLUME",
Self::Count => "IFCQUANTITYCOUNT",
Self::Weight => "IFCQUANTITYWEIGHT",
Self::Time => "IFCQUANTITYTIME",
Self::Number => "IFCQUANTITYNUMBER",
}
}
pub fn measure_name(self) -> &'static str {
match self {
Self::Length => "IFCLENGTHMEASURE",
Self::Area => "IFCAREAMEASURE",
Self::Volume => "IFCVOLUMEMEASURE",
Self::Count => "IFCCOUNTMEASURE",
Self::Weight => "IFCMASSMEASURE",
Self::Time => "IFCTIMEMEASURE",
Self::Number => "IFCNUMERICMEASURE",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct QuantityDraft<'a> {
pub kind: QuantityKind,
pub name: &'a str,
pub description: Option<&'a str>,
pub unit: Option<EntityId>,
pub value: f64,
pub formula: Option<&'a str>,
}
pub fn create_quantity(
tx: &mut Transaction,
model: &Model,
draft: QuantityDraft<'_>,
) -> CostAuthoringResult<EntityId> {
let entity = draft.kind.entity_name();
if draft.name.trim().is_empty() {
return Err(invalid(entity, "Name", "expected a non-empty name"));
}
if !draft.value.is_finite() {
return Err(invalid(entity, "Value", "expected a finite measure"));
}
if draft.kind != QuantityKind::Number && draft.value < 0.0 {
return Err(invalid(
entity,
"Value",
"expected a finite non-negative measure",
));
}
if draft.kind == QuantityKind::Count && draft.value.fract() != 0.0 {
return Err(invalid(entity, "CountValue", "expected a whole count"));
}
if let Some(unit) = draft.unit {
super::validate::reference_type(tx, model, entity, "Unit", unit, "IFCNAMEDUNIT")?;
}
Ok(tx.create(Entity::new(
entity,
vec![
Value::Text(draft.name.into()),
draft
.description
.map_or(Value::Null, |d| Value::Text(d.into())),
draft.unit.map_or(Value::Null, Value::Ref),
Value::Typed {
type_name: draft.kind.measure_name().into(),
value: Box::new(if draft.kind == QuantityKind::Count {
#[allow(clippy::cast_possible_truncation)]
Value::Integer(draft.value as i64)
} else {
Value::Real(draft.value)
}),
},
draft.formula.map_or(Value::Null, |f| Value::Text(f.into())),
],
)))
}
pub fn assign_cost_quantities(
tx: &mut Transaction,
model: &Model,
cost_item: EntityId,
quantities: &[EntityId],
) -> CostAuthoringResult<()> {
super::validate::reference_type(
tx,
model,
"IFCCOSTITEM",
"CostQuantities",
cost_item,
"IFCCOSTITEM",
)?;
super::validate::non_empty_unique("IFCCOSTITEM", "CostQuantities", quantities)?;
for &quantity in quantities {
require_quantity(tx, model, quantity)?;
}
tx.set_attribute(
cost_item,
crate::item::slot::COST_QUANTITIES,
Value::List(quantities.iter().copied().map(Value::Ref).collect()),
);
Ok(())
}
fn require_quantity(tx: &Transaction, model: &Model, target: EntityId) -> CostAuthoringResult<()> {
const ACCEPTED: &[&str] = &[
"IFCQUANTITYLENGTH",
"IFCQUANTITYAREA",
"IFCQUANTITYVOLUME",
"IFCQUANTITYCOUNT",
"IFCQUANTITYWEIGHT",
"IFCQUANTITYTIME",
"IFCPHYSICALCOMPLEXQUANTITY",
];
let Some(actual) = super::validate::projected_type(tx, model, target) else {
return Err(crate::mutation::CostAuthoringError::MissingReference {
entity: "IFCCOSTITEM",
attribute: "CostQuantities",
target,
});
};
if ACCEPTED.iter().any(|k| actual.eq_ignore_ascii_case(k)) {
return Ok(());
}
Err(crate::mutation::CostAuthoringError::WrongReferenceType {
entity: "IFCCOSTITEM",
attribute: "CostQuantities",
target,
actual,
expected: "IFCPHYSICALQUANTITY",
})
}