Skip to main content

ifc_properties/quantity/
simple.rs

1//! `IfcPhysicalSimpleQuantity`: length, area, volume, count, weight, time.
2//!
3//! # Slots, verified against the EXPRESS schemas
4//!
5//! ```text
6//! IfcPhysicalQuantity        0 = Name   1 = Description   (IFC2X3, IFC4, IFC4X3)
7//! IfcPhysicalSimpleQuantity  2 = Unit   (OPTIONAL IfcNamedUnit)
8//! IfcQuantity<Kind>          3 = <Kind>Value              (not OPTIONAL)
9//!                            4 = Formula                  (IFC4, IFC4X3 only)
10//! ```
11//!
12//! `LengthValue`, `AreaValue`, `VolumeValue`, `CountValue`, `WeightValue`
13//! and `TimeValue` are declared without `OPTIONAL` in all three releases.
14//! IFC2X3 has no `Formula`, so its records end at slot 3 and `formula`
15//! reads as `None`.
16//!
17//! IFC4X3 adds `IfcQuantityNumber` (`NumberValue : IfcNumericMeasure`,
18//! `Formula : OPTIONAL IfcLabel`, no WHERE rule; `IfcNumericMeasure =
19//! NUMBER`). It is [`QuantityKind::Number`] only in a model whose declared
20//! release declares the entity, and both slots are then looked up by name in
21//! that release's table. IFC2X3 and IFC4 do not declare it, so there, and in
22//! a model with no single known release, it reads as
23//! [`Quantity::Unsupported`], as any entity the crate cannot place does.
24//!
25//! # A quantity without a number still exists
26//!
27//! A value slot that is `$`, missing from a truncated record, or not a
28//! number leaves the quantity without a value, not without an identity.
29//! It is returned as [`Quantity::Unresolved`], in place, so a caller listing
30//! a set's quantities still sees it, and the fault is also reported as
31//! [`PropertyAnomaly::QuantityValueMissing`] or
32//! [`PropertyAnomaly::QuantityValueNotNumeric`]. It is never read as 0.
33
34use std::sync::Arc;
35
36use ifc_model::{Entity, EntityId, Model, Value};
37use ifc_schema::{for_version, SchemaVersion};
38
39use crate::error::PropertyAnomaly;
40use crate::nesting::Nesting;
41use crate::quantity::set::{Quantity, QuantityKind};
42use crate::unit::unit_type;
43
44const DESCRIPTION: usize = 1;
45const UNIT: usize = 2;
46const VALUE: usize = 3;
47const FORMULA: usize = 4;
48
49/// Where a simple quantity keeps its value and its `Formula`.
50#[derive(Debug, Clone, Copy)]
51pub(super) struct ValueSlots {
52    value: usize,
53    formula: usize,
54}
55
56/// The slots of `kind` in `model`, or `None` when the model's declared
57/// release does not declare the entity.
58///
59/// The six kinds every release declares share fixed slots. `IfcQuantityNumber`
60/// is looked up by name in the declared release's table, which must be one
61/// release that has it.
62pub(super) fn value_slots(model: &Model, kind: QuantityKind) -> Option<ValueSlots> {
63    if kind != QuantityKind::Number {
64        return Some(ValueSlots {
65            value: VALUE,
66            formula: FORMULA,
67        });
68    }
69    let [token] = model.header().schema.as_slice() else {
70        return None;
71    };
72    let schema = for_version(SchemaVersion::from_header_token(token)?)?;
73    let names = schema.attribute_names("IFCQUANTITYNUMBER");
74    let slot = |name: &str| names.iter().position(|n| n.eq_ignore_ascii_case(name));
75    Some(ValueSlots {
76        value: slot("NumberValue")?,
77        formula: slot("Formula")?,
78    })
79}
80
81/// Why a simple quantity has no value to report.
82#[derive(Debug, Clone, PartialEq, Eq)]
83#[non_exhaustive]
84pub enum UnresolvedValue {
85    /// The value attribute is `$`, or the record ends before it.
86    Missing,
87    /// The value attribute holds something other than a number, such as
88    /// text or a reference.
89    NotNumeric {
90        /// The value found, rendered for the message.
91        found: String,
92    },
93}
94
95/// Read simple quantity `id` of `kind`, reporting schema-rule anomalies.
96pub(super) fn read_simple(
97    model: &Model,
98    id: EntityId,
99    entity: &Entity,
100    kind: QuantityKind,
101    slots: ValueSlots,
102    name: Option<Arc<str>>,
103    nesting: &mut Nesting<'_>,
104) -> Quantity {
105    let description = entity.attributes.get(DESCRIPTION).and_then(text);
106    let formula = entity.attributes.get(slots.formula).and_then(text);
107    let unit = entity.attributes.get(UNIT).and_then(one_ref);
108
109    let value = match entity.attributes.get(slots.value) {
110        None | Some(Value::Null) => Err(UnresolvedValue::Missing),
111        Some(stated) => stated
112            .unwrap_typed()
113            .as_f64()
114            .ok_or_else(|| UnresolvedValue::NotNumeric {
115                found: format!("{stated:?}"),
116            }),
117    };
118    match &value {
119        Err(UnresolvedValue::Missing) => {
120            nesting.report(PropertyAnomaly::QuantityValueMissing { quantity: id });
121        }
122        Err(UnresolvedValue::NotNumeric { found }) => {
123            nesting.report(PropertyAnomaly::QuantityValueNotNumeric {
124                quantity: id,
125                found: found.clone(),
126            });
127        }
128        // WR22 (WR21 for a count); IfcQuantityNumber has no such rule.
129        Ok(value) if *value < 0.0 && kind.requires_non_negative() => {
130            nesting.report(PropertyAnomaly::NegativeQuantity {
131                quantity: id,
132                value: *value,
133            })
134        }
135        Ok(_) => {}
136    }
137    // WR21: a stated unit must match the quantity kind, whether or not the
138    // value could be read.
139    if let (Some(unit_id), Some(expected)) = (unit, kind.required_unit()) {
140        if let Some(found) = unit_type(model, unit_id) {
141            if &*found != expected {
142                nesting.report(PropertyAnomaly::QuantityUnitMismatch {
143                    quantity: id,
144                    unit: unit_id,
145                    expected,
146                    found: found.to_string(),
147                });
148            }
149        }
150    }
151
152    match value {
153        Ok(value) => Quantity::Simple {
154            id,
155            name,
156            description,
157            kind,
158            value,
159            unit,
160            formula,
161        },
162        Err(reason) => Quantity::Unresolved {
163            id,
164            name,
165            description,
166            kind,
167            unit,
168            formula,
169            reason,
170        },
171    }
172}
173
174pub(super) fn text(value: &Value) -> Option<Arc<str>> {
175    match value.unwrap_typed() {
176        Value::Text(t) => Some(t.clone()),
177        _ => None,
178    }
179}
180
181fn one_ref(value: &Value) -> Option<EntityId> {
182    match value.unwrap_typed() {
183        Value::Ref(id) => Some(*id),
184        _ => None,
185    }
186}