ifc_properties/quantity/set.rs
1//! `IfcElementQuantity` and the physical quantities it carries.
2//!
3//! # Slots, verified against the IFC4 EXPRESS schema
4//!
5//! ```text
6//! IfcElementQuantity 4 = MethodOfMeasurement 5 = Quantities
7//! IfcPhysicalQuantity 0 = Name 1 = Description
8//! IfcPhysicalSimpleQty 2 = Unit
9//! IfcQuantityLength 3 = LengthValue 4 = Formula
10//! IfcQuantityArea 3 = AreaValue 4 = Formula
11//! IfcQuantityVolume 3 = VolumeValue 4 = Formula
12//! IfcQuantityCount 3 = CountValue 4 = Formula
13//! IfcQuantityWeight 3 = WeightValue 4 = Formula
14//! IfcQuantityTime 3 = TimeValue 4 = Formula
15//! IfcPhysicalComplexQty 2 = HasQuantities 3 = Discrimination
16//! ```
17//!
18//! The value slot is 3 for every simple quantity because `Unit` occupies slot
19//! 2 on the shared supertype. The COMPLEX quantity has no `Unit`, so its
20//! contents start at slot 2 instead -- reading it like a simple quantity
21//! finds a list where a unit belongs. `simple.rs` reads the simple
22//! quantities, including those whose value cannot be read.
23//!
24//! # A quantity is an assertion, not a measurement
25//!
26//! This crate never computes shape. `IfcQuantityArea` is what the authoring
27//! tool claimed, and it may disagree with the geometry. Reporting the claim
28//! faithfully -- including a negative one that breaks `WR22` -- is the job.
29
30use std::sync::Arc;
31
32use ifc_model::{EntityId, Model};
33
34use crate::error::PropertyAnomaly;
35use crate::nesting::Nesting;
36use crate::quantity::complex::complex_quantities;
37use crate::quantity::simple::{read_simple, text, value_slots, UnresolvedValue};
38use crate::unit::UnitKind;
39
40const NAME: usize = 0;
41const COMPLEX_DISCRIMINATION: usize = 3;
42const SET_METHOD: usize = 4;
43const SET_QUANTITIES: usize = 5;
44
45/// What a physical quantity measures.
46///
47/// The kind is the entity type, not a guess from the unit: a file may omit
48/// the unit entirely, and `IfcQuantityArea` still measures area.
49///
50/// Non-exhaustive: a later release may add a quantity subtype, as IFC4X3
51/// added `IfcQuantityNumber`.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum QuantityKind {
55 /// `IfcQuantityLength`.
56 Length,
57 /// `IfcQuantityArea`.
58 Area,
59 /// `IfcQuantityVolume`.
60 Volume,
61 /// `IfcQuantityCount`.
62 Count,
63 /// `IfcQuantityWeight`, measured as mass.
64 Weight,
65 /// `IfcQuantityTime`.
66 Time,
67 /// `IfcQuantityNumber`: a number that is not a physical measure, such as
68 /// a ratio or a figure a take-off rule defines. Declared only by IFC4X3;
69 /// the permissive readers resolve it only in a model whose declared
70 /// release has it, and read it as [`Quantity::Unsupported`] elsewhere.
71 Number,
72}
73
74impl QuantityKind {
75 /// The kind named by an entity type, or `None` if it is not a simple
76 /// quantity. Case-insensitive, because a type name reaching here may
77 /// come from a caller rather than the upper-cased parser.
78 ///
79 /// This maps names only; it does not check that a release declares the
80 /// entity (`IfcQuantityNumber` is IFC4X3 only).
81 #[must_use]
82 pub fn from_type_name(name: &str) -> Option<Self> {
83 Self::from_type(&name.to_ascii_uppercase())
84 }
85
86 /// The entity type name for this kind, in schema casing.
87 #[must_use]
88 pub fn type_name(self) -> &'static str {
89 match self {
90 Self::Length => "IfcQuantityLength",
91 Self::Area => "IfcQuantityArea",
92 Self::Volume => "IfcQuantityVolume",
93 Self::Count => "IfcQuantityCount",
94 Self::Weight => "IfcQuantityWeight",
95 Self::Time => "IfcQuantityTime",
96 Self::Number => "IfcQuantityNumber",
97 }
98 }
99
100 /// The measure type this kind's value slot carries.
101 ///
102 /// Fixed by the schema per subtype: an `IfcQuantityArea` holds an
103 /// `IfcAreaMeasure` and nothing else.
104 #[must_use]
105 pub fn measure_type(self) -> &'static str {
106 match self {
107 Self::Length => "IfcLengthMeasure",
108 Self::Area => "IfcAreaMeasure",
109 Self::Volume => "IfcVolumeMeasure",
110 Self::Count => "IfcCountMeasure",
111 Self::Weight => "IfcMassMeasure",
112 Self::Time => "IfcTimeMeasure",
113 Self::Number => "IfcNumericMeasure",
114 }
115 }
116
117 fn from_type(name: &str) -> Option<Self> {
118 Some(match name {
119 "IFCQUANTITYLENGTH" => Self::Length,
120 "IFCQUANTITYAREA" => Self::Area,
121 "IFCQUANTITYVOLUME" => Self::Volume,
122 "IFCQUANTITYCOUNT" => Self::Count,
123 "IFCQUANTITYWEIGHT" => Self::Weight,
124 "IFCQUANTITYTIME" => Self::Time,
125 "IFCQUANTITYNUMBER" => Self::Number,
126 _ => return None,
127 })
128 }
129
130 /// The `IfcUnitEnum` this quantity's unit must carry, per `WR21`.
131 ///
132 /// `IfcQuantityCount` has no such rule -- a count is dimensionless --
133 /// and neither has IFC4X3 `IfcQuantityNumber`, so they constrain
134 /// nothing.
135 pub fn required_unit(self) -> Option<&'static str> {
136 Some(match self {
137 Self::Length => "LENGTHUNIT",
138 Self::Area => "AREAUNIT",
139 Self::Volume => "VOLUMEUNIT",
140 Self::Weight => "MASSUNIT",
141 Self::Time => "TIMEUNIT",
142 Self::Count | Self::Number => return None,
143 })
144 }
145
146 /// The name of the value attribute, the same in every release that
147 /// declares the entity (`LengthValue`, ..., IFC4X3 `NumberValue`).
148 pub(crate) fn value_attribute(self) -> &'static str {
149 match self {
150 Self::Length => "LengthValue",
151 Self::Area => "AreaValue",
152 Self::Volume => "VolumeValue",
153 Self::Count => "CountValue",
154 Self::Weight => "WeightValue",
155 Self::Time => "TimeValue",
156 Self::Number => "NumberValue",
157 }
158 }
159
160 /// Whether the value must be `>= 0`: `WR22` on every `IfcQuantity*`
161 /// except `IfcQuantityCount` (its `WR21`) and IFC4X3
162 /// `IfcQuantityNumber`, which declares no rule at all.
163 pub(crate) fn requires_non_negative(self) -> bool {
164 self != Self::Number
165 }
166}
167
168/// One physical quantity.
169///
170/// Non-exhaustive: a match needs a wildcard arm, and code that sums or
171/// compares values should match [`Quantity::Simple`] and decide explicitly
172/// what an [`Quantity::Unresolved`] quantity means for it.
173#[derive(Debug, Clone, PartialEq)]
174#[non_exhaustive]
175pub enum Quantity {
176 /// A simple measured value.
177 Simple {
178 /// The entity.
179 id: EntityId,
180 /// `Name`, required by the schema.
181 name: Option<Arc<str>>,
182 /// `Description`.
183 description: Option<Arc<str>>,
184 /// What it measures.
185 kind: QuantityKind,
186 /// The stated value.
187 value: f64,
188 /// The unit override, when stated.
189 unit: Option<EntityId>,
190 /// `Formula`: how the author says it was derived. Free text.
191 formula: Option<Arc<str>>,
192 },
193 /// A simple quantity whose value attribute holds no number: `$`, missing
194 /// from a truncated record, or not numeric.
195 ///
196 /// Kept in its place so the set still lists it; there is deliberately
197 /// no `value` to read as 0. The same fault is reported as
198 /// [`PropertyAnomaly::QuantityValueMissing`] or
199 /// [`PropertyAnomaly::QuantityValueNotNumeric`].
200 Unresolved {
201 /// The entity.
202 id: EntityId,
203 /// `Name`, required by the schema.
204 name: Option<Arc<str>>,
205 /// `Description`.
206 description: Option<Arc<str>>,
207 /// What it measures.
208 kind: QuantityKind,
209 /// The unit override, when stated.
210 unit: Option<EntityId>,
211 /// `Formula`: how the author says it was derived. Free text.
212 formula: Option<Arc<str>>,
213 /// Why there is no value.
214 reason: UnresolvedValue,
215 },
216 /// A nested group of quantities.
217 Complex {
218 /// The entity.
219 id: EntityId,
220 /// `Name`.
221 name: Option<Arc<str>>,
222 /// `Discrimination`: what distinguishes the parts.
223 discrimination: Option<Arc<str>>,
224 /// Contained quantities.
225 quantities: Vec<Quantity>,
226 },
227 /// A concrete quantity type this crate does not model.
228 Unsupported {
229 /// The entity.
230 id: EntityId,
231 /// Declared type, upper-cased.
232 type_name: Arc<str>,
233 },
234}
235
236impl Quantity {
237 /// The entity id, whatever the variant.
238 pub fn id(&self) -> EntityId {
239 match self {
240 Self::Simple { id, .. }
241 | Self::Unresolved { id, .. }
242 | Self::Complex { id, .. }
243 | Self::Unsupported { id, .. } => *id,
244 }
245 }
246
247 /// The name, whatever the variant.
248 pub fn name(&self) -> Option<&str> {
249 match self {
250 Self::Simple { name, .. }
251 | Self::Unresolved { name, .. }
252 | Self::Complex { name, .. } => name.as_deref(),
253 Self::Unsupported { .. } => None,
254 }
255 }
256}
257
258/// An `IfcElementQuantity` with its quantities resolved.
259#[derive(Debug, Clone, PartialEq)]
260pub struct QuantitySet {
261 /// The entity.
262 pub id: EntityId,
263 /// `Name`.
264 pub name: Option<Arc<str>>,
265 /// `MethodOfMeasurement`: the standard the author measured against.
266 ///
267 /// Free text such as `BaseQuantities`. It is the only statement about
268 /// HOW a quantity was arrived at, so it is carried rather than dropped.
269 pub method: Option<Arc<str>>,
270 /// Quantities in file order.
271 pub quantities: Vec<Quantity>,
272}
273
274impl QuantitySet {
275 /// Look up a quantity by name.
276 ///
277 /// `UniqueQuantityNames` makes the first match the only match in a
278 /// well-formed file.
279 pub fn quantity(&self, name: &str) -> Option<&Quantity> {
280 self.quantities.iter().find(|q| q.name() == Some(name))
281 }
282}
283
284/// Read an `IfcElementQuantity` by id, with schema-rule anomalies.
285///
286/// Returns `None` when `id` is absent or is not an `IfcElementQuantity`.
287/// A member that cannot be represented is left out of `quantities` and
288/// reported, never dropped silently: an absent id
289/// ([`PropertyAnomaly::MissingMember`]), a list item that is not an entity
290/// reference ([`PropertyAnomaly::MemberNotReference`]) or a member listed
291/// again ([`PropertyAnomaly::DuplicateMember`], read once). A simple
292/// quantity with no value or a non-numeric one stays in `quantities`, in
293/// file order, as [`Quantity::Unresolved`], and is also reported as
294/// [`PropertyAnomaly::QuantityValueMissing`] or
295/// [`PropertyAnomaly::QuantityValueNotNumeric`]. Nested complex
296/// quantities are followed along a tracked path; a cycle, over-deep nesting
297/// or an exhausted member budget is reported as
298/// [`PropertyAnomaly::ComplexCycle`],
299/// [`PropertyAnomaly::ComplexTooDeep`] or
300/// [`PropertyAnomaly::ComplexBudgetExceeded`].
301pub fn quantity_set(model: &Model, id: EntityId) -> Option<(QuantitySet, Vec<PropertyAnomaly>)> {
302 let entity = model.get(id)?;
303 if !entity.type_name.eq_ignore_ascii_case("IFCELEMENTQUANTITY") {
304 return None;
305 }
306 let mut anomalies = Vec::new();
307 let mut nesting = Nesting::new(&mut anomalies);
308 let mut quantities = Vec::new();
309 for member in nesting.members(id, "Quantities", entity.attributes.get(SET_QUANTITIES)) {
310 if !nesting.admit(model, id, member) {
311 continue;
312 }
313 if let Some(quantity) = read_quantity(model, member, &mut nesting) {
314 quantities.push(quantity);
315 }
316 }
317 Some((
318 QuantitySet {
319 id,
320 name: entity.attributes.get(2).and_then(text),
321 method: entity.attributes.get(SET_METHOD).and_then(text),
322 quantities,
323 },
324 anomalies,
325 ))
326}
327
328/// Read one physical quantity. `None` means the entity is absent; every
329/// present quantity is represented, and its anomalies are reported through
330/// `nesting`.
331pub(super) fn read_quantity(
332 model: &Model,
333 id: EntityId,
334 nesting: &mut Nesting<'_>,
335) -> Option<Quantity> {
336 let entity = model.get(id)?;
337 let ty = entity.type_name.to_ascii_uppercase();
338 let name = entity.attributes.get(NAME).and_then(text);
339
340 if ty == "IFCPHYSICALCOMPLEXQUANTITY" {
341 return Some(Quantity::Complex {
342 id,
343 name,
344 discrimination: entity.attributes.get(COMPLEX_DISCRIMINATION).and_then(text),
345 quantities: complex_quantities(model, id, entity, nesting),
346 });
347 }
348
349 let Some(kind) = QuantityKind::from_type(&ty) else {
350 return Some(Quantity::Unsupported {
351 id,
352 type_name: ty.as_str().into(),
353 });
354 };
355
356 // A kind the declared release does not have (IfcQuantityNumber outside
357 // IFC4X3) is foreign to the model; it is not read as if it belonged.
358 let Some(slots) = value_slots(model, kind) else {
359 return Some(Quantity::Unsupported {
360 id,
361 type_name: ty.as_str().into(),
362 });
363 };
364 Some(read_simple(model, id, entity, kind, slots, name, nesting))
365}
366
367/// Every `IfcElementQuantity` in the file, with anomalies.
368pub fn quantity_sets(model: &Model) -> (Vec<QuantitySet>, Vec<PropertyAnomaly>) {
369 let mut sets = Vec::new();
370 let mut anomalies = Vec::new();
371 let mut ids: Vec<_> = model.ids_of_type("IFCELEMENTQUANTITY").to_vec();
372 ids.sort_unstable();
373 for id in ids {
374 if let Some((set, mut found)) = quantity_set(model, id) {
375 sets.push(set);
376 anomalies.append(&mut found);
377 }
378 }
379 (sets, anomalies)
380}
381
382/// Resolve the unit kind for a quantity, following its explicit unit only.
383///
384/// An [`Quantity::Unresolved`] quantity still states its unit, so it has
385/// one here even though it has no value. An IFC4X3 `IfcQuantityNumber`
386/// may state any `IfcNamedUnit` (it has no `WR21`); it is returned as
387/// stated, and a number that states none has none.
388///
389/// Project-default units are NOT applied here: falling back to the project
390/// context would report a unit the quantity never stated. Callers that want
391/// the effective unit combine this with [`crate::unit::project_units`].
392pub fn stated_unit(model: &Model, quantity: &Quantity) -> Option<UnitKind> {
393 match quantity {
394 Quantity::Simple { unit, .. } | Quantity::Unresolved { unit, .. } => {
395 let id = (*unit)?;
396 crate::unit::unit(model, id)
397 }
398 _ => None,
399 }
400}