1use crate::array::{Array, Data};
5use crate::dtype::DType;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum BoxStyle {
10 Fenced,
12 Spaced,
16}
17
18#[derive(Clone, Copy, Debug)]
20pub struct FmtOpts {
21 pub neg: char,
23 pub imag: char,
26 pub boxes: BoxStyle,
27}
28
29impl FmtOpts {
30 pub const J: FmtOpts = FmtOpts { neg: '_', imag: 'j', boxes: BoxStyle::Fenced };
31 pub const APL: FmtOpts = FmtOpts { neg: '¯', imag: 'J', boxes: BoxStyle::Spaced };
32}
33
34const SIG_DIGITS: usize = 6;
36
37pub fn format_array(a: &Array, opts: &FmtOpts) -> String {
39 if let Some(s) = a.sparse_parts() {
41 return format_sparse(a, s, opts);
42 }
43 if a.shape.contains(&0) {
45 return String::new();
46 }
47 if !a.is_row_major() {
51 return format_array(&a.to_row_major(), opts);
52 }
53 if a.dtype() == DType::Box {
54 match mixed_simple_texts(a, opts) {
59 Some(texts) if opts.boxes == BoxStyle::Spaced && a.rank() == 1 => {
60 return mixed_vector_line(a, &texts)
61 }
62 Some(texts) if opts.boxes == BoxStyle::Spaced => {
63 return laid_out(&a.shape, texts, Cells::Right)
64 }
65 None if opts.boxes == BoxStyle::Spaced && a.rank() == 1 => {
68 return nested_vector_line(a, opts)
69 }
70 _ => return format_boxed(a, opts),
71 }
72 }
73 let texts: Vec<String> = (0..a.count()).map(|i| format_atom(&a.data, i, opts)).collect();
74 laid_out(&a.shape, texts, Cells::of(a.dtype()))
75}
76
77fn format_sparse(a: &Array, s: &crate::sparse::Sparse, opts: &FmtOpts) -> String {
83 if s.entries == 0 {
84 return String::new();
85 }
86 let k = s.axes.len();
87 let width = s.cell_size(&a.shape);
88 let index: Vec<String> = s.indices.iter().map(|&i| format_i64(i as i64, opts)).collect();
89 let value: Vec<String> =
90 (0..s.entries * width).map(|i| format_atom(&a.data, i, opts)).collect();
91 let index_widths = column_widths(&index, k.max(1));
92 let value_widths = column_widths(&value, width.max(1));
93 let mut out = String::new();
94 for e in 0..s.entries {
95 if e > 0 {
96 out.push('\n');
97 }
98 push_row(&mut out, &index[e * k..(e + 1) * k], &index_widths, Cells::Right);
99 out.push_str(" | ");
100 push_row(&mut out, &value[e * width..(e + 1) * width], &value_widths, Cells::Right);
101 }
102 out
103}
104
105#[derive(Clone, Copy, PartialEq)]
107enum Cells {
108 Right,
110 Text,
112 Left,
115}
116
117impl Cells {
118 fn of(dtype: DType) -> Cells {
119 match dtype {
120 DType::Char => Cells::Text,
121 DType::Symbol => Cells::Left,
122 _ => Cells::Right,
123 }
124 }
125}
126
127fn peel(a: &Array) -> (usize, &Array) {
132 let mut n = 0;
133 let mut cur = a;
134 while cur.rank() == 0 && cur.dtype() == DType::Box {
135 cur = &cur.as_boxes().expect("boxed scalar")[0];
136 n += 1;
137 }
138 (n, cur)
139}
140
141fn mixed_simple_texts(a: &Array, opts: &FmtOpts) -> Option<Vec<String>> {
145 let boxes = a.as_boxes()?;
146 let mut texts = Vec::with_capacity(boxes.len());
147 for b in boxes {
148 let (_, leaf) = peel(b);
149 if leaf.rank() != 0 {
150 return None;
151 }
152 texts.push(format_atom(&leaf.data, 0, opts));
153 }
154 Some(texts)
155}
156
157fn mixed_vector_line(a: &Array, texts: &[String]) -> String {
162 let letters: Vec<bool> = match a.as_boxes() {
163 Some(items) => items.iter().map(|e| peel(e).1.dtype() == DType::Char).collect(),
164 None => vec![false; texts.len()],
165 };
166 let mut out = String::new();
167 for (i, t) in texts.iter().enumerate() {
168 if i > 0 && !(letters[i] && letters[i - 1]) {
169 out.push(' ');
170 }
171 out.push_str(t);
172 }
173 out
174}
175
176fn gap_extra(leaf: &Array) -> usize {
182 match leaf.rank() {
183 0 => 0,
184 r if leaf.dtype() == DType::Char => r - 1,
185 r => r,
186 }
187}
188
189fn nested_gap(left: &Array, right: &Array) -> usize {
193 let text_run = left.rank() == 0
194 && right.rank() == 0
195 && left.dtype() == DType::Char
196 && right.dtype() == DType::Char;
197 if text_run { 0 } else { 1 + gap_extra(left).max(gap_extra(right)) }
198}
199
200fn nested_vector_line(a: &Array, opts: &FmtOpts) -> String {
207 let items = a.as_boxes().expect("boxed vector");
208 let peeled: Vec<(usize, &Array)> = items.iter().map(peel).collect();
209 let cells: Vec<(Vec<String>, usize)> = peeled.iter().map(|(_, leaf)| block(leaf, opts)).collect();
210 let height = cells.iter().map(|(lines, _)| lines.len()).max().unwrap_or(1);
211 let lead = " ".repeat(peeled.first().map_or(1, |(n, _)| (*n).max(1)));
212 let trail = " ".repeat(peeled.last().map_or(1, |(n, _)| (*n).max(1)));
213 let mut rows = vec![String::new(); height];
214 for i in 0..peeled.len() {
215 if i > 0 {
216 let sep = " ".repeat(nested_gap(peeled[i - 1].1, peeled[i].1));
217 for row in &mut rows {
218 row.push_str(&sep);
219 }
220 }
221 let (lines, w) = &cells[i];
222 for (r, row) in rows.iter_mut().enumerate() {
223 let text = lines.get(r).map(String::as_str).unwrap_or("");
224 row.push_str(text);
225 for _ in 0..w.saturating_sub(width(text)) {
226 row.push(' ');
227 }
228 }
229 }
230 rows.iter().map(|r| format!("{lead}{r}{trail}")).collect::<Vec<_>>().join("\n")
231}
232
233fn laid_out(shape: &[usize], texts: Vec<String>, cells: Cells) -> String {
236 let rank = shape.len();
237 let a_shape = shape;
238 match rank {
239 0 => texts.into_iter().next().unwrap_or_default(),
240 1 if cells == Cells::Text => texts.concat(),
241 1 => texts.join(" "),
242 _ => {
243 let ncols = a_shape[rank - 1];
244 let nrows = a_shape[rank - 2];
245 let widths = if cells == Cells::Text {
248 vec![0; ncols]
249 } else {
250 column_widths(&texts, ncols)
251 };
252 let frame = &a_shape[..rank - 2];
253 let plane_size = nrows * ncols;
254 let planes: usize = frame.iter().product();
255 let mut out = String::new();
256 for p in 0..planes {
257 if p > 0 {
258 out.push_str(&"\n".repeat(plane_gap(frame, p) + 1));
260 }
261 for r in 0..nrows {
262 if r > 0 {
263 out.push('\n');
264 }
265 let start = p * plane_size + r * ncols;
266 push_row(&mut out, &texts[start..start + ncols], &widths, cells);
267 }
268 }
269 out
270 }
271 }
272}
273
274fn format_boxed(a: &Array, opts: &FmtOpts) -> String {
278 let boxes = a.as_boxes().expect("boxed data");
279 let blocks: Vec<(Vec<String>, usize)> = boxes.iter().map(|b| block(b, opts)).collect();
280 let rank = a.rank();
281 let (nrows, ncols) = match rank {
282 0 => (1, 1),
283 1 => (1, a.shape[0]),
284 _ => (a.shape[rank - 2], a.shape[rank - 1]),
285 };
286 let mut widths = vec![0usize; ncols];
288 for (i, (_, w)) in blocks.iter().enumerate() {
289 widths[i % ncols] = widths[i % ncols].max(*w);
290 }
291 let frame: &[usize] = if rank > 2 { &a.shape[..rank - 2] } else { &[] };
292 let planes: usize = frame.iter().product();
293 let plane_size = nrows * ncols;
294 let mut out = String::new();
295 for p in 0..planes.max(1) {
296 if p > 0 {
297 out.push_str(&"\n".repeat(plane_gap(frame, p) + 1));
298 }
299 push_boxed_plane(
300 &mut out,
301 &blocks[p * plane_size..(p + 1) * plane_size],
302 nrows,
303 ncols,
304 &widths,
305 opts,
306 );
307 }
308 out
309}
310
311fn block(a: &Array, opts: &FmtOpts) -> (Vec<String>, usize) {
320 if a.count() == 0 && a.rank() > 0 {
321 let rank = a.rank();
322 let rows: usize = a.shape[..rank - 1].iter().product();
323 let w = a.shape[rank - 1];
324 return (vec![" ".repeat(w); rows], w);
325 }
326 let text = format_array(a, opts);
327 if text.is_empty() {
328 return (vec![String::new()], 0);
329 }
330 let lines: Vec<String> = text.lines().map(str::to_string).collect();
331 let w = lines.iter().map(|l| width(l)).max().unwrap_or(0);
332 (lines, w)
333}
334
335fn push_boxed_plane(
336 out: &mut String,
337 blocks: &[(Vec<String>, usize)],
338 nrows: usize,
339 ncols: usize,
340 widths: &[usize],
341 opts: &FmtOpts,
342) {
343 let fence = opts.boxes == BoxStyle::Fenced;
344 let border: String = if fence {
345 let mut s = String::from("+");
346 for &w in widths {
347 s.push_str(&"-".repeat(w));
348 s.push('+');
349 }
350 s
351 } else {
352 String::new()
353 };
354 let mut lines: Vec<String> = Vec::new();
355 for r in 0..nrows {
356 if fence {
357 lines.push(border.clone());
358 }
359 let row = &blocks[r * ncols..(r + 1) * ncols];
360 let height = row.iter().map(|(lines, _)| lines.len()).max().unwrap_or(1);
363 for k in 0..height {
364 let mut line = String::new();
365 line.push(if fence { '|' } else { ' ' });
366 for (c, (cell, _)) in row.iter().enumerate() {
367 if !fence && c > 0 {
368 line.push(' ');
369 }
370 let text = cell.get(k).map(String::as_str).unwrap_or("");
371 line.push_str(text);
372 for _ in 0..widths[c].saturating_sub(width(text)) {
373 line.push(' ');
374 }
375 if fence {
376 line.push('|');
377 }
378 }
379 if !fence {
380 line.push(' ');
381 }
382 lines.push(line);
383 }
384 }
385 if fence {
386 lines.push(border);
387 }
388 out.push_str(&lines.join("\n"));
389}
390
391fn plane_gap(frame: &[usize], p: usize) -> usize {
394 let mut gap = 1;
397 let mut rest = p;
398 for &n in frame.iter().rev() {
399 if rest % n != 0 {
400 break;
401 }
402 rest /= n;
403 gap += 1;
404 }
405 gap
406}
407
408fn column_widths(texts: &[String], ncols: usize) -> Vec<usize> {
410 let mut widths = vec![0usize; ncols];
411 for (i, t) in texts.iter().enumerate() {
412 let j = i % ncols;
413 widths[j] = widths[j].max(width(t));
414 }
415 widths
416}
417
418fn push_row(out: &mut String, row: &[String], widths: &[usize], cells: Cells) {
419 for (j, cell) in row.iter().enumerate() {
420 if cells == Cells::Text {
421 out.push_str(cell);
422 continue;
423 }
424 if j > 0 {
425 out.push(' ');
426 }
427 let pad = widths[j].saturating_sub(width(cell));
428 if cells == Cells::Right {
429 for _ in 0..pad {
430 out.push(' ');
431 }
432 }
433 out.push_str(cell);
434 if cells == Cells::Left {
435 for _ in 0..pad {
436 out.push(' ');
437 }
438 }
439 }
440}
441
442fn width(s: &str) -> usize {
444 s.chars().count()
445}
446
447fn format_atom(data: &Data, i: usize, opts: &FmtOpts) -> String {
448 match data {
449 Data::Bool(v) => (if v[i] != 0 { "1" } else { "0" }).to_string(),
450 Data::I64(v) => format_i64(v[i], opts),
451 Data::Ext(v) => with_neg_sign(&v[i].to_string(), opts),
452 Data::Rat(v) => with_neg_sign(&v[i].to_string(), opts),
453 Data::F64(v) => format_f64(v[i], opts),
454 Data::Complex(v) => format_complex(v[i], opts),
455 Data::Char(v) => v[i].to_string(),
456 Data::Symbol(v) => format!("`{}", crate::symbol::name(v[i])),
458 Data::Box(_) => String::new(),
460 }
461}
462
463fn format_complex(z: crate::complex::Cx, opts: &FmtOpts) -> String {
468 if z[1] == 0.0 {
469 return format_f64(z[0], opts);
470 }
471 format!("{}{}{}", format_f64(z[0], opts), opts.imag, format_f64(z[1], opts))
472}
473
474fn format_i64(v: i64, opts: &FmtOpts) -> String {
475 with_neg_sign(&v.to_string(), opts)
476}
477
478fn with_neg_sign(s: &str, opts: &FmtOpts) -> String {
483 match s.strip_prefix('-') {
484 Some(rest) => with_sign(rest, opts),
485 None => s.to_string(),
486 }
487}
488
489fn format_f64(x: f64, opts: &FmtOpts) -> String {
490 if x.is_nan() {
491 return format!("{}.", opts.neg);
492 }
493 if x.is_infinite() {
494 return match (opts.neg, x > 0.0) {
496 ('_', true) => "_".to_string(),
497 ('_', false) => "__".to_string(),
498 (_, true) => "∞".to_string(),
499 (neg, false) => format!("{neg}∞"),
500 };
501 }
502 let magnitude = x.abs();
503 let sci = format!("{:.*e}", SIG_DIGITS - 1, magnitude);
506 let (mantissa, exponent) = sci.split_once('e').expect("scientific form has an exponent");
507 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
508 let exponent: i32 = exponent.parse().expect("exponent is an integer");
509 let body = if exponent >= 12 || exponent <= -6 {
510 let mut s = trim_fraction(&place_point(&digits, 1));
511 s.push('e');
512 if exponent < 0 {
513 s.push_str(&with_sign(&(-(exponent as i64)).to_string(), opts));
514 } else {
515 s.push_str(&exponent.to_string());
516 }
517 s
518 } else {
519 positional(&digits, exponent)
520 };
521 if x < 0.0 { with_sign(&body, opts) } else { body }
522}
523
524fn positional(digits: &str, exponent: i32) -> String {
526 if exponent < 0 {
527 let zeros = (-exponent - 1) as usize;
528 return trim_fraction(&format!("0.{}{}", "0".repeat(zeros), digits));
529 }
530 let int_len = exponent as usize + 1;
531 if int_len >= digits.len() {
532 return format!("{}{}", digits, "0".repeat(int_len - digits.len()));
535 }
536 trim_fraction(&place_point(digits, int_len))
537}
538
539fn place_point(digits: &str, int_len: usize) -> String {
541 format!("{}.{}", &digits[..int_len], &digits[int_len..])
542}
543
544fn trim_fraction(s: &str) -> String {
546 if !s.contains('.') {
547 return s.to_string();
548 }
549 s.trim_end_matches('0').trim_end_matches('.').to_string()
550}
551
552fn with_sign(body: &str, opts: &FmtOpts) -> String {
553 let mut s = String::with_capacity(body.len() + opts.neg.len_utf8());
554 s.push(opts.neg);
555 s.push_str(body);
556 s
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use crate::array::Buf;
563 use rstest::rstest;
564
565 fn j(a: &Array) -> String {
566 format_array(a, &FmtOpts::J)
567 }
568
569 fn apl(a: &Array) -> String {
570 format_array(a, &FmtOpts::APL)
571 }
572
573 fn fj(x: f64) -> String {
574 format_f64(x, &FmtOpts::J)
575 }
576
577 #[rstest]
580 #[case(0, "0")]
581 #[case(7, "7")]
582 #[case(-3, "_3")]
583 #[case(-1234, "_1234")]
584 #[case(i64::MIN, "_9223372036854775808")]
585 fn integers_j(#[case] v: i64, #[case] want: &str) {
586 assert_eq!(format_i64(v, &FmtOpts::J), want);
587 }
588
589 #[rstest]
590 #[case(-3, "¯3")]
591 #[case(3, "3")]
592 fn integers_apl(#[case] v: i64, #[case] want: &str) {
593 assert_eq!(format_i64(v, &FmtOpts::APL), want);
594 }
595
596 #[rstest]
597 #[case(0.0, "0")]
598 #[case(0.5, "0.5")]
599 #[case(2.0, "2")]
600 #[case(-2.0, "_2")]
601 #[case(1.0 / 3.0, "0.333333")]
602 #[case(-1.0 / 3.0, "_0.333333")]
603 #[case(2.0 / 3.0, "0.666667")]
604 #[case(1.25, "1.25")]
605 #[case(100.0, "100")]
606 #[case(1e-5, "0.00001")]
607 #[case(0.000012345678, "0.0000123457")]
608 #[case(1e11, "100000000000")]
609 #[case(123456789.0, "123457000")]
610 fn floats_positional(#[case] x: f64, #[case] want: &str) {
611 assert_eq!(fj(x), want);
612 }
613
614 #[rstest]
615 #[case(1e-7, "1e_7")]
616 #[case(-1e-7, "_1e_7")]
617 #[case(1.5e13, "1.5e13")]
618 #[case(1e12, "1e12")]
619 #[case(-2.5e20, "_2.5e20")]
620 #[case(1.234567e-9, "1.23457e_9")]
621 fn floats_exponent(#[case] x: f64, #[case] want: &str) {
622 assert_eq!(fj(x), want);
623 }
624
625 #[test]
626 fn floats_apl_signs() {
627 assert_eq!(format_f64(-0.5, &FmtOpts::APL), "¯0.5");
628 assert_eq!(format_f64(1e-7, &FmtOpts::APL), "1e¯7");
629 assert_eq!(format_f64(-1e-7, &FmtOpts::APL), "¯1e¯7");
630 }
631
632 #[test]
633 fn negative_zero_prints_unsigned() {
634 assert_eq!(fj(-0.0), "0");
635 }
636
637 #[test]
638 fn nan_and_infinities() {
639 assert_eq!(fj(f64::NAN), "_.");
640 assert_eq!(fj(f64::INFINITY), "_");
641 assert_eq!(fj(f64::NEG_INFINITY), "__");
642 assert_eq!(format_f64(f64::NAN, &FmtOpts::APL), "¯.");
643 assert_eq!(format_f64(f64::INFINITY, &FmtOpts::APL), "∞");
644 assert_eq!(format_f64(f64::NEG_INFINITY, &FmtOpts::APL), "¯∞");
645 }
646
647 #[test]
648 fn scalars() {
649 assert_eq!(j(&Array::scalar_i64(-3)), "_3");
650 assert_eq!(apl(&Array::scalar_i64(-3)), "¯3");
651 assert_eq!(j(&Array::scalar_f64(0.5)), "0.5");
652 assert_eq!(j(&Array::scalar_bool(true)), "1");
653 assert_eq!(j(&Array::scalar_bool(false)), "0");
654 assert_eq!(j(&Array::new(vec![], Data::Char(vec!['q'].into()))), "q");
655 }
656
657 #[test]
660 fn integer_vector() {
661 let a = Array::from_i64(vec![1, -22, 333]);
662 assert_eq!(j(&a), "1 _22 333");
663 assert_eq!(apl(&a), "1 ¯22 333");
664 }
665
666 #[test]
667 fn float_vector_trims_independently() {
668 let a = Array::from_f64(vec![0.5, 2.0, 1.0 / 3.0, -1e-7]);
669 assert_eq!(j(&a), "0.5 2 0.333333 _1e_7");
670 }
671
672 #[test]
673 fn bool_vector() {
674 let a = Array::new(vec![4], Data::Bool(vec![1, 0, 0, 1].into()));
675 assert_eq!(j(&a), "1 0 0 1");
676 }
677
678 #[test]
679 fn char_vector_is_a_plain_string() {
680 let a = Array::from_chars("hello".chars().collect());
681 assert_eq!(j(&a), "hello");
682 }
683
684 #[test]
687 fn matrix_columns_align_right() {
688 let a = Array::new(vec![2, 3], Data::I64(vec![1, 22, 333, 4444, 5, 66].into()));
689 assert_eq!(j(&a), " 1 22 333\n4444 5 66");
690 }
691
692 #[test]
693 fn matrix_negatives_widen_their_column() {
694 let a = Array::new(vec![2, 2], Data::I64(vec![-1, 10, 100, -2].into()));
695 assert_eq!(j(&a), " _1 10\n100 _2");
696 assert_eq!(apl(&a), " ¯1 10\n100 ¯2");
698 }
699
700 #[test]
701 fn matrix_of_floats() {
702 let a = Array::new(vec![2, 2], Data::F64(vec![0.5, 2.0, -1.0 / 3.0, 10.0].into()));
703 assert_eq!(j(&a), " 0.5 2\n_0.333333 10");
704 }
705
706 #[test]
707 fn matrix_of_bools() {
708 let a = Array::new(vec![2, 3], Data::Bool(vec![1, 0, 1, 0, 1, 0].into()));
709 assert_eq!(j(&a), "1 0 1\n0 1 0");
710 }
711
712 #[test]
713 fn single_column_matrix() {
714 let a = Array::new(vec![3, 1], Data::I64(vec![1, -20, 300].into()));
715 assert_eq!(j(&a), " 1\n_20\n300");
716 }
717
718 #[test]
721 fn rank_3_separates_planes_with_one_blank_line() {
722 let a = Array::new(vec![2, 2, 2], Data::I64(vec![1, 2, 3, 4, 5, 6, 7, 8].into()));
723 assert_eq!(j(&a), "1 2\n3 4\n\n5 6\n7 8");
724 }
725
726 #[test]
727 fn rank_3_column_widths_are_global() {
728 let a = Array::new(vec![2, 1, 2], Data::I64(vec![1, 2, 300, 4].into()));
729 assert_eq!(j(&a), " 1 2\n\n300 4");
730 }
731
732 #[test]
733 fn rank_4_separates_groups_with_two_blank_lines() {
734 let a = Array::new(vec![2, 2, 1, 2], Data::I64(vec![1, 2, 3, 4, 5, 6, 7, 8].into()));
735 assert_eq!(j(&a), "1 2\n\n3 4\n\n\n5 6\n\n7 8");
736 }
737
738 #[test]
739 fn rank_5_gap_grows_with_the_axis() {
740 let a = Array::new(vec![2, 1, 1, 1, 1], Data::I64(vec![1, 2].into()));
741 assert_eq!(j(&a), "1\n\n\n\n2");
743 }
744
745 #[rstest]
746 #[case(&[2], 1, 1)]
748 #[case(&[2, 3], 1, 1)]
750 #[case(&[2, 3], 2, 1)]
751 #[case(&[2, 3], 3, 2)]
752 #[case(&[2, 3], 4, 1)]
753 fn plane_gaps(#[case] frame: &[usize], #[case] p: usize, #[case] want: usize) {
754 assert_eq!(plane_gap(frame, p), want);
755 }
756
757 #[test]
760 fn char_matrix_is_lines() {
761 let a = Array::new(vec![2, 3], Data::Char("abcdef".chars().collect()));
762 assert_eq!(j(&a), "abc\ndef");
763 }
764
765 #[test]
766 fn char_matrix_keeps_spaces_unpadded() {
767 let a = Array::new(vec![2, 3], Data::Char("a bcd".chars().collect()));
768 assert_eq!(j(&a), "a \nbcd");
769 }
770
771 #[test]
772 fn char_rank_3_separates_planes() {
773 let a = Array::new(vec![2, 2, 2], Data::Char("abcdefgh".chars().collect()));
774 assert_eq!(j(&a), "ab\ncd\n\nef\ngh");
775 }
776
777 fn boxed(shape: &[usize], items: Vec<Array>) -> Array {
780 Array::new(shape.to_vec(), Data::Box(items.into()))
781 }
782
783 #[test]
784 fn a_box_is_drawn_as_a_fenced_cell() {
785 let a = boxed(&[], vec![Array::from_i64(vec![1, 2])]);
786 assert_eq!(j(&a), "+---+\n|1 2|\n+---+");
787 assert_eq!(apl(&a), " 1 2 ");
789 }
790
791 #[test]
792 fn a_boxed_vector_is_a_row_of_cells() {
793 let a = boxed(
794 &[3],
795 vec![
796 Array::scalar_i64(1),
797 Array::from_i64(vec![2, 3]),
798 Array::from_chars("abc".chars().collect()),
799 ],
800 );
801 assert_eq!(j(&a), "+-+---+---+\n|1|2 3|abc|\n+-+---+---+");
802 assert_eq!(apl(&a), " 1 2 3 abc ");
806 }
807
808 #[test]
815 fn a_char_run_merges_through_a_box() {
816 let ch = |c: char| Array::new(vec![], Data::Char(vec![c].into()));
819 let a = boxed(&[2], vec![ch('a'), boxed(&[], vec![ch('b')])]);
820 assert_eq!(apl(&a), "ab");
821 }
822
823 #[test]
824 fn a_nonscalar_item_widens_its_own_gap() {
825 let a = boxed(&[2], vec![Array::scalar_i64(1), Array::from_i64(vec![1, 2])]);
827 assert_eq!(apl(&a), " 1 1 2 ");
828 }
829
830 #[test]
831 fn a_boxed_matrix_costs_two_columns() {
832 let a = boxed(
834 &[2],
835 vec![Array::scalar_i64(1), Array::new(vec![2, 2], Data::I64(vec![1, 2, 3, 4].into()))],
836 );
837 assert_eq!(apl(&a), " 1 1 2 \n 3 4 ");
838 }
839
840 #[test]
841 fn a_character_vector_costs_one_column_less_than_its_rank() {
842 let a = boxed(&[2], vec![Array::scalar_i64(1), Array::from_chars("abc".chars().collect())]);
844 assert_eq!(apl(&a), " 1 abc ");
845 }
846
847 #[test]
848 fn the_outer_margin_follows_the_edge_items_own_box_depth() {
849 let inner = boxed(&[], vec![Array::from_i64(vec![1, 2])]);
851 let a = boxed(&[2], vec![boxed(&[], vec![inner]), Array::scalar_i64(1)]);
852 assert_eq!(apl(&a), " 1 2 1 ");
853 }
854
855 #[test]
856 fn a_tall_cell_pads_the_others_below_it() {
857 let a = boxed(
858 &[2],
859 vec![
860 Array::scalar_i64(1),
861 Array::new(vec![2, 2], Data::I64(vec![1, 2, 3, 4].into())),
862 ],
863 );
864 assert_eq!(j(&a), "+-+---+\n|1|1 2|\n| |3 4|\n+-+---+");
865 }
866
867 #[test]
868 fn a_nested_box_draws_inside_its_cell() {
869 let inner = boxed(&[], vec![Array::scalar_i64(5)]);
870 assert_eq!(j(&boxed(&[], vec![inner])), "+---+\n|+-+|\n||5||\n|+-+|\n+---+");
871 }
872
873 #[test]
874 fn a_box_matrix_fences_every_row() {
875 let a = boxed(&[2, 2], (1..=4).map(Array::scalar_i64).collect());
876 assert_eq!(j(&a), "+-+-+\n|1|2|\n+-+-+\n|3|4|\n+-+-+");
877 assert_eq!(apl(&a), "1 2\n3 4");
880 }
881
882 #[test]
883 fn a_boxed_empty_is_a_cell_of_width_zero() {
884 let a = boxed(&[], vec![Array::empty(DType::I64)]);
885 assert_eq!(j(&a), "++\n||\n++");
886 assert_eq!(j(&Array::new(vec![0], Data::Box(Buf::new()))), "");
888 }
889
890 #[rstest]
893 #[case(DType::Bool)]
894 #[case(DType::I64)]
895 #[case(DType::F64)]
896 #[case(DType::Char)]
897 fn empty_vectors_print_nothing(#[case] dtype: DType) {
898 assert_eq!(j(&Array::empty(dtype)), "");
899 }
900
901 #[rstest]
902 #[case(&[0, 3])]
903 #[case(&[3, 0])]
904 #[case(&[2, 0, 4])]
905 fn any_empty_axis_prints_nothing(#[case] shape: &[usize]) {
906 let a = Array::new(shape.to_vec(), Data::I64(vec![].into()));
907 assert_eq!(j(&a), "");
908 }
909
910 #[test]
911 fn no_trailing_newline_or_spaces() {
912 let a = Array::new(vec![2, 2, 2], Data::I64(vec![1, 22, 3, 4, 5, 6, 7, 8].into()));
913 let s = j(&a);
914 assert!(!s.ends_with('\n'));
915 for line in s.lines() {
916 assert_eq!(line.trim_end(), line, "line has trailing space: {line:?}");
917 }
918 }
919}