Skip to main content

runmat_runtime/builtins/table/
display.rs

1use 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}