Skip to main content

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}