Skip to main content

ifc_properties/
value.rs

1//! `IfcValue` measure types and their interpretation.
2//!
3//! # The measure name is data, not decoration
4//!
5//! IFC writes property values as typed wrappers: `IFCLENGTHMEASURE(2.5)`,
6//! `IFCCOUNTMEASURE(2.)`, `IFCTHERMALTRANSMITTANCEMEASURE(0.24)`. All three
7//! are a STEP real. Discarding the wrapper and keeping `2.5` loses the only
8//! statement the file makes about what the number MEANS -- and 2.5 metres,
9//! 2.5 items and 2.5 W/m2K are not interchangeable.
10//!
11//! So a measure keeps its declared type name and exposes the scalar
12//! separately. `Model::text_at`/`f64_at` already unwrap typed values, which is
13//! right for plain attributes and wrong here.
14//!
15//! # Untyped values are legal
16//!
17//! `IfcValue` is a SELECT, and exporters do write bare literals where a
18//! measure was expected. That is not an error to reject: it is a value whose
19//! measure is unstated, and `measure: None` says exactly that.
20
21use std::sync::Arc;
22
23use ifc_model::Value;
24
25/// The scalar payload of a property value.
26///
27/// Mirrors the STEP literal forms `IfcValue` can bottom out in. `Real` and
28/// `Integer` stay distinct because `IfcCountMeasure` is an integer count and
29/// silently widening it to `f64` invites `2.9999999` counts.
30#[derive(Debug, Clone, PartialEq)]
31pub enum Scalar {
32    /// `.T.` / `.F.`
33    Bool(bool),
34    /// `.U.` -- the third STEP boolean state, distinct from absent.
35    LogicalUnknown,
36    /// An integer literal.
37    Integer(i64),
38    /// A real literal.
39    Real(f64),
40    /// A quoted string.
41    Text(Arc<str>),
42    /// An unquoted enumeration constant.
43    Enum(Arc<str>),
44    /// A binary literal.
45    Binary(Arc<str>),
46}
47
48impl Scalar {
49    /// The numeric value, when the scalar is a number.
50    ///
51    /// Integers widen to `f64` here because a caller asking for a number has
52    /// already accepted floating point. The distinction is preserved in the
53    /// enum for callers that care.
54    pub fn as_f64(&self) -> Option<f64> {
55        match self {
56            Self::Real(v) => Some(*v),
57            Self::Integer(v) => Some(*v as f64),
58            _ => None,
59        }
60    }
61
62    /// The text, when the scalar is text or an enumeration constant.
63    pub fn as_str(&self) -> Option<&str> {
64        match self {
65            Self::Text(v) | Self::Enum(v) => Some(v),
66            _ => None,
67        }
68    }
69}
70
71/// A property value together with the measure type the file declared.
72#[derive(Debug, Clone, PartialEq)]
73pub struct MeasureValue {
74    /// The declared measure, upper-cased as written, e.g. `IFCLENGTHMEASURE`.
75    ///
76    /// `None` when the file wrote a bare literal. Legal, and materially
77    /// different from a stated measure: nothing can be converted or compared
78    /// dimensionally without it.
79    pub measure: Option<Arc<str>>,
80    /// The underlying scalar.
81    pub scalar: Scalar,
82}
83
84impl MeasureValue {
85    /// Read a value, keeping its measure wrapper.
86    ///
87    /// Returns `None` for `$` (absent) and for aggregates: `IfcValue` is a
88    /// SELECT of single measures, so a list where a measure belongs is
89    /// malformed rather than a value this type can represent.
90    pub fn read(value: &Value) -> Option<Self> {
91        Self::read_inner(value, None)
92    }
93
94    fn read_inner(value: &Value, measure: Option<Arc<str>>) -> Option<Self> {
95        let scalar = match value {
96            // Nested wrappers do occur; the OUTERMOST name is the declared
97            // measure, so an inner one must not overwrite it.
98            Value::Typed { type_name, value } => {
99                let name = measure.unwrap_or_else(|| type_name.clone());
100                return Self::read_inner(value, Some(name));
101            }
102            Value::Bool(v) => Scalar::Bool(*v),
103            Value::LogicalUnknown => Scalar::LogicalUnknown,
104            Value::Integer(v) => Scalar::Integer(*v),
105            Value::Real(v) => Scalar::Real(*v),
106            Value::Text(v) => Scalar::Text(v.clone()),
107            Value::Enum(v) => Scalar::Enum(v.clone()),
108            Value::Binary(v) => Scalar::Binary(v.clone()),
109            // `$`, `*`, references and aggregates are not single measures.
110            Value::Null | Value::Derived | Value::Ref(_) | Value::List(_) => return None,
111        };
112        Some(Self { measure, scalar })
113    }
114
115    /// Read a list of values, e.g. `IfcPropertyListValue.ListValues`.
116    ///
117    /// A non-list yields `None`; an element that is not a measure is skipped
118    /// rather than dropping the whole list, because one malformed entry
119    /// should not hide the rest.
120    pub fn read_list(value: &Value) -> Option<Vec<Self>> {
121        match value {
122            Value::List(items) => Some(items.iter().filter_map(Self::read).collect()),
123            _ => None,
124        }
125    }
126
127    /// The numeric value, when there is one.
128    pub fn as_f64(&self) -> Option<f64> {
129        self.scalar.as_f64()
130    }
131
132    /// The declared measure, when the file stated one.
133    pub fn measure(&self) -> Option<&str> {
134        self.measure.as_deref()
135    }
136}