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 a.shape.contains(&0) {
41 return String::new();
42 }
43 if !a.is_row_major() {
47 return format_array(&a.to_row_major(), opts);
48 }
49 if a.dtype() == DType::Box {
50 match mixed_simple_texts(a, opts) {
54 Some(texts) if opts.boxes == BoxStyle::Spaced => {
55 return laid_out(&a.shape, texts, false)
56 }
57 _ => return format_boxed(a, opts),
58 }
59 }
60 let texts: Vec<String> = (0..a.count()).map(|i| format_atom(&a.data, i, opts)).collect();
61 laid_out(&a.shape, texts, a.dtype() == DType::Char)
63}
64
65fn mixed_simple_texts(a: &Array, opts: &FmtOpts) -> Option<Vec<String>> {
68 let boxes = a.as_boxes()?;
69 let mut texts = Vec::with_capacity(boxes.len());
70 for b in boxes {
71 if b.rank() != 0 || b.dtype() == DType::Box {
72 return None;
73 }
74 texts.push(format_atom(&b.data, 0, opts));
75 }
76 Some(texts)
77}
78
79fn laid_out(shape: &[usize], texts: Vec<String>, text_layout: bool) -> String {
82 let rank = shape.len();
83 let a_shape = shape;
84 match rank {
85 0 => texts.into_iter().next().unwrap_or_default(),
86 1 if text_layout => texts.concat(),
87 1 => texts.join(" "),
88 _ => {
89 let ncols = a_shape[rank - 1];
90 let nrows = a_shape[rank - 2];
91 let widths = if text_layout { vec![0; ncols] } else { column_widths(&texts, ncols) };
94 let frame = &a_shape[..rank - 2];
95 let plane_size = nrows * ncols;
96 let planes: usize = frame.iter().product();
97 let mut out = String::new();
98 for p in 0..planes {
99 if p > 0 {
100 out.push_str(&"\n".repeat(plane_gap(frame, p) + 1));
102 }
103 for r in 0..nrows {
104 if r > 0 {
105 out.push('\n');
106 }
107 let start = p * plane_size + r * ncols;
108 push_row(&mut out, &texts[start..start + ncols], &widths, text_layout);
109 }
110 }
111 out
112 }
113 }
114}
115
116fn format_boxed(a: &Array, opts: &FmtOpts) -> String {
120 let boxes = a.as_boxes().expect("boxed data");
121 let blocks: Vec<(Vec<String>, usize)> = boxes.iter().map(|b| block(b, opts)).collect();
122 let rank = a.rank();
123 let (nrows, ncols) = match rank {
124 0 => (1, 1),
125 1 => (1, a.shape[0]),
126 _ => (a.shape[rank - 2], a.shape[rank - 1]),
127 };
128 let mut widths = vec![0usize; ncols];
130 for (i, (_, w)) in blocks.iter().enumerate() {
131 widths[i % ncols] = widths[i % ncols].max(*w);
132 }
133 let frame: &[usize] = if rank > 2 { &a.shape[..rank - 2] } else { &[] };
134 let planes: usize = frame.iter().product();
135 let plane_size = nrows * ncols;
136 let mut out = String::new();
137 for p in 0..planes.max(1) {
138 if p > 0 {
139 out.push_str(&"\n".repeat(plane_gap(frame, p) + 1));
140 }
141 push_boxed_plane(
142 &mut out,
143 &blocks[p * plane_size..(p + 1) * plane_size],
144 nrows,
145 ncols,
146 &widths,
147 opts,
148 );
149 }
150 out
151}
152
153fn block(a: &Array, opts: &FmtOpts) -> (Vec<String>, usize) {
162 if a.count() == 0 && a.rank() > 0 {
163 let rank = a.rank();
164 let rows: usize = a.shape[..rank - 1].iter().product();
165 let w = a.shape[rank - 1];
166 return (vec![" ".repeat(w); rows], w);
167 }
168 let text = format_array(a, opts);
169 if text.is_empty() {
170 return (vec![String::new()], 0);
171 }
172 let lines: Vec<String> = text.lines().map(str::to_string).collect();
173 let w = lines.iter().map(|l| width(l)).max().unwrap_or(0);
174 (lines, w)
175}
176
177fn push_boxed_plane(
178 out: &mut String,
179 blocks: &[(Vec<String>, usize)],
180 nrows: usize,
181 ncols: usize,
182 widths: &[usize],
183 opts: &FmtOpts,
184) {
185 let fence = opts.boxes == BoxStyle::Fenced;
186 let border: String = if fence {
187 let mut s = String::from("+");
188 for &w in widths {
189 s.push_str(&"-".repeat(w));
190 s.push('+');
191 }
192 s
193 } else {
194 String::new()
195 };
196 let mut lines: Vec<String> = Vec::new();
197 for r in 0..nrows {
198 if fence {
199 lines.push(border.clone());
200 }
201 let row = &blocks[r * ncols..(r + 1) * ncols];
202 let height = row.iter().map(|(lines, _)| lines.len()).max().unwrap_or(1);
205 for k in 0..height {
206 let mut line = String::new();
207 line.push(if fence { '|' } else { ' ' });
208 for (c, (cell, _)) in row.iter().enumerate() {
209 if !fence && c > 0 {
210 line.push(' ');
211 }
212 let text = cell.get(k).map(String::as_str).unwrap_or("");
213 line.push_str(text);
214 for _ in 0..widths[c].saturating_sub(width(text)) {
215 line.push(' ');
216 }
217 if fence {
218 line.push('|');
219 }
220 }
221 if !fence {
222 line.push(' ');
223 }
224 lines.push(line);
225 }
226 }
227 if fence {
228 lines.push(border);
229 }
230 out.push_str(&lines.join("\n"));
231}
232
233fn plane_gap(frame: &[usize], p: usize) -> usize {
236 let mut gap = 1;
239 let mut rest = p;
240 for &n in frame.iter().rev() {
241 if rest % n != 0 {
242 break;
243 }
244 rest /= n;
245 gap += 1;
246 }
247 gap
248}
249
250fn column_widths(texts: &[String], ncols: usize) -> Vec<usize> {
252 let mut widths = vec![0usize; ncols];
253 for (i, t) in texts.iter().enumerate() {
254 let j = i % ncols;
255 widths[j] = widths[j].max(width(t));
256 }
257 widths
258}
259
260fn push_row(out: &mut String, row: &[String], widths: &[usize], text_layout: bool) {
261 for (j, cell) in row.iter().enumerate() {
262 if text_layout {
263 out.push_str(cell);
264 continue;
265 }
266 if j > 0 {
267 out.push(' ');
268 }
269 for _ in 0..widths[j].saturating_sub(width(cell)) {
270 out.push(' ');
271 }
272 out.push_str(cell);
273 }
274}
275
276fn width(s: &str) -> usize {
278 s.chars().count()
279}
280
281fn format_atom(data: &Data, i: usize, opts: &FmtOpts) -> String {
282 match data {
283 Data::Bool(v) => (if v[i] != 0 { "1" } else { "0" }).to_string(),
284 Data::I64(v) => format_i64(v[i], opts),
285 Data::Ext(v) => with_neg_sign(&v[i].to_string(), opts),
286 Data::Rat(v) => with_neg_sign(&v[i].to_string(), opts),
287 Data::F64(v) => format_f64(v[i], opts),
288 Data::Complex(v) => format_complex(v[i], opts),
289 Data::Char(v) => v[i].to_string(),
290 Data::Box(_) => String::new(),
292 }
293}
294
295fn format_complex(z: crate::complex::Cx, opts: &FmtOpts) -> String {
300 if z[1] == 0.0 {
301 return format_f64(z[0], opts);
302 }
303 format!("{}{}{}", format_f64(z[0], opts), opts.imag, format_f64(z[1], opts))
304}
305
306fn format_i64(v: i64, opts: &FmtOpts) -> String {
307 with_neg_sign(&v.to_string(), opts)
308}
309
310fn with_neg_sign(s: &str, opts: &FmtOpts) -> String {
315 match s.strip_prefix('-') {
316 Some(rest) => with_sign(rest, opts),
317 None => s.to_string(),
318 }
319}
320
321fn format_f64(x: f64, opts: &FmtOpts) -> String {
322 if x.is_nan() {
323 return format!("{}.", opts.neg);
324 }
325 if x.is_infinite() {
326 return match (opts.neg, x > 0.0) {
328 ('_', true) => "_".to_string(),
329 ('_', false) => "__".to_string(),
330 (_, true) => "∞".to_string(),
331 (neg, false) => format!("{neg}∞"),
332 };
333 }
334 let magnitude = x.abs();
335 let sci = format!("{:.*e}", SIG_DIGITS - 1, magnitude);
338 let (mantissa, exponent) = sci.split_once('e').expect("scientific form has an exponent");
339 let digits: String = mantissa.chars().filter(char::is_ascii_digit).collect();
340 let exponent: i32 = exponent.parse().expect("exponent is an integer");
341 let body = if exponent >= 12 || exponent <= -6 {
342 let mut s = trim_fraction(&place_point(&digits, 1));
343 s.push('e');
344 if exponent < 0 {
345 s.push_str(&with_sign(&(-(exponent as i64)).to_string(), opts));
346 } else {
347 s.push_str(&exponent.to_string());
348 }
349 s
350 } else {
351 positional(&digits, exponent)
352 };
353 if x < 0.0 { with_sign(&body, opts) } else { body }
354}
355
356fn positional(digits: &str, exponent: i32) -> String {
358 if exponent < 0 {
359 let zeros = (-exponent - 1) as usize;
360 return trim_fraction(&format!("0.{}{}", "0".repeat(zeros), digits));
361 }
362 let int_len = exponent as usize + 1;
363 if int_len >= digits.len() {
364 return format!("{}{}", digits, "0".repeat(int_len - digits.len()));
367 }
368 trim_fraction(&place_point(digits, int_len))
369}
370
371fn place_point(digits: &str, int_len: usize) -> String {
373 format!("{}.{}", &digits[..int_len], &digits[int_len..])
374}
375
376fn trim_fraction(s: &str) -> String {
378 if !s.contains('.') {
379 return s.to_string();
380 }
381 s.trim_end_matches('0').trim_end_matches('.').to_string()
382}
383
384fn with_sign(body: &str, opts: &FmtOpts) -> String {
385 let mut s = String::with_capacity(body.len() + opts.neg.len_utf8());
386 s.push(opts.neg);
387 s.push_str(body);
388 s
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::array::Buf;
395 use rstest::rstest;
396
397 fn j(a: &Array) -> String {
398 format_array(a, &FmtOpts::J)
399 }
400
401 fn apl(a: &Array) -> String {
402 format_array(a, &FmtOpts::APL)
403 }
404
405 fn fj(x: f64) -> String {
406 format_f64(x, &FmtOpts::J)
407 }
408
409 #[rstest]
412 #[case(0, "0")]
413 #[case(7, "7")]
414 #[case(-3, "_3")]
415 #[case(-1234, "_1234")]
416 #[case(i64::MIN, "_9223372036854775808")]
417 fn integers_j(#[case] v: i64, #[case] want: &str) {
418 assert_eq!(format_i64(v, &FmtOpts::J), want);
419 }
420
421 #[rstest]
422 #[case(-3, "¯3")]
423 #[case(3, "3")]
424 fn integers_apl(#[case] v: i64, #[case] want: &str) {
425 assert_eq!(format_i64(v, &FmtOpts::APL), want);
426 }
427
428 #[rstest]
429 #[case(0.0, "0")]
430 #[case(0.5, "0.5")]
431 #[case(2.0, "2")]
432 #[case(-2.0, "_2")]
433 #[case(1.0 / 3.0, "0.333333")]
434 #[case(-1.0 / 3.0, "_0.333333")]
435 #[case(2.0 / 3.0, "0.666667")]
436 #[case(1.25, "1.25")]
437 #[case(100.0, "100")]
438 #[case(1e-5, "0.00001")]
439 #[case(0.000012345678, "0.0000123457")]
440 #[case(1e11, "100000000000")]
441 #[case(123456789.0, "123457000")]
442 fn floats_positional(#[case] x: f64, #[case] want: &str) {
443 assert_eq!(fj(x), want);
444 }
445
446 #[rstest]
447 #[case(1e-7, "1e_7")]
448 #[case(-1e-7, "_1e_7")]
449 #[case(1.5e13, "1.5e13")]
450 #[case(1e12, "1e12")]
451 #[case(-2.5e20, "_2.5e20")]
452 #[case(1.234567e-9, "1.23457e_9")]
453 fn floats_exponent(#[case] x: f64, #[case] want: &str) {
454 assert_eq!(fj(x), want);
455 }
456
457 #[test]
458 fn floats_apl_signs() {
459 assert_eq!(format_f64(-0.5, &FmtOpts::APL), "¯0.5");
460 assert_eq!(format_f64(1e-7, &FmtOpts::APL), "1e¯7");
461 assert_eq!(format_f64(-1e-7, &FmtOpts::APL), "¯1e¯7");
462 }
463
464 #[test]
465 fn negative_zero_prints_unsigned() {
466 assert_eq!(fj(-0.0), "0");
467 }
468
469 #[test]
470 fn nan_and_infinities() {
471 assert_eq!(fj(f64::NAN), "_.");
472 assert_eq!(fj(f64::INFINITY), "_");
473 assert_eq!(fj(f64::NEG_INFINITY), "__");
474 assert_eq!(format_f64(f64::NAN, &FmtOpts::APL), "¯.");
475 assert_eq!(format_f64(f64::INFINITY, &FmtOpts::APL), "∞");
476 assert_eq!(format_f64(f64::NEG_INFINITY, &FmtOpts::APL), "¯∞");
477 }
478
479 #[test]
480 fn scalars() {
481 assert_eq!(j(&Array::scalar_i64(-3)), "_3");
482 assert_eq!(apl(&Array::scalar_i64(-3)), "¯3");
483 assert_eq!(j(&Array::scalar_f64(0.5)), "0.5");
484 assert_eq!(j(&Array::scalar_bool(true)), "1");
485 assert_eq!(j(&Array::scalar_bool(false)), "0");
486 assert_eq!(j(&Array::new(vec![], Data::Char(vec!['q'].into()))), "q");
487 }
488
489 #[test]
492 fn integer_vector() {
493 let a = Array::from_i64(vec![1, -22, 333]);
494 assert_eq!(j(&a), "1 _22 333");
495 assert_eq!(apl(&a), "1 ¯22 333");
496 }
497
498 #[test]
499 fn float_vector_trims_independently() {
500 let a = Array::from_f64(vec![0.5, 2.0, 1.0 / 3.0, -1e-7]);
501 assert_eq!(j(&a), "0.5 2 0.333333 _1e_7");
502 }
503
504 #[test]
505 fn bool_vector() {
506 let a = Array::new(vec![4], Data::Bool(vec![1, 0, 0, 1].into()));
507 assert_eq!(j(&a), "1 0 0 1");
508 }
509
510 #[test]
511 fn char_vector_is_a_plain_string() {
512 let a = Array::from_chars("hello".chars().collect());
513 assert_eq!(j(&a), "hello");
514 }
515
516 #[test]
519 fn matrix_columns_align_right() {
520 let a = Array::new(vec![2, 3], Data::I64(vec![1, 22, 333, 4444, 5, 66].into()));
521 assert_eq!(j(&a), " 1 22 333\n4444 5 66");
522 }
523
524 #[test]
525 fn matrix_negatives_widen_their_column() {
526 let a = Array::new(vec![2, 2], Data::I64(vec![-1, 10, 100, -2].into()));
527 assert_eq!(j(&a), " _1 10\n100 _2");
528 assert_eq!(apl(&a), " ¯1 10\n100 ¯2");
530 }
531
532 #[test]
533 fn matrix_of_floats() {
534 let a = Array::new(vec![2, 2], Data::F64(vec![0.5, 2.0, -1.0 / 3.0, 10.0].into()));
535 assert_eq!(j(&a), " 0.5 2\n_0.333333 10");
536 }
537
538 #[test]
539 fn matrix_of_bools() {
540 let a = Array::new(vec![2, 3], Data::Bool(vec![1, 0, 1, 0, 1, 0].into()));
541 assert_eq!(j(&a), "1 0 1\n0 1 0");
542 }
543
544 #[test]
545 fn single_column_matrix() {
546 let a = Array::new(vec![3, 1], Data::I64(vec![1, -20, 300].into()));
547 assert_eq!(j(&a), " 1\n_20\n300");
548 }
549
550 #[test]
553 fn rank_3_separates_planes_with_one_blank_line() {
554 let a = Array::new(vec![2, 2, 2], Data::I64(vec![1, 2, 3, 4, 5, 6, 7, 8].into()));
555 assert_eq!(j(&a), "1 2\n3 4\n\n5 6\n7 8");
556 }
557
558 #[test]
559 fn rank_3_column_widths_are_global() {
560 let a = Array::new(vec![2, 1, 2], Data::I64(vec![1, 2, 300, 4].into()));
561 assert_eq!(j(&a), " 1 2\n\n300 4");
562 }
563
564 #[test]
565 fn rank_4_separates_groups_with_two_blank_lines() {
566 let a = Array::new(vec![2, 2, 1, 2], Data::I64(vec![1, 2, 3, 4, 5, 6, 7, 8].into()));
567 assert_eq!(j(&a), "1 2\n\n3 4\n\n\n5 6\n\n7 8");
568 }
569
570 #[test]
571 fn rank_5_gap_grows_with_the_axis() {
572 let a = Array::new(vec![2, 1, 1, 1, 1], Data::I64(vec![1, 2].into()));
573 assert_eq!(j(&a), "1\n\n\n\n2");
575 }
576
577 #[rstest]
578 #[case(&[2], 1, 1)]
580 #[case(&[2, 3], 1, 1)]
582 #[case(&[2, 3], 2, 1)]
583 #[case(&[2, 3], 3, 2)]
584 #[case(&[2, 3], 4, 1)]
585 fn plane_gaps(#[case] frame: &[usize], #[case] p: usize, #[case] want: usize) {
586 assert_eq!(plane_gap(frame, p), want);
587 }
588
589 #[test]
592 fn char_matrix_is_lines() {
593 let a = Array::new(vec![2, 3], Data::Char("abcdef".chars().collect()));
594 assert_eq!(j(&a), "abc\ndef");
595 }
596
597 #[test]
598 fn char_matrix_keeps_spaces_unpadded() {
599 let a = Array::new(vec![2, 3], Data::Char("a bcd".chars().collect()));
600 assert_eq!(j(&a), "a \nbcd");
601 }
602
603 #[test]
604 fn char_rank_3_separates_planes() {
605 let a = Array::new(vec![2, 2, 2], Data::Char("abcdefgh".chars().collect()));
606 assert_eq!(j(&a), "ab\ncd\n\nef\ngh");
607 }
608
609 fn boxed(shape: &[usize], items: Vec<Array>) -> Array {
612 Array::new(shape.to_vec(), Data::Box(items.into()))
613 }
614
615 #[test]
616 fn a_box_is_drawn_as_a_fenced_cell() {
617 let a = boxed(&[], vec![Array::from_i64(vec![1, 2])]);
618 assert_eq!(j(&a), "+---+\n|1 2|\n+---+");
619 assert_eq!(apl(&a), " 1 2 ");
621 }
622
623 #[test]
624 fn a_boxed_vector_is_a_row_of_cells() {
625 let a = boxed(
626 &[3],
627 vec![
628 Array::scalar_i64(1),
629 Array::from_i64(vec![2, 3]),
630 Array::from_chars("abc".chars().collect()),
631 ],
632 );
633 assert_eq!(j(&a), "+-+---+---+\n|1|2 3|abc|\n+-+---+---+");
634 assert_eq!(apl(&a), " 1 2 3 abc ");
635 }
636
637 #[test]
638 fn a_tall_cell_pads_the_others_below_it() {
639 let a = boxed(
640 &[2],
641 vec![
642 Array::scalar_i64(1),
643 Array::new(vec![2, 2], Data::I64(vec![1, 2, 3, 4].into())),
644 ],
645 );
646 assert_eq!(j(&a), "+-+---+\n|1|1 2|\n| |3 4|\n+-+---+");
647 }
648
649 #[test]
650 fn a_nested_box_draws_inside_its_cell() {
651 let inner = boxed(&[], vec![Array::scalar_i64(5)]);
652 assert_eq!(j(&boxed(&[], vec![inner])), "+---+\n|+-+|\n||5||\n|+-+|\n+---+");
653 }
654
655 #[test]
656 fn a_box_matrix_fences_every_row() {
657 let a = boxed(&[2, 2], (1..=4).map(Array::scalar_i64).collect());
658 assert_eq!(j(&a), "+-+-+\n|1|2|\n+-+-+\n|3|4|\n+-+-+");
659 assert_eq!(apl(&a), "1 2\n3 4");
662 }
663
664 #[test]
665 fn a_boxed_empty_is_a_cell_of_width_zero() {
666 let a = boxed(&[], vec![Array::empty(DType::I64)]);
667 assert_eq!(j(&a), "++\n||\n++");
668 assert_eq!(j(&Array::new(vec![0], Data::Box(Buf::new()))), "");
670 }
671
672 #[rstest]
675 #[case(DType::Bool)]
676 #[case(DType::I64)]
677 #[case(DType::F64)]
678 #[case(DType::Char)]
679 fn empty_vectors_print_nothing(#[case] dtype: DType) {
680 assert_eq!(j(&Array::empty(dtype)), "");
681 }
682
683 #[rstest]
684 #[case(&[0, 3])]
685 #[case(&[3, 0])]
686 #[case(&[2, 0, 4])]
687 fn any_empty_axis_prints_nothing(#[case] shape: &[usize]) {
688 let a = Array::new(shape.to_vec(), Data::I64(vec![].into()));
689 assert_eq!(j(&a), "");
690 }
691
692 #[test]
693 fn no_trailing_newline_or_spaces() {
694 let a = Array::new(vec![2, 2, 2], Data::I64(vec![1, 22, 3, 4, 5, 6, 7, 8].into()));
695 let s = j(&a);
696 assert!(!s.ends_with('\n'));
697 for line in s.lines() {
698 assert_eq!(line.trim_end(), line, "line has trailing space: {line:?}");
699 }
700 }
701}