Skip to main content

ifc_tabular/
table.rs

1//! `IfcTable`, `IfcTableRow` and `IfcTableColumn`.
2
3use ifc_model::{Entity, EntityId, Transaction, Value};
4
5use crate::error::{TabularError, TabularResult};
6
7const TABLE: &str = "IFCTABLE";
8const ROW: &str = "IFCTABLEROW";
9const COLUMN: &str = "IFCTABLECOLUMN";
10
11/// Stage an `IfcTableRow`.
12///
13/// `cells` are `IfcValue`s: pass `Value::Typed { .. }` to declare a measure,
14/// or a bare literal to leave the value dimensionless. This crate does not
15/// choose for you.
16///
17/// # Errors
18///
19/// Refuses an empty cell list: `RowCells` is `LIST [1:?]`.
20pub fn add_table_row(
21    tx: &mut Transaction,
22    cells: Vec<Value>,
23    is_heading: bool,
24) -> TabularResult<EntityId> {
25    if cells.is_empty() {
26        return Err(TabularError::EmptyList {
27            entity: ROW,
28            attribute: "RowCells",
29        });
30    }
31    let attributes = vec![Value::List(cells), Value::Bool(is_heading)];
32    Ok(tx.create(Entity::new(ROW, attributes)))
33}
34
35/// Attributes of an `IfcTableColumn`.
36#[derive(Debug, Clone, Copy, Default)]
37pub struct ColumnDraft<'a> {
38    /// `Identifier`.
39    pub identifier: Option<&'a str>,
40    /// `Name`.
41    pub name: Option<&'a str>,
42    /// `Description`.
43    pub description: Option<&'a str>,
44    /// `Unit`, an `IfcUnit` reference.
45    pub unit: Option<EntityId>,
46    /// `ReferencePath`, an `IfcReference` reference.
47    ///
48    /// Taken as an id rather than modelled here: `IfcReference` is a
49    /// property-path concept this crate does not own.
50    pub reference_path: Option<EntityId>,
51}
52
53fn optional_text(value: Option<&str>) -> Value {
54    value.map_or(Value::Null, |text| Value::Text(text.into()))
55}
56
57/// Stage an `IfcTableColumn`.
58///
59/// Every attribute is OPTIONAL in the schema, so a column carrying nothing
60/// is legal. A blank identifier is not: it would name a column that cannot
61/// be referenced.
62///
63/// # Errors
64///
65/// Refuses a whitespace-only `Identifier`.
66pub fn add_table_column(tx: &mut Transaction, draft: ColumnDraft<'_>) -> TabularResult<EntityId> {
67    if draft.identifier.is_some_and(|id| id.trim().is_empty()) {
68        return Err(TabularError::BlankRequired {
69            entity: COLUMN,
70            attribute: "Identifier",
71        });
72    }
73    let attributes = vec![
74        optional_text(draft.identifier),
75        optional_text(draft.name),
76        optional_text(draft.description),
77        draft.unit.map_or(Value::Null, Value::Ref),
78        draft.reference_path.map_or(Value::Null, Value::Ref),
79    ];
80    Ok(tx.create(Entity::new(COLUMN, attributes)))
81}
82
83/// One staged row: its id, its width, and whether it is the heading.
84///
85/// The width travels with the id because a staged entity cannot be read
86/// back out of a `Transaction`, and WR1 is stated over cell counts. Taking
87/// the count from the caller keeps the rule checkable before commit rather
88/// than deferring it to a validator that runs later, or never.
89pub type StagedRow = (EntityId, usize, bool);
90
91/// Stage an `IfcTable`.
92///
93/// # Errors
94///
95/// Refuses a ragged table (WR1) and more than one heading row (WR2).
96///
97/// # Derived attributes
98///
99/// `NumberOfCellsInRow`, `NumberOfHeadings` and `NumberOfDataRows` are
100/// DERIVEd under new names rather than redeclaring an inherited attribute,
101/// so they occupy no instance slots and are not written at all. Contrast
102/// `IfcSIUnit`, whose DERIVE redeclares `SELF\IfcNamedUnit.Dimensions` and
103/// therefore does take a slot, written as `*`.
104pub fn add_table(
105    tx: &mut Transaction,
106    name: Option<&str>,
107    rows: &[StagedRow],
108    columns: &[EntityId],
109) -> TabularResult<EntityId> {
110    if let Some((_, first_width, _)) = rows.first() {
111        for (index, (_, width, _)) in rows.iter().enumerate() {
112            if width != first_width {
113                return Err(TabularError::RaggedRow {
114                    entity: TABLE,
115                    row: index,
116                    expected: *first_width,
117                    found: *width,
118                });
119            }
120        }
121    }
122    let headings = rows.iter().filter(|(_, _, heading)| *heading).count();
123    if headings > 1 {
124        return Err(TabularError::TooManyHeadings {
125            entity: TABLE,
126            found: headings,
127        });
128    }
129    let refs = |ids: &[EntityId]| {
130        if ids.is_empty() {
131            Value::Null
132        } else {
133            Value::List(ids.iter().copied().map(Value::Ref).collect())
134        }
135    };
136    let row_ids: Vec<EntityId> = rows.iter().map(|(id, _, _)| *id).collect();
137    let attributes = vec![optional_text(name), refs(&row_ids), refs(columns)];
138    Ok(tx.create(Entity::new(TABLE, attributes)))
139}