use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::Schema;
use super::decode::{Need, Slots};
use super::issue::TabularIssue;
pub(crate) const TABLE: &str = "IFCTABLE";
const ROW: &str = "IFCTABLEROW";
const COLUMN: &str = "IFCTABLECOLUMN";
#[derive(Debug, Clone)]
pub struct Table<'m> {
id: EntityId,
name: Option<&'m str>,
rows: Vec<TableRow<'m>>,
columns: Vec<TableColumn<'m>>,
issues: Vec<TabularIssue>,
}
impl<'m> Table<'m> {
#[must_use]
pub const fn id(&self) -> EntityId {
self.id
}
#[must_use]
pub const fn name(&self) -> Option<&'m str> {
self.name
}
#[must_use]
pub fn rows(&self) -> &[TableRow<'m>] {
&self.rows
}
#[must_use]
pub fn columns(&self) -> &[TableColumn<'m>] {
&self.columns
}
#[must_use]
pub fn heading(&self) -> Option<&TableRow<'m>> {
let mut headings = self.rows.iter().filter(|row| row.is_heading == Some(true));
let first = headings.next();
if headings.next().is_some() {
None
} else {
first
}
}
pub fn data_rows(&self) -> impl Iterator<Item = &TableRow<'m>> {
self.rows.iter().filter(|row| row.is_heading != Some(true))
}
#[must_use]
pub fn issues(&self) -> &[TabularIssue] {
&self.issues
}
}
#[derive(Debug, Clone, Copy)]
pub struct TableRow<'m> {
id: EntityId,
cells: Option<&'m [Value]>,
is_heading: Option<bool>,
}
impl<'m> TableRow<'m> {
#[must_use]
pub const fn id(&self) -> EntityId {
self.id
}
#[must_use]
pub const fn cells(&self) -> Option<&'m [Value]> {
self.cells
}
#[must_use]
pub const fn is_heading(&self) -> Option<bool> {
self.is_heading
}
}
#[derive(Debug, Clone, Copy)]
pub struct TableColumn<'m> {
id: EntityId,
identifier: Option<&'m str>,
name: Option<&'m str>,
description: Option<&'m str>,
unit: Option<EntityId>,
reference_path: Option<EntityId>,
}
impl<'m> TableColumn<'m> {
#[must_use]
pub const fn id(&self) -> EntityId {
self.id
}
#[must_use]
pub const fn identifier(&self) -> Option<&'m str> {
self.identifier
}
#[must_use]
pub const fn name(&self) -> Option<&'m str> {
self.name
}
#[must_use]
pub const fn description(&self) -> Option<&'m str> {
self.description
}
#[must_use]
pub const fn unit(&self) -> Option<EntityId> {
self.unit
}
#[must_use]
pub const fn reference_path(&self) -> Option<EntityId> {
self.reference_path
}
}
pub(crate) fn read_table<'m>(
model: &'m Model,
schema: &'m Schema,
id: EntityId,
entity: &'m Entity,
) -> Table<'m> {
let mut issues = Vec::new();
let mut slots = Slots::open(model, schema, id, entity, TABLE, &mut issues);
let name = slots.text("Name", Need::Optional);
let row_refs = slots.records("Rows", Need::Optional, ROW);
let column_refs = slots.records("Columns", Need::Optional, COLUMN);
let rows: Vec<Option<TableRow<'m>>> = row_refs
.into_iter()
.map(|found| found.map(|(row, record)| read_row(model, schema, row, record, &mut issues)))
.collect();
let columns = column_refs
.into_iter()
.flatten()
.map(|(column, record)| read_column(model, schema, column, record, &mut issues))
.collect();
check_wr1(id, &rows, &mut issues);
check_wr2(id, &rows, &mut issues);
Table {
id,
name,
rows: rows.into_iter().flatten().collect(),
columns,
issues,
}
}
fn read_row<'m>(
model: &'m Model,
schema: &'m Schema,
id: EntityId,
entity: &'m Entity,
issues: &mut Vec<TabularIssue>,
) -> TableRow<'m> {
let mut slots = Slots::open(model, schema, id, entity, ROW, issues);
TableRow {
id,
cells: slots.values("RowCells", Need::Optional),
is_heading: slots.boolean("IsHeading", Need::Optional),
}
}
fn read_column<'m>(
model: &'m Model,
schema: &'m Schema,
id: EntityId,
entity: &'m Entity,
issues: &mut Vec<TabularIssue>,
) -> TableColumn<'m> {
let mut slots = Slots::open(model, schema, id, entity, COLUMN, issues);
TableColumn {
id,
identifier: slots.text("Identifier", Need::Optional),
name: slots.text("Name", Need::Optional),
description: slots.text("Description", Need::Optional),
unit: slots.reference("Unit", Need::Optional),
reference_path: slots.reference("ReferencePath", Need::Optional),
}
}
fn check_wr1(table: EntityId, rows: &[Option<TableRow<'_>>], issues: &mut Vec<TabularIssue>) {
let Some(expected) = rows
.first()
.and_then(|row| row.as_ref())
.and_then(|row| row.cells)
.map(<[Value]>::len)
else {
return;
};
for (index, row) in rows.iter().enumerate() {
let Some(row) = row else { continue };
let Some(found) = row.cells.map(<[Value]>::len) else {
continue;
};
if found != expected {
issues.push(TabularIssue::RaggedRow {
table,
row: row.id,
index,
expected,
found,
});
}
}
}
fn check_wr2(table: EntityId, rows: &[Option<TableRow<'_>>], issues: &mut Vec<TabularIssue>) {
let found = rows
.iter()
.flatten()
.filter(|row| row.is_heading == Some(true))
.count();
if found > 1 {
issues.push(TabularIssue::TooManyHeadings { table, found });
}
}