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