1use ifc_model::{Entity, EntityId, Model, Transaction, Value};
20use ifc_schema::TypeKind;
21
22use super::assignment::prefix_exponent;
23use crate::quantity::release::bind;
24use crate::{PropertyError, PropertyResult};
25
26#[derive(Debug, Clone, Copy)]
28#[non_exhaustive]
29pub struct SiUnitDraft<'a> {
30 pub unit_type: &'a str,
32 pub name: &'a str,
34 pub prefix: Option<&'a str>,
39}
40
41impl<'a> SiUnitDraft<'a> {
42 #[must_use]
45 pub fn new(unit_type: &'a str, name: &'a str) -> Self {
46 Self {
47 unit_type,
48 name,
49 prefix: None,
50 }
51 }
52
53 #[must_use]
60 pub fn prefix(mut self, value: &'a str) -> Self {
61 self.prefix = Some(value);
62 self
63 }
64}
65
66#[derive(Debug, Clone, Copy)]
68#[non_exhaustive]
69pub struct MonetaryUnitDraft<'a> {
70 pub currency: &'a str,
75}
76
77impl<'a> MonetaryUnitDraft<'a> {
78 #[must_use]
80 pub fn new(currency: &'a str) -> Self {
81 Self { currency }
82 }
83}
84
85pub fn add_si_unit(tx: &mut Transaction, draft: SiUnitDraft<'_>) -> PropertyResult<EntityId> {
92 require_enum("IFCSIUNIT", "UnitType", draft.unit_type)?;
93 require_enum("IFCSIUNIT", "Name", draft.name)?;
94 if let Some(prefix) = draft.prefix {
95 if prefix_exponent(prefix).is_none() {
96 return Err(authoring_invalid("IFCSIUNIT", "Prefix", prefix));
97 }
98 }
99 Ok(tx.create(Entity::new(
100 "IFCSIUNIT",
101 vec![
102 Value::Derived,
103 enum_value(draft.unit_type),
104 draft.prefix.map_or(Value::Null, enum_value),
105 enum_value(draft.name),
106 ],
107 )))
108}
109
110#[deprecated(
121 note = "writes IFC4 text even in IFC2X3; use `create_monetary_unit`, which binds the model's release (#232)"
122)]
123pub fn add_monetary_unit(
124 tx: &mut Transaction,
125 draft: MonetaryUnitDraft<'_>,
126) -> PropertyResult<EntityId> {
127 require_currency(draft.currency)?;
128 Ok(tx.create(Entity::new(
129 MONETARY_UNIT,
130 vec![Value::Text(draft.currency.into())],
131 )))
132}
133
134pub fn create_monetary_unit(
159 tx: &mut Transaction,
160 model: &Model,
161 draft: MonetaryUnitDraft<'_>,
162) -> PropertyResult<EntityId> {
163 let layout = bind(model)?;
164 require_currency(draft.currency)?;
165 let schema = layout.schema();
166 let declared = schema
167 .attributes(MONETARY_UNIT)
168 .into_iter()
169 .find(|attribute| attribute.name.eq_ignore_ascii_case("Currency"))
170 .ok_or(PropertyError::AuthoringNotInSchema {
171 entity: MONETARY_UNIT,
172 attribute: "Currency",
173 schema: layout.version(),
174 })?;
175 let currency = draft.currency.trim();
176 let value = match schema.type_def(&declared.type_name).map(|t| &t.kind) {
177 Some(TypeKind::Enumeration(members)) => {
178 let token = members
179 .iter()
180 .find(|member| member.eq_ignore_ascii_case(currency))
181 .ok_or_else(|| {
182 authoring_invalid(
183 MONETARY_UNIT,
184 "Currency",
185 format!(
186 "{currency:?} is not an {} member in {:?}",
187 declared.type_name,
188 layout.version()
189 ),
190 )
191 })?;
192 Value::Enum(token.as_str().into())
193 }
194 _ => Value::Text(draft.currency.into()),
195 };
196 let record = layout.named_record(MONETARY_UNIT, vec![("Currency", value)])?;
197 Ok(tx.create(record))
198}
199
200const MONETARY_UNIT: &str = "IFCMONETARYUNIT";
201
202fn require_currency(currency: &str) -> PropertyResult<()> {
203 if currency.trim().is_empty() {
204 return Err(authoring_invalid(
205 MONETARY_UNIT,
206 "Currency",
207 "expected a currency code",
208 ));
209 }
210 Ok(())
211}
212
213#[derive(Debug, Clone, Copy)]
215#[non_exhaustive]
216pub struct ConversionBasedUnitDraft<'a> {
217 pub unit_type: &'a str,
219 pub name: &'a str,
221 pub conversion_factor: EntityId,
223 pub dimensions: EntityId,
225}
226
227impl<'a> ConversionBasedUnitDraft<'a> {
228 #[must_use]
231 pub fn new(
232 unit_type: &'a str,
233 name: &'a str,
234 conversion_factor: EntityId,
235 dimensions: EntityId,
236 ) -> Self {
237 Self {
238 unit_type,
239 name,
240 conversion_factor,
241 dimensions,
242 }
243 }
244}
245
246pub fn add_conversion_based_unit(
252 tx: &mut Transaction,
253 draft: ConversionBasedUnitDraft<'_>,
254) -> PropertyResult<EntityId> {
255 require_enum("IFCCONVERSIONBASEDUNIT", "UnitType", draft.unit_type)?;
256 if draft.name.trim().is_empty() {
257 return Err(authoring_invalid(
258 "IFCCONVERSIONBASEDUNIT",
259 "Name",
260 "expected a non-empty name",
261 ));
262 }
263 Ok(tx.create(Entity::new(
264 "IFCCONVERSIONBASEDUNIT",
265 vec![
266 Value::Ref(draft.dimensions),
267 enum_value(draft.unit_type),
268 Value::Text(draft.name.into()),
269 Value::Ref(draft.conversion_factor),
270 ],
271 )))
272}
273
274pub fn add_dimensional_exponents(tx: &mut Transaction, exponents: [i64; 7]) -> EntityId {
280 tx.create(Entity::new(
281 "IFCDIMENSIONALEXPONENTS",
282 exponents.iter().copied().map(Value::Integer).collect(),
283 ))
284}
285
286pub fn add_derived_unit_element(
288 tx: &mut Transaction,
289 unit: EntityId,
290 exponent: i64,
291) -> PropertyResult<EntityId> {
292 if exponent == 0 {
293 return Err(authoring_invalid(
294 "IFCDERIVEDUNITELEMENT",
295 "Exponent",
296 "expected a non-zero exponent",
297 ));
298 }
299 Ok(tx.create(Entity::new(
300 "IFCDERIVEDUNITELEMENT",
301 vec![Value::Ref(unit), Value::Integer(exponent)],
302 )))
303}
304
305pub fn add_derived_unit(
307 tx: &mut Transaction,
308 elements: &[EntityId],
309 unit_type: &str,
310 user_defined_type: Option<&str>,
311) -> PropertyResult<EntityId> {
312 if elements.is_empty() {
313 return Err(authoring_invalid(
314 "IFCDERIVEDUNIT",
315 "Elements",
316 "expected at least one derived unit element",
317 ));
318 }
319 require_enum("IFCDERIVEDUNIT", "UnitType", unit_type)?;
320 Ok(tx.create(Entity::new(
321 "IFCDERIVEDUNIT",
322 vec![
323 Value::List(elements.iter().copied().map(Value::Ref).collect()),
324 enum_value(unit_type),
325 user_defined_type.map_or(Value::Null, |v| Value::Text(v.into())),
326 ],
327 )))
328}
329
330pub fn assign_units(tx: &mut Transaction, units: &[EntityId]) -> PropertyResult<EntityId> {
335 if units.is_empty() {
336 return Err(authoring_invalid(
337 "IFCUNITASSIGNMENT",
338 "Units",
339 "expected at least one unit",
340 ));
341 }
342 Ok(tx.create(Entity::new(
343 "IFCUNITASSIGNMENT",
344 vec![Value::List(units.iter().copied().map(Value::Ref).collect())],
345 )))
346}
347
348pub fn add_measure_with_unit(
362 tx: &mut Transaction,
363 value: Value,
364 unit: EntityId,
365) -> PropertyResult<EntityId> {
366 if matches!(value, Value::Null) {
367 return Err(authoring_invalid(
368 "IFCMEASUREWITHUNIT",
369 "ValueComponent",
370 "expected a value",
371 ));
372 }
373 crate::pset::value_form::require_ifc_value("IFCMEASUREWITHUNIT", "ValueComponent", &value)?;
374 Ok(tx.create(Entity::new(
375 "IFCMEASUREWITHUNIT",
376 vec![value, Value::Ref(unit)],
377 )))
378}
379
380pub fn add_context_dependent_unit(
390 tx: &mut Transaction,
391 dimensions: EntityId,
392 unit_type: &str,
393 name: &str,
394) -> PropertyResult<EntityId> {
395 require_enum("IFCCONTEXTDEPENDENTUNIT", "UnitType", unit_type)?;
396 if name.trim().is_empty() {
397 return Err(authoring_invalid(
398 "IFCCONTEXTDEPENDENTUNIT",
399 "Name",
400 "expected a non-empty name",
401 ));
402 }
403 Ok(tx.create(Entity::new(
404 "IFCCONTEXTDEPENDENTUNIT",
405 vec![
406 Value::Ref(dimensions),
407 enum_value(unit_type),
408 Value::Text(name.into()),
409 ],
410 )))
411}
412
413fn require_enum(entity: &'static str, attribute: &'static str, value: &str) -> PropertyResult<()> {
414 if value.trim().is_empty() || !value.bytes().all(|b| b.is_ascii_uppercase() || b == b'_') {
415 return Err(authoring_invalid(entity, attribute, value));
416 }
417 Ok(())
418}
419
420fn enum_value(value: &str) -> Value {
421 Value::Enum(value.into())
422}
423
424fn authoring_invalid(
425 entity: &'static str,
426 attribute: &'static str,
427 value: impl Into<String>,
428) -> PropertyError {
429 PropertyError::AuthoringInvalid {
430 entity,
431 attribute,
432 value: value.into(),
433 }
434}
435
436pub fn add_conversion_based_unit_with_offset(
449 tx: &mut Transaction,
450 draft: ConversionBasedUnitDraft<'_>,
451 conversion_offset: f64,
452) -> PropertyResult<EntityId> {
453 const ENTITY: &str = "IFCCONVERSIONBASEDUNITWITHOFFSET";
454 require_enum(ENTITY, "UnitType", draft.unit_type)?;
455 if draft.name.trim().is_empty() {
456 return Err(authoring_invalid(
457 ENTITY,
458 "Name",
459 "expected a non-empty name",
460 ));
461 }
462 if !conversion_offset.is_finite() {
463 return Err(authoring_invalid(
464 ENTITY,
465 "ConversionOffset",
466 "expected a finite offset",
467 ));
468 }
469 Ok(tx.create(Entity::new(
470 ENTITY,
471 vec![
472 Value::Ref(draft.dimensions),
473 enum_value(draft.unit_type),
474 Value::Text(draft.name.into()),
475 Value::Ref(draft.conversion_factor),
476 Value::Real(conversion_offset),
477 ],
478 )))
479}