1use std::{
7 collections::HashMap,
8 fmt::{Display, Write},
9};
10
11pub struct Table {
13 header: Box<[Box<dyn Display>]>,
15 body: HashMap<(usize, usize), Box<dyn Display>>,
16 nrows: usize,
17}
18
19impl Table {
20 pub fn new<I>(header: I, nrows: usize) -> Self
21 where
22 I: IntoIterator<Item: Display + 'static>,
23 {
24 fn as_dyn_display<T: Display + 'static>(x: T) -> Box<dyn Display> {
25 Box::new(x)
26 }
27
28 let header: Box<[_]> = header.into_iter().map(as_dyn_display).collect();
29 Self {
30 header,
31 body: HashMap::new(),
32 nrows,
33 }
34 }
35
36 pub fn nrows(&self) -> usize {
37 self.nrows
38 }
39
40 pub fn ncols(&self) -> usize {
41 self.header.len()
42 }
43
44 pub fn insert<T>(&mut self, item: T, row: usize, col: usize) -> bool
45 where
46 T: Display + 'static,
47 {
48 self.check_bounds(row, col);
49 self.body.insert((row, col), Box::new(item)).is_some()
50 }
51
52 pub fn get(&self, row: usize, col: usize) -> Option<&dyn Display> {
53 self.check_bounds(row, col);
54 self.body.get(&(row, col)).map(|x| &**x)
55 }
56
57 pub fn row(&mut self, row: usize) -> Row<'_> {
58 self.check_bounds(row, 0);
59 Row::new(self, row)
60 }
61
62 #[expect(clippy::panic, reason = "table interfaces are bounds checked")]
63 fn check_bounds(&self, row: usize, col: usize) {
64 if row >= self.nrows() {
65 panic!("row {} is out of bounds (max {})", row, self.nrows());
66 }
67 if col >= self.ncols() {
68 panic!("col {} is out of bounds (max {})", col, self.ncols());
69 }
70 }
71}
72
73pub struct Row<'a> {
74 table: &'a mut Table,
75 row: usize,
76}
77
78impl<'a> Row<'a> {
79 fn new(table: &'a mut Table, row: usize) -> Self {
81 Self { table, row }
82 }
83
84 pub fn insert<T>(&mut self, item: T, col: usize) -> bool
86 where
87 T: Display + 'static,
88 {
89 self.table.insert(item, self.row, col)
90 }
91}
92
93impl Display for Table {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 const SEP: &str = ", ";
96
97 struct Count(usize);
99
100 impl Write for Count {
101 fn write_str(&mut self, s: &str) -> std::fmt::Result {
102 self.0 += s.len();
103 Ok(())
104 }
105 }
106
107 fn formatted_size<T>(x: &T) -> usize
108 where
109 T: Display + ?Sized,
110 {
111 let mut buf = Count(0);
112 match write!(&mut buf, "{}", x) {
113 Ok(()) => buf.0,
115 Err(_) => 0,
116 }
117 }
118
119 let mut widths: Vec<usize> = self.header.iter().map(formatted_size).collect();
120 for row in 0..self.nrows() {
121 for (col, width) in widths.iter_mut().enumerate() {
122 if let Some(v) = self.body.get(&(row, col)) {
123 *width = (*width).max(formatted_size(v))
124 }
125 }
126 }
127
128 let header_width: usize = widths.iter().sum::<usize>() + (widths.len() - 1) * SEP.len();
129
130 let mut buf = String::new();
131 std::iter::zip(widths.iter(), self.header.iter())
133 .enumerate()
134 .try_for_each(|(col, (width, head))| {
135 buf.clear();
136 write!(buf, "{}", head)?;
137 write!(f, "{:>width$}", buf)?;
138 if col + 1 != self.ncols() {
139 write!(f, "{}", SEP)?;
140 }
141 Ok(())
142 })?;
143
144 write!(f, "\n{:=>header_width$}\n", "")?;
146
147 for row in 0..self.nrows() {
149 for (col, width) in widths.iter_mut().enumerate() {
150 match self.body.get(&(row, col)) {
151 Some(v) => {
152 buf.clear();
153 write!(buf, "{}", v)?;
154 write!(f, "{:>width$}", buf)?;
155 }
156 None => write!(f, "{:>width$}", "")?,
157 }
158 if col + 1 != self.ncols() {
159 write!(f, "{}", SEP)?;
160 } else {
161 writeln!(f)?;
162 }
163 }
164 }
165 Ok(())
166 }
167}
168
169pub(crate) struct Banner<'a>(&'a str);
174
175impl<'a> Banner<'a> {
176 pub(crate) fn new(message: &'a str) -> Self {
177 Self(message)
178 }
179}
180
181impl std::fmt::Display for Banner<'_> {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 let st = format!("# {} #", self.0);
184 let len = st.len();
185 writeln!(f, "{:#>len$}", "")?;
186 writeln!(f, "{}", st)?;
187 writeln!(f, "{:#>len$}", "")?;
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, Copy)]
209pub struct Indent<'a> {
210 string: &'a str,
211 spaces: usize,
212}
213
214impl<'a> Indent<'a> {
215 pub fn new(string: &'a str, spaces: usize) -> Self {
217 Self { string, spaces }
218 }
219}
220
221impl std::fmt::Display for Indent<'_> {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 let spaces = self.spaces;
224 self.string
225 .lines()
226 .try_for_each(|ln| writeln!(f, "{: >spaces$}{}", "", ln))
227 }
228}
229
230pub struct Delimit<'a, I> {
263 itr: std::cell::Cell<Option<I>>,
264 delimiter: &'a str,
265 last: &'a str,
266 pair: Option<&'a str>,
267}
268
269impl<'a, I> Delimit<'a, I> {
270 pub fn new(itr: impl IntoIterator<IntoIter = I>, delimiter: &'a str) -> Self {
276 Self {
277 itr: std::cell::Cell::new(Some(itr.into_iter())),
278 delimiter,
279 last: delimiter,
280 pair: None,
281 }
282 }
283
284 pub fn with_last(mut self, last: &'a str) -> Self {
286 self.last = last;
287 self
288 }
289
290 pub fn with_pair(mut self, pair: &'a str) -> Self {
292 self.pair = Some(pair);
293 self
294 }
295}
296
297impl<I> std::fmt::Display for Delimit<'_, I>
298where
299 I: Iterator<Item: std::fmt::Display>,
300{
301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302 let Some(mut itr) = self.itr.take() else {
303 return write!(f, "<missing>");
304 };
305
306 let mut count = 0;
307 let mut current = if let Some(item) = itr.next() {
308 item
309 } else {
310 return Ok(());
312 };
313
314 loop {
315 match itr.next() {
316 None => {
317 let delimiter = if count == 0 {
324 ""
325 } else if count == 1 {
326 self.pair.unwrap_or(self.last)
327 } else {
328 self.last
329 };
330
331 return write!(f, "{}{}", delimiter, current);
332 }
333 Some(next) => {
334 let delimiter = if count == 0 { "" } else { self.delimiter };
336
337 write!(f, "{}{}", delimiter, current)?;
338 count += 1;
339 current = next;
340 }
341 }
342 }
343 }
344}
345
346#[derive(Debug, Clone, Copy)]
361pub struct Quote<T>(pub T);
362
363impl<T> std::fmt::Display for Quote<T>
364where
365 T: std::fmt::Display,
366{
367 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368 write!(f, "\"{}\"", self.0)
369 }
370}
371
372#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_banner() {
382 let b = Banner::new("hello world");
383 let s = b.to_string();
384
385 let expected = "###############\n\
386 # hello world #\n\
387 ###############\n";
388
389 assert_eq!(s, expected);
390
391 let b = Banner::new("");
392 let s = b.to_string();
393
394 let expected = "####\n\
395 # #\n\
396 ####\n";
397
398 assert_eq!(s, expected);
399
400 let b = Banner::new("foo");
401 let s = b.to_string();
402
403 let expected = "#######\n\
404 # foo #\n\
405 #######\n";
406
407 assert_eq!(s, expected);
408 }
409
410 #[test]
411 fn test_format() {
412 {
414 let headers = ["h 0"];
415 let mut table = Table::new(headers, 3);
416 table.insert("a", 0, 0);
417 table.insert("hello world", 1, 0);
418 table.insert(62, 2, 0);
419
420 let s = table.to_string();
421 let expected = r#"
422 h 0
423===========
424 a
425hello world
426 62
427"#;
428 assert_eq!(s, expected.strip_prefix('\n').unwrap());
429 }
430
431 {
433 let headers = ["a really really long header", "h1"];
434 let mut table = Table::new(headers, 3);
435 table.insert("a", 0, 0);
436 table.insert("b", 0, 1);
437
438 table.insert("hello world", 1, 0);
439 table.insert("hello world version 2", 1, 1);
440
441 table.insert(7, 2, 0);
442 table.insert("bar", 2, 1);
443
444 let s = table.to_string();
445 let expected = r#"
446a really really long header, h1
447====================================================
448 a, b
449 hello world, hello world version 2
450 7, bar
451"#;
452 assert_eq!(s, expected.strip_prefix('\n').unwrap());
453 }
454 }
455
456 #[test]
457 fn test_row_api() {
458 let mut table = Table::new(["a", "b", "c"], 2);
459 let mut row = table.row(0);
460 row.insert(1, 0);
461 row.insert("long", 1);
462 row.insert("s", 2);
463
464 let mut row = table.row(1);
465 row.insert("string", 0);
466 row.insert(2, 1);
467 row.insert(3, 2);
468
469 let s = table.to_string();
470
471 let expected = r#"
472 a, b, c
473===================
474 1, long, s
475string, 2, 3
476"#;
477 assert_eq!(s, expected.strip_prefix('\n').unwrap());
478 }
479
480 #[test]
481 fn missing_values() {
482 let mut table = Table::new(["a", "loong", "c"], 1);
483 let mut row = table.row(0);
484 row.insert("string", 0);
485 row.insert("string", 2);
486
487 let s = table.to_string();
488 let expected = r#"
489 a, loong, c
490=========================
491string, , string
492"#;
493 assert_eq!(s, expected.strip_prefix('\n').unwrap());
494 }
495
496 #[test]
497 #[should_panic(expected = "row 3 is out of bounds (max 2)")]
498 fn test_panic_row() {
499 let mut table = Table::new([1, 2, 3], 2);
500 let _ = table.row(3);
501 }
502
503 #[test]
504 #[should_panic(expected = "col 3 is out of bounds (max 2)")]
505 fn test_panic_col() {
506 let mut table = Table::new([1, 2], 1);
507 let mut row = table.row(0);
508 row.insert(1, 3);
509 }
510
511 #[test]
512 fn test_indent_single_line() {
513 let s = Indent::new("hello", 4).to_string();
514 assert_eq!(s, " hello\n");
515 }
516
517 #[test]
518 fn test_indent_multi_line() {
519 let s = Indent::new("hello\nworld\nfoo", 2).to_string();
520 assert_eq!(s, " hello\n world\n foo\n");
521 }
522
523 #[test]
524 fn test_indent_zero_spaces() {
525 let s = Indent::new("hello\nworld", 0).to_string();
526 assert_eq!(s, "hello\nworld\n");
527 }
528
529 #[test]
530 fn test_indent_empty_string() {
531 let s = Indent::new("", 4).to_string();
532 assert_eq!(s, "");
533 }
534
535 #[test]
536 fn test_delimit_empty() {
537 let d = Delimit::new(std::iter::empty::<&str>(), ", ");
538 assert_eq!(d.to_string(), "");
539 }
540
541 #[test]
542 fn test_delimit_single_item() {
543 let d = Delimit::new(["a"], ", ").with_last(", and ");
544 assert_eq!(d.to_string(), "a");
545 }
546
547 #[test]
548 fn test_delimit_two_items_with_last() {
549 let d = Delimit::new(["a", "b"], ", ").with_last(", and ");
550 assert_eq!(d.to_string(), "a, and b");
551 }
552
553 #[test]
554 fn test_delimit_two_items_with_pair() {
555 let d = Delimit::new(["a", "b"], ", ")
556 .with_last(", and ")
557 .with_pair(" and ");
558 assert_eq!(d.to_string(), "a and b");
559 }
560
561 #[test]
562 fn test_delimit_three_items_with_last() {
563 let d = Delimit::new(["a", "b", "c"], ", ")
564 .with_last(", and ")
565 .with_pair(" and ");
566 assert_eq!(d.to_string(), "a, b, and c");
567 }
568
569 #[test]
570 fn test_delimit_without_last() {
571 let d = Delimit::new(["x", "y", "z"], " | ");
572 assert_eq!(d.to_string(), "x | y | z");
573 }
574
575 #[test]
576 fn test_delimit_second_display_prints_missing() {
577 let d = Delimit::new(["a", "b"], ", ");
578 assert_eq!(d.to_string(), "a, b");
579 assert_eq!(d.to_string(), "<missing>");
580 }
581
582 #[test]
583 fn test_quote() {
584 assert_eq!(Quote("hello").to_string(), "\"hello\"");
585 }
586
587 #[test]
588 fn test_quote_with_integer() {
589 assert_eq!(Quote(42).to_string(), "\"42\"");
590 }
591
592 #[test]
593 fn test_delimit_with_quote() {
594 let d = Delimit::new(["topk", "range"].iter().map(Quote), ", ")
595 .with_last(", and ")
596 .with_pair(" and ");
597 assert_eq!(d.to_string(), "\"topk\" and \"range\"");
598 }
599}