1use 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#[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 #[must_use]
43 pub const fn id(&self) -> EntityId {
44 self.id
45 }
46
47 #[must_use]
49 pub const fn name(&self) -> Option<&'m str> {
50 self.name
51 }
52
53 #[must_use]
58 pub fn rows(&self) -> &[TableRow<'m>] {
59 &self.rows
60 }
61
62 #[must_use]
64 pub fn columns(&self) -> &[TableColumn<'m>] {
65 &self.columns
66 }
67
68 #[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 pub fn data_rows(&self) -> impl Iterator<Item = &TableRow<'m>> {
82 self.rows.iter().filter(|row| row.is_heading != Some(true))
83 }
84
85 #[must_use]
88 pub fn issues(&self) -> &[TabularIssue] {
89 &self.issues
90 }
91}
92
93#[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 #[must_use]
104 pub const fn id(&self) -> EntityId {
105 self.id
106 }
107
108 #[must_use]
112 pub const fn cells(&self) -> Option<&'m [Value]> {
113 self.cells
114 }
115
116 #[must_use]
118 pub const fn is_heading(&self) -> Option<bool> {
119 self.is_heading
120 }
121}
122
123#[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 #[must_use]
137 pub const fn id(&self) -> EntityId {
138 self.id
139 }
140
141 #[must_use]
143 pub const fn identifier(&self) -> Option<&'m str> {
144 self.identifier
145 }
146
147 #[must_use]
149 pub const fn name(&self) -> Option<&'m str> {
150 self.name
151 }
152
153 #[must_use]
155 pub const fn description(&self) -> Option<&'m str> {
156 self.description
157 }
158
159 #[must_use]
161 pub const fn unit(&self) -> Option<EntityId> {
162 self.unit
163 }
164
165 #[must_use]
167 pub const fn reference_path(&self) -> Option<EntityId> {
168 self.reference_path
169 }
170}
171
172pub(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
240fn 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
267fn 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}