Skip to main content

ifc_resource/usage/
quantity.rs

1//! `IfcPhysicalQuantity` — bounded IFC4 usage-quantity projections.
2//!
3//! Scope: simple physical quantities (area, count, length, time, volume,
4//! weight) and complex composite quantities, as reachable from
5//! `IfcConstructionResource.BaseQuantity` or `IfcConstructionResourceType.BaseQuantity`.
6//! This module preserves authored measure values; it does not evaluate
7//! formulas, resolve units against a project unit assignment, or compute
8//! derived quantities.
9
10use ifc_model::EntityId;
11
12use crate::error::{ResourceError, ResourceResult};
13use crate::view::Record;
14
15/// A concrete `IfcPhysicalSimpleQuantity` measure kind.
16#[derive(Debug, Clone, Copy, PartialEq)]
17#[non_exhaustive]
18pub enum SimpleQuantityValue {
19    /// `IfcQuantityArea.AreaValue`.
20    Area(f64),
21    /// `IfcQuantityCount.CountValue`.
22    Count(f64),
23    /// `IfcQuantityLength.LengthValue`.
24    Length(f64),
25    /// `IfcQuantityTime.TimeValue`.
26    Time(f64),
27    /// `IfcQuantityVolume.VolumeValue`.
28    Volume(f64),
29    /// `IfcQuantityWeight.WeightValue`.
30    Weight(f64),
31}
32
33fn simple_kind_from_type(type_name: &str) -> Option<&'static str> {
34    if type_name.eq_ignore_ascii_case("IfcQuantityArea") {
35        Some("AreaValue")
36    } else if type_name.eq_ignore_ascii_case("IfcQuantityCount") {
37        Some("CountValue")
38    } else if type_name.eq_ignore_ascii_case("IfcQuantityLength") {
39        Some("LengthValue")
40    } else if type_name.eq_ignore_ascii_case("IfcQuantityTime") {
41        Some("TimeValue")
42    } else if type_name.eq_ignore_ascii_case("IfcQuantityVolume") {
43        Some("VolumeValue")
44    } else if type_name.eq_ignore_ascii_case("IfcQuantityWeight") {
45        Some("WeightValue")
46    } else {
47        None
48    }
49}
50
51fn simple_value(type_name: &str, raw: f64) -> SimpleQuantityValue {
52    if type_name.eq_ignore_ascii_case("IfcQuantityArea") {
53        SimpleQuantityValue::Area(raw)
54    } else if type_name.eq_ignore_ascii_case("IfcQuantityCount") {
55        SimpleQuantityValue::Count(raw)
56    } else if type_name.eq_ignore_ascii_case("IfcQuantityLength") {
57        SimpleQuantityValue::Length(raw)
58    } else if type_name.eq_ignore_ascii_case("IfcQuantityTime") {
59        SimpleQuantityValue::Time(raw)
60    } else if type_name.eq_ignore_ascii_case("IfcQuantityVolume") {
61        SimpleQuantityValue::Volume(raw)
62    } else {
63        SimpleQuantityValue::Weight(raw)
64    }
65}
66
67/// A borrowed, schema-resolved `IfcPhysicalSimpleQuantity` projection.
68///
69/// Enforces the shared non-negativity rule declared on every concrete
70/// simple-quantity subtype (`WR21`/`WR22` in the official schema).
71#[derive(Debug, Clone, Copy)]
72pub struct SimpleQuantity<'m, 's> {
73    record: Record<'m, 's>,
74    attribute: &'static str,
75}
76
77impl<'m, 's> SimpleQuantity<'m, 's> {
78    pub(crate) fn from_record(record: Record<'m, 's>) -> ResourceResult<Self> {
79        let attribute = simple_kind_from_type(&record.entity.type_name).ok_or_else(|| {
80            ResourceError::WrongType {
81                id: record.id,
82                expected: "concrete IfcPhysicalSimpleQuantity",
83                actual: record.entity.type_name.to_string(),
84            }
85        })?;
86        Ok(Self { record, attribute })
87    }
88
89    /// The entity id of the projected quantity.
90    #[must_use]
91    pub fn id(&self) -> EntityId {
92        self.record.id
93    }
94
95    /// The `Name` attribute.
96    pub fn name(&self) -> ResourceResult<&'m str> {
97        self.record.required_text("Name")
98    }
99
100    /// The `Description` attribute, when authored.
101    pub fn description(&self) -> ResourceResult<Option<&'m str>> {
102        self.record.optional_text("Description")
103    }
104
105    /// The `Unit` attribute, when authored; falls back to the project unit
106    /// assignment otherwise.
107    pub fn unit(&self) -> ResourceResult<Option<EntityId>> {
108        self.record.optional_ref("Unit", "IfcNamedUnit")
109    }
110
111    /// The `Formula` attribute, when authored.
112    pub fn formula(&self) -> ResourceResult<Option<&'m str>> {
113        self.record.optional_text("Formula")
114    }
115
116    /// The authored measure value, typed by the concrete quantity kind.
117    pub fn value(&self) -> ResourceResult<SimpleQuantityValue> {
118        let raw = self.record.required_non_negative_number(self.attribute)?;
119        Ok(simple_value(&self.record.entity.type_name, raw))
120    }
121}
122
123/// A borrowed, schema-resolved `IfcPhysicalComplexQuantity` projection.
124///
125/// `HasQuantities` is a `SET`: the official schema forbids a complex
126/// quantity from listing itself among its own members (`NoSelfReference`).
127/// This projection enforces that rule and reports authored-order members as
128/// entity references rather than eagerly recursing, since a complex
129/// quantity may nest further complex quantities without a fixed bound.
130#[derive(Debug, Clone, Copy)]
131pub struct ComplexQuantity<'m, 's> {
132    record: Record<'m, 's>,
133}
134
135impl<'m, 's> ComplexQuantity<'m, 's> {
136    pub(crate) fn from_record(record: Record<'m, 's>) -> ResourceResult<Self> {
137        let quantity = Self { record };
138        let members = quantity.member_ids()?;
139        if members.contains(&quantity.record.id) {
140            return Err(ResourceError::SemanticViolation {
141                entity: Some(quantity.record.id),
142                rule: "IfcPhysicalComplexQuantity.NoSelfReference",
143            });
144        }
145        Ok(quantity)
146    }
147
148    /// The entity id of the projected quantity.
149    #[must_use]
150    pub fn id(&self) -> EntityId {
151        self.record.id
152    }
153
154    /// The `Name` attribute.
155    pub fn name(&self) -> ResourceResult<&'m str> {
156        self.record.required_text("Name")
157    }
158
159    /// The `Description` attribute, when authored.
160    pub fn description(&self) -> ResourceResult<Option<&'m str>> {
161        self.record.optional_text("Description")
162    }
163
164    /// The `Discrimination` attribute: the basis this composite groups by.
165    pub fn discrimination(&self) -> ResourceResult<&'m str> {
166        self.record.required_text("Discrimination")
167    }
168
169    /// The `Quality` attribute, when authored.
170    pub fn quality(&self) -> ResourceResult<Option<&'m str>> {
171        self.record.optional_text("Quality")
172    }
173
174    /// The `Usage` attribute, when authored.
175    pub fn usage(&self) -> ResourceResult<Option<&'m str>> {
176        self.record.optional_text("Usage")
177    }
178
179    /// The `HasQuantities` attribute: authored-order member references.
180    pub fn member_ids(&self) -> ResourceResult<Vec<EntityId>> {
181        self.record
182            .refs("HasQuantities", "IfcPhysicalQuantity", 1, false, true)
183    }
184}