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];