Skip to main content

runmat_value/
display.rs

1use crate::array::{should_expand_nd_display, write_nd_pages};
2use crate::*;
3use runmat_thread_local::runmat_thread_local;
4use std::cell::RefCell;
5use std::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum FormatMode {
9    /// 4 decimal places, fixed or scientific (MATLAB default).
10    #[default]
11    Short,
12    /// 15 decimal places, fixed or scientific.
13    Long,
14    /// Always scientific notation, 4 decimal places.
15    ShortE,
16    /// Always scientific notation, 14 decimal places.
17    LongE,
18    /// Compact: shorter of fixed/scientific, 5 significant digits.
19    ShortG,
20    /// Compact: shorter of fixed/scientific, 15 significant digits.
21    LongG,
22    /// Rational approximation (p/q).
23    Rational,
24    /// IEEE 754 hexadecimal representation.
25    Hex,
26}
27
28runmat_thread_local! {
29    static DISPLAY_FORMAT: RefCell<FormatMode> = const { RefCell::new(FormatMode::Short) };
30}
31
32pub fn set_display_format(mode: FormatMode) {
33    DISPLAY_FORMAT.with(|c| *c.borrow_mut() = mode);
34}
35
36pub fn get_display_format() -> FormatMode {
37    DISPLAY_FORMAT.with(|c| *c.borrow())
38}
39
40/// Format a number using the current thread-local display format.
41pub fn format_number(value: f64) -> String {
42    if value.is_nan() {
43        return "NaN".to_string();
44    }
45    if value.is_infinite() {
46        return if value.is_sign_negative() {
47            "-Inf"
48        } else {
49            "Inf"
50        }
51        .to_string();
52    }
53    let mode = get_display_format();
54    if mode == FormatMode::Hex {
55        return fmt_hex(value);
56    }
57    let v = if value == 0.0 { 0.0 } else { value };
58    match mode {
59        FormatMode::Short => fmt_short(v),
60        FormatMode::Long => fmt_long(v),
61        FormatMode::ShortE => fmt_sci(v, 4),
62        FormatMode::LongE => fmt_sci(v, 14),
63        FormatMode::ShortG => fmt_compact(v, 5),
64        FormatMode::LongG => fmt_compact(v, 15),
65        FormatMode::Rational => fmt_rational(v),
66        FormatMode::Hex => unreachable!("hex mode handled before zero normalization"),
67    }
68}
69
70/// Reformat Rust's `e`-notation exponent into MATLAB style (`e+02`, `e-03`).
71fn matlab_exp(s: &str) -> String {
72    if let Some(e_pos) = s.find('e') {
73        let mantissa = &s[..e_pos];
74        let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0);
75        let sign = if exp >= 0 { '+' } else { '-' };
76        format!("{mantissa}e{sign}{:02}", exp.unsigned_abs())
77    } else {
78        s.to_string()
79    }
80}
81
82fn fmt_sci(v: f64, dec: usize) -> String {
83    if v == 0.0 {
84        return format!("0.{:0>dec$}e+00", 0, dec = dec);
85    }
86    let s = format!("{v:.dec$e}");
87    matlab_exp(&s)
88}
89
90fn fmt_short(v: f64) -> String {
91    let abs = v.abs();
92    if abs == 0.0 {
93        return "0".to_string();
94    }
95    if v.fract() == 0.0 && abs < 1e15 {
96        return format!("{}", v as i64);
97    }
98    if (0.001..10000.0).contains(&abs) {
99        format!("{:.4}", v)
100    } else {
101        fmt_sci(v, 4)
102    }
103}
104
105fn fmt_long(v: f64) -> String {
106    let abs = v.abs();
107    if abs == 0.0 {
108        return "0".to_string();
109    }
110    if v.fract() == 0.0 && abs < 1e15 {
111        return format!("{}", v as i64);
112    }
113    if (0.001..10000.0).contains(&abs) {
114        format!("{:.15}", v)
115    } else {
116        fmt_sci(v, 14)
117    }
118}
119
120fn fmt_compact(v: f64, sig_digits: usize) -> String {
121    let abs = v.abs();
122    if abs == 0.0 {
123        return "0".to_string();
124    }
125    let use_scientific = !(1e-4..1e6).contains(&abs);
126    if use_scientific {
127        let dec = sig_digits - 1;
128        let s = format!("{v:.dec$e}");
129        // trim trailing zeros in mantissa then reformat exponent
130        if let Some(e_pos) = s.find('e') {
131            let exp_part = &s[e_pos..];
132            let mut mantissa = s[..e_pos].to_string();
133            if let Some(dot) = mantissa.find('.') {
134                let mut end = mantissa.len();
135                while end > dot + 1 && mantissa.as_bytes()[end - 1] == b'0' {
136                    end -= 1;
137                }
138                if mantissa.as_bytes()[end - 1] == b'.' {
139                    end -= 1;
140                }
141                mantissa.truncate(end);
142            }
143            return matlab_exp(&format!("{mantissa}{exp_part}"));
144        }
145        return matlab_exp(&s);
146    }
147    let exp10 = abs.log10().floor() as i32;
148    let decimals = ((sig_digits as i32 - 1 - exp10).max(0)) as usize;
149    let pow = 10f64.powi(decimals as i32);
150    let rounded = (v * pow).round() / pow;
151    let mut s = format!("{rounded:.decimals$}");
152    if let Some(dot) = s.find('.') {
153        let mut end = s.len();
154        while end > dot + 1 && s.as_bytes()[end - 1] == b'0' {
155            end -= 1;
156        }
157        if s.as_bytes()[end - 1] == b'.' {
158            end -= 1;
159        }
160        s.truncate(end);
161    }
162    if s.is_empty() || s == "-0" {
163        s = "0".to_string();
164    }
165    s
166}
167
168fn fmt_rational(v: f64) -> String {
169    if v == 0.0 {
170        return "0".to_string();
171    }
172    let negative = v < 0.0;
173    let abs = v.abs();
174    if v.fract() == 0.0 && abs < 1e15 {
175        return format!("{}", v as i64);
176    }
177    // Continued fraction convergents; stop at the first one within MATLAB's
178    // 5e-7 relative tolerance (matches `format rational` behaviour for pi → 355/113).
179    let tol = 5e-7 * abs;
180    let max_d = 1_000_000i64;
181    let mut n0: i64 = 1;
182    let mut n1: i64 = abs.floor() as i64;
183    let mut d0: i64 = 0;
184    let mut d1: i64 = 1;
185    let mut a = abs;
186    let mut best_n = n1;
187    let mut best_d = d1;
188    for _ in 0..50 {
189        if (abs - best_n as f64 / best_d as f64).abs() <= tol {
190            break;
191        }
192        let f = a.fract();
193        if f < 1e-10 {
194            break;
195        }
196        a = 1.0 / f;
197        let q = a.floor() as i64;
198        let Some(n2) = q.checked_mul(n1).and_then(|v| v.checked_add(n0)) else {
199            break;
200        };
201        let Some(d2) = q.checked_mul(d1).and_then(|v| v.checked_add(d0)) else {
202            break;
203        };
204        if d2 > max_d {
205            break;
206        }
207        best_n = n2;
208        best_d = d2;
209        n0 = n1;
210        n1 = n2;
211        d0 = d1;
212        d1 = d2;
213    }
214    let sign = if negative { "-" } else { "" };
215    if best_d == 1 {
216        format!("{sign}{best_n}")
217    } else {
218        format!("{sign}{best_n}/{best_d}")
219    }
220}
221
222fn fmt_hex(v: f64) -> String {
223    format!("{:016x}", v.to_bits())
224}
225
226impl fmt::Display for Value {
227    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228        match self {
229            Value::Int(i) => write!(f, "{}", i.decimal_string()),
230            Value::Num(n) => write!(f, "{}", format_number(*n)),
231            Value::Complex(re, im) => {
232                if *im == 0.0 {
233                    write!(f, "{}", format_number(*re))
234                } else if *re == 0.0 {
235                    write!(f, "{}i", format_number(*im))
236                } else if *im < 0.0 {
237                    write!(f, "{}-{}i", format_number(*re), format_number(im.abs()))
238                } else {
239                    write!(f, "{}+{}i", format_number(*re), format_number(*im))
240                }
241            }
242            Value::Bool(b) => write!(f, "{}", if *b { 1 } else { 0 }),
243            Value::LogicalArray(la) => write!(f, "{la}"),
244            Value::String(s) => write!(f, "'{s}'"),
245            Value::StringArray(sa) => write!(f, "{sa}"),
246            Value::CharArray(ca) => write!(f, "{ca}"),
247            Value::Tensor(m) => write!(f, "{m}"),
248            Value::SparseTensor(m) => write!(f, "{m}"),
249            Value::ComplexTensor(m) => write!(f, "{m}"),
250            Value::Symbolic(expr) => write!(f, "{expr}"),
251            Value::SymbolicArray(array) => write!(f, "{array}"),
252            Value::Cell(ca) => ca.fmt(f),
253
254            Value::GpuTensor(h) => write!(
255                f,
256                "GpuTensor(shape={:?}, device={}, buffer={})",
257                h.shape, h.device_id, h.buffer_id
258            ),
259            Value::Object(obj) => write!(f, "{}(props={})", obj.class_name, obj.properties.len()),
260            Value::ObjectArray(array) => write!(f, "{array}"),
261            Value::HandleObject(h) => {
262                write!(
263                    f,
264                    "<handle {} @0x{:x} valid={}>",
265                    h.class_name,
266                    h.target.addr(),
267                    h.valid
268                )
269            }
270            Value::Listener(l) => {
271                write!(
272                    f,
273                    "<listener id={} {}@0x{:x} '{}' enabled={} valid={}>",
274                    l.id,
275                    l.class_name(),
276                    l.target.addr(),
277                    l.event_name,
278                    l.enabled,
279                    l.valid
280                )
281            }
282            Value::Struct(st) => {
283                write!(f, "struct {{")?;
284                for (i, (key, val)) in st.fields.iter().enumerate() {
285                    if i > 0 {
286                        write!(f, ", ")?;
287                    }
288                    write!(f, "{}: {}", key, val)?;
289                }
290                write!(f, "}}")
291            }
292            Value::OutputList(values) => {
293                write!(f, "[")?;
294                for (i, value) in values.iter().enumerate() {
295                    if i > 0 {
296                        write!(f, ", ")?;
297                    }
298                    write!(f, "{}", value)?;
299                }
300                write!(f, "]")
301            }
302            Value::FunctionHandle(name)
303            | Value::ExternalFunctionHandle(name)
304            | Value::MethodFunctionHandle(name) => {
305                write!(f, "@{name}")
306            }
307            Value::BoundFunctionHandle { name, .. } => write!(f, "@{name}"),
308            Value::Closure(c) => write!(
309                f,
310                "<closure {} captures={}>",
311                c.function_name,
312                c.captures.len()
313            ),
314            Value::ClassRef(name) => write!(f, "<class {name}>"),
315            Value::MException(e) => write!(
316                f,
317                "MException(identifier='{}', message='{}')",
318                e.identifier, e.message
319            ),
320            Value::Future(handle) => write!(f, "<future {}>", handle.id),
321            Value::Task(handle) => write!(f, "<task {}>", handle.id),
322            Value::Pool(handle) => write!(f, "<pool {}>", handle.id),
323            Value::Job(handle) => write!(f, "<job {}>", handle.id),
324            Value::Foreign(reference) => write!(
325                f,
326                "<foreign {}:{}@{}>",
327                reference.type_identity.family, reference.type_identity.name, reference.generation
328            ),
329        }
330    }
331}
332
333impl fmt::Display for ComplexTensor {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        match self.shape.len() {
336            0 | 1 => {
337                write!(f, "[")?;
338                for i in 0..self.len() {
339                    if i > 0 {
340                        write!(f, " ")?;
341                    }
342                    let s = self.format_element(i);
343                    write!(f, "{s}")?;
344                }
345                write!(f, "]")
346            }
347            2 => {
348                let rows = self.rows;
349                let cols = self.cols;
350                write!(f, "[")?;
351                for r in 0..rows {
352                    for c in 0..cols {
353                        if c > 0 {
354                            write!(f, " ")?;
355                        }
356                        let s = self.format_element(r + c * rows);
357                        write!(f, "{s}")?;
358                    }
359                    if r + 1 < rows {
360                        write!(f, "; ")?;
361                    }
362                }
363                write!(f, "]")
364            }
365            _ => {
366                if should_expand_nd_display(&self.shape) {
367                    write_nd_pages(f, &self.shape, |f, idx| {
368                        write!(f, "{}", self.format_element(idx))
369                    })
370                } else {
371                    write!(f, "ComplexTensor(shape={:?})", self.shape)
372                }
373            }
374        }
375    }
376}
377
378#[cfg(test)]
379mod display_tests {
380    use super::{
381        fmt_rational, format_number, set_display_format, ComplexTensor, FormatMode,
382        IntegerComplexStorage, IntegerStorage, LogicalArray, Tensor,
383    };
384
385    #[test]
386    fn fmt_rational_large_value_with_tiny_fract_does_not_overflow() {
387        // abs ~1e15 with a small fractional part: q*n1 would overflow i64 without
388        // checked arithmetic.
389        let result = std::panic::catch_unwind(|| fmt_rational(1_000_000_000_000_000.000_1));
390        assert!(
391            result.is_ok(),
392            "fmt_rational panicked on large value with tiny fract"
393        );
394
395        // Negative counterpart.
396        let result = std::panic::catch_unwind(|| fmt_rational(-1_000_000_000_000_000.000_1));
397        assert!(
398            result.is_ok(),
399            "fmt_rational panicked on negative large value with tiny fract"
400        );
401    }
402
403    #[test]
404    fn tensor_nd_display_uses_page_headers() {
405        let tensor = Tensor::new(
406            vec![1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
407            vec![2, 3, 2],
408        )
409        .expect("tensor");
410        let rendered = tensor.to_string();
411        assert!(rendered.contains("(:, :, 1) ="));
412        assert!(rendered.contains("(:, :, 2) ="));
413        assert!(rendered.contains("  1  0  0"));
414    }
415
416    #[test]
417    fn dense_integer_tensor_display_uses_exact_storage_values() {
418        let vector = Tensor::new_integer(
419            IntegerStorage::U64(vec![u64::MAX, 9_007_199_254_740_993]),
420            vec![2],
421        )
422        .expect("uint64 vector");
423        assert_eq!(
424            vector.to_string(),
425            "[18446744073709551615 9007199254740993]"
426        );
427
428        let matrix = Tensor::new_integer(
429            IntegerStorage::I64(vec![i64::MIN, -1, 1, i64::MAX]),
430            vec![2, 2],
431        )
432        .expect("int64 matrix");
433        let rendered = matrix.to_string();
434        assert!(rendered.contains("-9223372036854775808"));
435        assert!(rendered.contains("9223372036854775807"));
436    }
437
438    #[test]
439    fn dense_integer_nd_display_uses_exact_storage_values() {
440        let tensor = Tensor::new_integer(
441            IntegerStorage::U64(vec![u64::MAX, 9_007_199_254_740_993, 7, 8]),
442            vec![1, 2, 2],
443        )
444        .expect("uint64 nd tensor");
445        let rendered = tensor.to_string();
446        assert!(rendered.contains("(:, :, 1) ="));
447        assert!(rendered.contains("(:, :, 2) ="));
448        assert!(rendered.contains("18446744073709551615"));
449        assert!(rendered.contains("9007199254740993"));
450    }
451
452    #[test]
453    fn tensor_nd_display_falls_back_for_large_arrays() {
454        let tensor = Tensor::new(vec![0.0; 4097], vec![1, 1, 4097]).expect("tensor");
455        assert_eq!(tensor.to_string(), "Tensor(shape=[1, 1, 4097])");
456    }
457
458    #[test]
459    fn logical_nd_display_uses_headers_and_fallback_summary() {
460        let logical =
461            LogicalArray::new(vec![1, 0, 0, 1, 1, 0, 0, 1], vec![2, 2, 2]).expect("logical");
462        let rendered = logical.to_string();
463        assert!(rendered.contains("(:, :, 1) ="));
464        assert!(rendered.contains("(:, :, 2) ="));
465
466        let large = LogicalArray::new(vec![1; 4097], vec![1, 1, 4097]).expect("large logical");
467        assert_eq!(large.to_string(), "1x1x4097 logical array");
468    }
469
470    #[test]
471    fn complex_nd_display_uses_page_headers() {
472        let complex = ComplexTensor::new(
473            vec![(1.0, 0.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)],
474            vec![2, 1, 2],
475        )
476        .expect("complex");
477        let rendered = complex.to_string();
478        assert!(rendered.contains("(:, :, 1) ="));
479        assert!(rendered.contains("(:, :, 2) ="));
480    }
481
482    #[test]
483    fn typed_complex_integer_display_uses_exact_components() {
484        let storage = IntegerComplexStorage::new(
485            IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]),
486            IntegerStorage::U64(vec![7, 0]),
487        )
488        .expect("matching components");
489        let tensor = ComplexTensor::new_integer(storage, vec![1, 2]).expect("typed complex");
490        assert_eq!(
491            tensor.to_string(),
492            format!("[{}+7i {}]", u64::MAX, 1_u64 << 63)
493        );
494
495        let negative_imaginary = IntegerComplexStorage::new(
496            IntegerStorage::I64(vec![1]),
497            IntegerStorage::I64(vec![i64::MIN]),
498        )
499        .expect("matching components");
500        let tensor =
501            ComplexTensor::new_integer(negative_imaginary, vec![1, 1]).expect("typed complex");
502        assert_eq!(
503            tensor.to_string(),
504            format!("[1-{}i]", i64::MIN.unsigned_abs())
505        );
506    }
507
508    #[test]
509    fn format_hex_preserves_negative_zero_sign_bit() {
510        set_display_format(FormatMode::Hex);
511        assert_eq!(format_number(-0.0), "8000000000000000");
512        assert_eq!(format_number(0.0), "0000000000000000");
513        set_display_format(FormatMode::Short);
514    }
515}