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}