Skip to main content

ifc_cost/
value.rs

1//! `IfcCostValue` and the applied-value tree beneath it.
2//!
3//! # Slots, verified against IFC4 EXPRESS
4//!
5//! `IfcCostValue` adds nothing of its own; every slot comes from
6//! `IfcAppliedValue`:
7//!
8//! ```text
9//! 0 Name                1 Description        2 AppliedValue
10//! 3 UnitBasis           4 ApplicableDate     5 FixedUntilDate
11//! 6 Category            7 Condition          8 ArithmeticOperator
12//! 9 Components
13//! ```
14//!
15//! # A cost value is a tree, not a number
16//!
17//! `Components` holds nested `IfcAppliedValue`s and `ArithmeticOperator` says
18//! how to combine them. A value with components and an `ADD` operator is the
19//! sum of its children; the `AppliedValue` slot may then be absent entirely.
20//! Reading only slot 2 and calling that "the amount" silently reports nothing
21//! for every composed rate in the file.
22//!
23//! # What this module refuses to do
24//!
25//! It does not evaluate the tree. `ArithmeticOperator` is `ADD`, `DIVIDE`,
26//! `MULTIPLY` or `SUBTRACT` over a LIST whose order the schema does not
27//! constrain to be meaningful for non-commutative operators: `SUBTRACT` over
28//! `[a, b, c]` has no defined bracketing in the standard. Folding it anyway
29//! would produce a number that looks authoritative and is not. So the shape is
30//! reported and evaluation is left to a caller who knows their own convention.
31
32use ifc_model::{Entity, EntityId, Model, Value};
33
34/// `IfcAppliedValue` slots. `IfcCostValue` adds none of its own.
35mod slot {
36    /// `Name`.
37    pub const NAME: usize = 0;
38    /// `Description`.
39    pub const DESCRIPTION: usize = 1;
40    /// `AppliedValue`, usually `IFCMONETARYMEASURE`.
41    pub const APPLIED_VALUE: usize = 2;
42    /// `UnitBasis`, an `IfcMeasureWithUnit`.
43    pub const UNIT_BASIS: usize = 3;
44    /// `ApplicableDate`.
45    pub const APPLICABLE_DATE: usize = 4;
46    /// `FixedUntilDate`.
47    pub const FIXED_UNTIL_DATE: usize = 5;
48    /// `Category`.
49    pub const CATEGORY: usize = 6;
50    /// `Condition`.
51    pub const CONDITION: usize = 7;
52    /// `ArithmeticOperator`.
53    pub const ARITHMETIC_OPERATOR: usize = 8;
54    /// `Components`.
55    pub const COMPONENTS: usize = 9;
56}
57
58/// How an applied value combines its components.
59///
60/// `IfcArithmeticOperatorEnum`, verified against IFC4 EXPRESS.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum ArithmeticOperator {
63    /// `.ADD.`
64    Add,
65    /// `.DIVIDE.`
66    Divide,
67    /// `.MULTIPLY.`
68    Multiply,
69    /// `.SUBTRACT.`
70    Subtract,
71}
72
73impl ArithmeticOperator {
74    /// Parse the enum token, without its dots.
75    #[must_use]
76    pub fn parse(token: &str) -> Option<Self> {
77        Some(match token {
78            "ADD" => Self::Add,
79            "DIVIDE" => Self::Divide,
80            "MULTIPLY" => Self::Multiply,
81            "SUBTRACT" => Self::Subtract,
82            _ => return None,
83        })
84    }
85
86    /// Whether operand order changes the result.
87    ///
88    /// `ADD` and `MULTIPLY` are commutative, so folding a component list in
89    /// file order is safe. `SUBTRACT` and `DIVIDE` are not, and IFC does not
90    /// define the bracketing, so a caller folding them is choosing a
91    /// convention rather than reading one.
92    #[must_use]
93    pub fn is_order_sensitive(self) -> bool {
94        matches!(self, Self::Divide | Self::Subtract)
95    }
96}
97
98/// How a rate is expressed per unit of something.
99///
100/// `UnitBasis` is an `IfcMeasureWithUnit`: "45.50 per 1 cubic metre". Without
101/// it a cost value is a lump sum; with it, it is a rate and multiplying it by
102/// a quantity is meaningful.
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct UnitBasis {
105    /// The entity holding the basis.
106    pub id: EntityId,
107    /// The numeric component of the basis measure.
108    pub value: Option<f64>,
109    /// The measure type wrapping it, e.g. `IFCVOLUMEMEASURE`.
110    pub measure: Option<&'static str>,
111    /// The unit entity the basis names, if it resolves.
112    pub unit: Option<EntityId>,
113}
114
115/// A borrowed view of an `IfcCostValue` entity.
116#[derive(Debug, Clone, Copy)]
117pub struct CostValue<'m> {
118    id: EntityId,
119    entity: &'m Entity,
120}
121
122impl<'m> CostValue<'m> {
123    /// Wrap an entity known to be an `IfcCostValue`.
124    #[must_use]
125    pub fn new(id: EntityId, entity: &'m Entity) -> Self {
126        Self { id, entity }
127    }
128
129    /// The entity id in the file.
130    #[must_use]
131    pub fn id(&self) -> EntityId {
132        self.id
133    }
134
135    /// The value's name, e.g. `Estimate`.
136    #[must_use]
137    pub fn name(&self) -> Option<&'m str> {
138        self.entity.text(slot::NAME)
139    }
140
141    /// The value's description.
142    #[must_use]
143    pub fn description(&self) -> Option<&'m str> {
144        self.entity.text(slot::DESCRIPTION)
145    }
146
147    /// The directly stated amount.
148    ///
149    /// `None` for a composed value that states only `Components`. Use
150    /// [`CostValue::component_refs`] and [`CostValue::operator`] for those, or
151    /// [`CostValue::is_composed`] to tell the two shapes apart.
152    #[must_use]
153    pub fn amount(&self) -> Option<f64> {
154        self.entity
155            .attribute(slot::APPLIED_VALUE)?
156            .unwrap_typed()
157            .as_f64()
158    }
159
160    /// The measure type wrapping the applied value, e.g. `IFCMONETARYMEASURE`.
161    ///
162    /// A cost value is not required to be monetary: `IfcAppliedValueSelect`
163    /// also admits ratios and plain measures. Reporting the wrapper lets a
164    /// caller refuse to add a ratio to a currency.
165    #[must_use]
166    pub fn measure(&self) -> Option<&'m str> {
167        match self.entity.attribute(slot::APPLIED_VALUE)? {
168            Value::Typed { type_name, .. } => Some(type_name),
169            _ => None,
170        }
171    }
172
173    /// Whether the applied value is a monetary measure.
174    #[must_use]
175    pub fn is_monetary(&self) -> bool {
176        self.measure()
177            .is_some_and(|m| m.eq_ignore_ascii_case("IFCMONETARYMEASURE"))
178    }
179
180    /// The rate category, e.g. `Labour` or `Material`.
181    #[must_use]
182    pub fn category(&self) -> Option<&'m str> {
183        self.entity.text(slot::CATEGORY)
184    }
185
186    /// The condition under which the value applies.
187    #[must_use]
188    pub fn condition(&self) -> Option<&'m str> {
189        self.entity.text(slot::CONDITION)
190    }
191
192    /// The date from which the value applies, as authored.
193    #[must_use]
194    pub fn applicable_date(&self) -> Option<&'m str> {
195        self.entity.text(slot::APPLICABLE_DATE)
196    }
197
198    /// The date after which the value no longer holds, as authored.
199    #[must_use]
200    pub fn fixed_until_date(&self) -> Option<&'m str> {
201        self.entity.text(slot::FIXED_UNTIL_DATE)
202    }
203
204    /// How this value's components combine, if stated.
205    #[must_use]
206    pub fn operator(&self) -> Option<ArithmeticOperator> {
207        match self.entity.attribute(slot::ARITHMETIC_OPERATOR)? {
208            Value::Enum(token) => ArithmeticOperator::parse(token),
209            _ => None,
210        }
211    }
212
213    /// Ids of the nested `IfcAppliedValue` components, in file order.
214    ///
215    /// Order is preserved because it is the only ordering information the file
216    /// carries, and an order-sensitive operator needs it.
217    #[must_use]
218    pub fn component_refs(&self) -> Vec<EntityId> {
219        let mut out = Vec::new();
220        if let Some(v) = self.entity.attribute(slot::COMPONENTS) {
221            v.for_each_ref(&mut |id| out.push(id));
222        }
223        out
224    }
225
226    /// Whether this value is composed from components rather than stated.
227    #[must_use]
228    pub fn is_composed(&self) -> bool {
229        !self.component_refs().is_empty()
230    }
231
232    /// The rate basis, if this value is a rate rather than a lump sum.
233    ///
234    /// Resolves the `IfcMeasureWithUnit`: slot 0 is `ValueComponent`, slot 1
235    /// is `UnitComponent`.
236    #[must_use]
237    pub fn unit_basis(&self, model: &Model) -> Option<UnitBasis> {
238        let id = match self.entity.attribute(slot::UNIT_BASIS)? {
239            Value::Ref(id) => *id,
240            _ => return None,
241        };
242        let measure_with_unit = model.get(id)?;
243        let component = measure_with_unit.attribute(0);
244        Some(UnitBasis {
245            id,
246            value: component.and_then(|v| v.unwrap_typed().as_f64()),
247            measure: component.and_then(|v| match v {
248                // Leaked as 'static only when the name is one we know; a
249                // borrowed lifetime would tie UnitBasis to the model borrow
250                // and stop it being Copy.
251                Value::Typed { type_name, .. } => KNOWN_MEASURES
252                    .iter()
253                    .find(|m| type_name.eq_ignore_ascii_case(m))
254                    .copied(),
255                _ => None,
256            }),
257            unit: match measure_with_unit.attribute(1) {
258                Some(Value::Ref(unit)) => Some(*unit),
259                _ => None,
260            },
261        })
262    }
263}
264
265/// Measure names a unit basis is expected to carry.
266///
267/// Anything outside this list reports `None` rather than being invented: the
268/// point of the field is to let a caller check dimensional agreement, and an
269/// unrecognised name is not evidence of agreement.
270const KNOWN_MEASURES: &[&str] = &[
271    "IFCVOLUMEMEASURE",
272    "IFCAREAMEASURE",
273    "IFCLENGTHMEASURE",
274    "IFCMASSMEASURE",
275    "IFCCOUNTMEASURE",
276    "IFCTIMEMEASURE",
277    "IFCMONETARYMEASURE",
278];