mech_core/structures/
table.rs

1use crate::*;
2#[cfg(feature = "matrix")]
3use crate::matrix::Matrix;
4use indexmap::map::*;
5
6use nalgebra::{DMatrix, DVector, RowDVector};
7
8// Table ------------------------------------------------------------------
9
10#[cfg(feature = "table")]
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct MechTable {
13  pub rows: usize,
14  pub cols: usize,
15  pub data: IndexMap<u64,(ValueKind,Matrix<Value>)>,
16  pub col_names: HashMap<u64,String>,
17}
18
19#[cfg(feature = "table")]
20impl MechTable {
21
22  pub fn from_records(records: Vec<MechRecord>) -> MResult<MechTable> {
23    if records.is_empty() {
24      return Err(MechError { id: line!(), file: file!().to_string(), tokens: vec![], msg: "Cannot create MechTable from empty record list.".to_string(), kind: MechErrorKind::None});
25    }
26
27    let first = &records[0];
28    let rows = records.len();
29    let cols = first.cols;
30
31    let mut col_data: IndexMap<u64, Vec<Value>> = IndexMap::new();
32
33    for (&col_id, value) in &first.data {
34      col_data.insert(col_id, vec![value.clone()]);
35    }
36
37    let mut kinds = IndexMap::new();
38    for (col_id, kind) in first.data.keys().zip(&first.kinds) {
39      kinds.insert(*col_id, kind.clone());
40    }
41
42    let col_names = first.field_names.clone();
43
44    for record in records.iter().skip(1) {
45      first.check_record_schema(record)?;
46      for (&col_id, value) in &record.data {
47        col_data.entry(col_id).or_insert_with(Vec::new).push(value.clone());
48      }
49    }
50
51    let data: IndexMap<u64, (ValueKind, Matrix<Value>)> = col_data
52      .into_iter()
53      .map(|(col_id, values)| {
54        let kind = kinds[&col_id].clone();
55        let matrix = Matrix::DVector(Ref::new(DVector::from_vec(values)));
56        (col_id, (kind, matrix))
57      })
58      .collect();
59
60    Ok(MechTable {rows,cols,data,col_names})
61  }
62
63  pub fn from_kind(kind: ValueKind) -> MResult<MechTable> {
64    match kind {
65      ValueKind::Table(tbl,sze) => {
66        let mut data = IndexMap::new();
67        let mut col_names = HashMap::new();
68        for (col_id, col_kind) in &tbl {
69          let matrix = Matrix::DVector(Ref::new(DVector::from_vec(vec![Value::Empty; sze])));
70          col_names.insert(hash_str(col_id), col_id.clone());
71          data.insert(hash_str(&col_id), (col_kind.clone(), matrix));
72        }
73        Ok(MechTable {rows: sze, cols: tbl.len(), data, col_names})
74      }
75      _ => {
76        return Err(MechError { id: line!(), file: file!().to_string(), tokens: vec![], msg: "Cannot create MechTable from non-table kind.".to_string(), kind: MechErrorKind::None });
77      }
78    }
79  }
80
81  pub fn empty_table(&self, rows: usize) -> MechTable {
82    let mut data = IndexMap::new();
83    for col in self.data.iter() {
84      let (key, (kind, matrix)) = col;
85      // make a new vector the length of ix with values Value::Empty
86      let elements = vec![Value::Empty; rows];
87      let new_matrix = Matrix::DVector(Ref::new(DVector::from_vec(elements)));
88      data.insert(*key, (kind.clone(), new_matrix));
89    }
90    MechTable { rows: rows, cols: self.cols, data, col_names: self.col_names.clone() }
91  }
92
93  pub fn check_record_schema(&self, record: &MechRecord) -> MResult<bool> {
94
95    for (&col_id, record_value) in &record.data {
96      // Check that the column exists in the table
97      // self.get data col id _or continue to the next column
98      let (expected_kind, column_matrix) = match self.data.get(&col_id) {
99        Some(data) => data,
100        None => {
101          continue;
102        }
103      };
104
105      // Check actual value kind
106      let actual_kind = record_value.kind();
107
108      if expected_kind != &actual_kind {
109        return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} kind mismatch (expected: {:?}, found: {:?})",col_id, expected_kind, actual_kind),kind: MechErrorKind::None,});
110      }
111
112      // Check column name
113      if let Some(expected_name) = self.col_names.get(&col_id) {
114        if let Some(field_name) = record.field_names.get(&col_id) {
115          if expected_name != field_name {
116            return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} name mismatch (expected: '{}', found: '{}')",col_id, expected_name, field_name),kind: MechErrorKind::None,});
117          }
118        }
119      }
120    }
121
122    Ok(true)
123  }
124  
125  pub fn check_table_schema(&self, record: &MechTable) -> MResult<bool> {
126
127    // Check that the column names match
128    for (&col_id, col_name) in &self.col_names {
129      if let Some(record_name) = record.col_names.get(&col_id) {
130        if col_name != record_name {
131          return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} name mismatch (expected: '{}', found: '{}')",col_id, col_name, record_name),kind: MechErrorKind::None,});
132        }
133      } else {
134        return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} not found in record",col_id),kind: MechErrorKind::None,});
135      }
136    }
137
138    // Check that the data kinds match
139    for (&col_id, (expected_kind, _)) in &self.data {
140      if let Some((record_kind, _)) = record.data.get(&col_id) {
141        if expected_kind != record_kind {
142          return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} kind mismatch (expected: {:?}, found: {:?})",col_id, expected_kind, record_kind),kind: MechErrorKind::None,});
143        }
144      } else {
145        return Err(MechError {id: line!(),file: file!().to_string(),tokens: vec![],msg: format!("Schema mismatch: column {} not found in record",col_id),kind: MechErrorKind::None,});
146      }
147    }
148
149    Ok(true)
150  }
151
152  pub fn append_table(&mut self, other: &MechTable) -> MResult<()> {
153    self.check_table_schema(other)?;
154    for (&col_id, (_, other_matrix)) in &other.data {
155      let (_, self_matrix) = self.data.get_mut(&col_id).ok_or(MechError {
156        id: line!(),
157        file: file!().to_string(),
158        tokens: vec![],
159        msg: format!("Column {} not found in destination table", col_id),
160        kind: MechErrorKind::None,
161      })?;
162
163      self_matrix.append(other_matrix).map_err(|err| MechError {
164        id: line!(),
165        file: file!().to_string(),
166        tokens: vec![],
167        msg: "".to_string(),
168        kind: MechErrorKind::None,
169      })?;
170    }
171    self.rows += other.rows;
172    Ok(())
173  }
174
175  pub fn append_record(&mut self, record: MechRecord) -> MResult<()> {
176    // Validate schema (this includes column count, types, and optional name checks)
177    self.check_record_schema(&record)?;
178
179    // Append each value to the corresponding column in the matrix
180    for (&col_id, value) in &record.data {
181      if let Some((_kind, column_matrix)) = self.data.get_mut(&col_id) {
182        let result = column_matrix.push(value.clone());
183      } else {
184        continue;
185      }
186    }
187
188    // Increment row count
189    self.rows += 1;
190
191    Ok(())
192  }
193
194  pub fn get_record(&self, ix: usize) -> Option<MechRecord> {
195    if ix > self.rows {
196      return None;
197    }
198
199    let mut data: IndexMap<u64, Value> = IndexMap::new();
200    data = self.data.iter().map(|(key, (kind, matrix))| {
201      let value = matrix.index1d(ix);
202      let name = self.col_names.get(key).unwrap();
203      (hash_str(name), value.clone())
204    }).collect();
205
206    let mut kinds = Vec::with_capacity(self.cols);
207    kinds = self.data.iter().map(|(_, (kind, _))| kind.clone()).collect();
208
209    let mut field_names = self.col_names.clone();
210   
211    Some(MechRecord{cols: self.cols, kinds, data, field_names})
212  }
213
214  #[cfg(feature = "pretty_print")]
215  pub fn to_html(&self) -> String {
216    let mut html = String::new();
217
218    // Start table
219    html.push_str("<table class=\"mech-table\">");
220
221    // Build thead
222    html.push_str("<thead class=\"mech-table-header\"><tr>");
223    for (key, (kind, _matrix)) in self.data.iter() {
224        let col_name = self
225            .col_names
226            .get(key)
227            .cloned()
228            .unwrap_or_else(|| key.to_string());
229
230        let kind_str = format!(
231            "<span class=\"mech-kind-annotation\">&lt;<span class=\"mech-kind\">{}</span>&gt;</span>",
232            kind
233        );
234
235        html.push_str(&format!(
236            "<th class=\"mech-table-field\">\
237                <div class=\"mech-field\">\
238                  <span class=\"mech-field-name\">{}</span>\
239                  <span class=\"mech-field-kind\">{}</span>\
240                </div>\
241            </th>",
242            col_name, kind_str
243        ));
244    }
245    html.push_str("</tr></thead>");
246
247    // Build tbody
248    html.push_str("<tbody class=\"mech-table-body\">");
249    for row_idx in 1..=self.rows {
250        html.push_str("<tr class=\"mech-table-row\">");
251        for (_key, (_kind, matrix)) in self.data.iter() {
252            let value = matrix.index1d(row_idx);
253            html.push_str(&format!(
254                "<td class=\"mech-table-column\">{}</td>",
255                value.to_html()
256            ));
257        }
258        html.push_str("</tr>");
259    }
260    html.push_str("</tbody></table>");
261    html
262  }
263
264  pub fn new(rows: usize, cols: usize, data: IndexMap<u64,(ValueKind,Matrix<Value>)>, col_names: HashMap<u64,String>) -> MechTable {
265    MechTable{rows, cols, data, col_names}
266  }
267
268  pub fn kind(&self) -> ValueKind {
269    let column_kinds: Vec<(String, ValueKind)> = self.data.iter()
270      .filter_map(|(key, (kind, _))| {
271        let col_name = self.col_names.get(key)?;
272        Some((col_name.clone(), kind.clone()))
273      })
274      .collect();
275    ValueKind::Table(column_kinds, self.rows)
276  }
277  
278  pub fn size_of(&self) -> usize {
279    self.data.iter().map(|(_,(_,v))| v.size_of()).sum()
280  }
281
282  pub fn rows(&self) -> usize {
283    self.rows
284  }
285
286  pub fn cols(&self) -> usize {
287    self.cols
288  }
289
290  pub fn get(&self, key: &u64) -> Option<&(ValueKind,Matrix<Value>)> {
291    self.data.get(key)
292  }
293
294   pub fn shape(&self) -> Vec<usize> {
295    vec![self.rows,self.cols]
296  }
297}
298
299#[cfg(feature = "pretty_print")]
300impl PrettyPrint for MechTable {
301  fn pretty_print(&self) -> String {
302    let mut builder = Builder::default();
303    for (k,(knd,val)) in &self.data {
304      let name = self.col_names.get(k).unwrap();
305      let val_string: String = val.as_vec().iter()
306        .map(|x| x.pretty_print())
307        .collect::<Vec<String>>()
308        .join("\n");
309      let mut col_string = vec![format!("{}<{}>", name.to_string(), knd), val_string];
310      builder.push_column(col_string);
311    }
312    let mut table = builder.build();
313    table.with(Style::modern_rounded());
314    format!("{table}")
315  }
316}
317
318#[cfg(feature = "table")]
319impl Hash for MechTable {
320  fn hash<H: Hasher>(&self, state: &mut H) {
321    for (k,(knd,val)) in self.data.iter() {
322      k.hash(state);
323      knd.hash(state);
324      val.hash(state);
325    }
326  }
327}