use std::{
collections::HashMap,
fmt::{Display, Write},
};
pub struct Table {
header: Box<[Box<dyn Display>]>,
body: HashMap<(usize, usize), Box<dyn Display>>,
nrows: usize,
}
impl Table {
pub fn new<I>(header: I, nrows: usize) -> Self
where
I: IntoIterator<Item: Display + 'static>,
{
fn as_dyn_display<T: Display + 'static>(x: T) -> Box<dyn Display> {
Box::new(x)
}
let header: Box<[_]> = header.into_iter().map(as_dyn_display).collect();
Self {
header,
body: HashMap::new(),
nrows,
}
}
pub fn nrows(&self) -> usize {
self.nrows
}
pub fn ncols(&self) -> usize {
self.header.len()
}
pub fn insert<T>(&mut self, item: T, row: usize, col: usize) -> bool
where
T: Display + 'static,
{
self.check_bounds(row, col);
self.body.insert((row, col), Box::new(item)).is_some()
}
pub fn get(&self, row: usize, col: usize) -> Option<&dyn Display> {
self.check_bounds(row, col);
self.body.get(&(row, col)).map(|x| &**x)
}
pub fn row(&mut self, row: usize) -> Row<'_> {
self.check_bounds(row, 0);
Row::new(self, row)
}
#[expect(clippy::panic, reason = "table interfaces are bounds checked")]
fn check_bounds(&self, row: usize, col: usize) {
if row >= self.nrows() {
panic!("row {} is out of bounds (max {})", row, self.nrows());
}
if col >= self.ncols() {
panic!("col {} is out of bounds (max {})", col, self.ncols());
}
}
}
pub struct Row<'a> {
table: &'a mut Table,
row: usize,
}
impl<'a> Row<'a> {
fn new(table: &'a mut Table, row: usize) -> Self {
Self { table, row }
}
pub fn insert<T>(&mut self, item: T, col: usize) -> bool
where
T: Display + 'static,
{
self.table.insert(item, self.row, col)
}
}
impl Display for Table {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const SEP: &str = ", ";
struct Count(usize);
impl Write for Count {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
self.0 += s.len();
Ok(())
}
}
fn formatted_size<T>(x: &T) -> usize
where
T: Display + ?Sized,
{
let mut buf = Count(0);
match write!(&mut buf, "{}", x) {
Ok(()) => buf.0,
Err(_) => 0,
}
}
let mut widths: Vec<usize> = self.header.iter().map(formatted_size).collect();
for row in 0..self.nrows() {
for (col, width) in widths.iter_mut().enumerate() {
if let Some(v) = self.body.get(&(row, col)) {
*width = (*width).max(formatted_size(v))
}
}
}
let header_width: usize = widths.iter().sum::<usize>() + (widths.len() - 1) * SEP.len();
let mut buf = String::new();
std::iter::zip(widths.iter(), self.header.iter())
.enumerate()
.try_for_each(|(col, (width, head))| {
buf.clear();
write!(buf, "{}", head)?;
write!(f, "{:>width$}", buf)?;
if col + 1 != self.ncols() {
write!(f, "{}", SEP)?;
}
Ok(())
})?;
write!(f, "\n{:=>header_width$}\n", "")?;
for row in 0..self.nrows() {
for (col, width) in widths.iter_mut().enumerate() {
match self.body.get(&(row, col)) {
Some(v) => {
buf.clear();
write!(buf, "{}", v)?;
write!(f, "{:>width$}", buf)?;
}
None => write!(f, "{:>width$}", "")?,
}
if col + 1 != self.ncols() {
write!(f, "{}", SEP)?;
} else {
writeln!(f)?;
}
}
}
Ok(())
}
}
pub(crate) struct Banner<'a>(&'a str);
impl<'a> Banner<'a> {
pub(crate) fn new(message: &'a str) -> Self {
Self(message)
}
}
impl std::fmt::Display for Banner<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let st = format!("# {} #", self.0);
let len = st.len();
writeln!(f, "{:#>len$}", "")?;
writeln!(f, "{}", st)?;
writeln!(f, "{:#>len$}", "")?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct Indent<'a> {
string: &'a str,
spaces: usize,
}
impl<'a> Indent<'a> {
pub fn new(string: &'a str, spaces: usize) -> Self {
Self { string, spaces }
}
}
impl std::fmt::Display for Indent<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let spaces = self.spaces;
self.string
.lines()
.try_for_each(|ln| writeln!(f, "{: >spaces$}{}", "", ln))
}
}
pub struct Delimit<'a, I> {
itr: std::cell::Cell<Option<I>>,
delimiter: &'a str,
last: &'a str,
pair: Option<&'a str>,
}
impl<'a, I> Delimit<'a, I> {
pub fn new(itr: impl IntoIterator<IntoIter = I>, delimiter: &'a str) -> Self {
Self {
itr: std::cell::Cell::new(Some(itr.into_iter())),
delimiter,
last: delimiter,
pair: None,
}
}
pub fn with_last(mut self, last: &'a str) -> Self {
self.last = last;
self
}
pub fn with_pair(mut self, pair: &'a str) -> Self {
self.pair = Some(pair);
self
}
}
impl<I> std::fmt::Display for Delimit<'_, I>
where
I: Iterator<Item: std::fmt::Display>,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Some(mut itr) = self.itr.take() else {
return write!(f, "<missing>");
};
let mut count = 0;
let mut current = if let Some(item) = itr.next() {
item
} else {
return Ok(());
};
loop {
match itr.next() {
None => {
let delimiter = if count == 0 {
""
} else if count == 1 {
self.pair.unwrap_or(self.last)
} else {
self.last
};
return write!(f, "{}{}", delimiter, current);
}
Some(next) => {
let delimiter = if count == 0 { "" } else { self.delimiter };
write!(f, "{}{}", delimiter, current)?;
count += 1;
current = next;
}
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Quote<T>(pub T);
impl<T> std::fmt::Display for Quote<T>
where
T: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "\"{}\"", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_banner() {
let b = Banner::new("hello world");
let s = b.to_string();
let expected = "###############\n\
# hello world #\n\
###############\n";
assert_eq!(s, expected);
let b = Banner::new("");
let s = b.to_string();
let expected = "####\n\
# #\n\
####\n";
assert_eq!(s, expected);
let b = Banner::new("foo");
let s = b.to_string();
let expected = "#######\n\
# foo #\n\
#######\n";
assert_eq!(s, expected);
}
#[test]
fn test_format() {
{
let headers = ["h 0"];
let mut table = Table::new(headers, 3);
table.insert("a", 0, 0);
table.insert("hello world", 1, 0);
table.insert(62, 2, 0);
let s = table.to_string();
let expected = r#"
h 0
===========
a
hello world
62
"#;
assert_eq!(s, expected.strip_prefix('\n').unwrap());
}
{
let headers = ["a really really long header", "h1"];
let mut table = Table::new(headers, 3);
table.insert("a", 0, 0);
table.insert("b", 0, 1);
table.insert("hello world", 1, 0);
table.insert("hello world version 2", 1, 1);
table.insert(7, 2, 0);
table.insert("bar", 2, 1);
let s = table.to_string();
let expected = r#"
a really really long header, h1
====================================================
a, b
hello world, hello world version 2
7, bar
"#;
assert_eq!(s, expected.strip_prefix('\n').unwrap());
}
}
#[test]
fn test_row_api() {
let mut table = Table::new(["a", "b", "c"], 2);
let mut row = table.row(0);
row.insert(1, 0);
row.insert("long", 1);
row.insert("s", 2);
let mut row = table.row(1);
row.insert("string", 0);
row.insert(2, 1);
row.insert(3, 2);
let s = table.to_string();
let expected = r#"
a, b, c
===================
1, long, s
string, 2, 3
"#;
assert_eq!(s, expected.strip_prefix('\n').unwrap());
}
#[test]
fn missing_values() {
let mut table = Table::new(["a", "loong", "c"], 1);
let mut row = table.row(0);
row.insert("string", 0);
row.insert("string", 2);
let s = table.to_string();
let expected = r#"
a, loong, c
=========================
string, , string
"#;
assert_eq!(s, expected.strip_prefix('\n').unwrap());
}
#[test]
#[should_panic(expected = "row 3 is out of bounds (max 2)")]
fn test_panic_row() {
let mut table = Table::new([1, 2, 3], 2);
let _ = table.row(3);
}
#[test]
#[should_panic(expected = "col 3 is out of bounds (max 2)")]
fn test_panic_col() {
let mut table = Table::new([1, 2], 1);
let mut row = table.row(0);
row.insert(1, 3);
}
#[test]
fn test_indent_single_line() {
let s = Indent::new("hello", 4).to_string();
assert_eq!(s, " hello\n");
}
#[test]
fn test_indent_multi_line() {
let s = Indent::new("hello\nworld\nfoo", 2).to_string();
assert_eq!(s, " hello\n world\n foo\n");
}
#[test]
fn test_indent_zero_spaces() {
let s = Indent::new("hello\nworld", 0).to_string();
assert_eq!(s, "hello\nworld\n");
}
#[test]
fn test_indent_empty_string() {
let s = Indent::new("", 4).to_string();
assert_eq!(s, "");
}
#[test]
fn test_delimit_empty() {
let d = Delimit::new(std::iter::empty::<&str>(), ", ");
assert_eq!(d.to_string(), "");
}
#[test]
fn test_delimit_single_item() {
let d = Delimit::new(["a"], ", ").with_last(", and ");
assert_eq!(d.to_string(), "a");
}
#[test]
fn test_delimit_two_items_with_last() {
let d = Delimit::new(["a", "b"], ", ").with_last(", and ");
assert_eq!(d.to_string(), "a, and b");
}
#[test]
fn test_delimit_two_items_with_pair() {
let d = Delimit::new(["a", "b"], ", ")
.with_last(", and ")
.with_pair(" and ");
assert_eq!(d.to_string(), "a and b");
}
#[test]
fn test_delimit_three_items_with_last() {
let d = Delimit::new(["a", "b", "c"], ", ")
.with_last(", and ")
.with_pair(" and ");
assert_eq!(d.to_string(), "a, b, and c");
}
#[test]
fn test_delimit_without_last() {
let d = Delimit::new(["x", "y", "z"], " | ");
assert_eq!(d.to_string(), "x | y | z");
}
#[test]
fn test_delimit_second_display_prints_missing() {
let d = Delimit::new(["a", "b"], ", ");
assert_eq!(d.to_string(), "a, b");
assert_eq!(d.to_string(), "<missing>");
}
#[test]
fn test_quote() {
assert_eq!(Quote("hello").to_string(), "\"hello\"");
}
#[test]
fn test_quote_with_integer() {
assert_eq!(Quote(42).to_string(), "\"42\"");
}
#[test]
fn test_delimit_with_quote() {
let d = Delimit::new(["topk", "range"].iter().map(Quote), ", ")
.with_last(", and ")
.with_pair(" and ");
assert_eq!(d.to_string(), "\"topk\" and \"range\"");
}
}