lazytable/
lib.rs

1//! # `lazytable`: lazy tables with stuipd wrapping
2//!
3//! ## Why?
4//!
5//! [prettytable](https://github.com/phsym/prettytable-rs) is awesome.
6//! But wrapping in a teminal is no fun.
7//!
8//! ## Example
9//!
10//! ```rust
11//!     #[macro_use]
12//! extern crate lazytable;
13//! use lazytable::Table;
14//!
15//! fn main() {
16//!     let mut table = Table::with_width(23);
17//!     table.set_title(row!["who", "what", "when"]);
18//!     table.add_row(row!["da", "foobar foobar", "bar"]);
19//!     table.add_row(row!["da", "foobar!!", "bar"]);
20//!     print!("{}", table);
21//! }
22//! ```
23//!
24//! This will output:
25//! ```text
26//!  who | what     | when
27//! -----+----------+------
28//!  da  | foobar   | bar
29//!      | foobar   |
30//!  da  | foobar!! | bar
31//! ```
32//!
33//!
34//! ## What can it do?
35//!
36//! For now **lazytable** only produces a simple table like this (given a terminal width of 20):
37//!
38//! Given width of `20`:
39//!
40//! ```text
41//! ######################
42//! # da | foobar  | bar #
43//! #    | foobar  |     #
44//! # da | foobar! | bar #
45//! ######################
46//! ```
47//!
48//! Without a width or with [prettytable](https://github.com/phsym/prettytable-rs):
49//!
50//! ```text
51//! ######################
52//! # da | foobar foobar #
53//! #| bar               #
54//! # da | foobar! | bar #
55//! ######################
56//! ```
57extern crate itertools;
58use std::cmp;
59use std::fmt;
60use std::iter;
61use std::vec;
62
63use self::itertools::join;
64
65/// Type alias for a row.
66type Row = Vec<String>;
67
68/// This macro simplifies `Row` creation
69///
70/// # Example
71/// ```
72/// # #[macro_use] extern crate lazytable;
73///
74/// # fn main() {
75/// let row = row!["foo", "bar"];
76/// # }
77/// ```
78#[macro_export]
79macro_rules! row {
80     ($($content:expr), *) => ((vec![$($content.to_owned()), *]));
81}
82
83/// Width, padding and border strings of a table.
84pub struct TableConfig<'a> {
85    width: usize,
86    padding: usize,
87    border: (&'a str, &'a str, &'a str),
88}
89
90/// Default `TableConfig` with:
91/// * `width: 80`
92/// * `padding: 1`
93/// * `border: |-+`
94impl<'a> Default for TableConfig<'a> {
95    fn default() -> TableConfig<'a> {
96        TableConfig {
97            width: 80,
98            padding: 1,
99            border: ("|", "-", "+"),
100        }
101    }
102}
103
104#[derive(Default)]
105pub struct Table<'a> {
106    title: Option<Row>,
107    rows: Vec<Row>,
108    config: TableConfig<'a>,
109}
110
111impl<'a> Table<'a> {
112    pub fn new(config: TableConfig<'a>) -> Table {
113        Table {
114            title: None,
115            rows: vec![],
116            config: config,
117        }
118    }
119
120    /// Creates a table with a default config and `width`.
121    pub fn with_width(width: usize) -> Table<'a> {
122        let mut config = TableConfig::default();
123        config.width = width;
124        Table::new(config)
125    }
126
127    /// Set the title row.
128    pub fn set_title(&mut self, title: Row) {
129        self.title = Some(title);
130    }
131
132    /// Add a row.
133    pub fn add_row(&mut self, row: Row) {
134        self.rows.push(row);
135    }
136
137    /// Add multiple rows at once.
138    pub fn add_rows(&mut self, rows: &mut Vec<Row>) {
139        self.rows.append(rows);
140    }
141
142    fn dimensions(&self) -> Vec<usize> {
143        let dimensions = self.title
144            .iter()
145            .chain(self.rows.iter())
146            .map(|x| x.iter().map(|s| s.len()).collect::<Vec<_>>())
147            .fold(vec::Vec::<usize>::new(), |l, r| max_merge(&l, &r));
148        distribute(&dimensions, self.config.width, self.config.padding)
149    }
150
151    fn fmt_row(
152        &self,
153        row: &[String],
154        dimenstions: &[usize],
155        f: &mut fmt::Formatter,
156    ) -> fmt::Result {
157        let expanded = dimenstions
158            .iter()
159            .zip(row.iter())
160            .map(|(dim, cell)| split(cell, *dim))
161            .collect::<Vec<_>>();
162        let height = expanded.iter().map(|x| x.len()).max().unwrap_or(0);
163        for i in 0..height {
164            let row = join(
165                expanded
166                    .iter()
167                    .map(|x| {
168                        x.get(i)
169                            .and_then(|x| Some(x.to_owned()))
170                            .unwrap_or_default()
171                    })
172                    .zip(dimenstions.iter())
173                    .map(|(c, w)| {
174                        format!("{pad}{cell: <width$}{pad}", pad = " ", width = w, cell = c)
175                    }),
176                self.config.border.0,
177            );
178            write!(f, "{}\n", row)?;
179        }
180        Ok(())
181    }
182
183    fn fmt_seperator(&self, dimensions: &[usize], f: &mut fmt::Formatter) -> fmt::Result {
184        let row = join(
185            dimensions.iter().map(|dim| {
186                iter::repeat(self.config.border.1.to_string())
187                    .take(dim + self.config.padding * 2)
188                    .collect::<String>()
189            }),
190            self.config.border.2,
191        );
192        write!(f, "{}\n", row)
193    }
194}
195
196impl<'a> fmt::Display for Table<'a> {
197    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
198        let dimensions = self.dimensions();
199        if let Some(ref title) = self.title {
200            self.fmt_row(title, &dimensions, f)?;
201            self.fmt_seperator(&dimensions, f)?;
202        }
203        for row in &self.rows {
204            self.fmt_row(row, &dimensions, f)?;
205        }
206        Ok(())
207    }
208}
209
210fn split(cell: &str, w: usize) -> Vec<String> {
211    let mut lines = vec![];
212    let max = cell.len();
213    let mut from = 0;
214    while from < max {
215        let till = cmp::min(from + w, max);
216        let i = if till < max {
217            match cell[from..till].rfind(' ') {
218                Some(i) => i + 1,
219                None => w,
220            }
221        } else {
222            w
223        };
224        let till = cmp::min(from + i, max);
225        lines.push(cell[from..till].trim().to_owned());
226        from += i;
227    }
228    lines
229}
230
231fn flying(col_width: usize, cols: usize, width: usize, padding: usize) -> usize {
232    let space = cols * 2 * padding + (cols - 1);
233    let fair = (width - space) / cols;
234    cmp::min(col_width, fair)
235}
236
237fn max_merge(left: &[usize], right: &[usize]) -> Vec<usize> {
238    let mut merged = left.iter()
239        .zip(right.iter())
240        .map(|(l, r)| *cmp::max(l, r))
241        .collect::<Vec<_>>();
242    let both = merged.len();
243    merged.append(&mut left.iter().skip(both).cloned().collect::<Vec<_>>());
244    merged.append(&mut right.iter().skip(both).cloned().collect::<Vec<_>>());
245    merged
246}
247
248fn distribute(dimensions: &[usize], width: usize, padding: usize) -> Vec<usize> {
249    let mut indexed = dimensions.iter().cloned().enumerate().collect::<Vec<_>>();
250    indexed.sort_by(|a, b| a.1.cmp(&b.1));
251    let mut width = width;
252    let mut cols = dimensions.len();
253    let mut distributed = indexed
254        .iter()
255        .map(|&(i, x)| {
256            let size = flying(x, cols, width, padding);
257            cols -= 1;
258            if cols > 0 {
259                width -= size + 2 * padding + 1;
260            }
261            (i, size)
262        })
263        .collect::<Vec<_>>();
264    distributed.sort_by(|a, b| a.0.cmp(&b.0));
265    distributed.iter().map(|&(_, x)| x).collect()
266}
267
268#[cfg(test)]
269mod tests {
270    macro_rules! ownv {
271        ($($s:expr),*) => { vec!($($s.to_owned()), *) }
272    }
273
274    use super::*;
275    #[test]
276    fn it_works() {
277        let mut table = Table::default();
278        table.set_title(ownv!["who", "what"]);
279        table.add_rows(&mut vec![ownv!["a", "b"], ownv!["c", "d"]]);
280        table.add_row(ownv!["foobar", "foobar2000"]);
281        assert_eq!(table.dimensions(), vec![6, 10]);
282        let out = format!("{}", table);
283        let should = "\
284# who    | what       #
285#--------+------------#
286# a      | b          #
287# c      | d          #
288# foobar | foobar2000 #
289"
290            .replace("#", "");
291        assert_eq!(out, should);
292    }
293
294    #[test]
295    fn test_max_merge() {
296        let l = vec![1, 2, 3];
297        let r = vec![2, 0, 3, 4];
298        assert_eq!(max_merge(&l, &r), vec![2, 2, 3, 4]);
299        let l = vec![];
300        let r = vec![2, 0, 3, 4];
301        assert_eq!(max_merge(&l, &r), r);
302    }
303
304    #[test]
305    fn test_split() {
306        let cell = "foobar2000 foo";
307        assert_eq!(split(cell, 12), ownv!("foobar2000", "foo"));
308        let cell = "foobar2000    foo";
309        assert_eq!(split(cell, 12), ownv!("foobar2000", "foo"));
310        let cell = "foobar2000    foobar2000";
311        assert_eq!(split(cell, 12), ownv!("foobar2000", "foobar2000"));
312        let cell = "foobar2000     foobar2000";
313        assert_eq!(split(cell, 12), ownv!("foobar2000", "", "foobar2000"));
314    }
315
316    #[test]
317    fn test_distribute() {
318        let dims = vec![10, 5, 20, 15];
319        let dis = distribute(&dims, 40, 0);
320        assert_eq!(dis, vec![10, 5, 11, 11]);
321    }
322    #[test]
323    fn test_wrapping() {
324        let mut table = Table::with_width(20);
325        table.add_row(ownv!["da", "foobar foobar", "bar"]);
326        table.add_row(ownv!["da", "foobar!", "bar"]);
327        let out = format!("{}", table);
328        let should = "\
329# da | foobar  | bar #
330#    | foobar  |     #
331# da | foobar! | bar #
332"
333            .replace("#", "");
334        assert_eq!(out, should);
335    }
336
337}