runmat_runtime/builtins/table/
display.rs1use super::*;
2
3pub fn table_display_text(value: &Value) -> BuiltinResult<String> {
4 let object = match value {
5 Value::Object(object) if is_tabular_class(object) => object,
6 _ => return Err(invalid_argument("table display expects tabular object")),
7 };
8 let class_label = if object.is_class(TIMETABLE_CLASS) {
9 TIMETABLE_CLASS
10 } else {
11 TABLE_CLASS
12 };
13 let names = table_variable_names_from_object(object)?;
14 let variables = table_variables(object)?;
15 let rows = table_height(object)?;
16 let preview = rows.min(12);
17 let mut widths = names.iter().map(|name| name.len()).collect::<Vec<_>>();
18 let rendered_cols = names
19 .iter()
20 .enumerate()
21 .map(|(col, name)| {
22 let value = variables
23 .fields
24 .get(name)
25 .cloned()
26 .unwrap_or_else(|| Value::String(String::new()));
27 let cells = (0..preview)
28 .map(|row| render_table_cell(&value, row))
29 .collect::<Vec<_>>();
30 for cell in &cells {
31 widths[col] = widths[col].max(cell.len());
32 }
33 cells
34 })
35 .collect::<Vec<_>>();
36
37 let mut lines = Vec::new();
38 lines.push(format!("{rows}x{} {class_label}", names.len()));
39 if names.is_empty() {
40 return Ok(lines.join("\n"));
41 }
42 let header = names
43 .iter()
44 .enumerate()
45 .map(|(idx, name)| format!("{name:<width$}", width = widths[idx]))
46 .collect::<Vec<_>>()
47 .join(" ");
48 lines.push(header);
49 for row in 0..preview {
50 lines.push(
51 rendered_cols
52 .iter()
53 .enumerate()
54 .map(|(col, cells)| format!("{:<width$}", cells[row], width = widths[col]))
55 .collect::<Vec<_>>()
56 .join(" "),
57 );
58 }
59 if preview < rows {
60 lines.push(format!("... {} more rows", rows - preview));
61 }
62 Ok(lines.join("\n"))
63}
64
65pub fn table_summary_text(value: &Value) -> BuiltinResult<String> {
66 let object = match value {
67 Value::Object(object) if is_tabular_class(object) => object,
68 _ => return Err(invalid_argument("table display expects tabular object")),
69 };
70 let class_label = if object.is_class(TIMETABLE_CLASS) {
71 TIMETABLE_CLASS
72 } else {
73 TABLE_CLASS
74 };
75 Ok(format!(
76 "{}x{} {}",
77 table_height(object)?,
78 table_width(object)?,
79 class_label
80 ))
81}
82
83pub(super) fn render_table_cell(value: &Value, row: usize) -> String {
84 match value {
85 Value::Tensor(tensor) => tensor
86 .get2(row, 0)
87 .map(format_table_number)
88 .unwrap_or_default(),
89 Value::StringArray(array) => array.data.get(row).cloned().unwrap_or_default(),
90 Value::LogicalArray(array) => array
91 .data
92 .get(row)
93 .map(|value| if *value != 0 { "true" } else { "false" }.to_string())
94 .unwrap_or_default(),
95 Value::Object(obj) if obj.is_class("datetime") => {
96 crate::builtins::datetime::datetime_string_array(value)
97 .ok()
98 .flatten()
99 .and_then(|array| array.data.get(row).cloned())
100 .unwrap_or_else(|| value.to_string())
101 }
102 Value::Object(obj) if obj.is_class(CATEGORICAL_CLASS) => {
103 categorical_label_at(obj, row).unwrap_or_else(|| value.to_string())
104 }
105 other => other.to_string(),
106 }
107}
108
109pub(crate) fn categorical_label_at(object: &ObjectInstance, row: usize) -> Option<String> {
110 let code = match object.properties.get("Codes")? {
111 Value::Tensor(tensor) => tensor.get2(row, 0).ok()?,
112 _ => return None,
113 };
114 if !code.is_finite() || code < 1.0 {
115 return Some("<undefined>".to_string());
116 }
117 let idx = code.round() as usize - 1;
118 match object.properties.get("Categories")? {
119 Value::StringArray(array) => array.data.get(idx).cloned(),
120 _ => None,
121 }
122}
123
124pub(super) fn format_table_number(value: f64) -> String {
125 if value.is_nan() {
126 "NaN".to_string()
127 } else if value.fract() == 0.0 && value.abs() < 1e15 {
128 format!("{}", value as i64)
129 } else {
130 trim_float(format!("{value:.6}"))
131 }
132}
133
134pub(super) fn format_key_number(value: f64) -> String {
135 if value.is_nan() {
136 "NaN".to_string()
137 } else if value.is_infinite() {
138 value.to_string()
139 } else {
140 trim_float(format!("{value:.17}"))
141 }
142}
143
144pub(super) fn trim_float(mut text: String) -> String {
145 if let Some(dot) = text.find('.') {
146 let mut end = text.len();
147 while end > dot + 1 && text.as_bytes()[end - 1] == b'0' {
148 end -= 1;
149 }
150 if end == dot + 1 {
151 end -= 1;
152 }
153 text.truncate(end);
154 }
155 text
156}