Skip to main content

ifc_tabular/read/
table.rs

1//! Reading `IfcTable` with its rows and columns.
2//!
3//! # WR1 and WR2 as the schema states them
4//!
5//! ```text
6//! WR1 : SIZEOF(QUERY( Temp <* Rows |
7//!         HIINDEX(Temp.RowCells) <> HIINDEX(Rows[1].RowCells))) = 0;
8//! WR2 : { 0 <= NumberOfHeadings <= 1 };   -- rows with IsHeading TRUE
9//! ```
10//!
11//! Both are identical in IFC4 ADD2 TC1 and IFC4X3 ADD2. `RowCells` and
12//! `IsHeading` are OPTIONAL, and a comparison with an indeterminate value
13//! is UNKNOWN, which `QUERY` does not select. So a row without cells is
14//! never ragged, a table whose `Rows[1]` has no cells (or does not
15//! resolve) has no WR1 reference width, and a row without `IsHeading` is
16//! not a heading. The checks below follow that reading exactly rather
17//! than a stricter one of their own.
18
19use ifc_model::{Entity, EntityId, Model, Value};
20use ifc_schema::Schema;
21
22use super::decode::{Need, Slots};
23use super::issue::TabularIssue;
24
25pub(crate) const TABLE: &str = "IFCTABLE";
26const ROW: &str = "IFCTABLEROW";
27const COLUMN: &str = "IFCTABLECOLUMN";
28
29/// A borrowed `IfcTable`, its resolved rows and columns, and every
30/// defect found on the way.
31#[derive(Debug, Clone)]
32pub struct Table<'m> {
33    id: EntityId,
34    name: Option<&'m str>,
35    rows: Vec<TableRow<'m>>,
36    columns: Vec<TableColumn<'m>>,
37    issues: Vec<TabularIssue>,
38}
39
40impl<'m> Table<'m> {
41    /// The table's id.
42    #[must_use]
43    pub const fn id(&self) -> EntityId {
44        self.id
45    }
46
47    /// `Name`.
48    #[must_use]
49    pub const fn name(&self) -> Option<&'m str> {
50        self.name
51    }
52
53    /// Rows in `Rows` order that resolved to an `IfcTableRow`.
54    ///
55    /// A member that did not resolve is absent here and reported in
56    /// [`Self::issues`].
57    #[must_use]
58    pub fn rows(&self) -> &[TableRow<'m>] {
59        &self.rows
60    }
61
62    /// Columns in `Columns` order that resolved to an `IfcTableColumn`.
63    #[must_use]
64    pub fn columns(&self) -> &[TableColumn<'m>] {
65        &self.columns
66    }
67
68    /// The row whose `IsHeading` is TRUE, when exactly one is.
69    #[must_use]
70    pub fn heading(&self) -> Option<&TableRow<'m>> {
71        let mut headings = self.rows.iter().filter(|row| row.is_heading == Some(true));
72        let first = headings.next();
73        if headings.next().is_some() {
74            None
75        } else {
76            first
77        }
78    }
79
80    /// Rows that are not headings, in order.
81    pub fn data_rows(&self) -> impl Iterator<Item = &TableRow<'m>> {
82        self.rows.iter().filter(|row| row.is_heading != Some(true))
83    }
84
85    /// Every defect found: malformed slots, dangling or mistyped
86    /// references, WR1 and WR2.
87    #[must_use]
88    pub fn issues(&self) -> &[TabularIssue] {
89        &self.issues
90    }
91}
92
93/// A borrowed `IfcTableRow`.
94#[derive(Debug, Clone, Copy)]
95pub struct TableRow<'m> {
96    id: EntityId,
97    cells: Option<&'m [Value]>,
98    is_heading: Option<bool>,
99}
100
101impl<'m> TableRow<'m> {
102    /// The row's id.
103    #[must_use]
104    pub const fn id(&self) -> EntityId {
105        self.id
106    }
107
108    /// `RowCells` as written: each cell is an `IfcValue`, typed or bare.
109    ///
110    /// `None` when the slot is unset or malformed.
111    #[must_use]
112    pub const fn cells(&self) -> Option<&'m [Value]> {
113        self.cells
114    }
115
116    /// `IsHeading`; `None` when unset or malformed.
117    #[must_use]
118    pub const fn is_heading(&self) -> Option<bool> {
119        self.is_heading
120    }
121}
122
123/// A borrowed `IfcTableColumn`.
124#[derive(Debug, Clone, Copy)]
125pub struct TableColumn<'m> {
126    id: EntityId,
127    identifier: Option<&'m str>,
128    name: Option<&'m str>,
129    description: Option<&'m str>,
130    unit: Option<EntityId>,
131    reference_path: Option<EntityId>,
132}
133
134impl<'m> TableColumn<'m> {
135    /// The column's id.
136    #[must_use]
137    pub const fn id(&self) -> EntityId {
138        self.id
139    }
140
141    /// `Identifier`.
142    #[must_use]
143    pub const fn identifier(&self) -> Option<&'m str> {
144        self.identifier
145    }
146
147    /// `Name`.
148    #[must_use]
149    pub const fn name(&self) -> Option<&'m str> {
150        self.name
151    }
152
153    /// `Description`.
154    #[must_use]
155    pub const fn description(&self) -> Option<&'m str> {
156        self.description
157    }
158
159    /// `Unit`, an `IfcUnit` reference.
160    #[must_use]
161    pub const fn unit(&self) -> Option<EntityId> {
162        self.unit
163    }
164
165    /// `ReferencePath`, an `IfcReference` reference.
166    #[must_use]
167    pub const fn reference_path(&self) -> Option<EntityId> {
168        self.reference_path
169    }
170}
171
172/// Read an `IfcTable` the caller has already type-checked.
173pub(crate) fn read_table<'m>(
174    model: &'m Model,
175    schema: &'m Schema,
176    id: EntityId,
177    entity: &'m Entity,
178) -> Table<'m> {
179    let mut issues = Vec::new();
180    let mut slots = Slots::open(model, schema, id, entity, TABLE, &mut issues);
181    let name = slots.text("Name", Need::Optional);
182    let row_refs = slots.records("Rows", Need::Optional, ROW);
183    let column_refs = slots.records("Columns", Need::Optional, COLUMN);
184
185    let rows: Vec<Option<TableRow<'m>>> = row_refs
186        .into_iter()
187        .map(|found| found.map(|(row, record)| read_row(model, schema, row, record, &mut issues)))
188        .collect();
189    let columns = column_refs
190        .into_iter()
191        .flatten()
192        .map(|(column, record)| read_column(model, schema, column, record, &mut issues))
193        .collect();
194
195    check_wr1(id, &rows, &mut issues);
196    check_wr2(id, &rows, &mut issues);
197
198    Table {
199        id,
200        name,
201        rows: rows.into_iter().flatten().collect(),
202        columns,
203        issues,
204    }
205}
206
207fn read_row<'m>(
208    model: &'m Model,
209    schema: &'m Schema,
210    id: EntityId,
211    entity: &'m Entity,
212    issues: &mut Vec<TabularIssue>,
213) -> TableRow<'m> {
214    let mut slots = Slots::open(model, schema, id, entity, ROW, issues);
215    TableRow {
216        id,
217        cells: slots.values("RowCells", Need::Optional),
218        is_heading: slots.boolean("IsHeading", Need::Optional),
219    }
220}
221
222fn read_column<'m>(
223    model: &'m Model,
224    schema: &'m Schema,
225    id: EntityId,
226    entity: &'m Entity,
227    issues: &mut Vec<TabularIssue>,
228) -> TableColumn<'m> {
229    let mut slots = Slots::open(model, schema, id, entity, COLUMN, issues);
230    TableColumn {
231        id,
232        identifier: slots.text("Identifier", Need::Optional),
233        name: slots.text("Name", Need::Optional),
234        description: slots.text("Description", Need::Optional),
235        unit: slots.reference("Unit", Need::Optional),
236        reference_path: slots.reference("ReferencePath", Need::Optional),
237    }
238}
239
240/// WR1, measured against `Rows[1]`: the first list position, resolved.
241fn check_wr1(table: EntityId, rows: &[Option<TableRow<'_>>], issues: &mut Vec<TabularIssue>) {
242    let Some(expected) = rows
243        .first()
244        .and_then(|row| row.as_ref())
245        .and_then(|row| row.cells)
246        .map(<[Value]>::len)
247    else {
248        return;
249    };
250    for (index, row) in rows.iter().enumerate() {
251        let Some(row) = row else { continue };
252        let Some(found) = row.cells.map(<[Value]>::len) else {
253            continue;
254        };
255        if found != expected {
256            issues.push(TabularIssue::RaggedRow {
257                table,
258                row: row.id,
259                index,
260                expected,
261                found,
262            });
263        }
264    }
265}
266
267/// WR2: at most one row whose `IsHeading` is TRUE.
268fn check_wr2(table: EntityId, rows: &[Option<TableRow<'_>>], issues: &mut Vec<TabularIssue>) {
269    let found = rows
270        .iter()
271        .flatten()
272        .filter(|row| row.is_heading == Some(true))
273        .count();
274    if found > 1 {
275        issues.push(TabularIssue::TooManyHeadings { table, found });
276    }
277}