ifc_lite_core/project_units/
mod.rs1pub mod measure;
20pub mod symbols;
21
22use std::collections::BTreeMap;
23
24use crate::decoder::EntityDecoder;
25pub use measure::{measure_unit, MeasureUnit};
26use symbols::{compose_derived, conversion_unit_symbol, si_unit_symbol_and_scale};
27
28#[derive(Clone, Debug, PartialEq)]
33pub struct ResolvedUnit {
34 pub symbol: String,
35 pub si_scale: f64,
36}
37
38impl ResolvedUnit {
39 fn new(symbol: impl Into<String>, si_scale: f64) -> Self {
40 Self { symbol: symbol.into(), si_scale }
41 }
42}
43
44#[derive(Clone, Debug, Default, PartialEq)]
49pub struct ProjectUnits {
50 by_type: BTreeMap<String, ResolvedUnit>,
51 monetary: Option<ResolvedUnit>,
52}
53
54impl ProjectUnits {
55 pub fn resolve(decoder: &mut EntityDecoder, project_id: u32) -> Self {
59 let mut units = ProjectUnits::default();
60 let Ok(project) = decoder.decode_by_id(project_id) else {
61 return units;
62 };
63 if project.ifc_type.as_str() != "IFCPROJECT" {
64 return units;
65 }
66 let Some(units_ref) = project.get(8).and_then(|a| a.as_entity_ref()) else {
68 return units;
69 };
70 let Ok(assignment) = decoder.decode_by_id(units_ref) else {
71 return units;
72 };
73 if assignment.ifc_type.as_str() != "IFCUNITASSIGNMENT" {
74 return units;
75 }
76 let refs: Vec<u32> = match assignment.get(0).and_then(|a| a.as_list()) {
80 Some(list) => list.iter().filter_map(|a| a.as_entity_ref()).collect(),
81 None => return units,
82 };
83 for unit_ref in refs {
84 if let Some((unit_type, resolved, monetary)) = resolve_unit_by_ref(decoder, unit_ref) {
85 if monetary {
86 units.monetary = Some(resolved);
87 } else if let Some(t) = unit_type {
88 units.by_type.entry(t).or_insert(resolved);
91 }
92 }
93 }
94 units
95 }
96
97 pub fn unit_for_measure(&self, measure_type: &str) -> Option<ResolvedUnit> {
103 match measure_unit(measure_type)? {
104 MeasureUnit::Typed { unit_type, default_symbol } => Some(
105 self.by_type
106 .get(unit_type)
107 .cloned()
108 .unwrap_or_else(|| ResolvedUnit::new(default_symbol, 1.0)),
109 ),
110 MeasureUnit::Monetary => self.monetary.clone(),
111 MeasureUnit::Dimensionless => None,
112 }
113 }
114
115 pub fn resolved_for_unit_type(&self, unit_type: &str) -> Option<&ResolvedUnit> {
117 self.by_type.get(unit_type)
118 }
119
120 pub fn monetary(&self) -> Option<&ResolvedUnit> {
122 self.monetary.as_ref()
123 }
124
125 pub fn declared_len(&self) -> usize {
127 self.by_type.len()
128 }
129}
130
131pub fn resolve_unit_by_ref(
139 decoder: &mut EntityDecoder,
140 unit_ref: u32,
141) -> Option<(Option<String>, ResolvedUnit, bool)> {
142 resolve_unit_by_ref_depth(decoder, unit_ref, 0)
143}
144
145const MAX_UNIT_RESOLVE_DEPTH: u32 = 16;
150
151fn resolve_unit_by_ref_depth(
152 decoder: &mut EntityDecoder,
153 unit_ref: u32,
154 depth: u32,
155) -> Option<(Option<String>, ResolvedUnit, bool)> {
156 if depth > MAX_UNIT_RESOLVE_DEPTH {
157 return None;
158 }
159 let entity = decoder.decode_by_id(unit_ref).ok()?;
160 match entity.ifc_type.as_str() {
161 "IFCSIUNIT" => {
162 let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
164 let name = entity.get(3).and_then(|a| a.as_enum())?;
165 let prefix = entity
166 .get(2)
167 .filter(|a| !a.is_null())
168 .and_then(|a| a.as_enum());
169 let (symbol, scale) = si_unit_symbol_and_scale(name, prefix)?;
170 Some((unit_type, ResolvedUnit::new(symbol, scale), false))
171 }
172 "IFCCONVERSIONBASEDUNIT" => {
173 let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
175 let name = entity.get(2).and_then(|a| a.as_string()).unwrap_or("");
176 let symbol = conversion_unit_symbol(name);
177 let conv_ref = entity.get_ref(3);
178 let scale = conv_ref
179 .and_then(|r| conversion_factor_scale(decoder, r))
180 .unwrap_or(1.0);
181 Some((unit_type, ResolvedUnit::new(symbol, scale), false))
182 }
183 "IFCDERIVEDUNIT" => {
184 let unit_type = entity.get(1).and_then(|a| a.as_enum()).map(str_token);
186 let elem_refs: Vec<u32> = entity
187 .get(0)
188 .and_then(|a| a.as_list())
189 .map(|l| l.iter().filter_map(|a| a.as_entity_ref()).collect())
190 .unwrap_or_default();
191 let mut parts: Vec<(String, i32)> = Vec::new();
192 let mut scale = 1.0f64;
193 for er in elem_refs {
194 if let Some((sym, unit_scale, exponent)) =
195 resolve_derived_element(decoder, er, depth)
196 {
197 scale *= unit_scale.powi(exponent);
198 parts.push((sym, exponent));
199 }
200 }
201 let symbol = compose_derived(&parts);
202 if symbol.is_empty() {
203 return None;
204 }
205 Some((unit_type, ResolvedUnit::new(symbol, scale), false))
206 }
207 "IFCMONETARYUNIT" => {
208 let currency = entity
210 .get(0)
211 .and_then(|a| a.as_string().or_else(|| a.as_enum()))
212 .unwrap_or("");
213 Some((None, ResolvedUnit::new(currency_symbol(currency), 1.0), true))
214 }
215 _ => None,
216 }
217}
218
219fn resolve_derived_element(
221 decoder: &mut EntityDecoder,
222 elem_ref: u32,
223 depth: u32,
224) -> Option<(String, f64, i32)> {
225 let elem = decoder.decode_by_id(elem_ref).ok()?;
226 if elem.ifc_type.as_str() != "IFCDERIVEDUNITELEMENT" {
227 return None;
228 }
229 let unit_ref = elem.get_ref(0)?;
231 let exponent = elem.get(1).and_then(|a| a.as_int()).unwrap_or(1) as i32;
232 let (_ut, resolved, _mon) = resolve_unit_by_ref_depth(decoder, unit_ref, depth + 1)?;
233 Some((resolved.symbol, resolved.si_scale, exponent))
234}
235
236fn conversion_factor_scale(decoder: &mut EntityDecoder, measure_ref: u32) -> Option<f64> {
240 let measure = decoder.decode_by_id(measure_ref).ok()?;
241 if measure.ifc_type.as_str() != "IFCMEASUREWITHUNIT" {
242 return None;
243 }
244 let value = measure.get(0).and_then(|a| a.as_float())?;
246 if !(value.is_finite() && value > 0.0) {
247 return None;
248 }
249 let component_scale = measure
251 .get_ref(1)
252 .and_then(|r| {
253 let comp = decoder.decode_by_id(r).ok()?;
254 if comp.ifc_type.as_str() == "IFCSIUNIT" {
255 let name = comp.get(3).and_then(|a| a.as_enum())?;
256 let prefix = comp.get(2).filter(|a| !a.is_null()).and_then(|a| a.as_enum());
257 si_unit_symbol_and_scale(name, prefix).map(|(_, s)| s)
258 } else {
259 None
260 }
261 })
262 .unwrap_or(1.0);
263 Some(value * component_scale)
264}
265
266fn str_token(s: &str) -> String {
268 s.trim().trim_matches('.').to_ascii_uppercase()
269}
270
271fn currency_symbol(code: &str) -> String {
273 let c = code.trim().trim_matches('\'').trim_matches('.').trim();
274 match c.to_ascii_uppercase().as_str() {
275 "EUR" => "\u{20AC}".to_string(),
276 "USD" => "$".to_string(),
277 "GBP" => "\u{00A3}".to_string(),
278 "JPY" | "CNY" | "RMB" => "\u{00A5}".to_string(),
279 "" => "".to_string(),
280 _ => c.to_string(),
281 }
282}
283
284#[cfg(test)]
285mod tests;