1use ifc_model::{Entity, EntityId, Transaction, Value};
20
21use super::assignment::prefix_exponent;
22use crate::{PropertyError, PropertyResult};
23
24#[derive(Debug, Clone, Copy)]
26pub struct SiUnitDraft<'a> {
27 pub unit_type: &'a str,
29 pub name: &'a str,
31 pub prefix: Option<&'a str>,
36}
37
38#[derive(Debug, Clone, Copy)]
40pub struct MonetaryUnitDraft<'a> {
41 pub currency: &'a str,
43}
44
45pub fn add_si_unit(tx: &mut Transaction, draft: SiUnitDraft<'_>) -> PropertyResult<EntityId> {
52 require_enum("IFCSIUNIT", "UnitType", draft.unit_type)?;
53 require_enum("IFCSIUNIT", "Name", draft.name)?;
54 if let Some(prefix) = draft.prefix {
55 if prefix_exponent(prefix).is_none() {
56 return Err(authoring_invalid("IFCSIUNIT", "Prefix", prefix));
57 }
58 }
59 Ok(tx.create(Entity::new(
60 "IFCSIUNIT",
61 vec![
62 Value::Derived,
63 enum_value(draft.unit_type),
64 draft.prefix.map_or(Value::Null, enum_value),
65 enum_value(draft.name),
66 ],
67 )))
68}
69
70pub fn add_monetary_unit(
72 tx: &mut Transaction,
73 draft: MonetaryUnitDraft<'_>,
74) -> PropertyResult<EntityId> {
75 if draft.currency.trim().is_empty() {
76 return Err(authoring_invalid(
77 "IFCMONETARYUNIT",
78 "Currency",
79 "expected a currency code",
80 ));
81 }
82 Ok(tx.create(Entity::new(
83 "IFCMONETARYUNIT",
84 vec![Value::Text(draft.currency.into())],
85 )))
86}
87
88#[derive(Debug, Clone, Copy)]
90pub struct ConversionBasedUnitDraft<'a> {
91 pub unit_type: &'a str,
93 pub name: &'a str,
95 pub conversion_factor: EntityId,
97 pub dimensions: EntityId,
99}
100
101pub fn add_conversion_based_unit(
107 tx: &mut Transaction,
108 draft: ConversionBasedUnitDraft<'_>,
109) -> PropertyResult<EntityId> {
110 require_enum("IFCCONVERSIONBASEDUNIT", "UnitType", draft.unit_type)?;
111 if draft.name.trim().is_empty() {
112 return Err(authoring_invalid(
113 "IFCCONVERSIONBASEDUNIT",
114 "Name",
115 "expected a non-empty name",
116 ));
117 }
118 Ok(tx.create(Entity::new(
119 "IFCCONVERSIONBASEDUNIT",
120 vec![
121 Value::Ref(draft.dimensions),
122 enum_value(draft.unit_type),
123 Value::Text(draft.name.into()),
124 Value::Ref(draft.conversion_factor),
125 ],
126 )))
127}
128
129pub fn add_dimensional_exponents(tx: &mut Transaction, exponents: [i64; 7]) -> EntityId {
135 tx.create(Entity::new(
136 "IFCDIMENSIONALEXPONENTS",
137 exponents.iter().copied().map(Value::Integer).collect(),
138 ))
139}
140
141pub fn add_derived_unit_element(
143 tx: &mut Transaction,
144 unit: EntityId,
145 exponent: i64,
146) -> PropertyResult<EntityId> {
147 if exponent == 0 {
148 return Err(authoring_invalid(
149 "IFCDERIVEDUNITELEMENT",
150 "Exponent",
151 "expected a non-zero exponent",
152 ));
153 }
154 Ok(tx.create(Entity::new(
155 "IFCDERIVEDUNITELEMENT",
156 vec![Value::Ref(unit), Value::Integer(exponent)],
157 )))
158}
159
160pub fn add_derived_unit(
162 tx: &mut Transaction,
163 elements: &[EntityId],
164 unit_type: &str,
165 user_defined_type: Option<&str>,
166) -> PropertyResult<EntityId> {
167 if elements.is_empty() {
168 return Err(authoring_invalid(
169 "IFCDERIVEDUNIT",
170 "Elements",
171 "expected at least one derived unit element",
172 ));
173 }
174 require_enum("IFCDERIVEDUNIT", "UnitType", unit_type)?;
175 Ok(tx.create(Entity::new(
176 "IFCDERIVEDUNIT",
177 vec![
178 Value::List(elements.iter().copied().map(Value::Ref).collect()),
179 enum_value(unit_type),
180 user_defined_type.map_or(Value::Null, |v| Value::Text(v.into())),
181 ],
182 )))
183}
184
185pub fn assign_units(tx: &mut Transaction, units: &[EntityId]) -> PropertyResult<EntityId> {
190 if units.is_empty() {
191 return Err(authoring_invalid(
192 "IFCUNITASSIGNMENT",
193 "Units",
194 "expected at least one unit",
195 ));
196 }
197 Ok(tx.create(Entity::new(
198 "IFCUNITASSIGNMENT",
199 vec![Value::List(units.iter().copied().map(Value::Ref).collect())],
200 )))
201}
202
203pub fn add_measure_with_unit(
215 tx: &mut Transaction,
216 value: Value,
217 unit: EntityId,
218) -> PropertyResult<EntityId> {
219 if matches!(value, Value::Null) {
220 return Err(authoring_invalid(
221 "IFCMEASUREWITHUNIT",
222 "ValueComponent",
223 "expected a value",
224 ));
225 }
226 Ok(tx.create(Entity::new(
227 "IFCMEASUREWITHUNIT",
228 vec![value, Value::Ref(unit)],
229 )))
230}
231
232pub fn add_context_dependent_unit(
242 tx: &mut Transaction,
243 dimensions: EntityId,
244 unit_type: &str,
245 name: &str,
246) -> PropertyResult<EntityId> {
247 require_enum("IFCCONTEXTDEPENDENTUNIT", "UnitType", unit_type)?;
248 if name.trim().is_empty() {
249 return Err(authoring_invalid(
250 "IFCCONTEXTDEPENDENTUNIT",
251 "Name",
252 "expected a non-empty name",
253 ));
254 }
255 Ok(tx.create(Entity::new(
256 "IFCCONTEXTDEPENDENTUNIT",
257 vec![
258 Value::Ref(dimensions),
259 enum_value(unit_type),
260 Value::Text(name.into()),
261 ],
262 )))
263}
264
265fn require_enum(entity: &'static str, attribute: &'static str, value: &str) -> PropertyResult<()> {
266 if value.trim().is_empty() || !value.bytes().all(|b| b.is_ascii_uppercase() || b == b'_') {
267 return Err(authoring_invalid(entity, attribute, value));
268 }
269 Ok(())
270}
271
272fn enum_value(value: &str) -> Value {
273 Value::Enum(value.into())
274}
275
276fn authoring_invalid(
277 entity: &'static str,
278 attribute: &'static str,
279 value: impl Into<String>,
280) -> PropertyError {
281 PropertyError::AuthoringInvalid {
282 entity,
283 attribute,
284 value: value.into(),
285 }
286}
287
288pub fn add_conversion_based_unit_with_offset(
301 tx: &mut Transaction,
302 draft: ConversionBasedUnitDraft<'_>,
303 conversion_offset: f64,
304) -> PropertyResult<EntityId> {
305 const ENTITY: &str = "IFCCONVERSIONBASEDUNITWITHOFFSET";
306 require_enum(ENTITY, "UnitType", draft.unit_type)?;
307 if draft.name.trim().is_empty() {
308 return Err(authoring_invalid(
309 ENTITY,
310 "Name",
311 "expected a non-empty name",
312 ));
313 }
314 if !conversion_offset.is_finite() {
315 return Err(authoring_invalid(
316 ENTITY,
317 "ConversionOffset",
318 "expected a finite offset",
319 ));
320 }
321 Ok(tx.create(Entity::new(
322 ENTITY,
323 vec![
324 Value::Ref(draft.dimensions),
325 enum_value(draft.unit_type),
326 Value::Text(draft.name.into()),
327 Value::Ref(draft.conversion_factor),
328 Value::Real(conversion_offset),
329 ],
330 )))
331}