ifc_properties/unit/assignment.rs
1//! SI, conversion-based and derived units, and the project unit context.
2//!
3//! # Slots, verified against the IFC4 EXPRESS schema
4//!
5//! ```text
6//! IfcUnitAssignment 0 = Units
7//! IfcNamedUnit 0 = Dimensions 1 = UnitType
8//! IfcSIUnit 2 = Prefix 3 = Name
9//! IfcConversionBasedUnit 2 = Name 3 = ConversionFactor
10//! IfcConversionBasedUnitWithOffset 4 = ConversionOffset (IFC4)
11//! IfcMeasureWithUnit 0 = ValueComponent 1 = UnitComponent
12//! IfcDerivedUnit 0 = Elements 1 = UnitType 2 = UserDefinedType
13//! IfcDerivedUnitElement 0 = Unit 1 = Exponent
14//! IfcMonetaryUnit 0 = Currency
15//! ```
16//!
17//! `IfcSIUnit` inherits `Dimensions`/`UnitType` from `IfcNamedUnit`, so its
18//! own `Prefix` and `Name` start at slot 2. `IfcDerivedUnit` is NOT a
19//! `IfcNamedUnit`, so its slots start at 0 -- the two cannot share a reader.
20//!
21//! # Prefixes are exact
22//!
23//! `MILLI` is 1e-3 exactly as a decimal, and `f64` cannot hold it exactly.
24//! The factor is therefore returned as a power of ten and applied by the
25//! caller, so `mm -> m` is one multiplication rather than a chain of
26//! roundings.
27//!
28//! A prefix applies to the unit before its power: `MILLI SQUARE_METRE` is
29//! (10⁻³ m)², so its scale is 1e-6, not 1e-3 (ISO 80000-1).
30
31use std::sync::Arc;
32
33use ifc_model::{EntityId, Model, Value};
34
35const NAMED_UNIT_TYPE: usize = 1;
36const SI_PREFIX: usize = 2;
37const SI_NAME: usize = 3;
38const CONVERSION_NAME: usize = 2;
39const CONVERSION_FACTOR: usize = 3;
40const CONVERSION_OFFSET: usize = 4;
41const MEASURE_VALUE: usize = 0;
42const MEASURE_UNIT: usize = 1;
43const DERIVED_ELEMENTS: usize = 0;
44const DERIVED_TYPE: usize = 1;
45const DERIVED_ELEMENT_UNIT: usize = 0;
46const DERIVED_ELEMENT_EXPONENT: usize = 1;
47const MONETARY_CURRENCY: usize = 0;
48const ASSIGNMENT_UNITS: usize = 0;
49
50/// A unit as the file states it.
51#[derive(Debug, Clone, PartialEq)]
52pub enum UnitKind {
53 /// `IfcSIUnit`: a base SI unit with an optional decimal prefix.
54 Si {
55 /// `IfcUnitEnum`, e.g. `LENGTHUNIT`.
56 unit_type: Arc<str>,
57 /// `IfcSIUnitName`, e.g. `METRE`.
58 name: Arc<str>,
59 /// `IfcSIPrefix`, e.g. `MILLI`. `None` means unprefixed.
60 prefix: Option<Arc<str>>,
61 /// Decimal exponent of the prefix: `MILLI` is `Some(-3)`, absent is
62 /// `Some(0)`, and a prefix that is not an `IfcSIPrefix` is `None`.
63 ///
64 /// Exposed as an exponent rather than a factor so callers can apply
65 /// it exactly instead of multiplying by a rounded 0.001. An unknown
66 /// prefix is not read as "unprefixed": that would silently rescale
67 /// every value in the unit.
68 prefix_exponent: Option<i32>,
69 },
70 /// `IfcConversionBasedUnit`: a named unit defined by a factor.
71 Conversion {
72 /// `IfcUnitEnum`.
73 unit_type: Arc<str>,
74 /// The unit's name, e.g. `inch`.
75 name: Option<Arc<str>>,
76 /// The numeric conversion factor, when readable.
77 factor: Option<f64>,
78 /// The unit the factor is expressed in.
79 factor_unit: Option<EntityId>,
80 /// `IfcConversionBasedUnitWithOffset.ConversionOffset`, as stated.
81 ///
82 /// `None` for a plain conversion-based unit. Kept raw: IFC4's own
83 /// documentation contradicts itself on which direction the offset
84 /// applies, so this crate does not apply it.
85 offset: Option<f64>,
86 },
87 /// `IfcDerivedUnit`: a product of powers of other units.
88 Derived {
89 /// `IfcDerivedUnitEnum`, e.g. `VOLUMETRICFLOWRATEUNIT`.
90 unit_type: Arc<str>,
91 /// The (unit, exponent) elements.
92 elements: Vec<(EntityId, i64)>,
93 },
94 /// `IfcMonetaryUnit`: a currency, with no dimension.
95 Monetary {
96 /// ISO currency code as stated.
97 currency: Option<Arc<str>>,
98 },
99 /// `IfcContextDependentUnit` or another named unit form.
100 ContextDependent {
101 /// `IfcUnitEnum`.
102 unit_type: Arc<str>,
103 },
104}
105
106impl UnitKind {
107 /// The `IfcUnitEnum` this unit declares, when it has one.
108 ///
109 /// `IfcMonetaryUnit` has none: it is not an `IfcNamedUnit`.
110 pub fn unit_type(&self) -> Option<&str> {
111 match self {
112 Self::Si { unit_type, .. }
113 | Self::Conversion { unit_type, .. }
114 | Self::ContextDependent { unit_type } => Some(unit_type),
115 Self::Derived { unit_type, .. } => Some(unit_type),
116 Self::Monetary { .. } => None,
117 }
118 }
119
120 /// Multiplier converting a value in this unit to the unprefixed SI unit.
121 ///
122 /// The prefix is raised to the unit's power: `MILLI SQUARE_METRE` gives
123 /// 1e-6 and `MILLI CUBIC_METRE` 1e-9. `None` for an unknown prefix.
124 ///
125 /// Only defined for `Si`: a conversion-based unit needs its factor unit
126 /// resolved too, and a derived unit needs its elements combined, so
127 /// neither can answer honestly on its own. [`crate::exact_unit`] resolves
128 /// all three to the SI base unit.
129 pub fn si_scale(&self) -> Option<f64> {
130 match self {
131 Self::Si {
132 name,
133 prefix_exponent,
134 ..
135 } => prefix_exponent.map(|exponent| 10f64.powi(exponent * super::si::name_power(name))),
136 _ => None,
137 }
138 }
139}
140
141/// The decimal exponent of an `IfcSIPrefix`.
142///
143/// Returns `None` for an unrecognised constant rather than assuming 0: a
144/// silent 1.0 would misreport every value using it.
145pub fn prefix_exponent(prefix: &str) -> Option<i32> {
146 Some(match prefix {
147 "EXA" => 18,
148 "PETA" => 15,
149 "TERA" => 12,
150 "GIGA" => 9,
151 "MEGA" => 6,
152 "KILO" => 3,
153 "HECTO" => 2,
154 "DECA" => 1,
155 "DECI" => -1,
156 "CENTI" => -2,
157 "MILLI" => -3,
158 "MICRO" => -6,
159 "NANO" => -9,
160 "PICO" => -12,
161 "FEMTO" => -15,
162 "ATTO" => -18,
163 _ => return None,
164 })
165}
166
167/// Read one unit by id.
168pub fn unit(model: &Model, id: EntityId) -> Option<UnitKind> {
169 let entity = model.get(id)?;
170 let ty = entity.type_name.to_ascii_uppercase();
171 match ty.as_str() {
172 "IFCSIUNIT" => {
173 let prefix = entity.attributes.get(SI_PREFIX).and_then(enum_text);
174 Some(UnitKind::Si {
175 unit_type: entity
176 .attributes
177 .get(NAMED_UNIT_TYPE)
178 .and_then(enum_text)
179 .unwrap_or_else(|| "".into()),
180 name: entity
181 .attributes
182 .get(SI_NAME)
183 .and_then(enum_text)
184 .unwrap_or_else(|| "".into()),
185 prefix_exponent: prefix.as_deref().map_or(Some(0), prefix_exponent),
186 prefix,
187 })
188 }
189 "IFCCONVERSIONBASEDUNIT" | "IFCCONVERSIONBASEDUNITWITHOFFSET" => {
190 let factor_entity = entity.attributes.get(CONVERSION_FACTOR).and_then(one_ref);
191 let (factor, factor_unit) = match factor_entity.and_then(|f| model.get(f)) {
192 Some(measure) => (
193 measure
194 .attributes
195 .get(MEASURE_VALUE)
196 .and_then(|v| v.unwrap_typed().as_f64()),
197 measure.attributes.get(MEASURE_UNIT).and_then(one_ref),
198 ),
199 None => (None, None),
200 };
201 Some(UnitKind::Conversion {
202 unit_type: entity
203 .attributes
204 .get(NAMED_UNIT_TYPE)
205 .and_then(enum_text)
206 .unwrap_or_else(|| "".into()),
207 name: entity.attributes.get(CONVERSION_NAME).and_then(text),
208 factor,
209 factor_unit,
210 offset: if ty == "IFCCONVERSIONBASEDUNITWITHOFFSET" {
211 entity
212 .attributes
213 .get(CONVERSION_OFFSET)
214 .and_then(|v| v.unwrap_typed().as_f64())
215 } else {
216 None
217 },
218 })
219 }
220 "IFCDERIVEDUNIT" => {
221 let elements = entity
222 .attributes
223 .get(DERIVED_ELEMENTS)
224 .and_then(refs)
225 .unwrap_or_default()
226 .into_iter()
227 .filter_map(|e| {
228 let element = model.get(e)?;
229 let unit = element
230 .attributes
231 .get(DERIVED_ELEMENT_UNIT)
232 .and_then(one_ref)?;
233 let exponent = match element
234 .attributes
235 .get(DERIVED_ELEMENT_EXPONENT)?
236 .unwrap_typed()
237 {
238 Value::Integer(v) => *v,
239 Value::Real(v) => *v as i64,
240 _ => return None,
241 };
242 Some((unit, exponent))
243 })
244 .collect();
245 Some(UnitKind::Derived {
246 unit_type: entity
247 .attributes
248 .get(DERIVED_TYPE)
249 .and_then(enum_text)
250 .unwrap_or_else(|| "".into()),
251 elements,
252 })
253 }
254 "IFCMONETARYUNIT" => Some(UnitKind::Monetary {
255 currency: entity.attributes.get(MONETARY_CURRENCY).and_then(text),
256 }),
257 "IFCCONTEXTDEPENDENTUNIT" => Some(UnitKind::ContextDependent {
258 unit_type: entity
259 .attributes
260 .get(NAMED_UNIT_TYPE)
261 .and_then(enum_text)
262 .unwrap_or_else(|| "".into()),
263 }),
264 _ => None,
265 }
266}
267
268/// The `IfcUnitEnum` a unit declares, without building the whole value.
269///
270/// Used by the quantity reader to check `WR21` cheaply.
271pub fn unit_type(model: &Model, id: EntityId) -> Option<Arc<str>> {
272 let entity = model.get(id)?;
273 let ty = entity.type_name.to_ascii_uppercase();
274 let slot = if ty == "IFCDERIVEDUNIT" {
275 DERIVED_TYPE
276 } else {
277 NAMED_UNIT_TYPE
278 };
279 entity.attributes.get(slot).and_then(enum_text)
280}
281
282/// Project default units, from `IfcProject.UnitsInContext`.
283///
284/// Returned in file order. These are the units a measure without its own
285/// unit is expressed in, so a consumer needs them to interpret bare values.
286pub fn project_units(model: &Model) -> Vec<(EntityId, UnitKind)> {
287 let mut out = Vec::new();
288 let mut ids: Vec<_> = model.ids_of_type("IFCUNITASSIGNMENT").to_vec();
289 ids.sort_unstable();
290 for id in ids {
291 let Some(assignment) = model.get(id) else {
292 continue;
293 };
294 for unit_id in assignment
295 .attributes
296 .get(ASSIGNMENT_UNITS)
297 .and_then(refs)
298 .unwrap_or_default()
299 {
300 if let Some(kind) = unit(model, unit_id) {
301 out.push((unit_id, kind));
302 }
303 }
304 }
305 out
306}
307
308/// The project default unit for a given `IfcUnitEnum`.
309///
310/// `IfcCorrectUnitAssignment` makes at most one named unit per type, so the
311/// first match is the only match in a well-formed file. In a malformed one
312/// this permissive view still returns the first; [`crate::exact_unit`]
313/// refuses the duplicate instead.
314pub fn project_unit_for(model: &Model, unit_type_name: &str) -> Option<(EntityId, UnitKind)> {
315 project_units(model)
316 .into_iter()
317 .find(|(_, kind)| kind.unit_type() == Some(unit_type_name))
318}
319
320fn text(value: &Value) -> Option<Arc<str>> {
321 match value.unwrap_typed() {
322 Value::Text(t) => Some(t.clone()),
323 _ => None,
324 }
325}
326
327fn enum_text(value: &Value) -> Option<Arc<str>> {
328 match value.unwrap_typed() {
329 Value::Enum(t) | Value::Text(t) => Some(t.clone()),
330 _ => None,
331 }
332}
333
334fn one_ref(value: &Value) -> Option<EntityId> {
335 match value.unwrap_typed() {
336 Value::Ref(id) => Some(*id),
337 _ => None,
338 }
339}
340
341fn refs(value: &Value) -> Option<Vec<EntityId>> {
342 match value {
343 Value::List(items) => Some(items.iter().filter_map(one_ref).collect()),
344 _ => None,
345 }
346}