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 #[default]
11 Short,
12 Long,
14 ShortE,
16 LongE,
18 ShortG,
20 LongG,
22 Rational,
24 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
40pub 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
70fn 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 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 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 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 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}