tiempo-rs/src/tabulate.rs

471 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![allow(clippy::type_complexity)]
use std::borrow::Cow;
use ansi_term::Style;
use regex::Regex;
lazy_static! {
// https://en.wikipedia.org/wiki/ANSI_escape_code#DOS,_OS/2,_and_Windows
//
// For Control Sequence Introducer, or CSI, commands, the ESC [ is followed
// by any number (including none) of "parameter bytes" in the range
// 0x300x3F (ASCII 09:;<=>?), then by any number of "intermediate bytes"
// in the range 0x200x2F (ASCII space and !"#$%&'()*+,-./), then finally
// by a single "final byte" in the range 0x400x7E (ASCII @AZ[\]^_`az{|}~)
//
// The lazy regex bellow doesn't cover all of that. It just works on ansi
// colors.
pub static ref ANSI_REGEX: Regex = Regex::new("\x1b\\[[\\d;]*m").unwrap();
}
/// An abstract way of getting the visual size of a string in a terminal
pub trait VisualSize {
fn size(&self) -> usize;
}
impl VisualSize for &str {
fn size(&self) -> usize {
let s = ANSI_REGEX.replace_all(self, "");
s.chars().count()
}
}
impl VisualSize for String {
fn size(&self) -> usize {
self.as_str().size()
}
}
fn lpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.size()));
padding + s
}
fn rpad(s: &str, len: usize) -> String {
let padding = " ".repeat(len.saturating_sub(s.size()));
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 width = l.as_ref().size();
if width > *w {
*w = width;
}
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 ansi_term::Color::Fixed;
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")));
}
#[test]
fn sizes_of_things() {
assert_eq!("🥦".size(), 1);
assert_eq!("á".size(), 1);
assert_eq!(Fixed(10).paint("hola").to_string().size(), 4);
}
}