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(MechError { id: line!(), file: file!().to_string(), tokens: vec![], msg: "Cannot create MechTable from empty record list.".to_string(), kind: MechErrorKind::None});
65    }
66
67    let first = &records[0];
68    let rows = records.len();
69    let cols = first.cols;
70
71    let mut col_data: IndexMap<u64, Vec<Value>> = IndexMap::new();
72
73    for (&col_id, value) in &first.data {
74      col_data.insert(col_id, vec![value.clone()]);
75    }
76
77    let mut kinds = IndexMap::new();
78    for (col_id, kind) in first.data.keys().zip(&first.kinds) {
79      kinds.insert(*col_id, kind.clone());
80    }
81
82    let col_names = first.field_names.clone();
83
84    for record in records.iter().skip(1) {
85      first.check_record_schema(record)?;
86      for (&col_id, value) in &record.data {
87        col_data.entry(col_id).or_insert_with(Vec::new).push(value.clone());
88      }
89    }
90
91    let data: IndexMap<u64, (ValueKind, Matrix<Value>)> = col_data
92      .into_iter()
93      .map(|(col_id, values)| {
94        let kind = kinds[&col_id].clone();
95        let matrix = Matrix::DVector(Ref::new(DVector::from_vec(values)));
96        (col_id, (kind, matrix))
97      })
98      .collect();
99
100    Ok(MechTable {rows,cols,data,col_names})
101  }
102
103  pub fn from_kind(kind: ValueKind) -> MResult<MechTable> {
104    match kind {
105      ValueKind::Table(tbl,sze) => {
106        let mut data = IndexMap::new();
107        let mut col_names = HashMap::new();
108        for (col_id, col_kind) in &tbl {
109          let matrix = Matrix::DVector(Ref::new(DVector::from_vec(vec![Value::Empty; sze])));
110          col_names.insert(hash_str(col_id), col_id.clone());
111          data.insert(hash_str(&col_id), (col_kind.clone(), matrix));
112        }
113        Ok(MechTable {rows: sze, cols: tbl.len(), data, col_names})
114      }
115      _ => {
116        return Err(MechError { id: line!(), file: file!().to_string(), tokens: vec![], msg: "Cannot create MechTable from non-table kind.".to_string(), kind: MechErrorKind::None });
117      }
118    }
119  }
120
121  pub fn empty_table(&self, rows: usize) -> MechTable {
122    let mut data = IndexMap::new();
123    for col in self.data.iter() {
124      let (key, (kind, matrix)) = col;
125      // make a new vector the length of ix with values Value::Empty
126      let elements = vec![Value::Empty; rows];
127      let new_matrix = Matrix::DVector(Ref::new(DVector::from_vec(elements)));
128      data.insert(*key, (kind.clone(), new_matrix));
129    }
130    MechTable { rows: rows, cols: self.cols, data, col_names: self.col_names.clone() }
131  }
132
133  pub fn check_record_schema(&self, record: &MechRecord) -> MResult<bool> {
134
135    for (&col_id, record_value) in &record.data {
136      // Check that the column exists in the table
137      // self.get data col id _or continue to the next column
138      let (expected_kind, column_matrix) = match self.data.get(&col_id) {
139        Some(data) => data,
140        None => {
141          continue;
142        }
143      };
144
145      // Check actual value kind
146      let actual_kind = record_value.kind();
147
148      if expected_kind != &actual_kind {
149        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,});
150      }
151
152      // Check column name
153      if let Some(expected_name) = self.col_names.get(&col_id) {
154        if let Some(field_name) = record.field_names.get(&col_id) {
155          if expected_name != field_name {
156            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,});
157          }
158        }
159      }
160    }
161
162    Ok(true)
163  }
164  
165  pub fn check_table_schema(&self, record: &MechTable) -> MResult<bool> {
166
167    // Check that the column names match
168    for (&col_id, col_name) in &self.col_names {
169      if let Some(record_name) = record.col_names.get(&col_id) {
170        if col_name != record_name {
171          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,});
172        }
173      } else {
174        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,});
175      }
176    }
177
178    // Check that the data kinds match
179    for (&col_id, (expected_kind, _)) in &self.data {
180      if let Some((record_kind, _)) = record.data.get(&col_id) {
181        if expected_kind != record_kind {
182          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,});
183        }
184      } else {
185        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,});
186      }
187    }
188
189    Ok(true)
190  }
191
192  pub fn append_table(&mut self, other: &MechTable) -> MResult<()> {
193    self.check_table_schema(other)?;
194    for (&col_id, (_, other_matrix)) in &other.data {
195      let (_, self_matrix) = self.data.get_mut(&col_id).ok_or(MechError {
196        id: line!(),
197        file: file!().to_string(),
198        tokens: vec![],
199        msg: format!("Column {} not found in destination table", col_id),
200        kind: MechErrorKind::None,
201      })?;
202
203      self_matrix.append(other_matrix).map_err(|err| MechError {
204        id: line!(),
205        file: file!().to_string(),
206        tokens: vec![],
207        msg: "".to_string(),
208        kind: MechErrorKind::None,
209      })?;
210    }
211    self.rows += other.rows;
212    Ok(())
213  }
214
215  pub fn append_record(&mut self, record: MechRecord) -> MResult<()> {
216    // Validate schema (this includes column count, types, and optional name checks)
217    self.check_record_schema(&record)?;
218
219    // Append each value to the corresponding column in the matrix
220    for (&col_id, value) in &record.data {
221      if let Some((_kind, column_matrix)) = self.data.get_mut(&col_id) {
222        let result = column_matrix.push(value.clone());
223      } else {
224        continue;
225      }
226    }
227
228    // Increment row count
229    self.rows += 1;
230
231    Ok(())
232  }
233
234  pub fn get_record(&self, ix: usize) -> Option<MechRecord> {
235    if ix > self.rows {
236      return None;
237    }
238
239    let mut data: IndexMap<u64, Value> = IndexMap::new();
240    data = self.data.iter().map(|(key, (kind, matrix))| {
241      let value = matrix.index1d(ix);
242      let name = self.col_names.get(key).unwrap();
243      (hash_str(name), value.clone())
244    }).collect();
245
246    let mut kinds = Vec::with_capacity(self.cols);
247    kinds = self.data.iter().map(|(_, (kind, _))| kind.clone()).collect();
248
249    let mut field_names = self.col_names.clone();
250   
251    Some(MechRecord{cols: self.cols, kinds, data, field_names})
252  }
253
254  #[cfg(feature = "pretty_print")]
255  pub fn to_html(&self) -> String {
256    let mut html = String::new();
257
258    // Start table
259    html.push_str("<table class=\"mech-table\">");
260
261    // Build thead
262    html.push_str("<thead class=\"mech-table-header\"><tr>");
263    for (key, (kind, _matrix)) in self.data.iter() {
264        let col_name = self
265            .col_names
266            .get(key)
267            .cloned()
268            .unwrap_or_else(|| key.to_string());
269
270        let kind_str = format!(
271            "<span class=\"mech-kind-annotation\">&lt;<span class=\"mech-kind\">{}</span>&gt;</span>",
272            kind
273        );
274
275        html.push_str(&format!(
276            "<th class=\"mech-table-field\">\
277                <div class=\"mech-field\">\
278                  <span class=\"mech-field-name\">{}</span>\
279                  <span class=\"mech-field-kind\">{}</span>\
280                </div>\
281            </th>",
282            col_name, kind_str
283        ));
284    }
285    html.push_str("</tr></thead>");
286
287    // Build tbody
288    html.push_str("<tbody class=\"mech-table-body\">");
289    for row_idx in 1..=self.rows {
290        html.push_str("<tr class=\"mech-table-row\">");
291        for (_key, (_kind, matrix)) in self.data.iter() {
292            let value = matrix.index1d(row_idx);
293            html.push_str(&format!(
294                "<td class=\"mech-table-column\">{}</td>",
295                value.to_html()
296            ));
297        }
298        html.push_str("</tr>");
299    }
300    html.push_str("</tbody></table>");
301    html
302  }
303
304  pub fn new_table(names: Vec<String>, kinds: Vec<ValueKind>, cols: Vec<Vec<Value>>) -> MechTable {
305    let col_count = names.len();
306    let row_count = if !cols.is_empty() { cols[0].len() } else { 0 };
307    let mut col_names = HashMap::new();
308    for (i, name) in names.iter().enumerate() {
309      col_names.insert(i as u64, name.clone());
310    }
311    let mut data = IndexMap::new();
312    for (col_idx, (kind, values)) in kinds.iter().zip(cols.iter()).enumerate() {
313      assert_eq!(
314        values.len(),
315        row_count,
316        "Column {} has inconsistent length (expected {} rows, got {})",
317        col_idx,
318        row_count,
319        values.len()
320      );
321      let matrix = Matrix::DVector(Ref::new(DVector::from_vec(values.clone())));
322      data.insert(col_idx as u64, (kind.clone(), matrix));
323    }
324    MechTable::new(row_count, col_count, data, col_names)
325  }
326
327  pub fn new(rows: usize, cols: usize, data: IndexMap<u64,(ValueKind,Matrix<Value>)>, col_names: HashMap<u64,String>) -> MechTable {
328    MechTable{rows, cols, data, col_names}
329  }
330
331  pub fn kind(&self) -> ValueKind {
332    let column_kinds: Vec<(String, ValueKind)> = self.data.iter()
333      .filter_map(|(key, (kind, _))| {
334        let col_name = self.col_names.get(key)?;
335        Some((col_name.clone(), kind.clone()))
336      })
337      .collect();
338    ValueKind::Table(column_kinds, self.rows)
339  }
340  
341  pub fn size_of(&self) -> usize {
342    self.data.iter().map(|(_,(_,v))| v.size_of()).sum()
343  }
344
345  pub fn rows(&self) -> usize {
346    self.rows
347  }
348
349  pub fn cols(&self) -> usize {
350    self.cols
351  }
352
353  pub fn get(&self, key: &u64) -> Option<&(ValueKind,Matrix<Value>)> {
354    self.data.get(key)
355  }
356
357   pub fn shape(&self) -> Vec<usize> {
358    vec![self.rows,self.cols]
359  }
360}
361
362#[cfg(feature = "pretty_print")]
363impl PrettyPrint for MechTable {
364  fn pretty_print(&self) -> String {
365    let mut builder = Builder::default();
366    for (k,(knd,val)) in &self.data {
367      let name = self.col_names.get(k).unwrap();
368      let val_string: String = val.as_vec().iter()
369        .map(|x| x.pretty_print())
370        .collect::<Vec<String>>()
371        .join("\n");
372      let mut col_string = vec![format!("{}<{}>", name.to_string(), knd), val_string];
373      builder.push_column(col_string);
374    }
375    let mut table = builder.build();
376    table.with(Style::modern_rounded());
377    format!("{table}")
378  }
379}
380
381#[cfg(feature = "table")]
382impl Hash for MechTable {
383  fn hash<H: Hasher>(&self, state: &mut H) {
384    for (k,(knd,val)) in self.data.iter() {
385      k.hash(state);
386      knd.hash(state);
387      val.hash(state);
388    }
389  }
390}