tiempo-rs/src/tabulate.rs

429 lines
15 KiB
Rust

#![allow(clippy::type_complexity)]
use std::borrow::Cow;
use ansi_term::Style;
fn lpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
padding + s
}
fn rpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
s.to_string() + &padding
}
fn constrained_lines(text: &str, width: usize) -> Vec<Cow<'_, str>> {
textwrap::wrap(text, width)
}
#[derive(Copy, Clone)]
pub enum Align {
Left,
Right,
}
use Align::*;
#[derive(Clone)]
pub struct Col {
min_width: usize,
max_width: Option<usize>,
align: Align,
conditonal_styles: Vec<(Style, fn(&str) -> bool)>,
}
impl Col {
pub fn new() -> Col {
Col {
min_width: 0,
align: Align::Left,
max_width: None,
conditonal_styles: Vec::new(),
}
}
pub fn min_width(self, size: usize) -> Col {
Col {
min_width: size,
..self
}
}
pub fn and_alignment(self, align: Align) -> Col {
Col {
align,
..self
}
}
pub fn max_width(self, size: usize) -> Col {
Col {
max_width: Some(size),
..self
}
}
pub fn color_if(self, style: Style, f: fn(&str) -> bool) -> Col {
let mut conditonal_styles = self.conditonal_styles;
conditonal_styles.push((style, f));
Col {
conditonal_styles,
..self
}
}
}
impl Default for Col {
fn default() -> Col {
Col::new()
}
}
enum DataOrSep {
Data(Vec<String>),
Sep(char),
}
pub struct Tabulate {
cols: Vec<Col>,
widths: Vec<usize>,
data: Vec<DataOrSep>,
}
impl Tabulate {
pub fn with_columns(cols: Vec<Col>) -> Tabulate {
Tabulate {
widths: cols.iter().map(|c| c.min_width).collect(),
cols,
data: Vec::new(),
}
}
pub fn feed<T: AsRef<str>>(&mut self, data: Vec<T>) {
let mut lines: Vec<Vec<String>> = Vec::new();
for (col, ((w, d), c)) in self.widths.iter_mut().zip(data.iter()).zip(self.cols.iter()).enumerate() {
for (r1, dl) in d.as_ref().split('\n').enumerate() {
for (r2, l) in constrained_lines(dl, c.max_width.unwrap_or(usize::MAX)).into_iter().enumerate() {
let count = l.chars().count();
if count > *w {
*w = count;
}
if let Some(line) = lines.get_mut(r1 + r2) {
if let Some(pos) = line.get_mut(col) {
*pos = l.into();
} else {
line.push(l.into());
}
} else {
lines.push({
let mut prev: Vec<_> = if (r1 + r2) == 0 {
data[..col].iter().map(|s| s.as_ref().to_string()).collect()
} else {
(0..col).map(|_| "".into()).collect()
};
prev.push(l.into());
prev
});
}
}
}
}
for line in lines {
self.data.push(DataOrSep::Data(line));
}
}
pub fn separator(&mut self, c: char) {
self.data.push(DataOrSep::Sep(c));
}
pub fn print(self, color: bool) -> String {
let widths = self.widths;
let cols = self.cols;
self.data.into_iter().map(|row| match row {
DataOrSep::Sep(c) => {
if c == ' ' {
"\n".into()
} else {
c.to_string().repeat(widths.iter().sum::<usize>() + widths.len() -1) + "\n"
}
},
DataOrSep::Data(d) => {
d.into_iter().zip(widths.iter()).zip(cols.iter()).map(|((d, &w), c)| {
let style = c.conditonal_styles.iter().find(|(_s, f)| {
f(&d)
}).map(|(s, _f)| s);
let s = match c.align {
Left => rpad(&d, w),
Right => lpad(&d, w),
};
if let Some(style) = style {
if color {
style.paint(s).to_string()
} else {
s
}
} else {
s
}
}).collect::<Vec<_>>().join(" ").trim_end().to_string() + "\n"
},
}).collect::<Vec<_>>().join("")
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
const LONG_NOTE: &str = "chatting with bob about upcoming task, district sharing of images, how the user settings currently works etc. Discussing the fingerprinting / cache busting issue with CKEDITOR, suggesting perhaps looking into forking the rubygem and seeing if we can work in our own changes, however hard that might be.";
#[test]
fn test_constrained_lines_long_text() {
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
"chatting with bob about upcoming task,",
"district sharing of images, how the user",
"settings currently works etc. Discussing the",
"fingerprinting / cache busting issue with",
"CKEDITOR, suggesting perhaps looking into",
"forking the rubygem and seeing if we can work",
"in our own changes, however hard that might",
"be.",
]);
}
#[test]
fn test_constrained_lines_nowrap() {
assert_eq!(constrained_lines(LONG_NOTE, LONG_NOTE.len()), vec![
LONG_NOTE,
]);
}
#[test]
fn test_text_output() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).and_alignment(Left),
]);
tabs.feed(vec!["Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["Fri Oct 03, 2008", "12:00:00 - 14:00:00", "2:00:00", "entry 1"]);
tabs.feed(vec!["", "16:00:00 - 18:00:00", "2:00:00", "entry 2"]);
tabs.feed(vec!["", "", "4:00:00", ""]);
tabs.feed(vec!["Sun Oct 05, 2008", "16:00:00 - 18:00:00", "2:00:00", "entry 3"]);
tabs.feed(vec!["", "18:00:00 - ", "2:00:00", "entry 4"]);
tabs.feed(vec!["", "", "4:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["Total", "", "8:00:00", ""]);
assert_eq!(&tabs.print(false), "\
Day Start End Duration Notes
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
16:00:00 - 18:00:00 2:00:00 entry 2
4:00:00
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
18:00:00 - 2:00:00 entry 4
4:00:00
---------------------------------------------------------
Total 8:00:00
");
}
#[test]
fn test_text_output_long_duration() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).and_alignment(Left),
]);
tabs.feed(vec!["Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["Wed Oct 01, 2008", "12:00:00 - 14:00:00+2d", "50:00:00", "entry 1"]);
tabs.feed(vec!["", "", "50:00:00", ""]);
tabs.feed(vec!["Fri Oct 03, 2008", "12:00:00 - 14:00:00", "2:00:00", "entry 2"]);
tabs.feed(vec!["", "", "2:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["Total", "", "52:00:00", ""]);
assert_eq!(&tabs.print(false), "\
Day Start End Duration Notes
Wed Oct 01, 2008 12:00:00 - 14:00:00+2d 50:00:00 entry 1
50:00:00
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 2
2:00:00
----------------------------------------------------------
Total 52:00:00
");
}
#[test]
fn test_text_output_with_ids() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width(3).and_alignment(Right),
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).and_alignment(Left),
]);
tabs.feed(vec!["ID", "Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["1", "Fri Oct 03, 2008", "12:00:00 - 14:00:00", "2:00:00", "entry 1"]);
tabs.feed(vec!["2", "", "16:00:00 - 18:00:00", "2:00:00", "entry 2"]);
tabs.feed(vec!["", "", "", "4:00:00", ""]);
tabs.feed(vec!["3", "Sun Oct 05, 2008", "16:00:00 - 18:00:00", "2:00:00", "entry 3"]);
tabs.feed(vec!["4", "", "18:00:00 -", "2:00:00", "entry 4"]);
tabs.feed(vec!["", "", "", "4:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["", "Total", "", "8:00:00"]);
assert_eq!(&tabs.print(false), " ID Day Start End Duration Notes
1 Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
2 16:00:00 - 18:00:00 2:00:00 entry 2
4:00:00
3 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
4 18:00:00 - 2:00:00 entry 4
4:00:00
-------------------------------------------------------------
Total 8:00:00
");
}
#[test]
fn test_text_output_long_note_with_ids() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width(2).and_alignment(Right),
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).max_width(44).and_alignment(Left),
]);
tabs.feed(vec!["ID", "Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["60000", "Sun Oct 05, 2008", "16:00:00 - 18:00:00", "2:00:00", LONG_NOTE]);
tabs.feed(vec!["", "", "", "2:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["", "Total", "", "2:00:00"]);
assert_eq!(&tabs.print(false), " ID Day Start End Duration Notes
60000 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 chatting with bob about upcoming task,
district sharing of images, how the user
settings currently works etc. Discussing the
fingerprinting / cache busting issue with
CKEDITOR, suggesting perhaps looking into
forking the rubygem and seeing if we can
work in our own changes, however hard that
might be.
2:00:00
----------------------------------------------------------------------------------------------------
Total 2:00:00
");
}
#[test]
fn test_text_output_note_with_line_breaks() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).and_alignment(Left),
]);
tabs.feed(vec!["Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["Sun Oct 05, 2008", "16:00:00 - 18:00:00", "2:00:00", "first line\nand a second line"]);
tabs.feed(vec!["", "", "2:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["Total", "", "2:00:00", ""]);
assert_eq!(&tabs.print(false), "\
Day Start End Duration Notes
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 first line
and a second line
2:00:00
-------------------------------------------------------------------
Total 2:00:00
");
}
#[test]
fn note_with_accents() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
Col::new().min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
Col::new().min_width("Duration".len()).and_alignment(Right),
Col::new().min_width("Notes".len()).and_alignment(Left),
]);
tabs.feed(vec!["Day", "Start End", "Duration", "Notes"]);
tabs.feed(vec!["Sun Oct 05, 2008", "16:00:00 - 18:00:00", "2:00:00", "quiúbole"]);
tabs.feed(vec!["", "", "2:00:00", ""]);
tabs.separator('-');
tabs.feed(vec!["Total", "", "2:00:00", ""]);
assert_eq!(&tabs.print(false), "\
Day Start End Duration Notes
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 quiúbole
2:00:00
----------------------------------------------------------
Total 2:00:00
");
}
#[test]
fn tabulate_a_blank_row() {
let mut tabs = Tabulate::with_columns(vec![
Col::new()
]);
tabs.feed(vec!["Hola"]);
tabs.separator(' ');
tabs.feed(vec!["adiós"]);
tabs.separator('-');
tabs.feed(vec!["ta güeno"]);
assert_eq!(&tabs.print(false), "\
Hola
adiós
--------
ta güeno
");
}
#[test]
fn add_a_color_condition() {
let mut tabs = Tabulate::with_columns(vec![
Col::new().color_if(Style::new().dimmed(), |val| {
val == "key"
}),
Col::new(),
]);
tabs.feed(vec!["foo", "key"]);
tabs.feed(vec!["key", "foo"]);
assert_eq!(tabs.print(true), format!("\
foo key
{} foo
", Style::new().dimmed().paint("key")));
}
}