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}