mech_core/structures/
table.rs

1use crate::*;
2#[cfg(feature = "matrix")]
3use crate::matrix::Matrix;
4use indexmap::map::*;
5use std::cmp::Ordering;
6use nalgebra::{DMatrix, DVector, RowDVector};
7use std::collections::{HashMap, HashSet};
8
9// Table ------------------------------------------------------------------
10
11#[cfg(feature = "table")]
12#[derive(Clone, Debug)]
13pub struct MechTable {
14  pub rows: usize,
15  pub cols: usize,
16  pub data: IndexMap<u64,(ValueKind,Matrix<Value>)>,
17  pub col_names: HashMap<u64,String>,
18}
19
20#[cfg(feature = "table")]
21impl PartialEq for MechTable {
22  fn eq(&self, other: &Self) -> bool {
23    // Compare rows
24    if self.rows != other.rows {
25      return false;
26    }
27    // For each column in self
28    for (col_id, col_name) in &self.col_names {
29      // Find a column with the same name in the other table
30      let other_col_id = match other.col_names.iter().find(|(_, n)| *n == col_name) {
31        Some((id, _)) => *id, // take the ID from the other table
32        None => return false,
33      };
34      // Get the data
35      let (self_kind, self_column) = match self.data.get(col_id) {
36        Some(entry) => entry,
37        None => return false,
38      };
39      let (other_kind, other_column) = match other.data.get(&other_col_id) {
40        Some(entry) => entry,
41        None => return false,
42      };
43      // Compare ValueKind
44      if self_kind != other_kind {
45        return false;
46      }
47      // Compare Matrix element-wise
48      if self_column != other_column {
49        return false;
50      }
51    }
52    true
53  }
54}
55
56#[cfg(feature = "table")]
57impl Eq for MechTable {}
58
59#[cfg(feature = "table")]
60impl MechTable {
61
62  pub fn from_records(records: Vec<MechRecord>) -> MResult<MechTable> {
63    if records.is_empty() {
64      return Err(
65        MechError2::new(
66          CannotCreateTableFromEmptyRecordListError,
67          None
68        ).with_compiler_loc()
69      );
70    }
71
72    let first = &records[0];
73    let rows = records.len();
74    let cols = first.cols;
75
76    let mut col_data: IndexMap<u64, Vec<Value>> = IndexMap::new();
77
78    for (&col_id, value) in &first.data {
79      col_data.insert(col_id, vec![value.clone()]);
80    }
81
82    let mut kinds = IndexMap::new();
83    for (col_id, kind) in first.data.keys().zip(&first.kinds) {
84      kinds.insert(*col_id, kind.clone());
85    }
86
87    let col_names = first.field_names.clone();
88
89    for record in records.iter().skip(1) {
90      first.check_record_schema(record)?;
91      for (&col_id, value) in &record.data {
92        col_data.entry(col_id).or_insert_with(Vec::new).push(value.clone());
93      }
94    }
95
96    let data: IndexMap<u64, (ValueKind, Matrix<Value>)> = col_data
97      .into_iter()
98      .map(|(col_id, values)| {
99        let kind = kinds[&col_id].clone();
100        let matrix = Matrix::DVector(Ref::new(DVector::from_vec(values)));
101        (col_id, (kind, matrix))
102      })
103      .collect();
104
105    Ok(MechTable {rows,cols,data,col_names})
106  }
107
108  pub fn from_kind(kind: ValueKind) -> MResult<MechTable> {
109    match kind {
110      ValueKind::Table(tbl, sze) => {
111        let mut data = IndexMap::new();
112        let mut col_names = HashMap::new();
113        for (col_id, col_kind) in &tbl {
114          let matrix = Matrix::DVector(Ref::new(DVector::from_vec(vec![Value::Empty; sze])));
115          col_names.insert(hash_str(col_id), col_id.clone());
116          data.insert(hash_str(&col_id), (col_kind.clone(), matrix));
117        }
118        Ok(MechTable { rows: sze, cols: tbl.len(), data, col_names })
119      }
120      _ => {
121        return Err(
122          MechError2::new(
123            CannotCreateTableFromNonTableKindError,
124            None
125          ).with_compiler_loc()
126        );
127      }
128    }
129  }
130
131  pub fn empty_table(&self, rows: usize) -> MechTable {
132    let mut data = IndexMap::new();
133    for col in self.data.iter() {
134      let (key, (kind, matrix)) = col;
135      // make a new vector the length of ix with values Value::Empty
136      let elements = vec![Value::Empty; rows];
137      let new_matrix = Matrix::DVector(Ref::new(DVector::from_vec(elements)));
138      data.insert(*key, (kind.clone(), new_matrix));
139    }
140    MechTable { rows: rows, cols: self.cols, data, col_names: self.col_names.clone() }
141  }
142
143  pub fn check_record_schema(&self, record: &MechRecord) -> MResult<bool> {
144    for (&col_id, record_value) in &record.data {
145      // Check that the column exists in the table
146      let (expected_kind, _column_matrix) = match self.data.get(&col_id) {
147        Some(data) => data,
148        None => continue,
149      };
150
151      // Check actual value kind
152      let actual_kind = record_value.kind();
153      if expected_kind != &actual_kind {
154        return Err(
155          MechError2::new(
156            TableColumnKindMismatchError {
157              column_id: col_id,
158              expected_kind: expected_kind.clone(),
159              actual_kind: actual_kind.clone(),
160            },
161            None
162          ).with_compiler_loc()
163        );
164      }
165
166      // Check column name
167      if let Some(expected_name) = self.col_names.get(&col_id) {
168        if let Some(field_name) = record.field_names.get(&col_id) {
169          if expected_name != field_name {
170            return Err(
171              MechError2::new(
172                TableColumnNameMismatchError {
173                  column_id: col_id,
174                  expected_name: expected_name.clone(),
175                  actual_name: field_name.clone(),
176                },
177                None
178              ).with_compiler_loc()
179            );
180          }
181        }
182      }
183    }
184
185    Ok(true)
186  }
187  
188  pub fn check_table_schema(&self, record: &MechTable) -> MResult<bool> {
189    // Check that the column names match
190    for (&col_id, col_name) in &self.col_names {
191      match record.col_names.get(&col_id) {
192        Some(record_name) if col_name != record_name => {
193          return Err(MechError2::new(
194            TableColumnNameMismatchError {
195              column_id: col_id,
196              expected_name: col_name.clone(),
197              actual_name: record_name.clone(),
198            },
199            None
200          ).with_compiler_loc());
201        }
202        None => {
203          return Err(MechError2::new(
204            TableColumnNotFoundError { column_id: col_id },
205            None
206          ).with_compiler_loc());
207        }
208        _ => {}
209      }
210    }
211
212    // Check that the data kinds match
213    for (&col_id, (expected_kind, _)) in &self.data {
214      match record.data.get(&col_id) {
215        Some((record_kind, _)) if expected_kind != record_kind => {
216          return Err(MechError2::new(
217            TableColumnKindMismatchError {
218              column_id: col_id,
219              expected_kind: expected_kind.clone(),
220              actual_kind: record_kind.clone(),
221            },
222            None
223          ).with_compiler_loc());
224        }
225        None => {
226          return Err(MechError2::new(
227            TableColumnNotFoundError { column_id: col_id },
228            None
229          ).with_compiler_loc());
230        }
231        _ => {}
232      }
233    }
234
235    Ok(true)
236  }
237
238  pub fn append_table(&mut self, other: &MechTable) -> MResult<()> {
239    self.check_table_schema(other)?;
240
241    for (&col_id, (_, other_matrix)) in &other.data {
242      let (_, self_matrix) = self.data.get_mut(&col_id).ok_or_else(|| 
243        MechError2::new(
244          TableColumnNotFoundError { column_id: col_id },
245          None
246        ).with_compiler_loc()
247      )?;
248
249      self_matrix.append(other_matrix).map_err(|err| 
250        MechError2::new(
251          MatrixAppendToTableError { column_id: col_id },
252          None
253        ).with_compiler_loc()
254      )?;
255    }
256
257    self.rows += other.rows;
258    Ok(())
259  }
260
261  pub fn append_record(&mut self, record: MechRecord) -> MResult<()> {
262    // Validate schema (this includes column count, types, and optional name checks)
263    self.check_record_schema(&record)?;
264
265    // Append each value to the corresponding column in the matrix
266    for (&col_id, value) in &record.data {
267      if let Some((_kind, column_matrix)) = self.data.get_mut(&col_id) {
268        let result = column_matrix.push(value.clone());
269      } else {
270        continue;
271      }
272    }
273
274    // Increment row count
275    self.rows += 1;
276
277    Ok(())
278  }
279
280  pub fn get_record(&self, ix: usize) -> Option<MechRecord> {
281    if ix > self.rows {
282      return None;
283    }
284
285    let mut data: IndexMap<u64, Value> = IndexMap::new();
286    data = self.data.iter().map(|(key, (kind, matrix))| {
287      let value = matrix.index1d(ix);
288      let name = self.col_names.get(key).unwrap();
289      (hash_str(name), value.clone())
290    }).collect();
291
292    let mut kinds = Vec::with_capacity(self.cols);
293    kinds = self.data.iter().map(|(_, (kind, _))| kind.clone()).collect();
294
295    let mut field_names = self.col_names.clone();
296   
297    Some(MechRecord{cols: self.cols, kinds, data, field_names})
298  }
299
300  #[cfg(feature = "pretty_print")]
301  pub fn to_html(&self) -> String {
302    let mut html = String::new();
303
304    // Start table
305    html.push_str("<table class=\"mech-table\">");
306
307    // Build thead
308    html.push_str("<thead class=\"mech-table-header\"><tr>");
309    for (key, (kind, _matrix)) in self.data.iter() {
310        let col_name = self
311            .col_names
312            .get(key)
313            .cloned()
314            .unwrap_or_else(|| key.to_string());
315
316        let kind_str = format!(
317            "<span class=\"mech-kind-annotation\">&lt;<span class=\"mech-kind\">{}</span>&gt;</span>",
318            kind
319        );
320
321        html.push_str(&format!(
322            "<th class=\"mech-table-field\">\
323                <div class=\"mech-field\">\
324                  <span class=\"mech-field-name\">{}</span>\
325                  <span class=\"mech-field-kind\">{}</span>\
326                </div>\
327            </th>",
328            col_name, kind_str
329        ));
330    }
331    html.push_str("</tr></thead>");
332
333    // Build tbody
334    html.push_str("<tbody class=\"mech-table-body\">");
335    for row_idx in 1..=self.rows {
336        html.push_str("<tr class=\"mech-table-row\">");
337        for (_key, (_kind, matrix)) in self.data.iter() {
338            let value = matrix.index1d(row_idx);
339            html.push_str(&format!(
340                "<td class=\"mech-table-column\">{}</td>",
341                value.to_html()
342            ));
343        }
344        html.push_str("</tr>");
345    }
346    html.push_str("</tbody></table>");
347    html
348  }
349
350  pub fn new_table(names: Vec<String>, kinds: Vec<ValueKind>, cols: Vec<Vec<Value>>) -> MechTable {
351    let col_count = names.len();
352    let row_count = if !cols.is_empty() { cols[0].len() } else { 0 };
353    let mut col_names = HashMap::new();
354    for (i, name) in names.iter().enumerate() {
355      col_names.insert(i as u64, name.clone());
356    }
357    let mut data = IndexMap::new();
358    for (col_idx, (kind, values)) in kinds.iter().zip(cols.iter()).enumerate() {
359      assert_eq!(
360        values.len(),
361        row_count,
362        "Column {} has inconsistent length (expected {} rows, got {})",
363        col_idx,
364        row_count,
365        values.len()
366      );
367      let matrix = Matrix::DVector(Ref::new(DVector::from_vec(values.clone())));
368      data.insert(col_idx as u64, (kind.clone(), matrix));
369    }
370    MechTable::new(row_count, col_count, data, col_names)
371  }
372
373  pub fn new(rows: usize, cols: usize, data: IndexMap<u64,(ValueKind,Matrix<Value>)>, col_names: HashMap<u64,String>) -> MechTable {
374    MechTable{rows, cols, data, col_names}
375  }
376
377  pub fn kind(&self) -> ValueKind {
378    let column_kinds: Vec<(String, ValueKind)> = self.data.iter()
379      .filter_map(|(key, (kind, _))| {
380        let col_name = self.col_names.get(key)?;
381        Some((col_name.clone(), kind.clone()))
382      })
383      .collect();
384    ValueKind::Table(column_kinds, self.rows)
385  }
386  
387  pub fn size_of(&self) -> usize {
388    self.data.iter().map(|(_,(_,v))| v.size_of()).sum()
389  }
390
391  pub fn rows(&self) -> usize {
392    self.rows
393  }
394
395  pub fn cols(&self) -> usize {
396    self.cols
397  }
398
399  pub fn get(&self, key: &u64) -> Option<&(ValueKind,Matrix<Value>)> {
400    self.data.get(key)
401  }
402
403   pub fn shape(&self) -> Vec<usize> {
404    vec![self.rows,self.cols]
405  }
406}
407
408#[cfg(feature = "pretty_print")]
409impl PrettyPrint for MechTable {
410  fn pretty_print(&self) -> String {
411    let mut builder = Builder::default();
412    for (k,(knd,val)) in &self.data {
413      let name = self.col_names.get(k).unwrap();
414      let val_string: String = val.as_vec().iter()
415        .map(|x| x.pretty_print())
416        .collect::<Vec<String>>()
417        .join("\n");
418      let mut col_string = vec![format!("{}<{}>", name.to_string(), knd), val_string];
419      builder.push_column(col_string);
420    }
421    let mut table = builder.build();
422    table.with(Style::modern_rounded());
423    format!("{table}")
424  }
425}
426
427#[cfg(feature = "table")]
428impl Hash for MechTable {
429  fn hash<H: Hasher>(&self, state: &mut H) {
430    for (k,(knd,val)) in self.data.iter() {
431      k.hash(state);
432      knd.hash(state);
433      val.hash(state);
434    }
435  }
436}
437
438#[derive(Debug, Clone)]
439pub struct CannotCreateTableFromEmptyRecordListError;
440
441impl MechErrorKind2 for CannotCreateTableFromEmptyRecordListError {
442  fn name(&self) -> &str {
443    "EmptyRecordList"
444  }
445  fn message(&self) -> String {
446    "Cannot create MechTable from an empty record list.".to_string()
447  }
448}
449
450#[derive(Debug, Clone)]
451pub struct CannotCreateTableFromNonTableKindError;
452
453impl MechErrorKind2 for CannotCreateTableFromNonTableKindError {
454  fn name(&self) -> &str {
455    "CannotCreateTableFromNonTableKind"
456  }
457  fn message(&self) -> String {
458    "Cannot create MechTable from non-table kind.".to_string()
459  }
460}
461
462#[derive(Debug, Clone)]
463pub struct TableColumnKindMismatchError {
464  pub column_id: u64,
465  pub expected_kind: ValueKind,
466  pub actual_kind: ValueKind,
467}
468
469impl MechErrorKind2 for TableColumnKindMismatchError {
470  fn name(&self) -> &str { "ColumnKindMismatch" }
471  fn message(&self) -> String {
472    format!("Schema mismatch: column {} kind mismatch (expected: {}, found: {}).",
473            self.column_id, self.expected_kind, self.actual_kind)
474  }
475}
476
477#[derive(Debug, Clone)]
478pub struct TableColumnNameMismatchError {
479  pub column_id: u64,
480  pub expected_name: String,
481  pub actual_name: String,
482}
483
484impl MechErrorKind2 for TableColumnNameMismatchError {
485  fn name(&self) -> &str { "ColumnNameMismatch" }
486  fn message(&self) -> String {
487    format!("Schema mismatch: column {} name mismatch (expected: '{}', found: '{}').",
488            self.column_id, self.expected_name, self.actual_name)
489  }
490}
491
492#[derive(Debug, Clone)]
493pub struct TableColumnNotFoundError {
494  pub column_id: u64,
495}
496
497impl MechErrorKind2 for TableColumnNotFoundError {
498  fn name(&self) -> &str { "ColumnNotFound" }
499  fn message(&self) -> String {
500    format!("Schema mismatch: column {} not found in table.", self.column_id)
501  }
502}
503
504#[derive(Debug, Clone)]
505pub struct MatrixAppendToTableError {
506  pub column_id: u64,
507}
508
509impl MechErrorKind2 for MatrixAppendToTableError {
510  fn name(&self) -> &str { "MatrixAppendToTableError" }
511  fn message(&self) -> String {
512    format!("Failed to append matrix for column {}.", self.column_id)
513  }
514}