text_tables/
lib.rs

1//! A small library for pretty-printing tables in monospace text.
2//!
3//! Example
4//!
5//! ```
6//! let data = [["Some", "printable"], ["data", "fields"]];
7//! let mut out = Vec::new();
8//! text_tables::render(&mut out, data).unwrap();
9//! println!("{}", ::std::str::from_utf8(&out).unwrap());
10//! ```
11
12use std::{
13    cmp,
14    fmt::{Display, Write},
15    io,
16};
17
18const CORNER_STR: &'static str = "+";
19const HORIZ_BORDER_STR: &'static str = "-";
20const VERT_BORDER_STR: &'static str = "|";
21const SPACE_STR: &'static str = " ";
22const NEW_LINE_STR: &'static str = "\n";
23
24/// Render the table to a writer
25///
26/// Note that there are a lot of write calls, use a BufferedWriter if your writer is I/O for better
27/// performance.
28///
29/// # Panics
30///
31/// Will panic if all rows are not the same length
32pub fn render<W, T, R, C>(writer: &mut W, data: T) -> io::Result<()>
33where
34    W: io::Write,
35    T: AsRef<[R]>,
36    R: AsRef<[C]>,
37    C: Display,
38{
39    let widths = widths(&data);
40    let data = data.as_ref();
41
42    render_border_line(writer, &widths)?;
43    for row in data.iter() {
44        let row = row.as_ref();
45        render_text_line(writer, &widths, row)?;
46        render_border_line(writer, &widths)?;
47    }
48
49    Ok(())
50}
51
52// Internal helpers
53// ================
54
55/// Get the largest width of each column.
56fn widths<T, R, C>(data: T) -> Vec<usize>
57where
58    T: AsRef<[R]>,
59    R: AsRef<[C]>,
60    C: Display,
61{
62    // re-use a string to reduce memory allocs.
63    let mut string_buf = String::new();
64    let data = data.as_ref();
65    // bail early if there is nothing to do
66    if data.len() == 0 {
67        return vec![];
68    }
69    // this would panic without len check above
70    let row_len = data[0].as_ref().len();
71    let mut widths = vec![0; row_len];
72    for row in data.iter() {
73        let row = row.as_ref();
74        if row_len != row.len() {
75            // todo better handle this situation
76            panic!("rows must be the same length");
77        }
78        for (idx, cell) in row.iter().enumerate() {
79            string_buf.clear();
80            write!(string_buf, "{}", cell).unwrap(); // writing to a string cannot fail.
81            widths[idx] = cmp::max(widths[idx], string_buf.len());
82        }
83    }
84    widths
85}
86
87/// Render a border line
88fn render_border_line<W: io::Write>(writer: &mut W, lengths: &[usize]) -> io::Result<()> {
89    if lengths.len() == 0 || lengths[0] == 0 {
90        return Ok(());
91    }
92    write!(writer, "{}", CORNER_STR)?;
93    for len in lengths {
94        for _ in 0..(*len + 2) {
95            write!(writer, "{}", HORIZ_BORDER_STR)?;
96        }
97        write!(writer, "{}", CORNER_STR)?;
98    }
99    write!(writer, "\n")
100}
101
102/// Render a text line
103fn render_text_line<W, C>(writer: &mut W, lengths: &[usize], row: &[C]) -> io::Result<()>
104where
105    W: io::Write,
106    C: Display,
107{
108    if lengths.len() == 0 || lengths[0] == 0 {
109        return Ok(());
110    }
111    let mut string_buf = String::new();
112    write!(writer, "{}", VERT_BORDER_STR)?;
113    for (cell, len) in row.iter().zip(lengths.iter()) {
114        string_buf.clear();
115        write!(string_buf, "{}", cell).unwrap(); // writing to string cannot fail.
116        let extra = len - string_buf.len();
117        write!(writer, "{}{}", SPACE_STR, string_buf)?;
118        for _ in 0..extra + 1 {
119            write!(writer, "{}", SPACE_STR)?;
120        }
121        write!(writer, "{}", VERT_BORDER_STR)?;
122    }
123    write!(writer, "{}", NEW_LINE_STR)?;
124
125    Ok(())
126}
127
128#[cfg(test)]
129mod tests {
130
131    #[test]
132    fn render() {
133        let tables = vec![
134            (vec![], &b""[..]),
135            (vec![vec![]], &b""[..]),
136            (
137                vec![vec!["single", "line", "a"], vec!["second", "lines", "a"]],
138                &b"\
139+--------+-------+---+
140| single | line  | a |
141+--------+-------+---+
142| second | lines | a |
143+--------+-------+---+
144"[..],
145            ),
146        ];
147        for (table, result) in tables {
148            let mut out = Vec::new();
149            super::render(&mut out, &table).unwrap();
150            assert_eq!(out, &result[..], "{:#?}", table);
151        }
152    }
153}