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)]
37#[non_exhaustive]
38pub struct ColumnDraft<'a> {
39    /// `Identifier`.
40    pub identifier: Option<&'a str>,
41    /// `Name`.
42    pub name: Option<&'a str>,
43    /// `Description`.
44    pub description: Option<&'a str>,
45    /// `Unit`, an `IfcUnit` reference.
46    pub unit: Option<EntityId>,
47    /// `ReferencePath`, an `IfcReference` reference.
48    ///
49    /// Taken as an id rather than modelled here: `IfcReference` is a
50    /// property-path concept this crate does not own.
51    pub reference_path: Option<EntityId>,
52}
53
54impl<'a> ColumnDraft<'a> {
55    /// Starts a draft with every field unset.
56    #[must_use]
57    pub fn new() -> Self {
58        Self {
59            identifier: None,
60            name: None,
61            description: None,
62            unit: None,
63            reference_path: None,
64        }
65    }
66
67    /// Sets [`Self::identifier`]: `Identifier`.
68    #[must_use]
69    pub fn identifier(mut self, value: &'a str) -> Self {
70        self.identifier = Some(value);
71        self
72    }
73
74    /// Sets [`Self::name`]: `Name`.
75    #[must_use]
76    pub fn name(mut self, value: &'a str) -> Self {
77        self.name = Some(value);
78        self
79    }
80
81    /// Sets [`Self::description`]: `Description`.
82    #[must_use]
83    pub fn description(mut self, value: &'a str) -> Self {
84        self.description = Some(value);
85        self
86    }
87
88    /// Sets [`Self::unit`]: `Unit`, an `IfcUnit` reference.
89    #[must_use]
90    pub fn unit(mut self, value: EntityId) -> Self {
91        self.unit = Some(value);
92        self
93    }
94
95    /// Sets [`Self::reference_path`]: `ReferencePath`, an `IfcReference` reference.
96    #[must_use]
97    pub fn reference_path(mut self, value: EntityId) -> Self {
98        self.reference_path = Some(value);
99        self
100    }
101}
102
103fn optional_text(value: Option<&str>) -> Value {
104    value.map_or(Value::Null, |text| Value::Text(text.into()))
105}
106
107/// Stage an `IfcTableColumn`.
108///
109/// Every attribute is OPTIONAL in the schema, so a column carrying nothing
110/// is legal. A blank identifier is not: it would name a column that cannot
111/// be referenced.
112///
113/// # Errors
114///
115/// Refuses a whitespace-only `Identifier`.
116pub fn add_table_column(tx: &mut Transaction, draft: ColumnDraft<'_>) -> TabularResult<EntityId> {
117    if draft.identifier.is_some_and(|id| id.trim().is_empty()) {
118        return Err(TabularError::BlankRequired {
119            entity: COLUMN,
120            attribute: "Identifier",
121        });
122    }
123    let attributes = vec![
124        optional_text(draft.identifier),
125        optional_text(draft.name),
126        optional_text(draft.description),
127        draft.unit.map_or(Value::Null, Value::Ref),
128        draft.reference_path.map_or(Value::Null, Value::Ref),
129    ];
130    Ok(tx.create(Entity::new(COLUMN, attributes)))
131}
132
133/// One staged row: its id, its width, and whether it is the heading.
134///
135/// The width travels with the id because a staged entity cannot be read
136/// back out of a `Transaction`, and WR1 is stated over cell counts. Taking
137/// the count from the caller keeps the rule checkable before commit rather
138/// than deferring it to a validator that runs later, or never.
139pub type StagedRow = (EntityId, usize, bool);
140
141/// Stage an `IfcTable`.
142///
143/// # Errors
144///
145/// Refuses a ragged table (WR1) and more than one heading row (WR2).
146///
147/// # Derived attributes
148///
149/// `NumberOfCellsInRow`, `NumberOfHeadings` and `NumberOfDataRows` are
150/// DERIVEd under new names rather than redeclaring an inherited attribute,
151/// so they occupy no instance slots and are not written at all. Contrast
152/// `IfcSIUnit`, whose DERIVE redeclares `SELF\IfcNamedUnit.Dimensions` and
153/// therefore does take a slot, written as `*`.
154pub fn add_table(
155    tx: &mut Transaction,
156    name: Option<&str>,
157    rows: &[StagedRow],
158    columns: &[EntityId],
159) -> TabularResult<EntityId> {
160    if let Some((_, first_width, _)) = rows.first() {
161        for (index, (_, width, _)) in rows.iter().enumerate() {
162            if width != first_width {
163                return Err(TabularError::RaggedRow {
164                    entity: TABLE,
165                    row: index,
166                    expected: *first_width,
167                    found: *width,
168                });
169            }
170        }
171    }
172    let headings = rows.iter().filter(|(_, _, heading)| *heading).count();
173    if headings > 1 {
174        return Err(TabularError::TooManyHeadings {
175            entity: TABLE,
176            found: headings,
177        });
178    }
179    let refs = |ids: &[EntityId]| {
180        if ids.is_empty() {
181            Value::Null
182        } else {
183            Value::List(ids.iter().copied().map(Value::Ref).collect())
184        }
185    };
186    let row_ids: Vec<EntityId> = rows.iter().map(|(id, _, _)| *id).collect();
187    let attributes = vec![optional_text(name), refs(&row_ids), refs(columns)];
188    Ok(tx.create(Entity::new(TABLE, attributes)))
189}