use ifc_model::{EntityId, Model, Transaction, Value};
use crate::ArithmeticOperator;
use super::datetime::{patch, DateTimeValue, PLACEHOLDERS};
use super::draft::{CostValueDraft, CostValueKind};
use super::release::{bind, Release};
use super::validate::{invalid, reference_type};
use super::{CostAuthoringError, CostAuthoringResult};
pub fn create_cost_value(
tx: &mut Transaction,
model: &Model,
draft: CostValueDraft<'_>,
) -> CostAuthoringResult<EntityId> {
const ENTITY: &str = "IFCCOSTVALUE";
let release = bind(model)?;
let (applied_value, operator, components) = match draft.kind {
CostValueKind::Monetary(amount) => {
if !amount.is_finite() {
return Err(invalid(
ENTITY,
"AppliedValue",
"expected a finite monetary amount",
));
}
(
Value::Typed {
type_name: "IFCMONETARYMEASURE".into(),
value: Box::new(Value::Real(amount)),
},
Value::Null,
Value::Null,
)
}
CostValueKind::Components {
operator,
components,
} => {
if components.is_empty() {
return Err(invalid(
ENTITY,
"Components",
"expected at least one component",
));
}
for target in components {
reference_type(tx, model, ENTITY, "Components", *target, ENTITY)?;
}
(
Value::Null,
Value::Enum(operator_token(operator).into()),
refs(components),
)
}
};
let dates = [draft.applicable_date, draft.fixed_until_date];
for (date, attribute) in dates.iter().zip(["ApplicableDate", "FixedUntilDate"]) {
if let Some(date) = date {
check_date(release, ENTITY, attribute, *date)?;
}
}
let provisional = |slot: usize| {
dates[slot].map_or(Value::Null, |date: DateTimeValue<'_>| {
date.provisional(PLACEHOLDERS[slot])
})
};
let mut record = release.record(
ENTITY,
vec![
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("AppliedValue", applied_value),
("ApplicableDate", provisional(0)),
("FixedUntilDate", provisional(1)),
("Category", optional_text(draft.category)),
("Condition", optional_text(draft.condition)),
("ArithmeticOperator", operator),
("Components", components),
],
)?;
patch(tx, &mut record, &dates);
Ok(tx.create(record))
}
fn check_date(
release: Release,
entity: &'static str,
attribute: &'static str,
date: DateTimeValue<'_>,
) -> CostAuthoringResult<()> {
date.check()?;
let fits = match date.record_type() {
None => true,
Some(record) => release.accepts_record(entity, attribute, record)?,
};
if fits {
return Ok(());
}
let declared = release
.schema()
.attributes(entity)
.into_iter()
.find(|declared| declared.name.eq_ignore_ascii_case(attribute))
.map_or("", |declared| declared.type_name.as_str());
Err(CostAuthoringError::AuthoringValueType {
entity,
attribute,
declared,
schema: release.version(),
})
}
fn operator_token(operator: ArithmeticOperator) -> &'static str {
match operator {
ArithmeticOperator::Add => "ADD",
ArithmeticOperator::Divide => "DIVIDE",
ArithmeticOperator::Multiply => "MULTIPLY",
ArithmeticOperator::Subtract => "SUBTRACT",
}
}
pub(crate) fn optional_text(value: Option<&str>) -> Value {
value.map_or(Value::Null, |value| Value::Text(value.into()))
}
pub(crate) fn optional_enum(value: Option<&str>) -> Value {
value.map_or(Value::Null, |value| Value::Enum(value.into()))
}
pub(crate) fn refs(ids: &[EntityId]) -> Value {
Value::List(ids.iter().copied().map(Value::Ref).collect())
}
pub fn create_monetary_unit(
tx: &mut Transaction,
model: &Model,
currency: &str,
) -> CostAuthoringResult<EntityId> {
const ENTITY: &str = "IFCMONETARYUNIT";
let release = bind(model)?;
if currency.trim().is_empty() {
return Err(invalid(ENTITY, "Currency", "expected a currency label"));
}
let (_, declaration) = release.declared(ENTITY, "Currency")?;
let value = match release
.schema()
.type_def(&declaration.type_name)
.map(|t| &t.kind)
{
Some(ifc_schema::TypeKind::Enumeration(_)) => {
let token = release.enumerator(ENTITY, "Currency", currency)?.ok_or(
CostAuthoringError::AuthoringValueType {
entity: ENTITY,
attribute: "Currency",
declared: declaration.type_name.as_str(),
schema: release.version(),
},
)?;
Value::Enum(token.into())
}
_ => Value::Text(currency.into()),
};
let record = release.record(ENTITY, vec![("Currency", value)])?;
Ok(tx.create(record))
}
pub fn create_currency_relationship(
tx: &mut Transaction,
model: &Model,
relating: EntityId,
related: EntityId,
exchange_rate: f64,
rate_date_time: Option<DateTimeValue<'_>>,
) -> CostAuthoringResult<EntityId> {
const ENTITY: &str = "IFCCURRENCYRELATIONSHIP";
let release = bind(model)?;
for (target, attribute) in [
(relating, "RelatingMonetaryUnit"),
(related, "RelatedMonetaryUnit"),
] {
reference_type(tx, model, ENTITY, attribute, target, "IFCMONETARYUNIT")?;
}
if relating == related {
return Err(invalid(
ENTITY,
"RelatedMonetaryUnit",
"expected a unit other than the relating one",
));
}
if !exchange_rate.is_finite() || exchange_rate <= 0.0 {
return Err(invalid(
ENTITY,
"ExchangeRate",
"expected a positive finite ratio",
));
}
if let Some(date) = rate_date_time {
check_date(release, ENTITY, "RateDateTime", date)?;
}
let dates = [rate_date_time];
let mut record = release.record(
ENTITY,
vec![
("RelatingMonetaryUnit", Value::Ref(relating)),
("RelatedMonetaryUnit", Value::Ref(related)),
("ExchangeRate", Value::Real(exchange_rate)),
(
"RateDateTime",
rate_date_time.map_or(Value::Null, |date| date.provisional(PLACEHOLDERS[0])),
),
],
)?;
patch(tx, &mut record, &dates);
Ok(tx.create(record))
}